;
; 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 run time)
;
; - Supports both FAT-12 / FAT-16 file systems
;
; - Supports Sector sizes of 512, 1024, 2048, 4096, and 8192 bytes
;
; - Supports CHS/LBA - boots both floppies and any partition under 2Tb
;
; - Accepts boot disk number in DL (floppies:0,1  hard disks: 80h,81h)
;
; - MBR/BIOS should load this code at 0:7C00h - but any value will work
; - Relocates own code + buffer + stack to the top of free memory (640k)
; - This makes available continuous memory range (0050h..9D00h)(1..628k)
;
; - Works on any 80186 or 80286 but if ELF loader enabled you need i386
;
;
; This is usage break down of 512 bytes we have:
;
; Mandatory FAT-12/-16 fields and magic:   64 bytes
; Initialisaion and FAT chain load code: ~222 bytes
; Automatic CHS/LBA read_sector routine:  110 bytes
;
; This leaves ~116 bytes to pick and choose some optional features:

DO_DL_TEST_7E equ 1	;  9 bytes	; some BIOSes pass garbage in DL - test for that
DO_DISK_RESET equ 1	; 12 bytes	; on errors reset disk (int 13h, ah=0) (floppies)
DO_MEM_CHECK  equ 1	;  8 bytes	; enforce "640k should be enough for everyone" rule
DO_ELF_LOADER equ 1	; 85 bytes	; load staticly linked ELF executable to addresses <1M
DO_ERROR_MSSG equ 0	; ?? bytes	; print some error message
; The following two are not fully implemented at this time
DO_NAME_LIST  equ 0	; ?? bytes	; use list of file names instead of a single boot entry
DO_DOS_LOADER equ 0	; ?? bytes	; support legacy ??-DOS and Win9x (needs DO_NAME_LIST=1)

;%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:
		jmp short start	; Win9x LBA aware loader needs EBh ?? here
		nop				; Need to have 90h for Windows to read floppy
;BPB (Boot Parameters Block) with the default values for FAT12 on 1.44 floppy
bp_03h      dd  0,0	 ; Windows modifies this evrytime 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 (bits 0-32)
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)
bp_2Bh		DB "NO NAME    "; 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 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 number 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:
		push cx;=0	; default for "pop di" if no boot file to be found

		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_DOS_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 files
		;
		mov	si, RELOC_BASE+(first_name-_BOOT_F1X)	; first file name (to search for)
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
%if DO_NAME_LIST = 0		; check for a single file name
		pusha
		mov 	cx, 0Bh
		repe	cmpsb
		popa
		jne loop_root_entry	; NZ - name not matched
		push word [di+1Ah]	; starting cluster of the file
%else						; check for a list of file names

		push	si			; save ptr to the first name in the list
check_name:
		pusha
		lodsb
		mov 	cx, 0Bh
		repe	cmpsb
		rol 	al, 1
		popa
		;ZF=1 - name matched
		;CF=1 - last name in list
		;OF=1 - non-boot name
		lea 	si, [si-0Eh]; previous file name
		ja	check_name		; NC&NZ - name not matched - check prev name
		jne end_file_list	; NZ - name not matched, must be CF - last name in the list
		; name found
		push word [di+1Ah]	; starting cluster of the file
		jno	loop_root_entry ; don't pop si for boot file (WINBOOT.SYS, IO.SYS, IBMBIO.COM)
				; because we need to remember si and don't want to revisit low priority names
		pop	bx	; starting cluster of non-boot file (MSDOS.SYS, IBMDOS.COM) save to [0h:53Ah]
end_file_list:
		pop	si	; restore ptr to file name list
%endif
loop_root_entry:
		loop	next_root_entry

		; dx:ax starting sector of cluster 2 from the beginning of the partition
		pop	di	; first cluster of the boot file (or 0 if no name has matched)
		mov	sp, bp ; restore stack pointer
		pusha	; ax[bp-2], cx[bp-4]=0, dx[bp-6], bx, sp, bp, si, di[bp-10h]

