June 24, 2019

x86 Bootloader

When you first turn on your computer, the system is void and without form. There are no programs in memory to run and there is no OS to help you load a program into memory. Concepts that we take for granted such as the notion of a file are not available to us here. We do, however, have one single friend in this barren world, the BIOS (Actually, BIOS is an antiquated technology as the world has now moved to UEFI, but we're assuming a BIOS system for the purpose of this article). The BIOS helps us by loading instructions, called a bootloader, into memory. It plucks the bootloader from the very first segment on a harddrive, loads it into memory, and begins executing it. The booloader's job is to begin loading the OS into memory and turning over execution to it.

I always wanted to write a mini operating system so the bootloader seemed like the logical place to start. It was actually a huge pain. It turns out the bootloading in general is not perfectly standardized and different impelmentations have their quirks. In several cases, I would write code that would execute perfectly on an qemu (an emulator), and fail on my real laptop when I attempted to boot it from a USB stick. Also it turns out that some systems require a BIOS parameter block which is inconsistently documented across the internet.

After great pain, I did get this bootloader to work on my laptop. I consulted many different sources to aid in its construction. I won't go too much into detail but its here for anyone who is interested.


The process I used to build and test this code:

The code was compiled with nasm nasm -fbin boot.asm -o boot.bin
Then the binary was tested in the emulator, qemu qemu-system-i386 -fda boot.bin
Next, I changed the binary file extension to .iso, which then gave me the option to "Write to disk" when right-clicking (using Ubuntu). The ISO was burned to a USB stick.
Finally, I inserted the USB stick and pressed whichever function key brought up the boot options menu and chose to boot from the USB stick. (If you're doing this on a new computer, legacy-boot mode must be enabled).


The Code

; Written by Joe Fortune 2019

start:
jmp entry_point         ; Jump over the BIOS Parameter Block
TIMES 0x0b-$+start DB 0 ; Padding. Bios parameter block starts at 0x0b

; -----------| BIOS Parameter Block |-----------
bpbSectorSize:          DW 512  ; 512 bytes per sector
bpbSectorsPerCluster:   DB 1    ; Number of sector(s) per cluster (a logical collection of sectors)
bpbReservedSectors:     DW 1    ; Number of reserved sectors starting at sector 0
bpbFATsOnDisk:          DB 2    ; Number of FATs on disk
bpbRootDirEntries:      DW 224  ; Number of possible root directory entries
bpbTotalSectors:        DW 2880 ; Total number of sectors on disk
bpbMediaDescriptor:     DB 0xf0 ; Media descriptor byte. (0xf0 is 3.5", 1440kB floppy disk)
bpbSectorsPerFAT:       DW 9    ; Number of sectors per FAT
bpbSectorsPerTrack:     DW 18   ; Sectors per track (a complete circular ring of sectors)
bpbHeads:               DW 2    ; Number of heads (Physical sides of a platter)
bpbHiddenSectors:       DD 0    ; Number of hidden sectors
bpbTotalSectorsExt:     DD 0    ; Used to extend bpbTotalSectors if partition > 32MB. 
bsDriveNumber: 	        DB 0    ; Drive number (0 = floppy)
bsUnused: 	            DB 0
bsExtBootSignature: 	  DB 0x29 ; If present, indicates the presence of the following entries
bsSerialNumber:	        DD 0x00000000
bsVolumeLabel: 	        DB "JOE FLOPPY "
bsFileSystem: 	        DB "FAT12   "


; -----------| Data |-----------
BL_LOADING_STR:   db "Loading kernel...", 0
BL_DISK_ERR:      db "Disk error.", 0
BL_SECTORS_ERR:   db "Incorrect number of sectors read.", 0
BL_DONE_STR:      db "Done.", 0


; -----------| Functions |-----------
bl_print:
  mov     ah, 0x0e  ; BIOS teletype mode

.loop:
  lodsb			        ; Load character from address DS:SI into AL
	cmp     al, 0     ; Check for null-terminator
	je      .end      ; If null-terminator, jump to end
	int     10h			  ; Else, print character
	jmp     .loop
.end:
  ret


; -----------| Entry Point |-----------
entry_point:
  ; Init stack
  mov     bp, 0x8000
  mov     sp, bp

  ; Init data segment to reflect that the bootloader is loaded at 0x7c00
  mov     ax, 07C0h
  mov     ds, ax

  ; Loading message  
  mov     si, BL_LOADING_STR
  call    bl_print

  ; Set up args for disk-read function
  %define N_SECTORS 1
  mov     ah, 0x02      ; Interrupt 0x13 - function 0x02 (Disk Read)
  mov     al, N_SECTORS ; Number of sectors to read (0x01 .. 0x80)
  mov     ch, 0         ; Track/Cyinder number
  mov     cl, 2         ; Sector number
  mov     dh, 0         ; Head number
  mov     dl, 0         ; Drive number (floppy = 0)

  ; Pointer for disk-read function to write to (ES:BX)
  mov     bx, 0x0
  mov     es, bx
  mov     bx, 0x9000

  ; Call the disk-read function and check for errors
  int     0x13           ; BIOS interrupt
  jc      disk_error     ; if error (stored in the carry bit)
  mov     dh, N_SECTORS  ; Number of sectors that should have been read.
  cmp     al, dh         ; AL holds the number of sectors actually read.
  jne sectors_error
  jmp done

disk_error:
  mov si, BL_DISK_ERR
  call bl_print
  jmp hault

sectors_error:
  mov si, BL_SECTORS_ERR
  call bl_print
  jmp hault

hault:
  jmp $ ; loop forever

done:
  mov si, BL_DONE_STR
  call bl_print
  jmp 0x9000 - 0x7c00

  
; -----------| Boot Signature |-----------
  times 510-($-$$) db 0	; Pad remainder of the boot sector
	dw 0xAA55		          ; Boot Signature (The last 2 bytes of the 512-byte boot sector must be 0XAA55)

; -----------| Kernel |-----------
  

  
  mov ah, 0x0e
  mov al, '!'
  int 0x10