;
; Copyright (c) 1998-2010 Mikhail Ranish
;
; Permission to use, copy, modify, and/or distribute this software for any
; purpose with or without fee is hereby granted, provided that the above
; copyright notice and this permission notice appear in all copies.
;
; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
; WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
; MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
; ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
; WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
; ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
; OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;----------------------------------------------------------------------------
;
; This is universal boot sector code (all checks are done at the run time)
;
; - File systems: FAT-12 or FAT-16
; - Sector sizes: 512, 1024, 2048, 4096, or 8192 bytes
; - Int 13h mode: CHS for floppy or Cyl<1023, LBA for hard disks up to 2Tb
; - Boot devices: disk# passed in dl (floppies: 0, 1; hard disks: 80h, 81h)
; - CPU/Hardware: works on any 186, 286, 386+ (but needs 386+ to load ELFs)
; - Boot file name is user configurable (any time) by setting volume label
; - BIOS should load us at 0:7C00h, but we work on noncomlient systems too
;
; - Flat  binaries  are loaded at 0060h:0000h and jumped to 0060h:0000h
; - MZ  executables are loaded at 0060h:0000h and jumped to 0060h:0200h
; - ELF executables are loaded at proper linkaddr and jumped to (e_entry)
; - In all cases control is given in real mode, dl=disk#, and ss:bp -> BPB
;
; This code relocates itself to the top of conventional memory (640k-12k)
; which makes available continuous memory range (0050h..9D00h) (1k..628k)
; Max file size is ~626k. During loading we check against memory overruns.
;
; ELFs must be < min(512k,628k-linkaddr) no memory checks while relocating.
; Relocation is done in reverse so any linkaddr above 1000h should be safe.
;
; Errors: if boot file name is not found in root directory, file is too long,
; or there were >16 disk I/O errors we give control back to BIOS via int 18h.
;
;----------------------------------------------------------------------------
;
; Boot Sector size is 512 bytes; here is our usage breakdown:
;
; Mandatory FAT-12/FAT-16 fields and magic numbers:   64 bytes
; Initializaion, root dir search, and file loading:  206 bytes
; ReadSector routine with CHS / LBA auto-selection:  109 bytes
;
; This leaves exactly 133 bytes for the following optional features:
;
DO_DL_TEST_7E equ 1	;   9 bytes	; some BIOSes pass garbage in DL - test for that
DO_DISK_RESET equ 1	;   8 bytes	; on errors do a disk reset (helps dusty floppies)
DO_MEM_CHECK  equ 1	;   8 bytes	; enforce "640k should be enough for everyone" rule
DO_DOS_LOADER equ 1	;   2 bytes	; support for booting FreeDOS's KERNEL.SYS
DO_WIN_LOADER equ 1	;  18 bytes	; support for booting Win9x's IO.SYS / WINBOOT.SYS
DO_ELF_LOADER equ 1	;  88 bytes	; load staticly linked ELF executable to linkaddr <1M

;%if   	equ	if    	; FASM needs this line uncommented
;%else	equ	else  	; FASM needs this line uncommented
;%endif	equ	end if	; FASM needs this line uncommented
;byteim	equ       	; FASM needs this line uncommented

%define byteim byte	; NASM needs this line uncommented

RELOC_BASE	EQU	 23E0h	; Need 0??E0h to end up with ??EBh in di

_BOOT_F1X:
; BPB - Boot Parameters Block (with defaults for 1.44mb floppy)
		jmp short start	; Win9x LBA aware loader needs EBh ?? here
		nop				; Must be 90h for Windows to read file system
bp_03h	 db "OSDEV1.0"	; Windows overwrites this evreytime it changes filesystem
bp_0Bh		DW   512	; Sector size in bytes
bp_0Dh		DB     1	; Cluster size in sectors
bp_0Eh		DW     1	; Reserved sectors at the beginning of partition
bp_10h		DB     2	; number of copies of FAT table
bp_11h		DW   0E0h	; number of entries in root directory
bp_13h		DW  0B40h	; Short number of sectors (set to 0 if size > 64k)
bp_15h		DB	 0F0h	; Media descriptor byte (HD:F8 Floppy:F0,F9,FC-FF)
bp_16h		DW     9	; sectors in each copy of FAT
bp_18h		DW    12h	; sectors per track
bp_1Ah		DW     2	; tracks per cylinder (heads)
bp_1Ch		DD     0	; Starting sector of the partition
bp_20h		DD     0	; Long number of sectors in partition (if >= 64k)
bp_24h		DW     0	; Disk number  (Floppies:0,1,... HDs:80h,81h,...)
bp_26h		DB    29h	; Extended boot record signature
bp_27h		DD 12345678h; Volume serial number (random, yeah)

