A simple bootloader and kernel!

(Created 2026-03-20)

Some time ago, around February 8th of this year, I decided to try writing a simple bootloader in assembly, along with a dummy kernel in C. Here’s what I managed.

I am putting it here for a few reasons:

  1. Keeping track of my progress
  2. Testing this site’s ability to render syntax highlighted code
  3. It might help someone

In any case, here’s the bootloader itself, in ASM (src/boot.asm):

bits 16
org 0x7C00
global start

start:
	; Setup stack and register
	xor ax,ax
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov sp,0x7C00

	; Set video mode
	mov ah,0
	mov al,3
	int 0x10

	mov dl, 0
	mov dh, 0
	; Print boot message
	mov bp, boot_msg
	mov cx, boot_msg_len
	call print

	; Print the time to the user
	mov bp, time_msg
	mov cx, time_msg_len
	call print

	; Get current time
	clc
	mov ah, 0x02
	int 0x1A

	mov al, ch
	call print_bcd
	mov al, ':'
	int 0x10
	mov al, cl
	call print_bcd
	mov al, ':'
	int 0x10
	mov al, dh
	call print_bcd

	; Prepare to load kernel from disk
	; Print loading message
	mov ah, 0x03
	int 0x10
	mov bp, ld_krnl_msg
	mov cx, ld_krnl_msg_len
	call print

	; Reset disk system
	mov ah, 0x00
	mov dl, 0x80
	int 0x13
	call chk_dsk_op

	; Check for LBA support
	mov ah, 0x41
	mov bx, 0x55AA
	mov dl, 0x80
	int 0x13
	call chk_dsk_op

	; Finally, begin reading the kernel in memory
	mov si, dskap
	mov ah, 0x42
	mov dl, 0x80
	int 0x13
	call chk_dsk_op
	cmp ah, 0x00
	jne critical_err

	; Kernel is finally loaded, ready to jump
	mov ah, 0x03
	int 0x10
	mov bp, krnl_loaded_msg
	mov cx, krnl_loaded_msg_len
	call print

	; setup GDT
	lgdt [gdt_descriptor]
	
	; Disable interrupts
	cli	

	; enable protected mode
	mov eax, cr0
	or eax, 1
	mov cr0, eax

	jmp 0x08:pm_start  ; far jump to code segment

halt:
	hlt
	jmp halt

critical_err:
	mov ah, 0x03
	int 0x10
	mov bp, exit_err_msg
	mov cx, exit_err_msg_len
	call print_err
	jmp halt

; PRINT STRING HELPER
; BP = Message location
; CX = Message length
; DH = Row
; DL = Column
print:
	mov bx, 0x00
	mov ah, 0x13
	mov al, 0x01
	mov bh, 0x00
	mov bl, 0x03
	int 0x10
	ret

; PRINT ERROR
; BP = Message location
; CX = Message length
; DH = Row
; DL = Column
print_err:
	push bp
	push cx
	mov ah, 0x03
	int 0x10
	pop cx
	pop bp

	mov al, 0x01
	mov bh, 0x00
	mov bl, 0x4F
	mov ah, 0x13
	int 0x10
	ret

; PRINT PACKED BCD
; AL = BCD value
print_bcd:
	aam 16
	add ax, 0x3030
	push ax
	mov al, ah
	mov ah, 0x0E
	int 0x10
	pop ax
	mov ah, 0x0E
	int 0x10
	ret

; PRINT 8 BIT HEX NUMBER
; AL = Number
print_hex:
	mov bl,2
print_hex_loop:
	push ax
	and al,0xF0
	shr al,4
	cmp al,9
	jle add_48
	jg add_55
add_48:
	add al,48
	mov ah,0x0E
	int 0x10
	jmp add_exit
add_55:
	add al,55
	mov ah,0x0E
	int 0x10
add_exit:
	pop ax
	shl al,4
	dec bl
	jnz print_hex_loop
	mov al,'h'
	mov ah,0x0E
	int 0x10
	ret

; CHECK LAST DISK OPERATION
; DL = Drive
; Returns:
; AH = Status code
chk_dsk_op:
	mov bx, 0x00
	mov ah, 0x01
	int 0x13
	jnc exit_chk_dsk_op
	push ax
	push ax
	mov bp, dsk_error_msg
	mov cx, dsk_error_msg_len
	call print_err
	pop ax
	mov al, ah
	call print_hex
	pop ax
exit_chk_dsk_op:
	ret

boot_msg db 'Starting boot...'
boot_msg_len equ $-boot_msg
time_msg db 13,10,'Current time: '
time_msg_len equ $-time_msg

ld_krnl_msg db 13,10,10,'Loading kernel...'
ld_krnl_msg_len equ $-ld_krnl_msg

krnl_loaded_msg db 13,10,10,'Kernel loading complete! ^-^'
krnl_loaded_msg_len equ $-krnl_loaded_msg

dsk_error_msg db 13,10,'Disk error: '
dsk_error_msg_len equ $-dsk_error_msg 

exit_err_msg db 13,10,'Exiting.'
exit_err_msg_len equ $-exit_err_msg

dskap:  db 0x10, 0	
	dw 1		; Read 1 sectors
	dw 0x7E00	; Write to this address
	dw 0		; Memory page 0
	dd 1, 0 	; LBA start address

gdt_start:
	dq 0
	dq 0x00CF9A000000FFFF ; code
	dq 0x00CF92000000FFFF ; data
gdt_end:

gdt_descriptor:
	dw gdt_end - gdt_start - 1
	dd gdt_start

bits 32
pm_start:
	mov ax, 0x10
	mov ds, ax
	mov es, ax
	mov fs, ax
	mov gs, ax
	mov ss, ax
	mov esp, 0x9E00

	jmp 0x08:0x7E00

times 510 - ($ - $$) db 0
db 0x55, 0xAA

The protected mode ‘kernel’ (src/kernel.c):

#include <stdint.h>

#define VGA_MEMORY 0xB8000
#define WHITE_ON_BLACK 0x0F

void kmain() {
    volatile uint16_t *vga = (uint16_t*)VGA_MEMORY;
    const char *str = "Hello World from protected mode!";
    for (int i = 0; str[i] != 0; i++) {
        vga[i+80] = ((uint16_t)WHITE_ON_BLACK << 8) | str[i];
    }

    // halt CPU
    while (1) { __asm__ volatile ("hlt"); }
}

The linker script for tying everything together (src/link.ld):

ENTRY(kmain)

SECTIONS
{
    . = 0x7E00;

    .text : {
        *(.text*)
    }

    .rodata : {
        *(.rodata*)
    }

    .data : {
        *(.data*)
    }

    .bss : {
        *(.bss*)
        *(COMMON)
    }
}

And finally, the Makefile (build/Makefile):

.PHONY: build

build:
	nasm -f bin ../src/boot.asm -o boot.bin
	i686-elf-gcc -ffreestanding -m32 -c ../src/kernel.c -o kernel.o
	i686-elf-ld -m elf_i386 -T ../src/link.ld kernel.o -o kernel.elf
	cat boot.bin kernel.bin > os.img
	i686-elf-objcopy -O binary kernel.elf kernel.bin

run:
	qemu-system-i386 os.img -rtc base=localtime

Should this have been a github repo? I’m not sure, but I don’t think it’s worth it.