Second stage of bootloader prints garbage using Int 0x10/ah=0x0e

I can deduce you are using NASM (or NASM compatible) assembler. I don’t know what OS you are using to build the bootloader, but I’ll assume Linux or Windows. Other environments would be somewhat similar.


You should split your bootloader into two parts to make this easier. One is the bootloader, and the second being the second stage you load at 0x1000:0x0000. This allows us to properly define the origin point for our bootloader. The bootloader is expected to be loaded at physical address 0x07c00, and the second stage at 0x10000 ((0x1000<<4+)+0). We need the assembler to properly generate addresses for our data and code.

I have written a number of StackOverflow answers that describe some of the changes I have made to the code. A couple of the more relevant ones are:

  • General Boot Loader Tips which give general guidelines and assumptions you don’t want to make in a bootloader
  • Information on the pitfalls of not setting up DS properly and getting garbage when accessing memory variables. This applies somewhat to your second stage

If you don’t have a proper understanding of segment:offset pairs I recommend this article. I bring this up because there seems to be confusion in your question and in your code. You seem to think that physical memory address 0x1000 is the same as the segment:offset pair 0x1000:0x0000. In your question you say:

The following code loads the contents of a floppy drive to memory and jumps to it (starts loading at address 0x1000).

In your code you have this line and comment:

jmp 0x1000:0000 ;Jump to 0x1000, start of second program

If you review that link you’ll discover that segment:offset computes to a physical address by shifting the segment left 4 bits (multiply by 16 decimal) and then adding the offset. The equation usually appears as (segment<<4)+offset . In your case 0x1000:0x0000 is a segment of 0x1000 and offset of 0x0000 . Using the equation to get the physical address in memory you’d get (0x1000<<4)+0x0000 = 0x10000 (not 0x1000)


From your code it isn’t possible to tell how you are assembling with NASM. I provide an example of how it could be done, but the important part is splitting the bootloader up. Assume we put your bootloader in a file called bootload.asm:

[bits 16]
[ORG 0x7c00]    ; Bootloader starts at physical address 0x07c00

    ; BIOS sets DL to boot drive before jumping to the bootloader

    ; Since we specified an ORG(offset) of 0x7c00 we should make sure that
    ; Data Segment (DS) is set accordingly. The DS:Offset that would work
    ; in this case is DS=0 . That would map to segment:offset 0x0000:0x7c00
    ; which is physical memory address (0x0000<<4)+0x7c00 . We can't rely on
    ; DS being set to what we expect upon jumping to our code so we set it
    ; explicitly
    xor ax, ax
    mov ds, ax        ; DS=0

    cli               ; Turn off interrupts for SS:SP update
                      ; to avoid a problem with buggy 8088 CPUs
    mov ss, ax        ; SS = 0x0000
    mov sp, 0x7c00    ; SP = 0x7c00
                      ; We'll set the stack starting just below
                      ; where the bootloader is at 0x0:0x7c00. The
                      ; stack can be placed anywhere in usable and
                      ; unused RAM.
    sti               ; Turn interrupts back on

reset:                ; Resets floppy drive
    xor ax,ax         ; 0 = Reset floppy disk
    int 0x13
    jc reset          ; If carry flag was set, try again

    mov ax,0x1000     ; When we read the sector, we are going to read address 0x1000
    mov es,ax         ; Set ES with 0x1000

floppy:
    xor bx,bx   ;Ensure that the buffer offset is 0!
    mov ah,0x2  ;2 = Read floppy
    mov al,0x1  ;Reading one sector
    mov ch,0x0  ;Track 1
    mov cl,0x2  ;Sector 2, track 1
    mov dh,0x0  ;Head 1
    int 0x13
    jc floppy   ;If carry flag was set, try again
    jmp 0x1000:0000 ;Jump to 0x1000, start of second program

times 510 - ($ - $$) db 0       ;Fill the rest of sector with 0
dw 0xAA55   ;This is the boot signature

You should notice that I removed this line:

mov dl,0x0  ;Drive = 0 (Floppy)

This hard codes the boot drive to the Floppy A:. If you boot off of USB, hard drive, or Floppy B: your code won’t work because the drive number likely won’t be zero in those cases. The BIOS passes the actual boot drive that was used to load your bootloader. That value is in the register DL. This is the value you should be using for BIOS disk functions. Since DL already contains the boot drive, we just use it as-is.


