Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MBR works in QEMU/BOCHS but not on real hardware

I threw together a makeshift MBR for a chainloader project I have been working on for sometime now. I tested on QEMU and BOCHS and no issues, besides throwing earlier 386 BIOS images into the mix (BIOS images that don't support EDD). I made a simple MBR prior to testing my working MBR on real hardware to determine if my actual hardware had EDD support (which it did as it is 686 and was able to load my extended MBR image into memory)...

TLDR;

I have this MBR, which is working on QEMU and BOCHS, but real hardware throws me to the makeshift quick ErrorHandler() I made (doesn't do much but print a string and halt the hardware or throw it into an infinite loop).

I have a valid GPT following the MBR and a valid Protective MBR setup as well when I load the official working GPT into the MBR binary and check its integrity manually (hexedit).

Here is the code I am working with:

org 0x7c00
bits 16

_start:
    jmp 0:_glEntryHandle

FLT_ERR_MSG         db  "ERR: SYS HALT", 0

_glLegacyTeletypeOutput:
    pusha                               ; Push AX, CX, DX, BX, SP, BP, SI, DI onto the stack (in that order)
    mov ah, 0x0e                        ; AH ← 0Eh → BIOS teletype output (INT 10h)
    .l0:
        lodsb                           ; AL ← byte at DS:SI, SI++
        or al, al                       ; Check for null terminator (AL == 00h)
        jz .r0                          ; If null, jump to return
        int 0x10                        ; BIOS teletype output → display character in AL
        jmp .l0                         ; Loop to next character
    .r0:
        popa                            ; Restore all general-purpose registers
        ret                             ; Return to caller

_glErrorHandle:
    mov si, FLT_ERR_MSG                 ; SI ← pointer to error message string
                                        ; FLT_ERR_MSG must be null-terminated for teletype output
    call _glLegacyTeletypeOutput

    cli                                 ; Disable interrupts → prevent further execution or ISR interference
    hlt                                 ; Halt CPU → terminal failure state

    jmp $                               ; Infinite loop → ensures system remains halted
                                        ; Prevents accidental fall-through or reboot

_glServiceLocateBootablePartition:
    mov cx, 4                           ; Initialize loop counter for 4 MBR partition entries (each 16 bytes)

.l0:
    mov al, byte [si]                   ; Load boot flag byte from DS:SI into AL
    cmp al, 0x80                        ; Compare boot flag against 0x80 (active bootable marker)
    je .r0                              ; If match, jump to .r0 — bootable partition located

    add si, 16                          ; Advance SI to next partition entry (skip 16-byte structure)
    loop .l0                            ; Decrement CX and repeat if CX ≠ 0

    jmp _glErrorHandle                  ; No bootable partition found — invoke error handler

.r0:
    add si, 8                           ; SI ← skip 8 bytes (MBR partition LBA offset mbr.part[8])
    ret                                 ; Return — bootable partition located

_glServiceParseGPTPartitionEntries:
    enter 6, 0                          ; Create a stack frame, allocate 6 bytes of locals
    mov ecx, dword [ds:si+050h]         ; EBX ← total number of GPT partition entries
    cmp ecx, 128                        ; Validate entry size is exactly 128 bytes (GPT spec requirement)
    jne _glErrorHandle                  ; Bail out if entry size is unexpected → prevents misaligned parsing

    mov ecx, dword [si+054h]            ; EBX ← size of each GPT entry in bytes
    shr ecx, 2                          ; ECX ← ECX / 4 → number of sectors to load
    add si, 72                          ; SI ← SI + 72 → skip GPT header to reach first entry LBA

    .l0:
        mov bx, 0x1200                  ; BX ← buffer address for sector load
        call _glServiceDiskAddressPacketConstructor
        mov ah, 0x42                    ; AH ← 42h → INT 13h extended read (EDD)
        call _glExtensionEnhancedDiskDriveService

        push ecx                        ; Save remaining sector count
        add si, 8                       ; SI ← add 8 bytes (DAP[8] LBA pointer)
        push si                         ; Save current SI (entry pointer)
        mov si, bx                      ; SI ← buffer base (start of loaded sector BX = 1200h)
        mov cx, 4                       ; CX ← 4 entries per sector

    .l1:
        mov al, byte [si+0x30]          ; AL ← partition attribute flags (offset 48 in GPT entry)
        test al, 0x04                   ; Check bit 2 → indicates MBR bootable partition
        jnz .r0                         ; If bootable, jump to .r0 for handling

        add si, 128                     ; SI ← next GPT entry (128-byte stride)
        loop .l1                        ; Repeat for all entries in current sector
    
        pop si                          ; Restore SI (DAP[8] LBA pointer)
        push si                         ; Save again for post-processing
        mov di, si                      ; DI ← current SI pointer (DAP[8] LBA pointer)
        xor edx, edx                    ; Clear EDX for 64-bit LBA handling
        lodsd                           ; EAX ← lower 32 bits of LBA
        xchg edx, eax                   ; EDX ← lower 32 bits of LBA, EAX ← upper 32 bits of LBA (EDX was cleared (0))
        lodsd                           ; EAX ← upper 32 bits of LBA
        xchg edx, eax                   ; EDX ← upper 32 bits of LBA, EAX ← lower 32 bits of LBA
        clc                             ; Clear carry for addition (overflow)
        add eax, 1                      ; Increment lower 32 bits of LBA by 1 (lower 32 bits LBA + 1)
        adc edx, 0                      ; Propagate carry to upper 32 bits (EDX += CF, EAX ← 0)
        stosd                           ; Store lower 32 bits of adjusted LBA
        mov eax, edx                    ; EAX ← upper 32 bits
        stosd                           ; Store upper 32 bits of adjusted LBA
        pop si                          ; Restore SI (DAP[8] LBA pointer)

        pop ecx                         ; Restore remaining sector count
        loop .l0                        ; Continue parsing next sector

        jmp _glErrorHandle              ; No bootable partition found, jump to error handler

    .r0:
        leave                           ; Restore stack frame
        add si, 0x20                    ; SI ← skip 32 bytes (MBR bootable GPT partition LBA field offset)
        ret                             ; Return to caller

_lcServiceCyclicRedundancyCheck:
    mov ecx, dword [si+0x04]            ; ECX ← GPT header size (offset +04h)
    mov eax, dword [si+0x08]            ; EAX ← stored CRC32 (offset +08h)
    push eax                            ; Save original CRC32 for later comparison
    mov dword [si+0x08], 0              ; Zero CRC field before computing new CRC32
    mov ebx, 0xffffffff                 ; EBX ← initial CRC seed = FFFFFFFFh (IEEE 802.3)
    sub si, 8                           ; SI ← rewind to start of header (before signature)

    .l0:
        lodsb                           ; AL ← [SI], SI++ → load next byte
        xor bl, al                      ; BL ← BL ⊕ AL → feed byte into CRC accumulator
        push ecx                        ; Save outer loop counter
        mov ecx, 8                      ; ECX ← 08h → process 8 bits of current byte
    .l1:
        mov al, bl                      ; AL ← BL → isolate current CRC byte
        shr ebx, 1                      ; EBX ← EBX >> 1 → shift accumulator
        test al, 1                      ; Check LSB of AL (BL pre-shift reality check; avoids extra jumps; space-saving hack)
        jz .i0                          ; If zero, skip polynomial XOR
        xor ebx, 0xedb88320             ; EBX ← EBX ⊕ polynomial = EDB88320h (IEEE 802.3)
    .i0:
        loop .l1                        ; Repeat for 8 bits
    .i1:
        pop ecx                         ; Restore outer loop counter
        loop .l0                        ; Repeat for all header bytes
        
        pop eax                         ; Restore original CRC
        not ebx                         ; EBX ← ~EBX → finalize computed CRC32
        
        or ebx, eax                     ; Compare computed CRC with stored CRC
        jz _glErrorHandle               ; If mismatch, jump to error handler
    .r0:
        pop si                          ; Restore SI ← GPT header pointer
        mov dword [si+0x10], ebx        ; Store validated CRC32 at offset +10h
        ret                             ; Return to caller

_glServiceDiskAddressPacketConstructor:
    mov di, 0x7510                      ; DI ← base address for Disk Address Packet (DAP)
    push di                             ; preserve DI for later restoration
    ; - INT 13h AH=42h DAP structure setup (16 bytes total) -
    mov byte [di+0x00], 16              ; DAP[0] ← size of DAP structure (0x10 bytes)
    mov byte [di+0x01], 0               ; DAP[1] ← reserved (must be 0 per spec)
    mov word [di+0x02], 1               ; DAP[2-3] ← number of sectors to read (1 sector)
    mov word [di+0x04], bx              ; DAP[4-5] ← offset in memory to load sector
    mov word [di+0x06], 0               ; DAP[5-7] ← segment (0x0000:BX target address)
    add di, 8                           ; DI → DAP[8], start of 64-bit LBA field
    ; - Determine LBA width based on AH - 
    or ah, ah                           ; check if AH ≠ 0 → signals 64-bit LBA mode
    jnz .i0                             ; if AH ≠ 0, jump to 64-bit LBA setup (read lower 32-bit LBA, set upper 32-bit LBA = 0)
    ; - 32-bit LBA path -
    lodsd                               ; EAX ← [DS:SI], load 32-bit LBA
    stosd                               ; store EAX into DAP[8-11] (lower 32 bits)
    xor eax, eax                        ; clear EAX for upper 32 bits
    stosd                               ; store 0 into DAP[12-15] (upper 32 bits)
    jmp .r0                             ; skip 64-bit path
    ; - 64-bit LBA path -
    .i0:
        mov cx, 2                       ; CX ← 2 (two DWORDs to copy)
    .l0:
        lodsd                           ; EAX ← [DS:SI], load next DWORD of LBA
        stosd                           ; store EAX into DAP[DI], advance DI+=4
        loop .l0                        ; repeat for both DWORDs (64-bit LBA)
    .r0:
        pop si                          ; restore SI (caller's pointer to DAP source)
        ret                             ; return to caller

_glExtensionEnhancedDiskDriveService:
    pusha                               ; Push AX, CX, DX, BX, SP, BP, SI, DI onto the stack (in that order)

    cmp ah, 0x41                        ; Validate lower bound of EDD function range (AH must be ≥ 41h)
    jb _glErrorHandle                   ; AH < 41h → not an EDD function → jump to error handler

    cmp ah, 0x49                        ; Validate upper bound of EDD function range (AH must be ≤ 49h)
    ja _glErrorHandle                   ; AH > 49h → outside EDD range → jump to error handler

    mov dl, byte [_glProtectiveMasterBootRecordHeader.BootDriveID]

    mov bx, 0x55aa                      ; BX ← EDD signature required for AH=41h install check
    clc                                 ; Clear carry flag before INT 13h call (required for accurate error reporting)
    int 0x13                            ; Invoke BIOS disk service with EDD function specified in AH
    jc _glErrorHandle                   ; If carry flag is set → BIOS reported error → jump to error handler

    cmp ah, 0x41                        ; Check if this was an install check (AH = 41h)
    jne .r0                             ; If not AH=41h → skip signature validation and return

    cmp bx, 0xaa55                      ; BIOS must return 0xAA55 in BX to confirm EDD support
    jne _glErrorHandle                  ; Signature mismatch → EDD not supported → jump to error handler

.r0:
    popa                                ; Restore all general-purpose registers: AX, CX, DX, BX, SP, BP, SI, DI
    ret                                 ; Return to caller — EDD service succeeded or install check confirmed



_glEntryHandle:
    cli                                 ; Disable interrupts to ensure atomic setup of segment and stack registers
    mov ax, cs                          ; AX ← current code segment
                                        ; Assumes CS was flushed via far jump (jmp 0:7C00h) earlier in boot
    mov ds, ax                          ; DS ← CS → align data segment with code segment
    mov ax, 0x0650                      ; AX ← stack segment base (manually chosen, ensure no overlap)
    mov ss, ax                          ; SS ← AX → set up stack segment
    mov sp, 1000h                       ; SP ← top of stack (4 KiB stack space)
    mov bp, sp                          ; BP ← SP → establish base pointer for stack frame tracking
    sti                                 ; Re-enable interrupts after safe stack and segment setup

    cld                                 ; Clear direction flag → string ops will auto-increment (forward scanning)

    mov byte [_glProtectiveMasterBootRecordHeader.BootDriveID], dl
    mov ah, 0x41                        ; AH ← 41h → INT 13h function: "Check (EDD) Extensions Present"
    call _glExtensionEnhancedDiskDriveService

    mov si, _glMasterBootRecordPartitionTable
    call _glServiceLocateBootablePartition
                    
    xor ax, ax                          ; clear AX (used by DAP constructor for 32-bit or 64-bit LBA path)
    mov bx, 0x1000                      ; BX ← buffer address for sector read (DAP constructor always sets segment ← 0)
    call _glServiceDiskAddressPacketConstructor
    mov ah, 0x42                        ; AH ← 42h → INT 13h extended read (EDD)
    call _glExtensionEnhancedDiskDriveService

    mov si, 0x1000                      ; SI ← buffer address (1000h assumed to contain GPT header)
    call _glServiceParseGPTPartitionEntries

    mov ah, 0x0e
    mov bx, 0x7e00                      ; BX ← buffer address for sector read
    call _glServiceDiskAddressPacketConstructor
    mov ah, 0x42                        ; AH ← 42h → INT 13h extended read (EDD)
    call _glExtensionEnhancedDiskDriveService

    mov byte [_glProtectiveMasterBootRecordHeader.BootDriveID], 0

    jmp 0:0x7e00



times (440-($-$$))              db  0

_glProtectiveMasterBootRecordHeader:
    .UniqueDiskSignature:       dd  0
    .BootDriveID:               db  0
    .Revision:                  db  4

times (446-($-$$))              db  0

_glMasterBootRecordPartitionTable:
    .BootIndicator:             db  0
    .OSType:                    db  0
    .EndingCHS:     times   3   db  0
    .StartingLBA:               dd  0
    .SizeInLBA:                 dd  0

times (510-($-$$))              db  0
                                dw  0xaa55
like image 899
EchoXTeknology Avatar asked Oct 30 '25 05:10

EchoXTeknology


1 Answers

  1. You need to start your code with xor ax, ax. This is the recognizer used by modern BIOSes.

  2. You need to check if DL is trashed on entry. Modern BIOSes are pretty buggy.

We can fix the preamble as follows:

_start:
     xor  ax, ax
     test dl, 80h
     jnz  short .gdl
.bdl mov  dl, 80h
.gdl cmp  dl, 8Fh
     ja   short .bdl
     jmp  0:_glEntryHandle
  1. ES register is not initialized.

  2. You need to fix your error handler to report what failed.

Code maintenance note: it will be a lot easier to maintain (and a few bytes shorter) if you have SS=DS=ES throughout.

You will quickly find you don't actually have enough space for this. The first thing that can go is the CRC check. Then move your BSS data outside the MBR proper. Most of it doesn't need to be zero-initialized.

To get your CRC check back you either need to load the rest of your code from elsewhere (either a legacy partition slot or a hardcoded offset), or put this code on a serious diet.

like image 144
Joshua Avatar answered Nov 01 '25 13:11

Joshua