No history yet

Bootloader Development

The First 512 Bytes

When you press the power button on a computer, it doesn't magically load your operating system. The first piece of software to run is the , or Basic Input/Output System. Stored on a chip on the motherboard, its job is to initialize the hardware and then find something to boot. The BIOS scans storage devices like hard drives and floppy disks, looking for a very specific pattern: a special 16-bit number, 0xAA55, located at the very end of the disk's first 512-byte sector. This is the boot signature. If the BIOS finds this signature, it assumes the device is bootable. It then copies that entire 512-byte sector into memory at the physical address 0x7C00 and instructs the CPU to start executing the code it just loaded. That tiny 512-byte program is our bootloader.

Lesson image

This 512-byte limit is a strict constraint. Our initial code, known as the 'Stage 1' bootloader, must fit entirely within this space, including the boot signature. Its primary job is simple but crucial: load a larger, more capable program (our 'Stage 2' loader or kernel) from the disk into memory.

Waking Up in Real Mode

The CPU starts in a 16-bit environment called . In this mode, memory isn't accessed with a single, straightforward address. Instead, it uses a system called segmented memory. An address is formed by combining a 16-bit segment value with a 16-bit offset. The physical address is calculated as: PhysicalAddress=(Segment×16)+OffsetPhysicalAddress = (Segment \times 16) + Offset. Since multiplying by 16 is the same as a bitwise shift left by 4, this is often written as (Segment<<4)+Offset(Segment << 4) + Offset. This scheme allowed 16-bit registers to address 1 MB of memory (2202^{20} bytes). The CPU has several segment registers: CS (Code Segment), DS (Data Segment), ES (Extra Segment), and SS (Stack Segment). When our bootloader begins at 0x7C00, the BIOS has already set CS to 0x0000 and the instruction pointer IP to 0x7C00, but other segment registers can be unpredictable. A good first step is to normalize them, for instance by setting DS and ES to 0.

Before we can call functions or use the stack, we must set it up. The stack is a region of memory used for storing temporary data, like return addresses when a function is called. We define the stack by loading the SS register with the segment of our desired stack area and the SP (Stack Pointer) register with an offset. The stack grows downwards in memory, so we typically point SP to the top of the allocated stack region.

; Example of setting up segments and stack
org 0x7C00      ; Tell assembler our code is loaded at this address

mov ax, 0       ; Can't move 0 directly into DS
mov ds, ax      ; Set Data Segment to 0
mov es, ax      ; Set Extra Segment to 0

mov ss, ax      ; Stack Segment at 0
mov sp, 0x7C00  ; Stack Pointer starts just below our code

Talking to the BIOS

Our bootloader can't do everything from scratch. It relies on services provided by the BIOS, which are accessed through interrupts. An interrupt is a signal to the CPU to pause its current task and run a special function, called an Interrupt Service Routine (ISR). To call one, we load specific values into CPU registers to tell the BIOS what we want, and then execute the int instruction.

For example, to print a character to the screen, we use BIOS interrupt 10h. The specific function for 'Teletype output' is 0x0E, which is placed in the AH register. The character to print goes in the AL register.

; Print the character 'H'
mov ah, 0x0E    ; Function 0Eh: Teletype output
mov al, 'H'     ; Character to print
int 0x10        ; Video services interrupt

The most important service for our bootloader is reading from the disk. This is done with interrupt 13h. To use it, we need to specify several parameters:

  • ah = 0x02: The function for reading disk sectors.
  • al: The number of sectors to read.
  • ch: The cylinder number.
  • cl: The sector number to start from (sectors are 1-indexed).
  • dh: The head number.
  • dl: The drive number (e.g., 0x00 for the first floppy, 0x80 for the first hard disk).
  • es:bx: The memory address (Segment:Offset) where the loaded data should be stored.

If int 13h fails, the carry flag (CF) will be set, and the AH register will contain an error code. It's crucial to check this flag to handle potential disk read errors.

Putting It All Together

Now we can write a complete Stage 1 bootloader. Its goals are:

  1. Set up the segment registers and stack.
  2. Print a message to the screen to show it's running.
  3. Use int 13h to load our 'Stage 2' from the disk into a new memory location (e.g., 0x8000).
  4. Jump to the newly loaded code.

Finally, we must ensure the total compiled code is padded to 512 bytes and ends with the 0xAA55 signature. The times and dw directives in NASM are perfect for this.

; A minimal Stage 1 Bootloader
org 0x7C00

mov bp, 0x8000      ; Set up stack base pointer
mov sp, bp          ; Set up stack pointer

mov bx, HELLO_MSG   ; Load address of message
call print_string   ; Call our string printing function

; ... code to load Stage 2 using int 13h would go here ...

jmp $ ; Infinite loop for now

; Function to print a string
print_string:
    mov ah, 0x0E
.loop:
    mov al, [bx]
    cmp al, 0
    je .done
    int 0x10
    inc bx
    jmp .loop
.done:
    ret

HELLO_MSG: db 'Bootloader running...', 0

; Pad to 510 bytes, then add the boot signature
times 510-($-$$) db 0
dw 0xAA55

This simple program establishes a foundation. It takes control from the BIOS, prepares a basic execution environment, and has the logic needed to load the next, more complex part of our operating system. With this step complete, we are no longer just running on bare metal; we are beginning to build a system.