The second stage can be modified in this way. I’ll assume a file called stage2.asm:

[BITS 16]
[ORG 0x0000]      ; This code is intended to be loaded starting at 0x1000:0x0000
                  ; Which is physical address 0x10000. ORG represents the offset
                  ; from the beginning of our segment.

; Our bootloader jumped to 0x1000:0x0000 which sets CS=0x1000 and IP=0x0000
; We need to manually set the DS register so it can properly find our variables
; like 'var'

mov ax, cs
mov ds, ax       ; Copy CS to DS (we can't do it directly so we use AX temporarily)

mov bx, var
mov ah, 0x0e
mov al, [bx]
xor bh, bh       ; BH = 0 = Display on text mode page 0
int 0x10
jmp $

var:
db 'X'

I’ve made no attempt to streamline your code. The idea is to show how to add the glue to fix your issues. Both files specify an origin point using the ORG directive. Bootloaders need to be assembled so that they work at memory address 0x07c00 . You are loading the second stage at 0x1000:0x0000 that maps to physical address 0x10000. We set ORG to 0x0000 since the FAR JUMP jmp 0x1000:0000 will set CS=0x1000, and IP=0x0000 . Because IP is 0x0000 we want ORG to match it so that near memory references are relative to the beginning of our 64k segment.

This will allow the assembler to generate proper memory references for your variables and code. Because you didn’t properly do this in your code, your second stage was reading the wrong memory location for var and subsequently displayed an incorrect character.


Once you have the 2 files split you need to assemble them with NASM and then place them into a disk image. Unlike your question, I will use DD to build a 720k floppy disk image and then place the bootloader at the beginning (without truncating the disk) and then place the second stage starting at the sector right after. That can be accomplished like this:

# Assemble both components as binary images with NASM
nasm -f bin bootload.asm -o bootload.bin
nasm -f bin stage2.asm -o stage2.bin

# Create a 720k disk image
dd if=/dev/zero of=disk.img bs=1024 count=720

# Place bootload.bin at the beginning of disk.img without truncating
dd if=bootload.bin of=disk.img conv=notrunc

# Place stage2.bin starting at the second 512byte sector and write
# it without truncating the disk image. bs=512 seek=1 will skip the
# first 512 byte sector and start writing stage2.bin there. 
dd if=stage2.bin of=disk.img bs=512 seek=1 conv=notrunc

You could run such an image using QEMU with something like:

qemu-system-i386 -fda disk.img 

If using Windows, and you don’t have access to DD, you may be able to use this modification to stage2.asm:

[BITS 16]
[ORG 0x0000]      ; This code is intended to be loaded starting at 0x1000:0x0000
                  ; Which is physical address 0x10000. ORG represents the offset
                  ; from the beginning of our segment.

; Our bootloader jumped to 0x1000:0x0000 which sets CS=0x1000 and IP=0x0000
; We need to manually set the DS register so it can properly find our variables
; like 'var'

mov ax, cs
mov ds, ax       ; Copy CS to DS (we can't do it directly so we use AX temporarily)

mov bx, var
mov ah, 0x0e
mov al, [bx]
xor bh, bh       ; BH = 0 = Display on text mode page 0
int 0x10
jmp $

var:
db 'X'
; Extend the second stage to (720K - 512 bytes) 
; bootload.bin will take up first 512 bytes 
times 737280 - 512 - ($ - $$) db 0

And then assemble and build the 720K disk image with these commands:

nasm -f bin bootload.asm -o bootload.bin
nasm -f bin stage2.asm -o stage2.bin
copy /b bootload.bin+stage2.bin disk.img

disk.img would be the 720K disk image that should be usable by QEMU or Bochs. The final size of disk.img should be 737,280 bytes.


If you want to move a value from a memory address to a register, you can do it directly without an intermediate register. In your stage2.asm you have this:

mov bx, var
mov ah, 0x0e
mov al, [bx]

It could be written as:

mov ah, 0x0e
mov al, [var]

This would move a single byte from the memory location var and move it directly to AL . The size is determined by NASM to be a byte because the destination AL is an 8-bit register.

Leave a Comment