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 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).
; 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