# Chapter 21: Example — x86_64 Kernel Stub

This chapter puts the previous ones together in one small, complete kernel for x86_64. It boots under `qemu-system-x86_64 -kernel`, switches the CPU from 32-bit protected mode to 64-bit long mode, prints to the serial console, installs handlers for the timer, page faults and general-protection faults, programs the legacy interrupt controller and timer, and prints a line about once a second. A fault ends in a custom panic handler that reports what happened.

It is a template to read end to end, not a production kernel. The boot path, the port I/O, the interrupt-controller programming and the IDT encoding follow the kernels that the toolchain's own test suite boots under QEMU.

| Feature | Where it is used | Chapter |
|---|---|---|
| `@entry`, `@naked`, `@unsafe`, `@section` | the boot headers and the entry trampoline | 16, 17, 18 |
| `asm` with operands | port I/O, `lidt`, `cr2`, `sti`, `hlt` | 14 |
| `@interrupt`, `@interruptSafe` | the timer and fault handlers and everything they call | 16 |
| atomic intrinsics | the tick counter and the fault record | 19 |
| `@panic_handler` with `panic = "custom"` | reporting faults | 17 |
| `buffer[T, N]` with `bounds = "trap"` | the interrupt descriptor table | 13 |
| `[linker] script` | placing the boot headers and the stack | 18 |

## Project layout

```text
mykernel/
├── package.lppkg
├── scripts/
│   └── linker.ld
└── src/
    └── main.lpfree
```

This is the same layout as the `examples/helios-hello-freestanding` project in the Loup repository: a manifest at the root, the linker script under `scripts/`, and the program in `src/main.lpfree`.

## The manifest

```toml
name = "mykernel"
version = "0.1.0"

[build]
entry  = "src/main.lpfree"
target = "freestanding"
mode   = "freestanding"
arch   = "x86_64"
panic  = "custom"            # faults are reported by kernelPanic

[linker]
script = "scripts/linker.ld"
entry  = "_start"

[freestanding]
stack_size = 65536
bounds     = "trap"          # check run-time buffer indexes
```

*Listing 21-1: `package.lppkg`*

`panic = "custom"` is what makes the kernel's `@panic_handler` run; without it a panic would halt silently (chapter 17). `bounds = "trap"` guards every run-time index into a `buffer` whose size the compiler can see — here, the writes into the IDT.

## The linker script

```text
/* Loaded at 1 MiB. .boot holds the Multiboot2 header and the 32-bit entry
 * trampoline; .note.Xen carries the PVH entry note, in its own PT_NOTE segment
 * so that `qemu-system-x86_64 -kernel` finds it. */
ENTRY(_start)

PHDRS {
    text PT_LOAD FLAGS(7);   /* RWX */
    note PT_NOTE FLAGS(4);   /* R   */
}

SECTIONS {
    . = 0x100000;

    .boot : {
        KEEP(*(.multiboot))
        KEEP(*(.text.boot))
    } :text

    .note.Xen : { KEEP(*(.note.Xen)) } :text :note

    .text   : { *(.text .text.*) }       :text
    .rodata : { *(.rodata .rodata.*) }   :text
    .data   : { *(.data .data.*) }       :text
    .bss    : {
        *(.bss .bss.*)
        . = ALIGN(16);
        . = . + 65536;
        __stack_top = .;
    } :text
}
```

*Listing 21-2: `scripts/linker.ld`*

The Multiboot2 header must be within the first 32 KiB of the image, so `.multiboot` is the first thing in `.boot`. The trampoline below sets its own stack in low memory and does not use `__stack_top`; the reservation is there for code that wants a stack inside the image.

## `src/main.lpfree`

The rest of the chapter is the source file, in order. Put the Loup listings 21-3 to 21-9 one after another and you have the complete `src/main.lpfree`.

### Boot headers

QEMU's `-kernel` option loads an x86_64 ELF image only if it can find out how to enter it. The image carries two pieces of data for that: a Multiboot2 header and a Xen PVH ELF note whose value is the 32-bit entry address. Both are plain bytes, emitted by assembler directives from naked functions that are never called (chapter 18):

