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:
- Keeping track of my progress
- Testing this site’s ability to render syntax highlighted code
- 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.