; Copyright 2009 David Elliott. All rights reserved. ; ; License to be determined. ; ; Boot Loader: gpt0 ; ; A small boot sector intended to reside in the MBR which locates a specified ; GPT partition entry, loads the partition bootsector to memory, and jumps to it. ; ; So that partition bootsectors can remain oblivious to GPT the GPT entry is ; converted in-memory to a traditional partition entry and DS:SI will point ; to this entry when the partition bootsector is invoked. ; ; Space is extremely cramped. A very traditional MBR can use up to 446 bytes but ; anything intending to play nice with Windows can only use 440 bytes. Of this ; 440 (0x1b8) bytes we allocate 22 more for "constant" (at runtime) data. ; ; Thus the layout winds up looking like this: ; ; 0x0000 ; Boot code ; 0x01a2 ; Classic partition type (e.g. what to write into the fake partition entry) ; 0x01a3 ; Flags (low bit: clear = search type GUID, set = search entry GUID) ; 0x01a4 ; Starting partition number (where to start reading the GPT) ; 0x01a8 ; GUID to search for (either type or entry depending on the flag) ; 0x01b8 ; Microsoft 4-byte disk signature ; 0x01bc ; Microsoft 2-byte padding ; 0x01be ; Traditional four MBR partition entries ; 0x1fe ; Signature 0x55 0xaa ; ; The fake partition entry is generated from the GPT information with one small wrinkle. ; GPT supports 64-bit LBA. Traditional MBR supports 32-bit LBA. There is an unofficial ; specification for 48-bit LBA MBR involving reusing the starting sect/cyl to store the ; high word of the starting LBA and the ending sect/cyl to store the high word of the ; sector count. When this format is used the bootflag byte will be 0x81 instead of 0x80. ; Partition boot sectors wishing to support 48-bit LBA should check the low bit. Note that ; this format can be written to a traditional MBR and is (supposedly) in use by some people. ; ; The LBA48 format is only used when the LBA starting sector exceeds 32 bits. Should the ; LBA size exceed 32-bits when the LBA starting sector does not, a size of 0xffffffff will ; be used to indicate that the size overflowed. The vast majority of bootsectors don't ; use the size and it is assumed that an actual boot program will be large enough to handle ; GPT on its own thus ignoring the fake partition entry entirely. ; ; To illustrate this more clearly here are two examples: ; | flag | head | sect | cyl | type | endh | ends | endc | LBA start | Sector Count | ; | 0x80 | 0xff | 0xff | 0xff | 0xef | 0xff | 0xff | 0xff | 0x76543210 | 0x76543210 | ; | flag | ---- | LBA start H | type | ---- | Sec Count H | LBA start L | Sector Count L | ; | 0x81 | 0xff | 0xBA98 | 0xef | 0xff | 0xBA98 | 0x76543210 | 0x76543210 | ; ; Quite unfortunately, all of this code just _barely_ squeezes into the 418 available bytes. ; In lieu of error messages there are error characters. ; ; M - MBR doesn't contain protective 0xEE partition entry ; ! - Failed to read sector(s) ; G - GPT signature check failed ; . - Failed to find desired entry ;-------------------------------------------------------------------------- ; ; Various constants. ; kBoot0Segment EQU 0x0000 kBoot0Stack EQU 0xFFF0 ; boot0 stack pointer kBoot0LoadAddr EQU 0x7C00 ; boot0 load address kBoot0RelocAddr EQU 0xE000 ; boot0 relocated address kSectorBytes EQU 512 ; sector size in bytes kBootSignature EQU 0xAA55 ; boot sector signature kPartTypeEfi EQU 0xEE ; EFI protective partition kGptHeaderBuffer EQU 0x1000 ; GPT header is loaded here kGptTableBuffer EQU 0x1200 ; GPT sectors are loaded here kGptSignatureDWL EQU ('E' << 0) + ('F' << 8) + ('I' << 16) + (' ' << 24) kGptSignatureDWH EQU ('P' << 0) + ('A' << 8) + ('R' << 16) + ('T' << 24) kGptRevision EQU 0x10000 ;-------------------------------------------------------------------------- ; ; Format of fdisk partition entry. ; ; The symbol 'part_size' is automatically defined as an `EQU' ; giving the size of the structure. ; struc part .bootid: resb 1 ; bootable or not .head: resb 1 ; starting head, sector, cylinder .sect: resb 1 ; .cyl: resb 1 ; .type: resb 1 ; partition type .endhead resb 1 ; ending head, sector, cylinder .endsect: resb 1 ; .endcyl: resb 1 ; .lba: resd 1 ; starting lba .sectors: resd 1 ; size in sectors endstruc ;-------------------------------------------------------------------------- ; ; Format of unofficial LBA48 fdisk partition entry ; ; The symbol 'part_size' is automatically defined as an `EQU' ; giving the size of the structure. ; struc part48 .bootid: resb 1 ; bootable or not (0x81, 0x01) .resv1: resb 1 ; .lbaH resw 1 ; starting lba high word -- bits (47,32] .type: resb 1 ; partition type .resv2: resb 1 ; .sectorsH resw 1 ; size in sectors high word -- bits (47,32] .lba: resd 1 ; starting lba -- bits (31,0] .sectors resd 1 ; size in sectors -- bits (31,0] endstruc ;-------------------------------------------------------------------------- ; ; Format of the GPT header ; struc gpt_header .hdr_sig: resb 8 .hdr_revision: resd 1 .hdr_size: resd 1 .hdr_crc_self: resd 1 .hdr__reserved: resd 1 .lba_self: resq 1 ; LBA of Header .lba_alt: resq 1 ; LBA of alternate GPT Header .lba_start: resq 1 ; First usable LBA (for data) .lba_end: resq 1 ; Last usable LBA (for data) .disk_guid: resb 16 ; Disk GUID .lba_table: resq 1 ; LBA of table start .num_entries: resd 1 ; Number of entries .entsz: resd 1 ; Size of an entry .crc_table: resd 1 ; CRC of the partition entry array (does this include pad if any is used?) .padding: resd 1 ; Pad to QWORD alignment endstruc ;-------------------------------------------------------------------------- ; ; Format of an entry in the GPT ; Note that gpt_ent_size shouldn't be used. Use the entsz from the header ; struc gpt_ent .ent_type resb 16 ; Partition type GUID (e.g. FAT32/HFS+/etc.) EFI sys part has a special type .ent_uuid resb 16 ; Partition GUID. Unique identifier for this partition. .ent_lba_start resq 1 ; Starting LBA of partition's data .ent_lba_end resq 1 ; Ending LBA of partition's data .ent_attr resq 1 ; Attributes .ent_name resw 36 ; User-presentable name in UTF-16 LE endstruc ;-------------------------------------------------------------------------- ; Define a BSS that overlaps the first bytes of the booter which is the ; one-off relocation code that can't be used again anyway struc bss endstruc ;-------------------------------------------------------------------------- ; Start of text segment. SEGMENT .text ORG 0xE000 ; must match kBoot0RelocAddr ; CAUTION: Must be aligned in memory on a 256-byte boundary. ;-------------------------------------------------------------------------- ; Boot code is loaded at 0:7C00h. ; start ; ; Set up the stack to grow down from kBoot0Segment:kBoot0Stack. ; Interrupts should be off while the stack is being manipulated. ; cli ; interrupts off xor eax, eax ; zero eax mov ss, ax ; ss <- 0 mov sp, kBoot0Stack ; sp <- top of stack sti ; reenable interrupts mov es, ax ; es <- 0 mov ds, ax ; ds <- 0 ; ; Relocate boot0 code. ; mov si, kBoot0LoadAddr ; si <- source mov di, kBoot0RelocAddr ; di <- destination ; cld ; auto-increment SI and/or DI registers mov cx, kSectorBytes/2 ; copy 256 words rep movsw ; repeat string move (word) operation ; Code relocated, jump to start_reloc in relocated location. ; jmp 0:start_reloc ;-------------------------------------------------------------------------- ; Start execution from the relocated location. ; start_reloc: ;-------------------------------------------------------------------------- ; There must be at least one 0xEE entry. We don't use it, just check for it. mov bx, ptable + part.type next_pentry cmp byte [bx], kPartTypeEfi je got_protective_entry ; Space-saving hack! Saves 5 bytes over what was here before. ; Credit: Microsoft Windows XP FAT12/16 bootsector. ; ; Instead of incrementing BX, increment BL. We can get away with this because ; although the table itself starts at 0x1be the field we are interested in ; is 4 bytes forward from that putting it at 0x1c2. Adding 0x10 to it four ; times results in 0x202. If you do it using byte arithmetic on BL instead ; of word arithmetic on BX the end result is wrong (BH doesn't get incremented) ; but the carry flag is conveniently set giving us something to test on. ; CAUTION: At runtime this depends on code aligned on a 256-byte boundary. add bl, part_size ; Move to the next partition jnc next_pentry ; Or fall through to the error case. mov al, byte 'M' jmp error ;-------------------------------------------------------------------------- ; Read in the GPT Header got_protective_entry: mov bh, dl ; BH = Drive to read from mov bl, 1 ; BL = 1 sector ;xor eax, eax ; Already done above xor edx, edx mov al, 1 ; EDX:EAX = Starting LBA (1) mov di, kGptHeaderBuffer ; Read to the GPT header buffer ; ES:DI -> kGptHeaderBuffer call read_lba48 jnc .got_gpt_header ;mov al, byte '!' jmp error_cant_read .got_gpt_header: ;push di mov si, gpt_sig ; SI -> gpt_sig in our constant data area mov cx, 6 ; CX = 6 words (actually 3 DWORD) repe cmpsw ; Compare the words ;pop di je .good_gpt_header .error_bad_gpt mov al, byte 'G' jmp error ;-------------------------------------------------------------------------- ; Read in the GPT entries until we exhaust them .good_gpt_header: ; BH still equal drive mov bl, 2 ; BL = 2 sectors ; LBA = gpt_header.lba_table + ((i * gpt_header.entsz) / 512) ; off = (i * gpt_header.entsz) % 512 ; Outer loop prologue mov eax, dword [start_entry_i] ; EAX (i) == start_entry_i ; Make sure the starting index is sane. If it's >= the number of entries ; then we have to set it to 0 because we need it as a sentinel later. call wrap_gpt_entry_index mov dword [start_entry_i], eax ; In case EAX was zeroed, set the start entry index to it. ; outer loop (on sectors) Ldo_read_table_sectors ; EAX is the partition entry we are currently working with (e.g. i) push dword eax ; Stuff EAX (i) onto the stack while we figure out the LBA sector to read. mul dword [kGptHeaderBuffer + gpt_header.entsz] ; EDX:EAX = EAX * entry size mov ecx, eax ; ECX = EAX = (i * entsz) shr dword eax, 9 ; EAX = EAX / 512, assuming EDX == 0 add dword eax, [kGptHeaderBuffer + gpt_header.lba_table] adc dword edx, [kGptHeaderBuffer + gpt_header.lba_table + 4] ; EDX:EAX = ((i * gpt_header.entsz) / 512) + GPT Table LBA and dword ecx, (512-1) ; ECX = ECX % 512 (ECX was i * entsz from above) mov di, kGptTableBuffer ; ES:DI = kGptTableBuffer call read_lba48 pop dword eax ; Restore EAX (i) from the stack now that we've finished the read call. jnc .read_gpt_entries ;mov al, byte '!' jmp error_cant_read .read_gpt_entries: add di, cx ; DI += CX (CX == (i * gpt_header.entsz) % 512) ; DI -> entry we're interested in ;-------------------------------------------------------------------------- ; Inner loop (over entries within the sector) ; See if this entry matches the one we're looking for Ltest_gpt_entry: push di ; Save DI -> GPT entry mov si, desired_guid ; DS:SI -> dword we want to test against ; Check the entry and bail if match mov dl, [search_flags] ; Default (bit 0 = 0) is to test type GUID, bit 0 = 1 means to test entry GUID test dl, 1 jz .test_ent_type ;.test_ent_uuid add di, gpt_ent.ent_uuid ; DI -> entry GUID for entry we're testing .test_ent_type ; Do not have to add anything to test entry type ;add di, gpt_ent.ent_type ; DI -> type GUID for entry we're testing mov cx, 8 ; CX = 8 (number of WORD to be compared) repe cmpsw ; Compare the GUID pop di ; Restore DI -> GPT Entry je Lgotmatch ; We matched, so we're done .nomatch inc eax ; Move on to the next entry call wrap_gpt_entry_index ; EAX is now the next entry to be processed. ; Either EAX will be the entry we just processed + 1 or it will be 0 if we wraped. ; Compare this to the first entry we processed. If this is it, then we've ; already processed the entry so we need to bail. cmp dword eax, [start_entry_i] je .ran_out_of_entries add di, [kGptHeaderBuffer + gpt_header.entsz] ; DI -> next entry to be tested ; What we really want to know: is DI + entsz > buf + 1024 (because we read two sectors) ; What we do instead: is DI > buf + 512. Assume entsz is < 512 so we're safe. cmp di, kGptTableBuffer + 512 jb Ltest_gpt_entry ; Test the next entry so long as we know it's already in the buffer ; Entry starts in the next sector so loop back up and read it in. jmp short Ldo_read_table_sectors .ran_out_of_entries: mov al, byte '.' jmp error ;-------------------------------------------------------------------------- ; wrap_gpt_entry_index - Wraps EAX to 0 if it is >= the number of entries ; Inputs: ; EAX = Entry index ; The num_entries field in the GPT header buffer. ; Outputs: ; EAX = Entry index, or 0 if it exceeded the bounds ; Clobbers: ; flags wrap_gpt_entry_index: ; If EAX < the number of entries then EAX is the index of the next entry to process cmp dword eax, [kGptHeaderBuffer + gpt_header.num_entries] jb .nowrap ; Otherwise, wrap it to 0 xor dword eax, eax .nowrap ret ;-------------------------------------------------------------------------- ; Found the entry we want to boot.. so setup a fake partition table and boot it. Lgotmatch: ; DI -> Selected GPT entry push ebx ; Save EBX primarily to keep BH and also to keep high bits as booted mov eax, [di + gpt_ent.ent_lba_start] mov edx, [di + gpt_ent.ent_lba_start + 4] ; EDX:EAX = lba64 start from GPT mov ebx, [di + gpt_ent.ent_lba_end] mov ecx, [di + gpt_ent.ent_lba_end + 4] ; ECX:EBX = lba64 end from GPT sub ebx, eax sbb cx, dx ; CX:EBX -= DX:EAX add ebx, 1 ; NOTE: cannot use inc as we need carry. adc cx, 0 ; CX:EBX ++ ; STATE: ; DX:EAX = 48-bit start ; CX:EBX = 48-bit size ; Assume upper 16 bits of EDX and ECX are clear as it saves ; a lot of bytes by not having to do 32-bit arithmetic on them. ; Fill in some of the partition table fields mov si, ptable ; If DX == 0 then we want a normal LBA32, even if size might overflow test dx, dx jz .is_lba_32 ; LBA 48 mov byte [si + part.bootid], 0x81 ; Set the active flag to 0x81 which is the unofficial LBA48 bootable flag mov word [si + part48.lbaH], dx ; Put the high word of DX:EAX into the lbaH field jmp short .do_fill_ptable ; Jump to common fill of the remainder of the table .is_lba_32 ; LBA 32 mov byte [si + part.bootid], 0x80 ; Set the active flag, this is LBA32 mov word [si + part.sect], 0xffff ; Set part.sect = 0xff and part.cyl = 0xff test cx,cx ; Is CX (high WORD of size) == 0 mov cx, word 0xffff ; CX will become ending set/cyl which will always be 0xff 0xff for LBA32 jz .do_fill_ptable xor ebx, ebx dec ebx ; EBX = 0xffffffff indicating that the LBA32 size overflowed .do_fill_ptable: ; DS:SI -> ptable ; DX:EAX = start LBA48 if lba48, otherwise DX = start cyl/sec and EAX = start LBA32 ; CX:EBX = LBA48 size if lba48, otherwise CX = end cyl/sec and EBX = LBA32 size ; EAX was already moved to part.lba above since that doesn't depend on LBA32/48 mode ; DX was already moved to part.lbaH if LBA48 or the cyl/head were set to 0xff if not. ; Unfortunately we need to have EDX = 0 for LBA32 so we can't clobber DX with 0xffff mov word [si + part48.sectorsH], cx ; Fill in either sectorsH or endcyl/endhead (we clobbered CX with 0xffff for LBA32) mov dword [si + part.sectors], ebx ; Fill in sector count with EBX mov bl, byte [traditional_type] mov byte [si + part.type], bl ; set the type to the desired type mov byte [si + part.head], 0xff mov byte [si + part.endhead], 0xff ; head and endhead will be 0xff regardless of traditional or LBA48 boot (unused for LBA48) mov dword [si + part.lba], eax ; Fill in the low DWORD of the LBA start since it's the same for LBA48 and LBA32 ;-------------------------------------------------------------------------- ; Load the first sector of the partition ; EDX:EAX is still the 64-bit LBA we loaded from GPT entry pop ebx ; EBX restored for BH (drive) and high bits as booted mov bl, byte 1 ; BL = 1 sector mov di, kBoot0LoadAddr ; DS:DI -> kBoot0LoadAddr (0x7C00) call read_lba48 jc error_cant_read cmp word [0x7C00 + 0x200 - 2], kBootSignature je .start_boot1 mov al, 'X' jmp short error ;-------------------------------------------------------------------------- ; Boot! .start_boot1 mov dl, bh ; Put BH back into DL for bootsector jmp 0:0x7c00 ; Failed reads jump here. This saves 2 bytes for every n-1 times we ; jump here instead of doing mov al, byte '!' and jumping to error. error_cant_read: mov al, byte '!' ; Other errors set al then jump here error: ; Put error character call putchar ; Wait for the user to press a key mov ah, 0 int 0x16 ; Invoke INT 18 to try the next boot device int 0x18 ;-------------------------------------------------------------------------- ; putchar - Simple BIOS display one character. putchar: pusha mov bx, 1 ; BH=0, BL=1 (blue) mov ah, 0x0e ; bios INT 10, Function 0xE int 0x10 ; display byte in tty mode popa ret ;-------------------------------------------------------------------------- ; read_lba48 - Read sectors from a partition using LBA addressing. ; ; Arguments: ; EDX:EAX = Starting LBA (64-bit) ; BH = drive number (0x80 + unit number) ; BL = number of 512-byte sectors to read (valid from 1-127). ; ES:DI = pointer to where the sectors should be stored. ; ; Returns: ; CF = 0 success ; 1 error ; read_lba48: pushad mov bp, sp ; Save the stack pointer push dword edx ; [12] High DWORD of LBA QWORD push dword eax ; [ 8] Low DWORD of LBA QWORD push word es ; destination buffer seg/off push word di ; [ 4] mov dl, bh ; Put BH into DL for INT13 xor bh, bh push word bx ; [ 2] Push BL (BH has been zeroed) onto stack as a WORD push word 16 ; [ 0] Size of disk address packet mov si, sp ; DS:SI = SS:SP -> disk address packet mov ah, 0x42 int 0x13 ; INT 13/AH=42h mov sp, bp ; Restore the stack pointer popad ret ;-------------------------------------------------------------------------- ; This is the "EFI PART" sig followed by the 0x00010000 revision number to look for ; This one isn't intended to be changeable by tool, only by rebuilding. align 2 gpt_sig: db 'EFI PART' dd 0x00010000 ;-------------------------------------------------------------------------- ; Pad the rest of the 512 byte sized booter with zeroes. The last ; two bytes is the mandatory boot sector signature. ; ; If the booter code becomes too large, then nasm will complain ; that the 'times' argument is negative. pad_boot: ;-------------------------------------------------------------------------- ; Changeable constants (set with a tool) times 0x1a2-($-$$) db 0 ; 1 byte for the type to fill into the faked partition table entry traditional_type: %if 1 ; Default: EFI system partition type. db 0xef %else ; Type of HFS+ partition db 0xaf %endif times 0x1a3-($-$$) db 0 ; 1 byte for flags: ; bit 0: Clear = search type GUID, Set = search entry GUID search_flags: ; Default: Search type GUID db 0x00 times 0x1a4-($-$$) db 0 ; Entry to start with start_entry_i: ; Default: first entry (0) dd 0x00 times 0x1a8-($-$$) db 0 ; GUID to search for (either the ent_type or ent_uuid, depending on flags) desired_guid: %if 1 ; Default: Type GUID of EFI System Partition dd 0xC12A7328 dw 0xF81F dw 0x11d2 db 0xBA, 0x4B db 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B %else ; Type GUID of HFS+ Partition. dd 0x48465300 dw 0x0000 dw 0x11AA db 0xAA, 0x11 db 0x00, 0x30, 0x65, 0x43, 0xEC, 0xAC %endif ;-------------------------------------------------------------------------- ; Traditional MBR stuff like the disk signature word, the padding, ; the part table, and the boot signature times 0x1b8-($-$$) db 0 ; Microsoft NT disk signature DWORD and pad WORD disk_sig: dd 0 ms_unknown_resw: ; What is this? No one seems to know. It looks like MS just uses it ; for zero padding so the disk_sig dword is on a dword boundary. dw 0 times 446-($-$$) db 0 ; Partition table (16 * 4) ptable times 510-($-$$) db 0 ; Traditional 0x55 0xAA signature boot_sig dw kBootSignature ABSOLUTE 0xE400