```loup
@mode(freestanding)

# mykernel — a minimal x86_64 kernel for `qemu-system-x86_64 -kernel`.

const ACQUIRE: int = 2
const RELEASE: int = 3
const SEQ_CST: int = 5

# ---- Boot headers -----------------------------------------------------------

# Multiboot2 header: magic, architecture 0 (i386), length, checksum, end tag.
@section(".multiboot")
@naked
@unsafe("naked: data only, never called; the Multiboot2 header")
func multibootHeader(): void {
    asm {
        ".align 8"
        "mb2_start:"
        ".long 0xE85250D6"
        ".long 0"
        ".long mb2_end - mb2_start"
        ".long 0x100000000 - (0xE85250D6 + (mb2_end - mb2_start))"
        ".short 0"
        ".short 0"
        ".long 8"
        "mb2_end:"
    }
}

# Xen PVH note (type 18, XEN_ELFNOTE_PHYS32_ENTRY): where QEMU enters the image.
@section(".note.Xen")
@naked
@unsafe("naked: data only, never called; the PVH entry note")
func pvhNote(): void {
    asm {
        ".align 4"
        ".long 4"
        ".long 4"
        ".long 18"
        ".byte 0x58"
        ".byte 0x65"
        ".byte 0x6e"
        ".byte 0x00"
        ".align 4"
        ".long _start"
    }
}
```

*Listing 21-3: The memory-ordering constants and the boot headers*

### The entry trampoline

QEMU enters `_start` in 32-bit protected mode, with paging off and no stack. Loup code is compiled for 64-bit mode, so the entry is a naked function that builds a minimal set of page tables (the first 1 GiB identity-mapped with 2 MiB pages), turns on PAE, long mode and paging, loads a flat GDT, sets a stack, and only then calls `kernelMain`:

```loup
#~ @mode(freestanding)
#~
# ---- Entry: 32-bit protected mode to 64-bit long mode ----------------------

# Identity-map the first 1 GiB with 2 MiB pages (tables at 0x70000-0x72FFF),
# enable PAE and long mode, load a flat GDT (0x08 code, 0x10 data), set up a
# stack at 0x90000 and call kernelMain.
@entry
@naked
@unsafe("naked: entered by the PVH loader in 32-bit mode with no stack; builds page tables and enters long mode")
@section(".text.boot")
func _start(): void {
    asm {
        ".code32"
        "mov esp, 0x90000"
        "mov dword ptr [0x70000], 0x71003"
        "mov dword ptr [0x70004], 0"
        "mov dword ptr [0x71000], 0x72003"
        "mov dword ptr [0x71004], 0"
        "mov edi, 0x72000"
        "mov eax, 0x83"
        "mov ecx, 512"
        "fill_pd:"
        "mov dword ptr [edi], eax"
        "mov dword ptr [edi+4], 0"
        "add eax, 0x200000"
        "add edi, 8"
        "dec ecx"
        "jnz fill_pd"
        "mov eax, 0x70000"
        "mov cr3, eax"
        "mov eax, cr4"
        "or eax, 0x20"
        "mov cr4, eax"
        "mov ecx, 0xC0000080"
        "rdmsr"
        "or eax, 0x100"
        "wrmsr"
        "mov eax, cr0"
        "or eax, 0x80000001"
        "mov cr0, eax"
        "lgdt [gdt_ptr]"
        "jmp 0x08, long_mode"
        ".code64"
        "long_mode:"
        "mov ax, 0x10"
        "mov ds, ax"
        "mov es, ax"
        "mov ss, ax"
        "mov rsp, 0x90000"
        "call kernelMain"
        "cli"
        "hlt"
        ".align 8"
        "gdt_start:"
        ".quad 0x0000000000000000"
        ".quad 0x00209A0000000000"
        ".quad 0x0000920000000000"
        "gdt_ptr:"
        ".word 23"
        ".long gdt_start"
        clobbers: ["memory"]
    }
}
```

