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

1

3

I am trying to learn assembly and to write a bootloader. The following code loads the contents of a floppy drive to memory and jumps to it (starts loading at address 0x1000). This code is supposed to print "X" on the screen, but for some reason it prints a space. Can somebody please tell me what is wrong?

[bits 16]
jmp reset
reset:          ;Resets floppy drive
    xor ax,ax   ;0 = Reset floppy disk
    mov dl,0        ;Drive 0 is floppy
    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:
    mov ah,0x2  ;2 = Read floppy
    mov al,0x11 ;Reading one sector
    mov ch,0x0  ;Track 1 
    mov cl,0x2  ;Sector 2, track 1
    mov dh,0x0  ;Head 1
    mov dl,0x0  ;Drive = 0 (Floppy)
    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 signiture
;---
;--[segment 2]--
mov bx, var
mov ah, 0x0e
mov al, [bx]
int 0x10
jmp $

var:
db 'X'
times 737280 - ($ - $$) db 0
Soapstone answered 4/12, 2015 at 16:30 Comment(5)
You put 0x1000 in ES as the segment. That is fine, but int 13h_/ah=02h expects the memory location to be ES:BX . Where do you initialize BX=0 ._BX_ could be anything (but you need it to be 0) by the time it hits the bootloader?Michel
When a BIOS jumps to your bootloader the boot drive is in DL register. You should be using that value for DL when doing the disk reset and read (your code will work as long as the boot drive is floppy drive A)Michel
int 10h/ah=0x0e requires the page number (probably 0 in your case) to be in BH . The color is placed in BLMichel
You can move a byte from memory directly to an 8-bit register with something like mov al, [var]Michel
Your code is reading 17 sectors! (mov al,0x11 ;Reading one sector)Erinaceous
M
11

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.

Michel answered 4/12, 2015 at 19:20 Comment(13)
And before someone asks the - CLI/STI I put around the code to set up the stack is to avoid a bug on defective 8088's that were made in the early 80s. If there is no likelihood of running code on that type of processor then the CLI/STI pair can be removed.Michel
@VishnuShankar: You can download the windows DD version right from here: chrysocome.net/downloads/dd-0.6beta3.zip . I copied dd.exe (from insize the zip file) to a place on my Windows path, and the commands I used in my examples workedMichel
Can I convert them to .bin files and merge them and emulate the merged bin file?Soapstone
I get this error in Bochs emulator: 00020045565e[CPU0 ] prefetch: EIP [00010000] > CS.limit [0000ffff]Soapstone
@VishnuShankar When I wake up in the morning I'll take a look. But just tomake sure, are you using my latest change. At one point I did fix a bug in the code in my answer. In particular the right version should have [ORG 0x0000] at the top of stage2.asm . How did you put the file together? With DD or did you combine them with something like DOS's copy command?Michel
I merged the .bin files with a hex editor. Could not use DD, it does not work for some reason. Thank you for your reply. I will try to find out more. Addressing in assembly is really wierd. Yes, my stage 2 has that "[org 0x1000]" at the begining.Soapstone
@VishnuShankar It needs to be [ORG 0x0000] not [ORG 0x10000] . I just ran it on bochs here and it does work (it also works on QEMU).Michel
Sorry, I meant [org 0x0000], it was a typo. Will it work if I merge the bin files with a hex editor and the run the emulator?. You can answer this one in the morning if you want.Soapstone
@VishnuShankar You may be able to combine both together with copy /b bootload.bin+stage2.bin combined.binMichel
@VishnuShankar : The command I just gave in my last comment uses DOS's copy command to combine both files into one called combined.binMichel
@VishnuShankar I have added to the bottom of my original answer a change to stage2.asm to pad it out to the size of a 720K disk (very similar to what you had originally), and a DOS/Windows copy command that generates disk.img by combine both parts together. This should avoid the need for a hexeditor, and doesn't need DDMichel
@MichaelPetch Why did you ignore the BH parameter for the BIOS teletype function in your "stage2.asm" ? You did mention it in a comment to the original question!Erinaceous
@Erinaceous :Because I had a couple different variants of the code that I used and I selected an older (wrong) one. In this case it wasn't the root cause of their issue. I'll amend it. Thanks for pointing it out.Michel

© 2022 - 2024 — McMap. All rights reserved.