I am writing my own x86_64 operating system named Lavender. This is a sample of the bootloader, a two-stage Real Mode loader that first loads the rest of itself into memory and then does the standard things like setup the GDT, IDT, get native video mode information, etc.
This post, however, is JUST about the first stage of the bootloader. As it is literally THE first thing that runs on my operating system's boot, I want to get it right.
Is the below NASM assembly satisfactory? I am accepting any kind of criticism.
; This file provides the stage zero bootloader that will basically just load the
; second stage bootloader, which will load the kernel and other information.
; Author: Michael Sermir
;
; Copyright (c) 2026 the LavenderOS Project
; This program is free software: you can redistribute it and/or modify it under
; the terms of the GNU General Public License as published by the Free Software
; Foundation, either version 3 of the License, or (at your option) any later
; version.
;
; This program is distributed in the hope that it will be useful, but WITHOUT
; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License along with
; this program. If not, see <https://www.gnu.org/licenses/>.
; The BIOS will spit us out at address 0x7C00 in the wonderful 16-bit Real Mode.
org 0x7C00
bits 16
start:
.reset:
mov ah, 0
mov dh, 0
jnc .load
; Use AL as a failure counter so we can give the hardware some grace on
; this error-prone operation.
inc al
cmp al, 0x05
jne .reset
mov si, reset_failure_string
jmp fail
.load:
xor ax, ax ; Load the disk at 0x0000 (the current segment).
mov bx, 0x7E00 ; Load the disk at offset 0x7E00 (right after this program).
mov es, ax ; Move our immediate address (AX) into the Extra Segment.
mov ah, 0x02 ; "Read disk".
mov al, 0x05 ; Number of sectors to read.
mov ch, 0 ; Cylinder number.
mov cl, 0x02 ; Sector number to begin at.
mov dh, 0 ; Head number.
mov dl, 0 ; Drive number.
int 0x13
jnc .execute
mov si, load_failure_string
jmp fail
.execute:
jmp 0x0000:0x7E00 ; Jump to the second stage.
; NOTE: This routine prints the return code in reverse order, beginning with the
; smallest digit.
fail:
mov dl, ah ; Save the return code so we can print it in a second.
mov cl, 0xA ; Move 10 into CL so we can divide by it to print the error.
mov ah, 0x0E ; Teletype character.
xor bx, bx ; Page 0 and no background color.
.write_loop:
lodsb ; Load character into AL.
or al, al ; Check AL for NUL terminator.
jz .write_error_loop
int 0x10
jmp .write_loop
.write_error_loop:
xor ax, ax ; Clear AX to remove garbage data.
mov al, dl ; Move our return code to the low operand register.
div cl ; Divide by 10 (0xA).
add ah, 0x30 ; Add '0' to the produced digit to convert to ASCII.
mov dl, al ; Move our return back into storage.
mov al, ah ; Move the digit back into the character output register.
mov ah, 0x0E
int 0x10
; Check to see if we've consumed the whole return code.
or dl, dl
jnz .write_error_loop
cli
hlt
load_failure_string: db "DISK READ FAIL ", 0
reset_failure_string: db "DISK RESET FAIL ", 0
; Ensure the boot signature bookends the bootloader so the bootloader recognizes
; the sector as bootable.
times 510-($-$$) db 0
dw 0xAA55