# Chapter 18: Sections and the Linker

A freestanding image is a set of named sections that the linker places at addresses a linker script chooses. The boot stub has to sit where the loader jumps, the exception vectors where the CPU looks for them, the page tables on page boundaries, and the stack somewhere the entry stub can find. This chapter covers:

- `@section("name")` on functions and module-level variables;
- the `[linker]` table of the manifest;
- a linker script for a small AArch64 kernel, and the symbols it gives your code;
- zeroing `.bss`, per-CPU data, and data blobs such as a version record;
- checking the result: the link map and `max_image_size`.

## `@section("name")`

`@section("name")` puts a function, or a module-level `var` or `let`, into the named ELF section (Language Reference §3-3.12, ruling SECVAR-1). A section name is written as it appears in the object file: `.text.boot`, `.rodata.version`, `.bss.pagetables`.

```loup
@mode(freestanding)

# Boot-time page tables: their own section, page aligned.
@section(".bss.pagetables")
@align(4096)
var bootTables: buffer[uint64, 2048];       # four 4 KiB tables

# Per-CPU tick counters, kept together so the boot code can find them.
@section(".data.percpu")
var cpuTicks: buffer[uint64, 8];

# The idle loop in a section the linker script can place on its own.
@section(".text.idle")
func idle(): void {
    while (true) {
        asm { "wfi" }
    }
}
```

*Listing 18-1: Placing functions and module variables in named sections*

Two properties make `@section` reliable:

- **A `@section` function is always kept.** Like the `@entry` function and `@interrupt` handlers, it is a root of the image, so a handler that nothing in Loup calls — reached only through a vector table or from assembly — is not dropped.
- **A `@section` variable is kept even if nothing reads it.** The compiler pins it against dead-global elimination, because the linker script or a debugger may be its only reader.

`@section` applies to declarations that have a single, permanent location. A local variable lives in a stack frame, and a `const` has no storage at all, so both are rejected:

```loup,does_not_compile
@mode(freestanding)

func handleRequest(): uint64 {
    @section(".data.scratch")
    var scratch: uint64 = 0
    scratch = scratch + 1u64
    return scratch
}
```

```text
error[E0001]: E_ANNOTATION_TARGET: `@section` does not apply to a local binding — it applies to a function or a module-level `var` / `let` (§3-3.12)
 --> src/main.lpfree:4:5
  |
4 |     @section(".data.scratch")
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

```loup,does_not_compile
@mode(freestanding)

@section(".rodata.limits")
const MAX_CPUS: int = 8
```

```text
error[E0001]: E_ANNOTATION_TARGET: `@section` does not apply to a `const` binding — it applies to a function or a module-level `var` / `let` (§3-3.12)
 --> src/main.lpfree:3:1
  |
3 | @section(".rodata.limits")
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

Move the variable to module level if it really must live in that section, or drop the annotation. A `const` needs no section: it is folded into the code that uses it.

The Language Reference reserves `E_SECTION_INVALID_NAME` for names that are not valid section names; the 1.25 checker does not check names, so keep to letters, digits, `.` and `_`.

## The `[linker]` table

Three manifest keys connect your source to the linker:

```toml
[linker]
script = "scripts/linker.ld"   # the linker script, relative to the manifest's directory
entry  = "_start"              # the entry symbol; the name of your @entry function
map    = "kernel.map"          # optional: write a link map
```

All paths are relative to the directory that holds the manifest, not to the directory you run `loup build` from. The `llbe` backend refuses to build a freestanding image without a `script`, and a kernel needs one anyway to control where its code and stack go.

Two more keys are useful once the image works:

- `max_image_size = 8388608` fails the build (`E-BUILD-IMAGE-SIZE`, with a per-section breakdown) if the linked image is larger than that many bytes — for images that must fit a flash part or a loader's staging area.
- `size_opt = true` shrinks code by turning each function's slot-by-slot zeroing of its stack frame into one loop (chapter 24 has more on frame zeroing).

## A linker script