%if DO_DOS_LOADER = 1
		push	ds
		mov		ds,cx;=0
		mov		[0x51A],di
		mov		[0x53A],bx
		pop 	ds
%endif
%if DO_NAME_LIST = 0		; check for a single file name
		mov es, [si+0Bh]	; now es - destination segment

%else						; check for a list of file names
		mov es, [si+0Eh+0Ch]; now es - destination segment
%endif
		push	es	; destination segment

read_next_cluster:
		;assuming ch=0 here

		or	di, di			; is it a valid cluster number?
		jz	short no_boot_file ; or invalid FAT chain

		lea	ax, [di-2]
		mov	cl, [bp+0Dh]	; sectors per cluster (assuming ch=0)
		mul	cx
		add	ax, [bp-2]		; starting sector of cluster 2
		adc	dx, [bp-6]

read_next_sector:
		call	read_sector
%if DO_MEM_CHECK = 1
   		;if es>ss file is too long to fit in conv memory
		mov bx, es
		cmp bx, [bp+4]
		ja	short memory_overrun
%endif
		loop	read_next_sector

		; Need to set (di) 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, di		; 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		; initial value of si is around 25??h
		je		fat_in_memory
		mov 	si, ax

		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 (in: di;  out: di, ZF set if eof)
; Entire 6k of FAT-12 or 8k portion of FAT-16 is currently in memory at ds:0h

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

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

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

		pop	ds		; Segment where file was loaded at
		popa		; cx=0, di=first cluster
		mov si, cx	;=0

		lodsw ; File magic signature

		cmp	ax, 5A4Dh ; 'MZ' Win95/98, *.exe, ...
		je	short boot_mz

%if DO_ELF_LOADER = 1
		cmp 	ax, 457Fh	; 7F'E'
		jne		not_an_elf
		lodsw
		cmp 	ax, 464Ch	; 'LF'
		mov 	bx, 18h-4	; e_entry
		je  	elf_next32
%endif

not_an_elf:

no_boot_file:
memory_overrun:
		jmp	short error_message

%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:
		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, [si-4+1Ch]	; e_phoff
		mov 	di, [si-4+2Ah]	; e_phentsize
		sub 	di, byteim 0Ch