filename:				; Use volume label for boot file name

bp_2Bh	DB "OSLOADER   "; Volume label (11 bytes, space padded)
bp_36h	DB "FAT12   "	; FileSystem ID "FAT12" or "FAT16" (8b)


start:
		call start_lo
start_lo:
		pop 	si		; figure out our own load address
		lea 	si, [si-(start_lo-_BOOT_F1X)+0Bh] ; usually it will be 7C0Bh
		mov 	di, RELOC_BASE+0Bh ; reserve space for 8k buffer + ~1k stack
		mov 	cx, 0306h
		int 	12h		; ax = free conventional memory (kb)
		cld

%if DO_DL_TEST_7E = 1
		test	dl, 7Eh	; is there any "garbage" bits in DL ?
		jz  	dl_ok
		mov 	dl, [cs:si-0Bh+24h] ; then use disk# from BPB
dl_ok:
%endif
		shl 	ax, cl	; kbytes -> segments
		sub 	ah, ch	; top conv mem -=12k
		mov 	es, ax
		mov 	ds, ax
		mov 	ss, ax
		lea 	sp, [di-0Bh+10h] ; bp+10h
%if DO_MEM_CHECK = 1
        xchg	ax, bp
%endif
		pusha		; ax, cx, dx[+A], bx, sp, bp[+4], si[+2], di[+0]
		; dl=hdnum	; [bp+A] ; boot disk number (initial value of dl)
		; sp=23E0h	; [bp+6] ; using E0h to count up to 16 disk resets
		; bp=ss		; [bp+4] ; it's handy to have value of ss at [bp+4]
		; si=??0Bh	; [bp+2] ; if set to 0Ch or OEh Win 9x will use LBA
		; di=??EBh	; [bp+0] ; set to this to EBh as well to enable LBA
		mov		bp, sp
		rep cs movsb; copy own code to the top of conventional memory
		push	es
		push	RELOC_BASE + start_hi - _BOOT_F1X
		sti
		retf		; jump to a new location
start_hi:
		mov 	bx, cx	;=0

		mov 	cx,[bp+11h]	; number of entries in root directory

		;mov    ax, 2		; FAT32 has sector 2 reserved for additional boot code
		;jcxz 	@@go_fat32	; TO DO if space permits

		; it is FAT-12/FAT-16

%if DO_WIN_LOADER = 1
		mov [bp+24h], dl	; save dl = disk number
%endif
		;
		; Determine location of the root directory
		;
		mov 	al, [bp+10h]; now ax = number of FAT copies
		cbw 	;ah=0
		mul 	word[bp+16h];*fat_size -> now dx:ax - size of FATs
		add 	ax, [bp+0Eh]; reserved sectors at the beginning of partition
		adc 	dl, dh;=0	; now dx:ax - first sector of root rel partition
		;
		; Read root dir one sector at a time and look for boot file name
		;
next_root_sector:			; we will read one sector at a time
		push	es			; save destination address es:00
		call	read_sector	; will advance sector number in dx:ax
		pop 	es
		mov 	di, [bp+0Bh]; sector size
next_root_entry:
		sub 	di, byteim 20h		; previous root directory entry
		jc	next_root_sector

		mov	si, RELOC_BASE+(filename-_BOOT_F1X)	; file name to search for
		pusha
		mov 	cx, 0Bh
		repe	cmpsb
		popa
		jne loop_root_entry	; NZ - name not matched
		add 	bx,[di+1Ah]	; starting cluster of the file (ignoring volume label)
loop_root_entry:
		loop	next_root_entry

		; dx:ax starting sector of cluster 2 from the beginning of the partition
		; bx - first cluster of the boot file (or 0 if no name has matched)

		pusha	; ax[bp-2], cx=0, dx[bp-6], bx, sp, bp, si, di=0

		push	byteim 60h 	; read file to 60h:00h
		pop 	es
		push	es			; save destination segment