Here is a complete script for an AArch64 kernel on QEMU's `virt` machine, matching the entry stub of Listing 17-1. RAM starts at `0x4000_0000` and the kernel is linked at `0x4008_0000`:

```text
/* QEMU `virt` (AArch64): RAM at 0x40000000, the image at 0x40080000. */
ENTRY(_start)

SECTIONS {
    . = 0x40080000;

    .text.boot : { KEEP(*(.text.boot)) }        /* _start: first in the image */

    . = ALIGN(2048);                            /* VBAR_EL1 needs 2 KiB alignment */
    .vectors : { KEEP(*(.text.vectors)) }

    .text : { *(.text .text.*) }

    .rodata : {
        KEEP(*(.rodata.version))
        *(.rodata .rodata.*)
    }

    .data : { *(.data .data.*) }

    . = ALIGN(4096);
    .bss : {
        __bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        . = ALIGN(16);
        __bss_end = .;
    }

    . = ALIGN(16);
    . = . + 65536;                              /* 64 KiB boot stack */
    __stack_top = .;
}
```

*Listing 18-2: `scripts/linker.ld` for an AArch64 kernel*

Walking through it:

- `ENTRY(_start)` and `[linker] entry = "_start"` both name the `@entry` function.
- `.text.boot` comes first, at the load address, so the CPU starts in the naked stub.
- `.vectors` collects `@section(".text.vectors")` code on a 2 KiB boundary for the exception vector table (chapter 16).
- `*(.text .text.*)` collects ordinary functions and named text sections such as `.text.idle`.
- `KEEP(...)` stops `--gc-sections` from discarding a section nothing refers to.
- `.bss` holds zero-initialized data, including `@section(".bss.pagetables")`, between two symbols the boot code uses.
- The stack follows the image; `__stack_top` is its high end, because stacks grow down.

When `[freestanding] stack_size` is set (it is used by the stack budget check in chapter 22), make the script reserve the same amount.

## Symbols from the linker script

Symbols such as `__stack_top`, `__bss_start` and `__bss_end` exist only in the linker's symbol table. In C you would declare them `extern char __bss_start[];`, and the Loup spelling that looks closest is a mistake:

```loup,does_not_compile
@mode(freestanding)

extern native let __bss_start: uint
```

```text
error[E0001]: expected library name string after `extern native`
 --> src/main.lpfree:3:15
  |
3 | extern native let __bss_start: uint
  |               ^^^
  |
```

`extern native` declares a *function* in a named C library (chapter 20); there is no declaration form for a bare linker symbol. Assembly can name such symbols directly, which is how Listing 17-1 loads the stack pointer, so a Loup function that needs one as a number reads it through an `asm` block:

```loup
#~ @mode(freestanding)
#~
# The address the linker gave `__bss_start` (AArch64).
func bssStart(): uint64 {
    var a: uint64 = 0
    asm {
        "adrp x0, __bss_start"
        "add x0, x0, :lo12:__bss_start"
        out: a = "=x0"
    }
    return a
}
```

*Listing 18-3: Reading a linker-script symbol*

## Zeroing `.bss`

Loup code assumes that zero-initialized module globals read as zero. When QEMU loads an ELF file with `-kernel`, it clears `.bss` for you; a raw binary written to flash, or an image copied by your own boot loader, gets no such help. Zero `.bss` in the entry stub, *before* calling any Loup function:

```loup
#~ @mode(freestanding)
#~
@entry
@naked
@unsafe("naked: no stack yet; zeroes .bss 8 bytes at a time, sets sp, calls kernelMain")
@section(".text.boot")
func _start(): void {
    asm {
        "adrp x0, __bss_start"
        "add x0, x0, :lo12:__bss_start"
        "adrp x1, __bss_end"
        "add x1, x1, :lo12:__bss_end"
        "1: cmp x0, x1"
        "b.hs 2f"
        "str xzr, [x0], #8"
        "b 1b"
        "2: adrp x0, __stack_top"
        "add x0, x0, :lo12:__stack_top"
        "mov sp, x0"
        "bl kernelMain"
        "3: wfe"
        "b 3b"
    }
}

func kernelMain(): void {
    while (true) {
        asm { "wfi" }
    }
}
```