*Listing 21-4: The entry trampoline*

Step by step: the three tables at `0x70000` (PML4), `0x71000` (PDPT) and `0x72000` (page directory) map virtual address *n* to physical address *n* for the first 1 GiB, with flags `0x83` (present, writable, 2 MiB page). `cr4` bit 5 enables PAE, bit 8 of the `EFER` MSR (`0xC0000080`) enables long mode, and setting `cr0`'s PG and PE bits turns paging on. The far jump through the GDT's 64-bit code segment (`0x08`) switches the CPU into 64-bit mode at `long_mode`. The GDT itself is data inside the assembly, after the code. The memory below `0x90000` is free RAM on a PC, which is why the tables and the stack live there.

Because `kernelMain` is the function the entry calls, the compiler runs the module initializer at its start (chapter 17). This kernel's module variables all have constant initializers, so nothing depends on that.

### The serial console

On a PC, COM1 is a 16550 UART in the I/O-port space at `0x3F8`, reached with `in` and `out` instructions rather than through `Volatile` (chapter 15). The output functions are `@interruptSafe` because the fault path uses them; they take no locks and allocate nothing, so the annotation is honest for this kernel:

```loup
#~ @mode(freestanding)
#~
# ---- Serial console: the 16550 UART at I/O port 0x3F8 ----------------------

const COM1: uint16 = 0x3F8u16

@interruptSafe
func outb(port: uint16, value: uint8): void {
    asm { "out dx, al" in: "dx" = port, "al" = value clobbers: [] }
}

@interruptSafe
func inb(port: uint16): uint8 {
    var value: uint64 = 0
    asm { "in al, dx" out: value = "=a" in: "dx" = port clobbers: [] }
    return (value & 0xFFu64) as uint8
}

func serialInit(): void {
    outb(COM1 + 1u16, 0x00u8)      # no UART interrupts
    outb(COM1 + 3u16, 0x80u8)      # DLAB on: the next two writes set the divisor
    outb(COM1 + 0u16, 0x01u8)      # divisor 1: 115200 baud
    outb(COM1 + 1u16, 0x00u8)
    outb(COM1 + 3u16, 0x03u8)      # DLAB off; 8 data bits, no parity, 1 stop bit
    outb(COM1 + 2u16, 0xC7u8)      # enable and clear the FIFOs
}

@interruptSafe
func putc(c: uint8): void {
    while ((inb(COM1 + 5u16) & 0x20u8) == 0u8) { }     # wait for "transmit empty"
    outb(COM1, c)
}

@interruptSafe
func puts(s: string): void {
    for (c in s) {
        putc(c as uint8)
    }
}

@interruptSafe
func putDec(n: uint64): void {
    var divisor: uint64 = 1u64
    while (n / divisor >= 10u64) {
        divisor = divisor * 10u64
    }
    var rest = n
    while (divisor > 0u64) {
        putc((0x30u64 + rest / divisor) as uint8)
        rest = rest % divisor
        divisor = divisor / 10u64
    }
}

@interruptSafe
func putHex(n: uint64): void {
    puts("0x")
    var shift = 60
    while (shift >= 0) {
        let nibble = ((n >> (shift as uint64)) & 0xFu64) as uint8
        if (nibble < 10u8) {
            putc(0x30u8 + nibble)
        } else {
            putc(0x57u8 + nibble)
        }
        shift = shift - 4
    }
}
```

*Listing 21-5: Serial output over port I/O*

### The panic handler

The fault handlers record what they saw in module variables and then reach `unreachable`; with `panic = "custom"`, that calls `kernelPanic`, which prints the panic message and location and, when a fault was recorded, the fault details:

