; ; Copyright (c) 2014-2015 Zenith432 All rights reserved. ; ; Partition Boot Loader: boot1x ; This version of boot1x tries to find a stage2 boot file in the root folder. ; ; Credits: ; Portions based on boot1f32. ; Thanks to Robert Shullich for ; "Reverse Engineering the Microsoft exFAT File System" dated Dec 1, 2009. ; T13 Commitee document EDD-4 for information about BIOS int 0x13. ; ; This program is designed to reside in blocks 0 - 1 of an exFAT partition. ; It expects that the MBR has left the drive number in DL. ; ; This version requires a BIOS with EBIOS (LBA) support. ; ; This code is written for the NASM assembler. ; nasm -f bin -o boot1x boot1x.s ; ; Written by zenith432 during November 2014. ; Modified by zenith432 January 2015 ; Added Feature: Support digit keypress with two second delay (-DSELECTION_FEATURE) ; bits 16 %define SELECTION_FEATURE 1 %define VERBOSE 1 %define USESIDL 1 %define USEBP 1 kMaxBlockCount equ 127 ; Max block count supported by Int 0x13, function 0x42, old school kBootBlockBytes equ 512 ; Bytes in a exFAT Boot block kBootSignature equ 0xaa55 ; Boot block signature kBoot1StackAddress equ 0xfff0 ; Address of top-of-stack 0:0xfff0 kBoot1LoadAddr equ 0x7c00 ; Address of loaded boot block 0:0x7c00 kBoot2Segment equ 0x2000 ; Address for boot2 0x2000:0x200 kBoot2Address equ 512 kFATBuf equ 0x6c00 ; Address for FAT block buffer 0:0x6c00 (4K space) kRootDirBuf equ 0x5c00 ; Address for Root Directory block buffer 0:0x5c00 (4K space) kMaxCluster equ 0xfffffff7 ; exFAT max cluster value + 1 (for FAT32 it's 0x0ffffff8) kMaxContigClusters equ 1024 ; Max contiguous clusters returned by getRange kBootNameHash equ 0xdc36 ; exFAT name hash for 'BOOT' (in UTF16LE) kBoot2MaxBytes equ (512 * 1024 - 512) ; must fit between 0x20200 and 0xa0000 struc PartitionEntry ; MBR partition entry (truncated) times 8 resb 1 .lba: resd 1 ; starting lba endstruc struc BootParams ; BOOT file parameters .cluster: resd 1 ; 1st cluster of BOOT .size: resd 1 ; size of BOOT in bytes resw 1 .flag: resb 1 endstruc struc DirIterator ; exFAT Directory Iterator .entries_end: resb 1 ; beyond last 32-byte entry (possible values 16, 32, 64, 128) .cluster: resd 1 ; current cluster .lba_high: resd 1 ; upper 32 bits of lba .lba_end: resd 1 ; beyond last block (lower 32-bits) .lba: resd 1 ; current block .entry: resb 1 ; current 32-byte entry endstruc struc FATCache ; Manages cache state for FAT blocks .shift: resb 1 ; right shift for converting cluster # to FAT block address .mask: resw 1 ; bit mask for finding cluster # in FAT block .lba: resd 1 ; lba # cached in FAT block buffer (note that FAT block address is limited to 32 bits) endstruc %ifdef USEBP %define BPR bp - gPartitionOffset + %else %define BPR %endif section .text org kBoot1LoadAddr jmp start times (3 - $ + $$) nop gOEMName: times 8 db 0 ; 'EXFAT ' ; ; Scratch Area ; Used for data structures ; times (64 - BootParams_size - DirIterator_size - FATCache_size - $ + $$) db 0 gsParams: times BootParams_size db 0 gsIterator: times DirIterator_size db 0 gsFATCache: times FATCache_size db 0 ; ; exFAT BPB ; gPartitionOffset: dd 0, 0 gVolumeLength: dd 0, 0 gFATOffset: dd 0 gFATLength: dd 0 gClusterHeapOffset: dd 0 gClusterCount: dd 0 gRootDirectory1stCluster: dd 0 gVolumeSerialNubmer: dd 0 gFileSystemRevision: dw 0 ; 0x100 gVolumeFlags: dw 0 gBytesPerBlock: db 0 ; range 9 - 12 (power of 2) gBlocksPerCluster: db 0 ; gBytesPerBlock + gBlocksPerCluster <= 25 (power of 2) gNumberOfFATs: db 0 ; should be 1 gDriveSelect: db 0 ; probably 0x80 gPercentInUse: db 0 times 7 db 0 start: cli xor eax, eax mov ss, ax mov sp, kBoot1StackAddress sti mov ds, ax mov es, ax ; ; Initializing global variables. ; %ifdef USEBP mov bp, gPartitionOffset %endif %ifdef USESIDL ; ; Shouldn't be necessary to use DS:SI because ; 1) Existing gPartitionOffset must be correct in ; order for filesystem to work well when mounted. ; 2) LBA may be 64 bits if booted from GPT. ; 3) Not all MBR boot records pass DS:SI ; pointing to MBR partition entry. ; %if 0 mov ecx, [si + PartitionEntry.lba] mov [BPR gPartitionOffset + 4], eax mov [BPR gPartitionOffset], ecx %endif ; ; However, by convention BIOS passes boot ; drive number in dl, so use that instead ; of existing gDriveSelect ; mov [BPR gDriveSelect], dl %endif ; ; Initialize FAT Cache ; dec eax mov dword [BPR gsFATCache + FATCache.lba], eax ; alternatively store gFATLength here mov cl, [BPR gBytesPerBlock] sub cl, 2 ; range 7 - 10 mov [BPR gsFATCache + FATCache.shift], cl neg ax shl ax, cl dec ax mov [BPR gsFATCache + FATCache.mask], ax ; ; Initialize Iterator ; mov al, 1 sub cl, 3 ; range 4 - 7 shl al, cl mov [BPR gsIterator + DirIterator.entries_end], al mov [BPR gsIterator + DirIterator.entry], al xor eax, eax mov ecx, [BPR gRootDirectory1stCluster] mov [BPR gsIterator + DirIterator.lba_end], eax mov [BPR gsIterator + DirIterator.lba], eax mov [BPR gsIterator + DirIterator.cluster], ecx %ifdef VERBOSE mov di, init_str call log_string %endif %ifdef SELECTION_FEATURE call setBootFile %endif ; ; Search root directory for BOOT ; .loop: call nextDirEntry jc error cld lodsb .revert: test al, al ; end of root directory? jz error cmp al, 0x85 ; file/subdir entry? jnz .loop lodsb cmp al, 2 ; 2ndary count should be 2 jb .loop add si, 2 ; skip checksum lodsb test al, 0x10 ; file attributes - check not a directory jnz .loop call nextDirEntry jc error cld lodsb cmp al, 0xc0 ; stream extension entry? jnz .revert lodsb mov dl, al ; General 2ndary flag inc si lodsb .name_length_point: cmp al, 4 ; name length jnz .loop lodsw ; name hash .name_hash_point: cmp ax, kBootNameHash jnz .loop add si, 2 mov eax, [si + 4] ; high 32 bits of valid data length test eax, eax jz .more and dl, 0xfe ; if size too big, mark as no allocation .more: lodsd ; valid data length mov [BPR gsParams + BootParams.size], eax add si, 8 lodsd ; first cluster mov [BPR gsParams + BootParams.cluster], eax mov [BPR gsParams + BootParams.flag], dl call nextDirEntry jc error cld lodsb cmp al, 0xc1 jnz .revert inc si ; skip flags lodsd ; unicode chars 1 - 2 or eax, 0x200020 ; tolower cmp eax, 0x6f0062 ; 'bo' in UTF16LE jnz .loop lodsd ; unicode chars 3 - 4 or eax, 0x200020 ; tolower cmp eax, 0x74006f ; 'ot' in UTF16LE jnz .loop ; ; done - found boot file! ; mov dl, [BPR gsParams + BootParams.flag] test dl, 1 ; no allocation or length too big? jz error mov ebx, [BPR gsParams + BootParams.size] cmp ebx, kBoot2MaxBytes + 1 jnb error call BytesToBlocks ; convert size to blocks ; boot2 file size in blocks is in bx load_boot2: ; anchor for localizing next labels xor esi, esi ; no blocks after 1st range test dl, 2 ; FAT Chain? cmovnz edx, [BPR gsParams + BootParams.cluster] ; if not jnz .oneshot ; load contiguous file ; ; load via FAT ; mov si, bx ; total blocks to si .loop: mov eax, [BPR gsParams + BootParams.cluster] mov edx, eax call getRange test ebx, ebx jnz .nonempty test si, si jnz error jmp boot2 .nonempty: cmp ebx, esi cmovnb bx, si sub si, bx mov [BPR gsParams + BootParams.cluster], eax .oneshot: call ClusterToLBA mov ax, bx mov ecx, edx mov edx, (kBoot2Segment << 4) | kBoot2Address call readBlocks ; TODO: error test si, si jnz .loop ; fall through to boot2 boot2: mov dl, [BPR gDriveSelect] ; load BIOS drive number jmp kBoot2Segment:kBoot2Address error: %ifdef VERBOSE mov di, error_str call log_string %endif hang: hlt jmp hang ;-------------------------------------------------------------------------- ; ClusterToLBA - Converts cluster number to 64-bit LBA ; ; Arguments: ; EDX = cluster number ; ; Returns ; EDI:EDX = corresponding block address ; ; Assumes input cluster number is valid ; ClusterToLBA: push cx xor edi, edi sub edx, 2 mov cl, [BPR gBlocksPerCluster] shld edi, edx, cl shl edx, cl add edx, [BPR gClusterHeapOffset] adc edi, 0 pop cx ret ;-------------------------------------------------------------------------- ; BytesToBlocks - Converts byte size to blocks (rounding up to next block) ; ; Arguments: ; EBX = size in bytes ; ; Returns: ; EBX = size in blocks (rounded up) ; ; Clobbers eax, cl ; BytesToBlocks: xor eax, eax inc ax mov cl, [BPR gBytesPerBlock] shl ax, cl dec ax add ebx, eax shr ebx, cl ret times (kBootBlockBytes - 2 - $ + $$) nop dw kBootSignature block1_end: ;-------------------------------------------------------------------------- ; nextDirEntry - Locates the next 32-byte entry in Root Directory, ; loading block if necessary. ; ; Returns: ; CF set if end of Root Directory ; CF clear, and DS:SI points to next entry if exists ; ; Clobbers eax, ebx, ecx, edx, edi ; nextDirEntry: movzx ax, [BPR gsIterator + DirIterator.entry] cmp al, [BPR gsIterator + DirIterator.entries_end] jb .addressentry mov ecx, [BPR gsIterator + DirIterator.lba] mov edi, [BPR gsIterator + DirIterator.lba_high] cmp ecx, [BPR gsIterator + DirIterator.lba_end] jnz .readblock mov eax, [BPR gsIterator + DirIterator.cluster] mov edx, eax call getRange test ebx, ebx jnz .nonempty stc ret .nonempty: mov [BPR gsIterator + DirIterator.cluster], eax call ClusterToLBA mov ecx, edx add edx, ebx mov [BPR gsIterator + DirIterator.lba_high], edi mov [BPR gsIterator + DirIterator.lba_end], edx .readblock: mov al, 1 %if 0 mov edx, kRootDirBuf %else xor edx, edx mov dh, kRootDirBuf >> 8 %endif call readLBA ; TODO error inc ecx jnz .skip inc edi mov [BPR gsIterator + DirIterator.lba_high], edi .skip: mov [BPR gsIterator + DirIterator.lba], ecx xor ax, ax .addressentry: mov si, ax inc al mov [BPR gsIterator + DirIterator.entry], al shl si, 5 add si, kRootDirBuf clc ret ;-------------------------------------------------------------------------- ; getRange - Calculates contiguous range of clusters from FAT ; ; Arguments: ; EAX = start cluster ; ; Returns: ; EAX = next cluster after range ; EBX = number of contiguous blocks in range ; ; Range calculated is at most kMaxContigClusters clusters long ; getRange: push ecx push edx push edi push si xor edi, edi %if 0 mov edx, kFATBuf %else mov edx, edi mov dh, kFATBuf >> 8 %endif mov ebx, edi .loop: cmp eax, 2 jb .finishup cmp eax, -9 ;kMaxCluster jnb .finishup cmp bx, kMaxContigClusters jnb .finishup inc bx mov si, ax and si, [BPR gsFATCache + FATCache.mask] shl si, 2 mov ecx, eax inc ecx push ecx mov cl, [BPR gsFATCache + FATCache.shift] shr eax, cl cmp eax, [BPR gsFATCache + FATCache.lba] jz .iscached mov ecx, [BPR gFATOffset] add ecx, eax mov [BPR gsFATCache + FATCache.lba], eax mov al, 1 call readLBA ; TODO: error? .iscached: pop ecx mov eax, [kFATBuf + si] cmp eax, ecx jz .loop .finishup: mov cl, [BPR gBlocksPerCluster] shl ebx, cl pop si pop edi pop edx pop ecx ret %ifdef SELECTION_FEATURE ;-------------------------------------------------------------------------- ; setBootFile - Waits two seconds for a keypress. ; If keypress is digit '0' - '9' alters boot file from /boot to /boot ; If keypress anything else or no keypress - uses /boot. ; ; Arguments: ; None ; ; Returns: ; None ; ; Clobbers ax, cx, dx ; setBootFile: mov cx, 2000 ; loop counter = max 2000 miliseconds in total .loop: mov ah, 1 ; int 0x16, Func 0x01 - get keyboard status/preview key int 0x16 jnz .keypress ; got keypress ; wait for 1 ms: int 0x15, Func 0x86 (wait for cx:dx microseconds) push cx ; save loop counter xor cx, cx mov dx, 1000 mov ah, 0x86 int 0x15 pop cx ; restore loop counter loop .loop .done: ret .keypress: xor ah, ah ; read the char from buffer to spend it int 0x16 ; have a key - ASCII is in al cmp al, '0' jb .done cmp al, '9' + 1 jae .done ; ; Alter code so name length tested is 5 instead of 4 ; Compute new hash value with digit and alter code ; to check for modified hash value. ; Note: code continues to compare 4 characters 'boot'. ; For any other ascii character in 5th position, ; the hash value does not collide. There are ; non-ascii unicode characters in 5th position that ; collide with hash-value, but ignore those for simplicity. ; xor ah, ah inc byte [start.name_length_point + 1] add ax, (kBootNameHash >> 1) | ((kBootNameHash & 1) << 15) ror ax, 1 mov [start.name_hash_point + 1], ax ret %endif ;-------------------------------------------------------------------------- ; readBlocks - Reads more than kMaxBlockCount blocks using LBA addressing. ; ; Arguments: ; AX = number of blocks to read (valid from 1-1280). ; EDX = pointer to where the blocks should be stored. ; EDI:ECX = block offset in partition (64 bits) ; ; Returns: ; CF = 0 success ; 1 error ; readBlocks: pushad mov bx, ax .loop: xor eax, eax mov al, kMaxBlockCount cmp bx, ax cmovb ax, bx call readLBA ; TODO: error? sub bx, ax jz .exit add ecx, eax adc edi, 0 push cx mov cl, [BPR gBytesPerBlock] shl eax, cl pop cx add edx, eax jmp .loop .exit: popad ret ;-------------------------------------------------------------------------- ; readLBA - Read blocks from a partition using LBA addressing. ; ; Arguments: ; AL = number of blocks to read (valid from 1-kMaxBlockCount). ; EDX = pointer to where the blocks should be stored. ; EDI:ECX = block offset in partition (64 bits) ; [gDriveSelect] = drive number (0x80 + unit number) ; [gPartitionOffset] = partition location on drive ; ; Returns: ; CF = 0 success ; 1 error ; Presently, jumps to error on BIOS-reported failure ; readLBA: pushad ; save all registers push es ; save ES mov bp, sp ; save current SP ; ; Adjust to 16 bit segment:offset address ; to allow for reading up to 64K ; mov bl, dl and bx, 0xf shr edx, 4 mov es, dx ; ; Create the Disk Address Packet structure for the ; INT13/F42 (Extended Read Sectors) on the stack. ; add ecx, [gPartitionOffset] adc edi, [gPartitionOffset + 4] push edi push ecx push es push bx xor ah, ah push ax push word 16 ; ; INT13 Func 42 - Extended Read Sectors ; ; Arguments: ; AH = 0x42 ; DL = drive number (0x80 + unit number) ; DS:SI = pointer to Disk Address Packet ; ; Returns: ; AH = return status (sucess is 0) ; carry = 0 success ; 1 error ; ; Packet offset 2 indicates the number of sectors read ; successfully. ; mov dl, [gDriveSelect] ; load BIOS drive number mov si, sp mov ah, 0x42 int 0x13 jc error ; ; Issue a disk reset on error. ; Should this be changed to Func 0xD to skip the diskette controller ; reset? ; ; xor ax, ax ; Func 0 ; int 0x13 ; INT 13 ; stc ; set carry to indicate error ;.exit mov sp, bp pop es popad ret %ifdef VERBOSE ;-------------------------------------------------------------------------- ; Write a string with log_title_str prefix to the console. ; ; Arguments: ; DS:DI pointer to a NULL terminated string. ; log_string: pushad push di mov si, log_title_str call print_string pop si call print_string popad ret ;------------------------------------------------------------------------- ; Write a string to the console. ; ; Arguments: ; DS:SI pointer to a NULL terminated string. ; ; Clobber list: ; AX, BX, SI ; print_string: mov bx, 1 ; BH=0, BL=1 (blue) .loop: lodsb ; load a byte from DS:SI into AL test al, al ; Is it a NULL? jz .exit ; yes, all done mov ah, 0xE ; INT10 Func 0xE int 0x10 ; display byte in tty mode jmp .loop .exit: ret %endif ; VERBOSE ;-------------------------------------------------------------------------- ; Static data. ; %ifdef VERBOSE log_title_str: db 13, 10, 'boot1x: ', 0 init_str: db 'init', 0 error_str: db 'error', 0 %endif times (kBootBlockBytes - 4 - $ + block1_end) db 0 dw 0, kBootSignature