elf_next32:
		mov 	edx, [bx+4]
		mov 	bp, dx
		shr 	edx, 4
		push	dx
		and 	bp, byteim 0Fh
		push	bp
		jcxz	elf_header		; first time here - load Program Header info
		xchg	ax, si
		xchg	ax, di
		add 	bx, di
		loop	elf_next32

		mov	bp, [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_ph:
		pop	ax	; cnt: mod 16
		pop	cx	; cnt: segments

		pop	di	; dst: off
		pop	dx	; dst: segment

		pop	si	; src: off
		pop	bx	; src: segment

		add	bx, byteim 60h	; segment where file is loaded at

		; Copy in 16+ byte increments to handle segments over 64k

elf_copy16b:
		mov	ds, bx
		mov	es, dx

		pusha
		add 	al, 10h
		xchg	ax, cx
		rep	movsb
		popa

		inc	bx
		inc	dx
		loop elf_copy16b

		dec	bp
		jnz	elf_copy_ph

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

		jmp	short dl_retf
%endif

boot_mz:
		push	ds
		push	200h
dl_retf:
		mov		bp, sp
		mov 	dl,[bp+4+0Ah]	; disk number
		retf

%if 0	; This section has legacy code to boot DOS / Windows (doesn't work yet)

		pop	di	; first cluster of boot file
		pop	ds	; destination segment

		mov si, 200h
@@boot_not_mz:

		pop	bx
		pop	ax
		pop	dx		; dl=[bp+0] ; disk number
		;mov	[bp+24h], dl
		add	bx, [bp+1Ch]	; MS-DOS, PC-DOS, DR-DOS, NT4 LDR, ...
		adc	ax, [bp+1Eh]	; expect ax:bx = starting sector of cluster 2
		;jc 2Tb overflow (but no problem, since older loaders wouldn't work anyways)

	;	mov	[si+51Ah], di ; MS-DOS expects starting cluster of IO.SYS at 00:051Ah and MSDOS.SYS at 00:053Ah
	;   MSDOS.SYS, not a boot file, but store cluster# at 00h:053Ah
	;	mov	ch, [bp+15h]  ; as well as media descriptor byte in ch
		push	es
		jne	short @@dos_si; not 'MZ' -> DOS or NTLDRs
@@win95:
		; push ax:bx=(staring sector of cluster 2 rel disk) into [bp-2]:[bp-4]
		push	ax
		push	bx
		; Win9x boot loader pushes int 1Eh address but io.sys ignores it
		;push	ss;=0;word_ptr [si+7Ah]
		;push	ss;=0;word_ptr [si+78h]
		; following two words are not used by Win9x IO.SYS and could be anything
		;push	bp ; 7C00h
		;push	ss ; =0
		push	es ; ret cs
		mov	si, 200h	; ret ip
@@dos_si:
		push	si
		lea	si, [bp+0Bh] ; NTLDR expects DS:SI to point to [bp+0Bh]
		retf			; transfer control to the loaded file
		;ds=ss=0
		;es=cs=dest_seg
		;ip='MZ'?200h:0
		;bp=7C00h, sp=bp+2 or if 'MZ' then minus stuff above
		;si->bp+0Bh
		;di=first cluster of boot file
		;dx=init_dx
		;cx=0
		;ax:bx=starting sector of cluster 2 (absolute on disk)
%endif

gap0:
		times ( 200h - (_BOOT_F1X_END-gap1) - (gap0-_BOOT_F1X) ) db (90h)
gap1:

disk_error:
%if DO_DISK_RESET = 1
		mov	ah, 0
		mov	dl, [bp+0Ah]
		int	13h
		;jc	really_bad
		inc byte [bp+6]		; total budget is 16 resets before giving up
		jnz	@@disk_get_info	; would ja (NZ & NC) be better here?
%endif
error_message:
%if DO_ERROR_MSSG = 1
		mov	ax,0E00h+'x'	; too lazy to print error msg
		int 10h
%endif
		int 18h				; give control back to BIOS :)

;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 1	; 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

		mov	ax,[bp+0Bh]	; sector size in bytes
		div byte [si]	; /=10h convert from bytes to segments
		add	[si+10h],ax	; advance saved es for the next sector

@@disk_get_info:
		mov	dl,[bp+0Ah]	; disk number
		mov	ah, 8		; Get disk parameters
		int	13h
@@jc_disk_error:
		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_DOS_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:
		mov	ax, 420Eh
%if DO_DOS_LOADER = 1
		mov	[bp+2], al	; Force Win95/98 to use LBA
%endif
	@@int13:
		inc 	ax			; for CHS turn al=0 into 1 (read one sector)
		les 	bx,[si+04h]	; destination address
		mov 	dl,[bp+0Ah]	; disk number
		int 	13h
		jc	short @@jc_disk_error

		popa	; sp+=10h	; restore stack pointer
		pop 	es
		popa
		inc 	ax
		jnz 	@@no_inc_dx
		inc 	dx
;!;		;jz	short @@print_error	; relative sector 2Tb boundary overflow
@@no_inc_dx:
		ret

first_name:
%if DO_NAME_LIST = 1
;	DB	00h, "NTLDR      ", 00h, 20h
;	DB	00h, "IBMBIO  COM", 70h, 00h
;	DB	40h, "IBMDOS  COM", 00h, 00h
;	DB	00h, "WINBOOT SYS", 70h, 00h
;	DB	00h, "IO      SYS", 70h, 00h
;	DB	40h, "MSDOS   SYS",
;   DW			RELOC_BASE+200h-3 ; for win9x [bp+1EEh] -> [a,b,c,d,"ErrorMsgs",] 0
		db 0C0h
%endif
		db "OSLOADER   ", 60h, 00h

		db  055h, 0AAh

_BOOT_F1X_END:
