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
You need to start your code with xor ax, ax. This is the recognizer used by modern BIOSes.
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
ES register is not initialized.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With