read_next_cluster:
		;assuming ch=0 here

		lea	ax, [bx-2]
		mov	cl, [bp+0Dh]	; sectors per cluster (assuming ch=0)
		mul	cx
		add	ax, [bp-2]		; add starting sector of cluster 2
		adc	dx, [bp-6]

		or	bx, bx			; is it a valid cluster number?
%if DO_MEM_CHECK = 1
		jz	short je_fail_int18h
%else
		jz	short fail_int18h
%endif

read_next_sector:
		call	read_sector
%if DO_MEM_CHECK = 1
   		;if es>ss file is too long to fit in conv memory
		mov di, es
		cmp [bp+4], di
je_fail_int18h:
		jbe	short fail_int18h
%endif
		loop	read_next_sector

		; Need to set (bx) to the next cluster in the file

		; Find start sector of 8k FAT block corresponing to the current cluster
		; For FAT-12 it is always 0 (FAT-12 is 6k max by definition)
		; For FAT-16  si = ( di*2 / 8k ) * (8k / sect_size )

		mov 	ax, 20h		; ax = 8k / 256
		div 	byte[bp+0ch]; sector size (hi)
		xchg	ax, cx		; now cx = sectors in 8k block
		mov 	ax, bx		; there are 4096 entries in 8k of FAT-16
		shr 	ax, 12		; now ax = which 8k block we need to have
		mul 	cx			; now ax:dx sector number from the beginning of FAT

		cmp 	ax, si		; valid ax is <= 256, given max FAT size is 128k
		je  	fat_in_memory
		mov 	si, ax		; initial value of si is guaranteed to be > 256

		add 	ax,[bp+0Eh]	; reserved sectors prior to FAT
		adc 	dx, dx		; now dx:ax - starting sector of FAT (rel partition)

		push	es
		push	ds
		pop 	es
read_fat_sector:
		call	read_sector
		loop	read_fat_sector
		pop 	es

fat_in_memory:

		; Get next cluster # in the FAT-12/16 chain (bx -> bx)
		; Entire FAT-12 (6k) or FAt-16 8k block loaded at ds:0h

		mov	dx, 0FFFh	; FAT-12 mask
		and	bx, dx		; 4096 FAT-16 entries in 8k block

		mov	ax, bx
		add	bx, ax
		add	bx, ax		; FAT-12 entry size is 3/2 byte

		cmp	byte [bp+3Ah], '6'	; is it FAT-16 ?
		jne	@@fat12
@@fat16:
		add	bx, ax		; FAT-16 entry size is 4/2 byte
		mov	dh, dl		; FAT-16 mask -> 0FFFFh
@@fat12:
		shr	bx, 1		; finalize entry location in FAT
		mov	bx, [bx]
		jnc	@@no_shift	; odd FAT-12 entries need a shift
		shr	bx, 4
@@no_shift:
		and	bx, dx		; now bx is next cluster number
		cmp	bx, dx		; if bx=EOF it was last cluster

		jne	short read_next_cluster

read_done:			; Entire file is in memory

		pop	ds		; Segment where file was loaded at
		popa		; ax:dx=sector of cluster 2 (rel partition), bx=first cluster of the file, cx=0, di=0

