assemblyx86-64bootmbr

bootloader works in qemu but fails in virtualbox and on hardware


my bootloader consists of two 512 byte stages. stage 1 gets loaded by the bios into the MBR area. stage1 then proceeds to load stage2 from the drive and jumps to it.

i confirmed with a hex editor that the size of final binary "program.bin" is exactly 1024 byte long and contains both "signatures" (last two bytes of each stage, 0xAA55 for stage1 (MBR signature) and 0xCC77 for stage2).

expected outputs are:

1 // stage1 started
0000 or 0080 // drive# in hex
CC77 // stage2 "signature" in hex
2 // stage2 started

this works fine in QEMU but fails in virtualbox and on hardware. it looks to me like the stage2 load fails silently (error branch doesn't get called) and i am looking to solve this for two weeks now without success.

QEMU output
QEMU output

Hardware & Virtual Box output
VBox output

stage1.asm:

global _start
extern _stage2
extern _stage2data

BITS 16

_start:
; init registers
    xor ax, ax
    mov es, ax
    mov gs, ax
    mov ss, ax
    mov sp, 0x7C00      ; right before MBR, counting upwards

    mov ax, 0x7C0       ; set DS to 0x7c0 so pointing at 0x0 resolves to 0x7C0:x0000 = 0x7C00
    mov ds, ax

    cld                 ; set direction flag to make string operations count forward

 ; mark start of stage 1 by printing "1"
    mov al, '1'
    call real_mode_print_char
    call real_mode_new_line

print_drive_number:
; drive# is put into DL by BIOS
    mov dh, 0x0
    mov bx, dx
    call real_mode_print_hex

load_sector2:
    mov  al, 0x01           ; load 1 sector
    mov  bx, 0x7E00         ; destination, right after your bootloader
    mov  cx, 0x0002         ; cylinder 0, sector 2
    ; mov  dl, [BootDrv]      ; boot drive
    xor  dh, dh             ; head 0
    call read_sectors_16
    jnc execute_stage2           ; if carry flag is set, disk read failed
    jmp error

execute_stage2:
    mov bx, [_stage2data]       ; print data at _stage2data to confirm stage 2 was loaded
    call real_mode_print_hex

    jmp _stage2                 ; start execude instructions of _stage2

error:
; print "E" if an error occurs
    mov al, 'E'
    call real_mode_print_char

; infinite loop
loop:
    jmp loop

; read_sectors_16
;
; Reads sectors from disk into memory using BIOS services
;
; input:    dl      = drive
;           ch      = cylinder[7:0]
;           cl[7:6] = cylinder[9:8]
;           dh      = head
;           cl[5:0] = sector (1-63)
;           es:bx  -> destination
;           al      = number of sectors
;
; output:   cf (0 = success, 1 = failure)

read_sectors_16:
    push ax
    mov si, 0x02    ; maximum attempts - 1
.top:
    mov ah, 0x02    ; read sectors into memory (int 0x13, ah = 0x02)
    int 0x13
    jnc .end        ; exit if read succeeded
    dec si          ; decrement remaining attempts
    jc  .end        ; exit if maximum attempts exceeded
    xor ah, ah      ; reset disk system (int 0x13, ah = 0x00)
    int 0x13
    jnc .top        ; retry if reset succeeded, otherwise exit
.end:
    pop ax
    retn

# print a number in hex
# IN
#   bx: the number
# CLOBBER
#   al, cx
real_mode_print_hex:
    mov cx, 4
.lp:
    mov al, bh
    shr al, 4

    cmp al, 0xA
    jb .below_0xA

    add al, 'A' - 0xA - '0'
.below_0xA:
    add al, '0'

    call real_mode_print_char

    shl bx, 4
    loop .lp

    call real_mode_new_line

    ret

real_mode_new_line:
    mov al, 0x0D
    call real_mode_print_char
    mov al, 0x0A
    call real_mode_print_char
    ret

real_mode_print_char:
    push bx
    xor bx, bx              ; Attribute=0/Current Video Page=0
    mov ah, 0x0e
    int 0x10                ; Display character
    pop bx
    ret

; boot signature
TIMES 510-($-$$) db 0

mbr_id:
dw 0xAA55

stage2.asm:

global _stage2
global _stage2data

BITS 16

_stage2:
    mov al, '2'
    call bios_print_char

loop:
    jmp loop

    bios_print_char:
    push bx
    xor bx, bx              ; Attribute=0/Current Video Page=0
    mov ah, 0x0e
    int 0x10                ; Display character
    pop bx
    ret

; boot signature
TIMES 510-($-$$) db 0
_stage2data:
dw 0xCC77

linker script "linker.ld":

ENTRY(_start)
OUTPUT_FORMAT(binary)

SECTIONS
{
  output :
    {
      stage1.elf(.text)
      stage2.elf(.text)
    }
}

i use the following commands to compile and link everything together:

nasm -f elf64 stage1.asm -o stage1.elf
nasm -f elf64 stage2.asm -o stage2.elf
ld -m elf_x86_64 -o program.bin stage2.elf stage1.elf -nostdlib -T linker.ld

i run the binary on QEMU with:

qemu-system-x86_64 -drive format=raw,file=program.bin

to run it on hardware i write the binary to a USB with:

dd if=program.bin of=/dev/sdb1 && sync

Solution

  • Your bootloader actually looks pretty good. As @jester pointed out on real hardware if you are booting USB using Floppy Disk Emulation (FDD) then you will most likely need a BPB. There are indications in your screenshot that you are booting USB as Hard Disk Emulation (HDD) since the drive number appears to be 0x0080. If that is the case a BPB isn't necessary.

    When using USB HDD emulation you may need a partition table with one partition marked active/bootable for some BIOSes to recognize the drive as bootable. Without it some BIOSes may refuse to recognize the drive as something it should boot even if it has the proper disk signature (0xaa55) in the last 2 bytes.

    I believe the real problem lies in how you are writing to the USB drive. You are using:

    dd if=program.bin of=/dev/sdb1 && sync 
    

    /dev/sdb1 is actually the first partition, not the beginning of the drive. What it appears you want is to write to the beginning of the drive with:

    dd if=program.bin of=/dev/sdb && sync
    

    You may ask: how is it that the bootloader you wrote was actually running if it wasn't written to the beginning of the drive? My suspicion is that your USB drive was formatted with a Master Boot Record (MBR) that chain loaded the Volume Boot Record (VBR) bootloader in partition 1 and then started executing it. Such a chainloading MBR is very possible if the USB stick was formatted and partitioned in Windows. Windows usually formats USB as one large partition and places an MBR in the first sector of the drive that chain loads the VBR from the first sector of the first partition.


    Other Observations

    Since you appear to be booting everything as Hard Disk media, you may wish to consider using the Extended disk function like Int 13h/AH=42h rather than Int 13h/AH=2h. Int 13/AH=2 is very limited in what it can load with CHS addressing rather than LBA addressing when dealing with larger media (usually greater than about 8GiB).