```loup
#~ @mode(freestanding)
#~
#~ const ACQUIRE: int = 2
#~
#~ func puts(s: string): void { }
#~ func putDec(n: uint64): void { }
#~ func putHex(n: uint64): void { }
#~
# ---- Panics -----------------------------------------------------------------

# Filled in by the fault handlers before they stop the kernel.
var faultVector: uint64 = 0
var faultAddress: uint64 = 0
var faultCode: uint64 = 0
var faultRip: uint64 = 0

@panic_handler
func kernelPanic(msg: string, file: string, line: int): noreturn {
    asm { "cli" }
    puts("\nPANIC: ")
    puts(msg)
    puts(" at ")
    puts(file)
    puts(":")
    putDec(line as uint64)
    puts("\n")
    let vector = atomicLoadU64(addressOf(faultVector), ACQUIRE)
    if (vector != 0u64) {
        puts("  fault vector ")
        putDec(vector)
        puts(", rip ")
        putHex(atomicLoadU64(addressOf(faultRip), ACQUIRE))
        puts(", address ")
        putHex(atomicLoadU64(addressOf(faultAddress), ACQUIRE))
        puts(", error ")
        putHex(atomicLoadU64(addressOf(faultCode), ACQUIRE))
        puts("\n")
    }
    while (true) {
        asm { "hlt" }
    }
}
```

*Listing 21-6: The panic handler*

### Interrupt handlers

Three handlers: the timer counts ticks and acknowledges the interrupt controller; the page-fault and general-protection handlers record the fault and stop. Everything they call is `@interruptSafe` (`readCr2` here, `outb` above) or an intrinsic, which the colour rule of chapter 16 checks:

```loup
#~ @mode(freestanding)
#~
#~ const RELEASE: int = 3
#~ const SEQ_CST: int = 5
#~
#~ @interruptSafe
#~ func outb(port: uint16, value: uint8): void { }
#~
#~ var faultVector: uint64 = 0
#~ var faultAddress: uint64 = 0
#~ var faultCode: uint64 = 0
#~ var faultRip: uint64 = 0
#~
# ---- Interrupts ---------------------------------------------------------------

@repr(C)
struct InterruptFrame {
    let rip: uint64;
    let cs: uint64;
    let rflags: uint64;
    let rsp: uint64;
    let ss: uint64;
}

var ticks: uint64 = 0

@interrupt(arch: "x86_64", has_error_code: false)
func timerIsr(frame: borrow InterruptFrame): void {
    let _old = atomicFetchAddU64(addressOf(ticks), 1u64, SEQ_CST)
    outb(0x20u16, 0x20u8)                              # end of interrupt
}

@interruptSafe
func readCr2(): uint64 {
    var a: uint64 = 0
    asm { "mov rax, cr2" out: a = "=a" clobbers: [] }
    return a
}

@interrupt(arch: "x86_64", has_error_code: true)
func pageFaultIsr(frame: borrow InterruptFrame, error: uint64): void {
    atomicStoreU64(addressOf(faultAddress), readCr2(), RELEASE)
    atomicStoreU64(addressOf(faultCode), error, RELEASE)
    atomicStoreU64(addressOf(faultRip), frame.rip, RELEASE)
    atomicStoreU64(addressOf(faultVector), 14u64, RELEASE)
    unreachable
}

@interrupt(arch: "x86_64", has_error_code: true)
func generalProtectionIsr(frame: borrow InterruptFrame, error: uint64): void {
    atomicStoreU64(addressOf(faultCode), error, RELEASE)
    atomicStoreU64(addressOf(faultRip), frame.rip, RELEASE)
    atomicStoreU64(addressOf(faultVector), 13u64, RELEASE)
    unreachable
}
```

*Listing 21-7: Timer and fault handlers*

`loup check` prints the frame-parameter advisory of chapter 16 for each of the three handlers; it is expected.

### The IDT, the PIC and the timer

The IDT is a `buffer` global filled with gates as in chapter 16. The handler addresses come from `lea`, because `addressOf` takes variables only. The two 8259 interrupt controllers are reprogrammed so that their 16 interrupt lines arrive at vectors `0x20`–`0x2F` instead of colliding with the CPU's exception vectors, and only IRQ 0, the timer, is left unmasked. The 8254 timer (PIT) runs at 1 193 182 Hz; a divisor of 11 932 (`0x2E9C`) gives about 100 interrupts per second:

```loup
#~ @mode(freestanding)
#~
#~ @interruptSafe
#~ func outb(port: uint16, value: uint8): void { }
#~
func timerIsrAddress(): uint64 {
    var a: uint64 = 0
    asm { "lea rax, [rip + timerIsr]" out: a = "=a" clobbers: [] }
    return a
}

func pageFaultIsrAddress(): uint64 {
    var a: uint64 = 0
    asm { "lea rax, [rip + pageFaultIsr]" out: a = "=a" clobbers: [] }
    return a
}

func generalProtectionIsrAddress(): uint64 {
    var a: uint64 = 0
    asm { "lea rax, [rip + generalProtectionIsr]" out: a = "=a" clobbers: [] }
    return a
}

const KERNEL_CS: uint64 = 0x08u64
const GATE_INTERRUPT: uint64 = 0x8Eu64     # present, DPL 0, 64-bit interrupt gate

var idt: buffer[uint64, 512];               # 256 gates x 16 bytes
var idtr: buffer[uint64, 2];

func setGate(vector: int, handler: uint64): void {
    let low = (handler & 0xFFFFu64) | (KERNEL_CS << 16) | (GATE_INTERRUPT << 40)
        | (((handler >> 16) & 0xFFFFu64) << 48)
    idt[vector * 2] = low
    idt[vector * 2 + 1] = handler >> 32
}

func loadIdt(): void {
    let base = addressOf(idt)
    idtr[0] = 4095u64 | (base << 16)
    idtr[1] = base >> 48
    let p = addressOf(idtr)
    asm { "lidt [rax]" in: "rax" = p clobbers: ["memory"] }
}

# Remap the two 8259 PICs to vectors 0x20-0x2F and unmask only IRQ 0.
func remapPic(): void {
    outb(0x20u16, 0x11u8)
    outb(0xA0u16, 0x11u8)
    outb(0x21u16, 0x20u8)
    outb(0xA1u16, 0x28u8)
    outb(0x21u16, 0x04u8)
    outb(0xA1u16, 0x02u8)
    outb(0x21u16, 0x01u8)
    outb(0xA1u16, 0x01u8)
    outb(0x21u16, 0xFEu8)
    outb(0xA1u16, 0xFFu8)
}

# PIT channel 0, rate generator, divisor 11932: about 100 Hz.
func startPit(): void {
    outb(0x43u16, 0x34u8)
    outb(0x40u16, 0x9Cu8)
    outb(0x40u16, 0x2Eu8)
}

func interruptsOn(): void {
    asm { "sti" clobbers: ["memory"] }
}
```

*Listing 21-8: The descriptor table, the interrupt controller and the timer*

The writes `idt[vector * 2]` use run-time indexes, so `bounds = "trap"` guards them: a vector above 255 would reach `kernelPanic` with `buffer index out of range` instead of overwriting `idtr`.

### `kernelMain`

```loup
#~ @mode(freestanding)
#~
#~ const ACQUIRE: int = 2
#~
#~ var ticks: uint64 = 0
#~
#~ func serialInit(): void { }
#~ func puts(s: string): void { }
#~ func putDec(n: uint64): void { }
#~ func setGate(vector: int, handler: uint64): void { }
#~ func loadIdt(): void { }
#~ func remapPic(): void { }
#~ func startPit(): void { }
#~ func interruptsOn(): void { }
#~ func timerIsrAddress(): uint64 { return 0u64 }
#~ func pageFaultIsrAddress(): uint64 { return 0u64 }
#~ func generalProtectionIsrAddress(): uint64 { return 0u64 }
#~
# ---- The kernel -------------------------------------------------------------

func kernelMain(): void {
    serialInit()
    puts("mykernel: booting\n")

    setGate(13, generalProtectionIsrAddress())
    setGate(14, pageFaultIsrAddress())
    setGate(0x20, timerIsrAddress())
    loadIdt()
    remapPic()
    startPit()
    puts("mykernel: interrupts on\n")
    interruptsOn()

    var reported: uint64 = 0
    while (true) {
        asm { "hlt" }                                   # sleep until the next interrupt
        let now = atomicLoadU64(addressOf(ticks), ACQUIRE)
        if (now >= reported + 100u64) {
            reported = now
            puts("tick ")
            putDec(now)
            puts("\n")
        }
    }
}
```

