# Chapter 2: Building a Bare-Metal Binary

This chapter is the workflow you repeat for every freestanding project: lay out the files, write the manifest and linker script, type-check, build an image for a target architecture, and boot it in QEMU. It builds the hello-world program from chapter 1 for QEMU's AArch64 `virt` machine.

## The `@mode(freestanding)` directive

Every freestanding source file declares its mode as its first token; only comments may come before it (Language Reference §1-2.5.1, ruling MODE-2):

```loup
# Serial console driver.
@mode(freestanding)

const UART0_BASE: uint64 = 0x0900_0000u64
```

*Listing 2-1: The directive after a leading comment*

Anything else before the directive is an error:

```loup,does_not_compile
# Serial console driver.
const COM1: uint16 = 0x3F8u16
@mode(freestanding)

func port(): uint16 { return COM1 }
```

```text
error[E_MODE_POSITION]: E_MODE_POSITION: `@mode` must be the first token of the file — only comments and `@compiled` may precede it (§1-2.5.1)
 --> src/main.lpfree:3:1
  |
3 | @mode(freestanding)
  | ^^^^^^^^^^^^^^^^^^^
  |
```

A second directive in the same file is `E_MODE_DUPLICATE`. Two silent mistakes are worth knowing about, because the compiler does not report them: a file with **no** directive falls back to its extension and the project's `[build] mode`, and a **misspelled** or retired mode name — `@mode(native)`, `@mode(bare)` — is treated as `host.scripted` without any diagnostic. Name the file `.lpfree` and spell the directive exactly `@mode(freestanding)`.

A freestanding program is homogeneous. A freestanding file may import other freestanding files of the program and the few freestanding standard-library modules (chapter 20), and may call C code declared with `extern native "library" func …`. It may not import `host.*` or `web.*` modules — the import is rejected, as chapter 1 showed for `core.math` — and hosted code may not import freestanding modules either ("`@mode(host.native)` module cannot import `@mode(freestanding)` module").

## Project layout

```text
hello/
├── hello.lppkg          # the manifest
├── kernel.ld            # the linker script
└── src/
    └── main.lpfree      # Listing 1-2
```

The manifest is TOML, named `<package>.lppkg` (or `package.lppkg`). `loup-bootstrap init` scaffolds a hosted project; for a freestanding one, write the manifest by hand.

## The manifest

```toml
name = "hello"
version = "0.1.0"

[build]
entry  = "src/main.lpfree"
target = "freestanding"      # also spelled `delivery`
mode   = "freestanding"
arch   = "aarch64"
panic  = "halt"              # halt (the default) | abort | custom — chapter 17

[linker]
script = "kernel.ld"
entry  = "_start"            # matches the @entry function

[freestanding]
stack_size = 65536           # bytes; the linker script reserves the stack
```

*Listing 2-2: `hello.lppkg`*

The keys the 1.25 toolchain acts on:

| Section | Key | Meaning |
|---|---|---|
| `[build]` | `entry`, `mode`, `target`/`delivery`, `arch` | the root file, its mode, `"freestanding"`, the CPU architecture |
| `[build]` | `panic` | what a panic does: `"halt"` (default), `"abort"`, `"custom"` (calls your `@panic_handler`) — chapter 17 |
| `[build]` | `separate` | one cached object per package instead of one per program (LLVM lanes only) |
| `[linker]` | `script`, `entry` | the linker script and the entry symbol |
| `[linker]` | `map`, `size_opt`, `max_image_size` | write a link map; shrink zero-initialization prologues; fail the build if the image is larger than this many bytes (`E-BUILD-IMAGE-SIZE`) |
| `[freestanding]` | `stack_size`, `stack_check`, `interrupt_nesting` | the stack budget and the static worst-case stack check (`"off"`, `"report"`, `"enforce"`), which proves a bound on stack use from the call graph |
| `[freestanding]` | `bounds` | `"trap"` adds run-time index checks to buffers — chapter 13 |
| `[freestanding]` | `overlays`, `rawpoke` | how strictly unacknowledged raw-memory views are reported — chapter 13 |
| `[freestanding]` | `relocatable` | build a position-independent user program a kernel can load anywhere |

