CloverBootloader/BootHFS/boot1x.s

706 lines
16 KiB
ArmAsm

;
; 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 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<digit>
; 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