Skip to main content
Aeolos follows a structured initialization sequence when booting. The kernel receives control from the Stivale2 bootloader and proceeds through multiple stages to bring the system to a running state.

Boot Entry Point

The kernel entry point is kmain() in kernel/kmain.c:47. This function receives boot information from the Stivale2 bootloader and coordinates all system initialization.
kernel/kmain.c
_Noreturn void kmain(stivale2_struct* info)
{
    // convert the physical address to a virtual one, since we will be removing identity mapping later
    bootinfo = (stivale2_struct*)PHYS_TO_VIRT(info);

    // some info
    klog_printf("Aeolos x86_64 (alpha)\n");
    klog_printf("Built on "__DATE__" at "__TIME__".\n\n");

    idt_init();
    cpu_features_init();

    // system initialization
    pmm_init((stv2_struct_tag_mmap*)stv2_find_struct_tag(bootinfo, STV2_STRUCT_TAG_MMAP_ID));
    vmm_init();
    gdt_init();

    // initialize framebuffer and terminal
    fb_init((stv2_struct_tag_fb*)stv2_find_struct_tag(bootinfo, STV2_STRUCT_TAG_FB_ID));
    serial_init();
    term_init();

    // further system initialization
    acpi_init((stv2_struct_tag_rsdp*)stv2_find_struct_tag(bootinfo, STV2_STRUCT_TAG_RSDP_ID));
    hpet_init();
    apic_init();
    vfs_init();
    smp_init();

    // since we do not need the bootloader info anymore
    pmm_reclaim_bootloader_mem();

    // initialize multitasking
    sched_init(kinit);

    while (true)
        ;
}

Initialization Sequence

The system initializes in this specific order:
1

IDT Initialization

Sets up the Interrupt Descriptor Table to handle CPU exceptions and interrupts
2

CPU Feature Detection

Detects and enables CPU features like SSE, AVX, etc.
3

Memory Management

Initializes the Physical Memory Manager (PMM) and Virtual Memory Manager (VMM)
4

GDT Setup

Configures the Global Descriptor Table for memory segmentation
5

Display and I/O

Initializes framebuffer, serial port, and terminal
6

ACPI and Timers

Discovers hardware through ACPI and sets up HPET timer
7

APIC Configuration

Configures the Advanced Programmable Interrupt Controller
8

Filesystem

Mounts the initial ramfs filesystem
9

SMP Initialization

Brings up additional CPU cores
10

Scheduler

Starts the task scheduler with the first kernel task

Global Descriptor Table (GDT)

The GDT is reloaded during initialization to replace the bootloader’s temporary GDT.
The GDT setup is handled in kernel/sys/gdt.c:5:
kernel/sys/gdt.c
void gdt_init()
{
    gdt_t* gdt = (gdt_t*)kmalloc(sizeof(gdt_t));
    *gdt = (gdt_t) {
        .entry_null = GDT_ENTRY_NULL,
        .entry_kcode = GDT_ENTRY_KERNEL_CODE,
        .entry_kdata = GDT_ENTRY_KERNEL_DATA,
        .entry_ucode = GDT_ENTRY_USER_CODE,
        .entry_udata = GDT_ENTRY_USER_DATA
    };

    struct gdtr g = { .base = (uint64_t)gdt, .limit = sizeof(gdt_t) - 1};
    asm volatile("lgdt %0;"
                 "pushq $0x08;"
                 "pushq $reload_sr;"
                 "lretq;"
                 "reload_sr:"
                 "movw $0x10, %%ax;"
                 "movw %%ax, %%ds;"
                 "movw %%ax, %%es;"
                 "movw %%ax, %%ss;"
                 "movw %%ax, %%fs;"
                 "movw %%ax, %%gs;"
                 :
                 : "g"(g)
                 :);
}
The GDT contains five main segments:
  • Null segment (required by x86_64)
  • Kernel code segment (0x08)
  • Kernel data segment (0x10)
  • User code segment (0x18)
  • User data segment (0x20)
  • TSS (Task State Segment) - installed later per-CPU

TSS Installation