%if DO_WIN_LOADER = 1
; Win9x io.sys expects 2k (4 sectors) loaded at 70h:0
; [bp-2]:[bp-4] staring sector of cluster 2 (rel disk)
; di - first cluster of the boot file (si:di if FAT-32)
; ss:bp -> BPB -> Win9x will copy it to its own buffers
; ss:[bp+1EEh] -> [a,b,c,d,"ErrorMsgs",] 0 (must have 0 within 64 bytes)
; [ss:sp+4] int 1Eh address - Win9x restores it on errors (this isn't critical)
; jump to loadseg:200h (io.sys relocates itself to (int12h-64k) so 60h is safe)

		;mov [bp+1EEh], bp+0Bh ; This is done below at start+1F0h

		add 	ax, [bp+1Ch]
		adc		dx, [bp+1Eh]
		push	dx;->[bp-2]
		;push	ax:->[bp-4] done with pusha
%endif
		pusha	;ax, and other regs, di=0 at the bottom
		xchg 	di, bx	; now di=starting cluster, bx=0
		pop 	si	;=0
		mov 	dl,[bp+0Ah]	; disk number

		lodsw			; ax = file magic signature
		;bx=0,cx=0,si=2

		cmp 	ax, 5A4Dh	; 'MZ' Win9x io.sys, *.exe, ...
		jne  	short not_mz
		mov 	ch, 2		; entry point for 'MZ' is 200h
not_mz:
%if DO_ELF_LOADER = 1
		cmp 	ax, 457Fh	; 7F'E'
		jne		not_an_elf
		lodsw
		cmp 	ax, 464Ch	; 'LF'
		mov 	bl, 18h-4	; e_entry
		je  	elf_next32
not_an_elf:
%endif
		push	ds
		push	cx
%if DO_DOS_LOADER = 1
		mov 	bl, dl		; FreeDOS needs disk number in bl, jmp to 60h:00h
%endif
		retf

disk_error:
%if DO_DISK_RESET = 1
		xchg	ax, bx		; ah=0 ; Reset entire fd or hd subsystem
		;mov	dl, [bp+0Ah]; expecting disk number after read operation
		int 	13h
		inc 	byte [bp+6]	; total budget is 16 resets before giving up
		jnz 	@@disk_get_info
%endif
fail_int18h:
		int 	18h			; report to BIOS that this boot option failed

%if DO_ELF_LOADER = 1

; Read 32-bit values from ELF header and push on
; stack corrsponding 16-bit segment:offset pairs
;		[18h] 	; e_entry
; For each Program Header:
;		[+04]	; p_offset (src rel file)
;		[+08]	; p_vaddr  (dst rel mem)
;		[+16]	; p_filesz (cnt)

elf_header:
		pusha	; save dl=disk#, bp->BPB
		mov 	al, 3
		mul 	byte[si-4+2Ch]	; e_phnum
		xchg 	ax, cx			; cx = 3*e_phnum
		mov 	al, 8			; cx was 0 -> ax=8
		mov 	bx, [bx+si]		; e_phoff (bx=18h,si=4) [1Ch]
		mov 	bp, [si-4+2Ah]	; e_phentsize
		sub 	bp, byteim 12
elf_next32:
		mov 	edi, [bx+4]
		shr 	edi, 4
		push	di
		mov 	di, [bx+4]
		and 	di, byteim 0Fh
		push	di
		add 	bx, si			; bx+= 4(si), then +=8(ax), then +=(e_phentsize-12)(bp)
		jcxz	elf_header		; first time here - load Program Header info
		xchg	ax, si			; rotate values: ax->si, si->bp, bp->ax
		xchg	ax, bp
		loop	elf_next32

		mov 	cx, [si-4+2Ch]	; e_phnum - number of Program Headers

; For each Program Header copy p_filesz bytes from p_offset to p_vaddr

elf_copy_pheader:
		pop	ax	; cnt: mod 16
		pop	bp	; cnt: segments

		pop	di	; dst: off
		pop	dx	; dst: segment (rel mem)

		pop	si	; src: off
		pop	bx	; src: segment (rel file)

		lea 	bx,[bx+60h]	; file is loaded at 60h:0

		; Copy 16 bytes at a time in the reverse segment order
		; We can safely move up blocks of up to 512 kb in size

copy16b:
		pusha
		add 	bx, bp
		add 	dx, bp
		mov 	ds, bx
		mov 	es, dx
		xchg	ax, cx	; ax -> cx
		rep	movsb		; no need to jcxz ; if cx==0 rep will do nothing
		popa
		mov 	al, 16	; first iteration copies <16 bytes, subsequent copy 16
		dec 	bp
		jns 	copy16b	; do while(>=0) because segment count was rounded down

		loop	elf_copy_pheader

		popa			; restore dl=disk#, bp->BPB

		; Jump to ELF entry point e_entry (seg:offset)

		retf
%endif

;go_fat32:
;		mov ax, 2
;		cwd
;		push dx;=0

read_sector:
		;
		;  dx:ax - sector on partition (out: dx:ax will be advanced by 1)
		;  es:00 - destination address (out: es will be advanced by sector size)
		;
		;  assuming ds = ss, other registers unchanged
		;
		;   byte [bp+02h] - out: 0Eh if Win 9x should use LBA
		;   byte [bp+0Ah] - disk number
		;   word [bp+0Bh] - sector size
		;   word [bp+1Ah] - out: num heads as retutrned by BIOS
		;  dword [bp+1Ch] - partition start sector (rel disk)
		;

		pusha
		push	es

		xor		cx, cx

		add 	ax,[bp+1Ch]	; starting sector of the partition
		adc 	dx,[bp+1Eh]

		push	cx;=0		; rel_sect7,6 ; Prepare LBA parameters
		push	cx;=0		; rel_sect5,4
		push	dx			; rel_sect3,2
		push	ax			; rel_sect1,0
		push	es			; segment
		push	cx;=0		; address
		push	byteim 01	; num_sect = 1 sector
		push	byteim 16	; pkt_size = 16 bytes on stack
		mov 	si, sp		; pkt_addr

		adc		[si+0Ch],cx	; in case this partition extends beyond 2T

@@disk_get_info:
		mov 	dl,[bp+0Ah]	; disk number
		mov 	ah, 8		; Get disk parameters
		int 	13h
@@jc_disk_error:
		les 	bx,[si+04h]	; destination address
		jc	short disk_error

		mov 	ax, 3Fh
		and 	cx, ax		; Now cx = number of sectors per track
		mov 	al, dh		; Now al = number of heads - 1
		inc 	ax			; Now al = number of heads
%if DO_WIN_LOADER = 1
		mov 	[bp+1Ah],ax	; #heads depends on BIOS translation mode
%endif
		mul 	cx			; Now ax = number of sectors per cylinder
		xchg 	ax, di		; Now di = number of sectors per cylinder
		mov 	ax, [si+08h]; rel_sect low
		mov 	dx, [si+0Ah]; rel_sect high
;!;		jz  	@@lba		; one of CHS dimensions is zero - must use LBA
		cmp 	dx, di
		jae 	@@lba		; if cylinder >= 65536 must use LBA
		div 	di			; Now ax = cylinder, dx = sector on cylinder
		xchg	ax, dx		; Now ax = sector on cylinder, dx = cylinder
		div 	cl			; Now al = head, ah = sector on head - 1
		mov 	cl, 2
		xchg	ch, dl		; Now dh = cyl:15-8,  dl = 0, ch = cyl:7-0
		shr 	dx, cl		; Now dh = cyl:15-10, dl = cyl:9,8 in bits 7,6
		xchg	ah, cl		; Now ah = 2 (CHS read), cl = sector on head-1
		inc 	cx			; Now ah = 2 (CHS read), cl = sector on head
		or  	cl, dl		; Now cl = cyl:9,8(bits 7,6) and sector in 5-0
		xchg	dh, al		; Now dh = head, al = cyl:15-10 (0 in bits 7,6)
		or  	al,[si+0Ch]	; Is cylinder >= 1024 or lba bits 32-39 set?
		jz  	@@int13		; No, Cyl is less than 1024 - we may use CHS
	@@lba:
%if DO_WIN_LOADER = 1
		mov 	ax, 420Eh	; ah=42h  LBA read function
		mov 	[bp+2], al	; Force Win95/98 to use LBA
%else
		mov 	ah, 42h		; LBA read function
%endif
	@@int13:
		inc 	ax			; for CHS turn al=0 into 1 (read one sector)
		mov 	dl,[bp+0Ah]	; disk number
		int 	13h
		jc	short @@jc_disk_error

gap0:
		times ( 200h - (_BOOT_F1X_END-gap1) - (gap0-_BOOT_F1X) ) db (90h)
gap1:
		mov	ax, [RELOC_BASE+0Bh]; [bp+0Bh]; sector size in bytes
%if DO_WIN_LOADER = 1
		nop		; need this to force previous two bytes to be at bp+1EEh
%endif
		div 	byte [si]	; /=10h convert from bytes to segments
		add 	[si+10h],ax	; advance saved es for the next sector
		popa				; sp+=10h ; restore stack pointer
		pop 	es
		popa				; restore saved registers
		inc 	ax
		jnz 	@@no_inc_dx
		inc 	dx
;!;		jz short @@error	; relative sector 2Tb boundary overflow
@@no_inc_dx:
		ret

		db  055h, 0AAh

_BOOT_F1X_END:
