Axiom devlog #1: from boot.s to kernel_main
From a multiboot header to a prompt: the path through boot.s, the GDT, IDT, PIC and serial port, and the single sti that turns interrupts on.
Axiom is a hobby kernel for 32-bit x86, written in C with two small assembly files. Its banner currently reads AXIOM OS v0.2. This entry follows the boot path in the order the code runs it: from the multiboot header in boot.s to the prompt that kernel_main leaves on screen.
Build and run
Everything is built with an i686-elf cross compiler, freestanding, with no C library.
CC = i686-elf-gcc
AS = i686-elf-as
CFLAGS = -ffreestanding -O2 -Wall -Wextra -I include
LDFLAGS = -T linker.ld -ffreestanding -nostdlib -lgccmake run-kernel hands the ELF straight to QEMU, with the serial port wired to the terminal:
$ makei686-elf-as src/boot.s -o build/boot.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/io.c -o build/io.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/serial.c -o build/serial.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/terminal.c -o build/terminal.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/keyboard.c -o build/keyboard.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/gdt.c -o build/gdt.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/idt.c -o build/idt.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/pic.c -o build/pic.oi686-elf-as src/interrupt.s -o build/interrupt.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/string.c -o build/string.oi686-elf-gcc -ffreestanding -O2 -Wall -Wextra -I include -c src/kernel.c -o build/kernel.oi686-elf-gcc -T linker.ld -ffreestanding -nostdlib -lgcc build/boot.o build/io.o build/serial.o build/terminal.o build/keyboard.o build/gdt.o build/idt.o build/pic.o build/interrupt.o build/string.o build/kernel.o -o kernel.elf$ make run-kernelqemu-system-i386 -kernel kernel.elf -serial stdio -display cocoa,zoom-to-fit=onSerial initialized.That last line is the kernel talking over COM1, and it is all the serial port says for now. The rest goes to the screen.
The multiboot header
A multiboot loader (GRUB, or QEMU’s -kernel) scans the start of the image for a magic number. boot.s provides it, along with a stack.
.set ALIGN, 1<<0
.set MEMINFO, 1<<1
.set FLAGS, ALIGN | MEMINFO
.set MAGIC, 0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS)
.section .multiboot
.long MAGIC
.long FLAGS
.long CHECKSUM
.section .text
.global _start
.extern kernel_main
_start:
mov $stack_top, %esp
call kernel_main
hang:
hlt
jmp hang
.section .bss
.align 16
stack_bottom:
.skip 16384
stack_top:The three words must add up to zero, which is what CHECKSUM guarantees. FLAGS asks for modules aligned on page boundaries and for a memory map. The loader has to find the header within the first 8 KiB of the file, so the linker script puts .multiboot at the front of .text and loads the kernel at 1 MiB:
. = 1M;
.text : {
*(.multiboot)
*(.text)
} :text_start does the least it can: point %esp at the top of a 16 KiB stack reserved in .bss, and call C. If kernel_main ever returns, the CPU parks in a hlt loop.
kernel_main, in order
kernel_main reads like a boot log, because it prints one.
terminal_write("Initializing GDT... ");
gdt_init();
terminal_write("OK\n");
terminal_write("Initializing IDT... ");
idt_init();
terminal_write("OK\n");
terminal_write("Initializing PIC... ");
pic_init();
terminal_write("OK\n");
serial_init();
serial_print("Serial initialized.\n");
terminal_write("Enabling interrupts... ");
__asm__ volatile("sti");
terminal_write("OK\n\n");Text goes straight into VGA memory at 0xB8000, one 16-bit cell per character, attribute byte 0x0A for light green on black. This is the screen QEMU shows once the sequence completes:

A flat GDT
Three descriptors: the mandatory null entry, then code and data segments that both span the whole 4 GiB address space.
void gdt_init(void) {
gp.limit = sizeof(gdt) - 1;
gp.base = (unsigned int)&gdt;
gdt_set_entry(0, 0, 0, 0, 0);
gdt_set_entry(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
gdt_set_entry(2, 0, 0xFFFFFFFF, 0x92, 0xCF);0x9A is a present, ring 0, executable and readable segment; 0x92 is present, ring 0, writable data. 0xCF sets 4 KiB granularity and 32-bit operands. After lgdt, the data segment registers are loaded with 0x10 (entry 2), and a far jump, ljmp $0x08, $1f, reloads CS with entry 1, since CS cannot be moved into like the others.
One gate in the IDT
The IDT has room for 256 vectors. One is filled in.
idt_set_entry(0x21, (unsigned int)keyboard_handler_int, 0x08, 0x8E);Vector 0x21 points at the keyboard stub through the code selector 0x08. The 0x8E flags make it a present, ring 0, 32-bit interrupt gate, so the CPU clears the interrupt flag on entry.
Remapping the PIC
As the BIOS leaves them, the master 8259 PIC delivers IRQs 0 to 7 on vectors 8 to 15, which collide with CPU exceptions. pic_init moves the master to 0x20 and the slave to 0x28, which puts IRQ 1, the keyboard, on vector 0x21: the one gate in the IDT.
outb(PIC_SLAVE_CMD, ICW1_INIT | ICW1_ICW4);
outb(PIC_MASTER_DATA, 0x20);
outb(PIC_SLAVE_DATA, 0x28);
outb(PIC_MASTER_DATA, 0x04);
outb(PIC_SLAVE_DATA, 0x02);
outb(PIC_MASTER_DATA, ICW4_8086);
outb(PIC_SLAVE_DATA, ICW4_8086);
outb(PIC_MASTER_DATA, 0xFD);
outb(PIC_SLAVE_DATA, 0xFF);The final masks leave exactly one line open: 0xFD clears only bit 1 on the master, and 0xFF silences the slave entirely.
COM1 for logs
serial_init programs the UART at 0x3F8: interrupts off, divisor 3 for 38400 baud, 8 data bits with no parity and one stop bit, FIFOs on. serial_write then polls bit 0x20 of the line status register, “transmit holding register empty”, before each byte.Polling keeps the driver tiny, and serial output keeps working even when interrupts are misbehaving.
sti, then the keyboard
With the IDT loaded and the PIC remapped, sti is safe. When a key is pressed, IRQ 1 arrives on vector 0x21 and lands in the assembly stub:
keyboard_handler_int:
pusha
push %ds
push %es
push %fs
push %gs
mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
cld
call keyboard_handler
pop %gs
pop %fs
pop %es
pop %ds
popa
iretkeyboard_handler reads the scancode from port 0x60, tracks both shift keys, maps the code through one of two tables, pushes the character into a 256-byte ring buffer and sends end-of-interrupt with outb(0x20, 0x20). On the other side, keyboard_getchar executes hlt until the buffer has something, which is how the “Enter your name” loop waits without spinning.
Checkpoint
CheckpointAXIOM OS v0.2
Shipped 6
- Multiboot boot, from an ISO or straight from
qemu -kernel - Flat GDT: null, code and data segments
- IDT with the keyboard gate at
0x21 - PIC remapped to
0x20and0x28, only IRQ 1 unmasked - COM1 serial output
- Keyboard input with shift and backspace, echoed to VGA
Broke 2
terminal_putcharnever checksrowagainstVGA_HEIGHT, so there is no scrolling- No handlers for CPU exceptions: any fault ends in a triple fault
Next 2
- Gates for exceptions
0x00to0x1F - Scrolling in the VGA terminal