*Listing 18-4: An entry stub that clears `.bss` before running Loup code*

The order matters. Module globals with computed initializers are assigned at the start of `kernelMain` (chapter 17); zeroing `.bss` from inside `kernelMain` would wipe them again. The script aligns `__bss_start` and `__bss_end` to keep the 8-byte stores in bounds.

## Per-CPU data

Each CPU of a multi-core kernel usually has its own copy of some state: its current task, its run queue, its counters. One common layout puts the template in a section of its own and lets the boot code copy it once per CPU:

```text
    . = ALIGN(64);
    .data.percpu : {
        __percpu_start = .;
        KEEP(*(.data.percpu))
        __percpu_end = .;
    }
```

At boot, allocate `CPU count × (__percpu_end − __percpu_start)` bytes, copy the template into each slot, and give each CPU a register that points at its slot (`TPIDR_EL1` on AArch64, the `GS` base on x86_64). Code then reaches its own CPU's data relative to that register.

A simpler layout, enough for many kernels, is a module-level array indexed by CPU number, like `cpuTicks` in Listing 18-1: each CPU writes only its own element. If several CPUs' elements share a cache line, every write bounces the line between them; give each element a 64-byte slot (chapter 5's `@align(64)` struct) when that matters.

## Data blobs: a version record

Sometimes the linker or an external tool needs raw bytes at a known place: a Multiboot header, an ELF note, a version string a flashing tool reads from the image. A `@section` variable is not the right tool for a string: a `string` global is a pointer and a length, so its section would hold the descriptor, not the characters. Instead, emit the bytes with assembler data directives from a naked function that is never called:

```loup
#~ @mode(freestanding)
#~
# A version record a flashing tool can extract from the ELF file.
@section(".rodata.version")
@naked
@unsafe("naked: data only, never called; the bytes of the version record")
func versionRecord(): void {
    asm {
        ".ascii \"mykernel 0.3.1\""
        ".byte 0"
    }
}
```

*Listing 18-5: Emitting raw bytes into a section*

This is how the boot-tested x86_64 kernels emit their Multiboot2 header (`@section(".multiboot")`) and their PVH ELF note (`@section(".note.Xen")`); chapter 21 shows both. Because it is a `@section` function, the record is kept in the image, and `KEEP(*(.rodata.version))` in the script keeps the linker from discarding it. Extract it with your toolchain's `objcopy`:

```sh
llvm-objcopy -O binary --only-section=.rodata.version mykernel version.bin
```

## Checking the layout

When an address comes out wrong, look at what the linker did. With `map = "kernel.map"` in `[linker]`, the build writes a link map listing every output section, its address and size, and the input sections and symbols in it. `llvm-readelf -S mykernel` (sections) and `llvm-nm -n mykernel` (symbols sorted by address) answer the same questions from the image itself.

Loup module variables are emitted under compiler-generated, file-local names (on the LLVM lanes a variable `ring` becomes `global_ring`), and an `@interrupt` handler's body is `<name>__isr_body` behind the trampoline `<name>`. When you search a map or symbol listing, look for those names rather than the bare Loup identifiers; the section a variable was placed in is the more reliable thing to check.

## Summary

- `@section("name")` places a function or a module-level `var`/`let` in an ELF section; such functions and variables are always kept. On a local or a `const` it is `E_ANNOTATION_TARGET`.
- `[linker] script`, `entry` and `map` connect the build to the linker; paths are relative to the manifest. `max_image_size` fails an oversized image; `size_opt` shrinks frame zeroing.
- The linker script decides addresses and defines symbols (`__stack_top`, `__bss_start`, `__bss_end`) that assembly can name; Loup code reads them through `asm`.
- Zero `.bss` in the naked entry stub, before any Loup code runs.
- Per-CPU data: a template section copied per CPU, or a module-level array indexed by CPU.
- Raw data at a known place (headers, notes, version records) comes from assembler directives in a naked `@section` function.

Next: multi-core code and atomics.
