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.
_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:
IDT Initialization
Sets up the Interrupt Descriptor Table to handle CPU exceptions and interrupts
CPU Feature Detection
Detects and enables CPU features like SSE, AVX, etc.
Memory Management
Initializes the Physical Memory Manager (PMM) and Virtual Memory Manager (VMM)
GDT Setup
Configures the Global Descriptor Table for memory segmentation
Display and I/O
Initializes framebuffer, serial port, and terminal
ACPI and Timers
Discovers hardware through ACPI and sets up HPET timer
APIC Configuration
Configures the Advanced Programmable Interrupt Controller
Filesystem
Mounts the initial ramfs filesystem
SMP Initialization
Brings up additional CPU cores
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:
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:
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:
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:
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:
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:
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:
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:
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:
- Setting the APIC timer to maximum count
- Waiting 500ms using the HPET
- Reading how much the counter decreased
- Calculating the base frequency from the difference
First Kernel Task
After scheduler initialization, the first kernel task kinit() runs at kernel/kmain.c:23:
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.