Each CPU core has its own Task State Segment, installed via gdt_install_tss() at kernel/sys/gdt.c:33:
kernel/sys/gdt.c
void gdt_install_tss(tss_t* tss)
{
    struct gdtr gdtr;
    asm volatile("sgdt %0"
                 :
                 : "m"(gdtr)
                 : "memory");

    gdt_t* gdt = (gdt_t*)(gdtr.base);
    uint64_t baseaddr = (uint64_t)tss;
    uint64_t seglimit = sizeof(tss_t);

    gdt->tss.base_addr_0_15 = baseaddr & 0xFFFF;
    gdt->tss.base_addr_16_23 = (baseaddr >> 16) & 0xFF;
    gdt->tss.base_addr_24_31 = (baseaddr >> 24) & 0xFF;
    gdt->tss.base_addr_32_63 = baseaddr >> 32;
    gdt->tss.seg_limit_0_15 = seglimit & 0xFFFF;
    gdt->tss.flags_low = 0x89;
    gdt->tss.flags_high = 0;

    asm volatile("mov $0x28, %%ax;"
                 "ltr %%ax"
                 :
                 :
                 : "ax");
}

ACPI Initialization

ACPI initialization discovers system hardware through standardized tables. The initialization is in kernel/sys/acpi/acpi.c:40:
kernel/sys/acpi/acpi.c
void acpi_init(stv2_struct_tag_rsdp* rsdp_info)
{
    rsdp_t* rsdp = (rsdp_t*)PHYS_TO_VIRT(rsdp_info->rsdp);

    if (rsdp->revision == 2) {
        klog_info("v2.0 detected\n");
        xsdt = (acpi_sdt*)PHYS_TO_VIRT(rsdp->xsdt_addr);
        xsdt_present = true;
    } else {
        klog_info("v1.0 detected\n");
        rsdt = (acpi_sdt*)PHYS_TO_VIRT(rsdp->rsdt_addr);
        xsdt_present = false;
    }

    madt_init();
    klog_ok("done\n");
}
ACPI tables can be retrieved using acpi_get_sdt() with a 4-character signature:
kernel/sys/acpi/acpi.c
acpi_sdt* acpi_get_sdt(const char* sign)
{
    if (xsdt_present) {
        uint64_t len = (xsdt->hdr.length - sizeof(acpi_sdt)) / sizeof(uint64_t);
        for (uint64_t i = 0; i < len; i++) {
            acpi_sdt* table = (acpi_sdt*)PHYS_TO_VIRT(((uint64_t*)xsdt->data)[i]);
            if (memcmp(table->hdr.sign, sign, sizeof(table->hdr.sign))) {
                klog_info("found SDT \"%s\"\n", sign);
                return table;
            }
        }
    } else {
        // Similar logic for RSDT (32-bit addresses)
    }
    return NULL;
}

MADT Parsing

The Multiple APIC Description Table (MADT) provides information about APICs and CPU cores. It’s parsed in kernel/sys/acpi/madt.c:22:
kernel/sys/acpi/madt.c
void madt_init()
{
    madt = (madt_t*)acpi_get_sdt(SDT_SIGN_MADT);

    if (!madt)
        kernel_panic("MADT not found\n");

    uint64_t size = madt->hdr.length - sizeof(madt_t);
    for (uint64_t i = 0; i < size;) {
        madt_record_hdr* rec = (madt_record_hdr*)(madt->records + i);
        switch (rec->type) {

        case MADT_RECORD_TYPE_LAPIC: {
            if (num_lapic >= CPU_MAX)
                break;

            madt_record_lapic* lapic = (madt_record_lapic*)rec;
            lapics[num_lapic++] = lapic;
        } break;

        case MADT_RECORD_TYPE_IOAPIC: {
            if (num_ioapic > 2)
                break;

            madt_record_ioapic* ioapic = (madt_record_ioapic*)rec;
            io_apics[num_ioapic++] = ioapic;
        } break;
        }
        i += rec->len;
    }
}

HPET Timer