*Listing 21-9: The kernel's main function*

The main loop sleeps with `hlt`, wakes on each timer interrupt, and prints from normal context — never from the handler (chapter 16).

## Checking, building and running

`loup check` type-checks the project on any platform (on Linux x86-64, `loup-bootstrap check`). It reports no errors, only the three frame-parameter advisories mentioned above.

Building the image needs Linux x86-64 with `ld.lld` (chapter 2). For an x86_64 package with a linker script, `loup build` uses the `llvm` lane (clang); `--backend llbe` selects the clang-free lane instead:

```sh
loup build                      # writes ./mykernel
loup build --backend llbe       # the same, without clang
```

Boot it with the serial port on your terminal:

```sh
qemu-system-x86_64 -kernel ./mykernel -serial stdio -display none -no-reboot
```

When it boots, the console shows the two start-up lines and then a tick line about once a second:

```text
mykernel: booting
mykernel: interrupts on
tick 100
tick 200
tick 300
```

Stop QEMU with `Ctrl-C` in the terminal.

### Provoking a fault

Only the first 1 GiB is mapped. Add a read of an address above it to `kernelMain`, just before the `while` loop:

```loup
#~ @mode(freestanding)
#~
#~ func kernelMain(): void {
    unsafe("demo: deliberately read an unmapped address") {
        let _probe = bufGetU8Vol(0x8000_0000u64, 0)
    }
#~ }
```

*Listing 21-10: A deliberate read of unmapped memory*

`bufGetU8Vol` is a volatile byte read (chapter 13), so the compiler cannot drop it even though the value is unused. The CPU raises a page fault, `pageFaultIsr` records it and reaches `unreachable`, and `kernelPanic` prints ``PANIC: reached an `unreachable` expression at main.lpfree:`` followed by the line of that `unreachable`, then `fault vector 14`, the address of the faulting instruction, `address 0x0000000080000000` (from `cr2`) and the page-fault error code. With `panic = "halt"` instead, the same fault would stop the machine without a word — which is why `custom` is the right setting while you develop.

## What the stub leaves out

A real kernel needs much more, and each piece builds on the chapters before:

- **Memory management.** A physical frame allocator over the boot loader's memory map, page tables of your own instead of the fixed identity map, and an allocator you call directly (chapter 7; in 1.25 class construction does not go through `@allocator`).
- **The local APIC and SMP.** The legacy 8259 and PIT are enough on one CPU; multiple CPUs need the local APIC, its timer, and the start-up sequence for secondary CPUs, plus the locks of chapters 19 and 24.
- **Exceptions.** Only vectors 13, 14 and 0x20 have handlers. Any other exception finds an empty gate and turns into a double fault and then a triple fault, which resets the machine (QEMU exits because of `-no-reboot`). Install a handler for each of the 32 exception vectors, and an IST stack for the double fault.
- **User mode, system calls, a scheduler, drivers.**

## Summary

- A complete x86_64 kernel is a manifest, a linker script and one Loup file of about 350 lines.
- The image carries a Multiboot2 header and a PVH note (bytes from naked `@section` functions); `_start` is a naked trampoline that enters long mode and calls `kernelMain`.
- COM1 is driven with port I/O from `@interruptSafe` helpers; the IDT is a `buffer` filled with gates whose handler addresses come from `lea`.
- Handlers count ticks with atomics or record a fault and reach `unreachable`; `panic = "custom"` routes that to a handler that reports it.
- Build on Linux x86-64 with `ld.lld`; boot with `qemu-system-x86_64 -kernel`.

Next: testing and deploying firmware.