Relative paths are resolved against the directory that holds the manifest.

> ⚠ **Planned.** The Language Reference also lists `[freestanding] no_float`, `overflow`, `no_simd`, `double_fault` and `endianness` (§3-3.21). The 1.25 toolchain accepts these keys and ignores them: `no_float = true` does not reject floating-point code, and `overflow = "trap"` does not make arithmetic trap (chapter 3).

An invalid value for a key the checker reads is reported against the project. With `panic = "explode"` in `[build]`:

```text
error[E0001]: invalid [build].panic 'explode'; must be one of: halt, abort, custom
 --> src/main.lpfree:4:1
  |
4 | 
  | ^^^^^^^^^^^^^^^^^
  |
```

The source location underneath is not meaningful — the mistake is in the manifest. The build driver checks the other enumerated keys (`bounds`, `stack_check`, `overlays`, `rawpoke`) the same way: a misspelled safety switch fails the build instead of silently meaning "off".

## The linker script

QEMU's AArch64 `virt` machine has RAM at `0x4000_0000` and loads a `-kernel` ELF at its link address. This script links the image at `0x4008_0000`, puts `.text.boot` (where `_start` lives) first, and reserves a 64 KiB stack after `.bss`, ending at `__stack_top`:

```text
ENTRY(_start)

PHDRS {
    text PT_LOAD FLAGS(7);
}

SECTIONS {
    . = 0x40080000;
    .text.boot : { KEEP(*(.text.boot)) } :text
    .text      : { *(.text .text.*) }       :text
    .rodata    : { *(.rodata .rodata.*) }   :text
    .data      : { *(.data .data.*) }       :text
    .bss       : {
        *(.bss .bss.*)
        . = ALIGN(16);
        . = . + 65536;
        __stack_top = .;
    } :text
}
```

*Listing 2-3: `kernel.ld` for QEMU `virt` (AArch64)*

Chapter 18 explains sections and linker scripts in detail. One rule matters already: the `@entry` function is the root of the image. The build keeps `_start`, every `@section` function, and whatever they reach, and drops everything else.

## Type-checking

`loup check` type-checks the whole project without building it, on any platform the toolchain runs on:

```sh
cd hello
loup check
```

It prints nothing and exits 0 when the program is well-typed. On Linux x86-64, where the installed `loup` is the self-hosted driver, run `loup-bootstrap check` instead. `--format short` prints one line per diagnostic; `--format json` prints machine-readable records.

## Building

```sh
loup build                                          # target, arch and options from the manifest
loup build --target freestanding --arch aarch64     # or name them on the command line
```

The image is written to `./hello` — the package name — unless you pass `-o PATH`. Other useful flags: `--release` (an optimized build), `--debug`, `--backend llbe|llvm`, `--stack-report`/`--stack-enforce` (print, or enforce, the static stack budget for one build), `--no-cache`.

Freestanding builds run on **Linux x86-64** and need `ld.lld` on the `PATH` (the toolchain installer warns if it is missing). They are not available on macOS or Windows; on those platforms you can type-check freestanding code but not link an image.

There are two back ends:

- **`llbe`** — the default: Loup's own clang-free chain. `loup-llvm` emits LLVM IR text, `loup-llbe` turns it into machine code and an ELF object itself, and `ld.lld` links the image. No C compiler is involved.
- **`llvm`** (`--backend llvm`) — the same IR compiled by `clang`. Use it for a unit that `llbe`'s tier for your architecture cannot lower yet; `llbe` refuses such code by name rather than miscompiling it.

One exception to the default today: for an x86_64 package with a `[linker] script` (the "kernel route", which is how every x86_64 kernel is built) the self-hosted driver uses the `llvm` lane unless you pass `--backend llbe`. A build always runs the lane you name; it never silently substitutes another (`E_BACKEND_UNAVAILABLE`).

## Booting in QEMU