The High Precision Event Timer provides nanosecond-accurate timekeeping. Initialization is in kernel/sys/hpet.c:34:
kernel/sys/hpet.c
void hpet_init()
{
    hpet_sdt_t* hpet_sdt = (hpet_sdt_t*)acpi_get_sdt(SDT_SIGN_HPET);
    if (!hpet_sdt)
        kernel_panic("HPET not found\n");

    // map the hpet registers
    uint64_t hpet_phys = hpet_sdt->base_addr.address;
    vmm_map(NULL, PHYS_TO_VIRT(hpet_phys), hpet_phys, 1, VMM_FLAGS_MMIO);
    hpet_regs = (void*)PHYS_TO_VIRT(hpet_phys);

    // get time period in nanoseconds
    hpet_period = (hpet_read_reg(HPET_REG_GEN_CAP_ID) >> 32) / 1000000;

    // enable the counter
    hpet_write_reg(HPET_REG_GEN_CONF, hpet_read_reg(HPET_REG_GEN_CONF) | HPET_FLAG_ENABLE_CNF);
}
The HPET provides two key functions:
  • hpet_get_nanos() - Returns current time in nanoseconds
  • hpet_nanosleep(uint64_t nanos) - Busy-waits for specified duration

APIC Initialization

The Advanced Programmable Interrupt Controller handles interrupts in modern x86 systems. Initialization is in kernel/sys/apic/apic.c:44:
kernel/sys/apic/apic.c
void apic_init()
{
    lapic_base = (void*)PHYS_TO_VIRT(madt_get_lapic_base());
    vmm_map(NULL, (uint64_t)lapic_base, VIRT_TO_PHYS(lapic_base), 1, VMM_FLAGS_MMIO);

    // initialize the spurious interrupt register
    idt_set_handler(APIC_SPURIOUS_VECTOR_NUM, spurious_int_handler, false);
    apic_enable();

    // initialize the apic timer
    apic_timer_init();
}

APIC Timer

The APIC timer is calibrated using the HPET at kernel/sys/apic/timer.c:70:
kernel/sys/apic/timer.c
void apic_timer_init()
{
    // allocate a vector for the timer
    vector = idt_get_vector();
    idt_set_handler(vector, &apic_timer_handler, false);

    // unmask the apic timer interrupt and set divisor to 4
    apic_write_reg(APIC_REG_TIMER_LVT, APIC_TIMER_FLAG_MASKED | vector);
    apic_write_reg(APIC_REG_TIMER_DCR, 0b0001);
    divisor = 4;

    // calibrate the timer
    apic_write_reg(APIC_REG_TIMER_ICR, UINT32_MAX);
    hpet_nanosleep(MILLIS_TO_NANOS(500));
    base_freq = ((UINT32_MAX - apic_read_reg(APIC_REG_TIMER_CCR)) * 2) * divisor;
}
The calibration works by:
  1. Setting the APIC timer to maximum count
  2. Waiting 500ms using the HPET
  3. Reading how much the counter decreased
  4. Calculating the base frequency from the difference

First Kernel Task

After scheduler initialization, the first kernel task kinit() runs at kernel/kmain.c:23:
kernel/kmain.c
void kinit(tid_t tid)
{
    (void)tid;
    klog_show();
    klog_ok("first kernel task started\n");

    initrd_init((stv2_struct_tag_modules*)stv2_find_struct_tag(bootinfo, STV2_STRUCT_TAG_MODULES_ID));

    klog_printf("\n");
    char buff[4096] = { 0 };
    vfs_handle_t fh = vfs_open("/docs/test.txt", VFS_MODE_READ);
    klog_info("reading \"/docs/test.txt\":\n\n");
    int64_t nb = vfs_read(fh, 4096, buff);
    klog_printf("%s\n", buff, nb);
    klog_info("bytes read: %d\n", nb);
    vfs_close(fh);

    vfs_debug();
    pmm_dumpstats();

    klog_warn("This OS is a work in progress. The computer will now halt.");
    sched_kill(tid);
}
This task loads the initial ramdisk, demonstrates VFS functionality, and displays system statistics.

Build docs developers (and LLMs) love