This article explains how to write a very simple x86 bootloader script which can be used as the entry point to running a custom operating system. It assumes some knowledge of assembly, but hopefully all required knowledge should be available within this stand-alone article.
When an x86 computer turns on, it executes firmware located in motherboard ROM.
There are two main firmware standards:
This article will describe in detail how to write a BIOS bootloader, and does not discuss UEFI.
When an x86 cpu boots, the BIOS is loaded from firmware into memory. It performs various operations such as RAM detection, and other hardware detection/initializations - before finally attempting its boot sequence. During the boot sequence, the BIOS attempts to find a boot disk which will load the operating system - via a bootstrap program.
The BIOS generally checks for bootable disks in a specific order. This is potentially user-configurable, known as its bootdisk hierarchy. For instance, checking in the order: floppy disks, CD-ROM drive, then the first hard drive. The BIOS may handle each disk medium differently. For floppy disks the first 512 bytes are read into memory at a specific location, but extra steps may be required for hard drives which contain master boot record information, and CD-ROMs can be loaded entirely into memory and used as a RAM disk. Regardless of medium, the bootloader script will eventually be loaded at address 0x7C00.
As the BIOS iterates through the disk hierarchy it attempts find the first readable 512 bytes (called its boot sector) which ends with the magic number 0xAA55. Once found, the BIOS now gives control to the code which has been copied at address location 0x7C00.
Why the magic number 0xAA55? This the binary equivalent: 101010100101010. This may also be used to determine if your system is big endian or little endian - as it will read as either 0xAA55 or 0x55AA.
When the BIOS hands over control to your bootloader, the CPU is in 16-bit Real Mode, and the program counter will be running at physical address 0x7c00. Real mode was the only mode before the 80286 Intel processor which introduced protected mode. All processors initially run in real mode, for backwards compatibility purposes.
In real mode you can:
In protected mode you can:
The following examples are written using the nasm assembler.
apt-get install nasm
Create a new file called boot.asm
:
; Simple bootloader example for x86 systems that should print out a simple message to the user
bits 16 ; We're dealing with 16 bit code
org 0x7c00 ; Inform the assembler of the starting location for this code
boot:
mov si, message ; Point SI register to message
mov ah, 0x0e ; Set higher bits to the display character command
.loop:
lodsb ; Load the character within the AL register, and increment SI
cmp al, 0 ; Is the AL register a null byte?
je halt ; Jump to halt
int 0x10 ; Trigger video services interrupt
jmp .loop ; Loop again
halt:
hlt ; Stop
message:
db "Howdy!", 0
; Mark the device as bootable
times 510-($-$$) db 0 ; Add any additional zeroes to make 510 bytes in total
dw 0xAA55 ; Write the final 2 bytes as the magic number 0x55aa, remembering x86 little endian
You can compile the above assembly with:
nasm -f bin boot.asm -o boot.bin
At the heart of the above bootloader are various calls to the BIOS to request printing characters to the screen. A simplified example of printing to the screen can be shown:
mov ah, 0x0e ; Set higher bits to the display character command
mov al, 'a' ; Set the lower bits to our character
int 0x10 ; Call BIOS video service interrupt, which will output 'a'
It's possible to write the compiled binary above to a disk, and attach it to a real computer. It can instead be easier to make use of x86 emulators, such as Bochs or Qemu. These emulators will do exactly everything that a real computer would do, but everything is simulated using software instead of hardware.
Firstly install qemu:
apt-get install qemu qemu-kvm
Within the same directory as your code:
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -fda boot.bin
After running you should see the simple message appear:
Firstly install bochs:
apt-get install bochs bochs-x
Within the same directory as your code, create a file bochsrc.txt
:
megs: 32
romimage: file=/usr/share/bochs/BIOS-bochs-latest, address=0xfffe0000
vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
floppya: 1_44=boot.bin, status=inserted
boot: a
log: bochsout.txt
logprefix: %t-%e-@%i-%d
mouse: enabled=0
display_library: x, options="gui_debug"
Now you can run:
nasm -f bin boot.asm -o boot.bin
bochs
After running you should see the simple message appear:
To enter protected mode you must:
CR0
The lgdt
instruction takes a pointer to a structure in memory that is composed of two parts:
Each entry within the GDT is 8-bytes. A simple overview can be found on the osdev wiki - Global Descriptor Table
Register names
Global Descriptor Table
Interrupt Descriptor Table
String instructions
S
stands for String - MOVS, LODS, STOS, CMPS, SCAS.BIOS Calls
int 0x10
Labels
name:
.
is local to the above global label, i.e. .loop
Pseudo-instructions
$
evaluates to the assembly position at the beginning of the line containing the expression$$
evaluates to the beginning of the current section; so you can tell how far into the section you are by using ($-$$)