```sh
qemu-system-aarch64 -M virt -cpu cortex-a53 -m 256M -nographic -kernel ./hello
```

```text
Hello from Loup freestanding
```

`-nographic` connects the UART to your terminal; press `Ctrl-A`, then `x`, to quit. `-s -S` makes QEMU wait for GDB on port 1234 (chapter 22).

## Other architectures

The same source tree builds for another CPU by changing `arch` — plus whatever is architecture-specific: the boot code in `_start`, the device addresses, the `asm` blocks, and the linker script.

| `--arch` | Status in 1.25 |
|---|---|
| `x86_64` | Boot-verified under QEMU; the reference target |
| `aarch64` | Boot-verified under QEMU |
| `riscv64` | Boot-verified under QEMU |
| `armv7` (also `arm`) | Boot-verified under QEMU on both back ends |
| `thumbv8.1m.main`, `thumbv8m.main`, `thumbv7em`, `thumbv6m` | Cortex-M; boot-verified (`mps3-an547`) |
| `riscv32` | Boot-verified through the `llvm` back end; `llbe` cannot yet link cross-function calls |
| `x86` (32-bit) | C/LLVM path only; not boot-verified |
| `loongarch64`, `mips64el`, `powerpc64le` | Encoder-level only; no image has been booted |
| `s390x` | Specified, not implemented |

An architecture outside this list is rejected rather than encoded for a different CPU. The QEMU machines the toolchain's own boot tests use:

```sh
# x86_64 — the image needs a Multiboot2 header and a PVH note (chapter 21)
qemu-system-x86_64 -kernel ./kernel -serial stdio -display none

# riscv64 — no firmware; link the image at 0x8000_0000
qemu-system-riscv64 -M virt -bios none -m 128M -nographic -kernel ./kernel

# armv7 (A-profile)
qemu-system-arm -M virt -cpu cortex-a15 -m 256M -nographic -kernel ./kernel

# Cortex-M55
qemu-system-arm -M mps3-an547 -nographic -kernel ./kernel
```

On x86_64, `-kernel` loads an ELF only if it carries a Multiboot header or a Xen PVH note, and the CPU starts in 32-bit protected mode — so an x86_64 kernel starts with a 32-bit trampoline that enables long mode. Chapter 21 builds one.

## Image formats

The build produces an ELF image. For targets that need something else, convert it with standard tools:

```sh
llvm-objcopy -O binary ./hello hello.bin     # a flat binary, e.g. for flashing
llvm-strip ./hello                           # drop the symbol table
```

> ⚠ **Planned.** The `--output-format elf|raw|pe` and `--march` flags described in the Language Reference (§3-3.2) are not in the 1.25 command line, and there is no PE/COFF output for UEFI applications yet.

Keep symbols in development builds; for a release image, keep the link map (`[linker] map = "kernel.map"`) alongside a stripped binary so crash addresses can still be resolved.

## A small Makefile

```makefile
KERNEL = hello

run: build
	qemu-system-aarch64 -M virt -cpu cortex-a53 -m 256M -nographic -kernel ./$(KERNEL)

build:
	loup build

check:
	loup-bootstrap check

clean:
	rm -rf $(KERNEL) .loup-cache
```

## Summary

- Every `.lpfree` file starts with `@mode(freestanding)`; a misplaced directive is `E_MODE_POSITION`, but a misspelled one is silent.
- The manifest (`<name>.lppkg`) sets `[build] target/mode/arch/panic`, `[linker] script/entry`, and the `[freestanding]` keys; `no_float`, `overflow` and a few others are accepted but not yet acted on.
- `loup check` (or `loup-bootstrap check` on Linux x86-64) type-checks anywhere; `loup build --target freestanding --arch …` builds images on Linux x86-64 with `ld.lld`, through the clang-free `llbe` back end by default.
- x86_64, aarch64, riscv64, armv7 and Cortex-M images boot under QEMU; convert ELF to other formats with `llvm-objcopy`.

Next: integer types and wrapping arithmetic.
