kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit b4f2b7450e8b321b22ebed6233929eaecbae3203
parent bf9137427b2a9497d5c5436fdc159b9cc7a5e56a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 11:20:16 -0700

plan: ARM32 hosted

Diffstat:
Mdoc/plan/ARM32.md | 191++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 190 insertions(+), 1 deletion(-)

diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md @@ -1,4 +1,4 @@ -# Plan: 32-bit ARM (`arm-none-eabi`, ARMv7-M / ARMv7E-M, Thumb-2) +# Plan: 32-bit ARM (ARMv7 Thumb-2 — freestanding, Linux, FreeBSD) ## Status — 2026-06-17 — arm32 core closed @@ -72,6 +72,195 @@ are now checked there too). - [ ] (cosmetic) disassembler renders shifted-MOV (`mov.w rd,rs,lsl #N`) as plain `mov` — encodings are correct; listings lose the shift. +--- + +## Phase 3 — hosted ARM32: Linux + FreeBSD + +### Background + +Linux, FreeBSD, and (historically) Windows ship ARM32 for **A-profile** processors +(ARMv7-A, Cortex-A8/A9/A15) — not M-profile. A-profile has a full MMU, CP15 +system registers, and supports both A32 and Thumb-2 execution states. The key +fact for kit is that **user-space code on A-profile runs in Thumb-2** by default +on all these OSes (Linux armhf/armel, FreeBSD armv7, Windows ARM32 all compile to +Thumb-2). The Thumb-2 ISA is identical between M-profile and A-profile, so the +entire `src/arch/arm32/` backend — ISA encoding, AAPCS32 calling convention, +ELF32 output, relocations — carries over unchanged. + +The differences between the bare-metal core and a hosted target are: + +- **Thread pointer:** M-profile has no CP15; the bare-metal lane uses + TLS-as-static. A-profile reads the thread pointer via `MRC p15, 0, Rd, c13, + c0, 3` (TPIDRURO). This is the mechanism Linux, FreeBSD, and Android all use. +- **ARM attributes profile:** `src/obj/elf/emit.c` currently emits + `Tag_CPU_arch_profile = 'M'`; hosted objects must emit `'A'`. +- **Dynamic linking:** the bare-metal lane is static-only. Hosted targets need a + PLT/GOT and the standard `R_ARM_JUMP_SLOT` / `R_ARM_GLOB_DAT` / + `R_ARM_RELATIVE` / `R_ARM_COPY` relocations. This is the dominant work item. +- **Startup:** hosted targets use `_start` → `main` → `exit` via the C runtime + and dynamic linker; no semihosting. +- **Hard-float ABI (armhf):** `arm-linux-gnueabihf` passes float/double in VFP + registers (s0–s7 / d0–d3) rather than core registers. This requires the VFP + codegen follow-on (listed under "core follow-on variants" above) before it can + be tackled; it is a separate ABI vtable and is sequenced last. + +Windows ARM32 is out of scope (would need COFF/PE ARM support, which does not +exist, and ARM32 Windows is extremely niche today). + +### Phase 3 checklist (single source of truth) + +**Sequence A — arm-linux-gnueabi (soft-float):** +- [ ] **A-profile TLS** — replace `arm_tls_addr_of`'s static fallback in + `src/arch/arm32/native.c` with `MRC p15, 0, Rd, c13, c0, 3` (TPIDRURO) + conditioned on `KIT_OS_LINUX`. The M-profile path stays for freestanding. +- [ ] **ARM attributes profile field** — `src/obj/elf/emit.c:158` emits + `Tag_CPU_arch_profile = 'M'`; emit `'A'` when `target.os != KIT_OS_FREESTANDING`. +- [ ] **Driver triple** — add `arm-linux-gnueabi` → `{KIT_ARCH_ARM_32, KIT_OS_LINUX}` + in `driver/lib/target.c`; add a `arm-linux-gnueabi` runtime variant row in + `driver/lib/runtime.c` selecting `arm-eabi-thumb2` rt. +- [ ] **Predefined macros** — `__linux__`, `__gnu_linux__`, `__ELF__`, + `__ARM_ARCH=7`, `__ARM_ARCH_7A__`, `__ARMEL__`, soft-float guards + (`__SOFTFP__`, `__ARM_FP` undefined) for `KIT_OS_LINUX` in + `src/arch/arm32/arch.c`. +- [ ] **Dynamic linking / PLT/GOT** — the dominant item; see §3.1 below. New + relocations: `R_ARM_JUMP_SLOT`, `R_ARM_GLOB_DAT`, `R_ARM_RELATIVE`, + `R_ARM_COPY`, `R_ARM_TLS_DTPMOD32`, `R_ARM_TLS_DTPOFF32`, + `R_ARM_TLS_TPOFF32`. PLT stub + GOT generation in `src/arch/arm32/link.c`; + dynamic TLS via `__tls_get_addr`. +- [ ] **Test lane** — `arm32-linux` exec lane via `qemu-user-arm` + Debian armhf + sysroot (or a native arm32 Linux container); add to `scripts/hosted.sh` + + `test/lib/exec_target.sh`; `make provision TARGET=arm32-linux`. +- [ ] **Smoke test** — `test/smoke/arm32_linux.sh`: compile + link (dynamic) + run + under `qemu-user-arm`; cover i64, soft-double, TLS (`__thread`), and a + shared-library call. +- [ ] **Toy corpus lane** — `arm32-linux` cross lane (`DEPTH=smoke` then `full`); + gate on run-correctness, reds left red. + +**Sequence B — arm-freebsd-eabi (soft-float, after A is green):** +- [ ] **Driver triple** — `arm-freebsd-eabi` → `{KIT_ARCH_ARM_32, KIT_OS_FREEBSD}`; + FreeBSD predefined macros (`__FreeBSD__`, `__ARM_ARCH_7A__`, etc.). +- [ ] **Dynamic interp** — FreeBSD's interpreter path is `/libexec/ld-elf.so.1`; + confirm kit's linker writes the correct `.interp` for `KIT_OS_FREEBSD` arm32. +- [ ] **Test lane** — `arm32-freebsd` exec lane via the FreeBSD ARM32 VM or + `qemu-user-arm` + FreeBSD armv7 sysroot; `make provision TARGET=arm32-freebsd`. +- [ ] **Smoke test** — `test/smoke/arm32_freebsd.sh`; same coverage as arm32-linux. +- [ ] **Toy corpus lane** — `arm32-freebsd` cross lane. + +**Sequence C — arm-linux-gnueabihf (hard-float, after VFP follow-on lands):** +- [ ] **Prerequisite:** hard-float VFP codegen follow-on (listed in core follow-on + variants above: `arm-none-eabihf`, FPv4-SP, VFP register args). +- [ ] **ABI vtable** — `src/abi/abi_aapcs32hf.c`: float/double args in s0–s7/d0–d3 + (AAPCS VFP variant); `{KIT_ARCH_ARM_32, KIT_OBJ_ELF, KIT_OS_LINUX, + float_abi=hard, &aapcs32hf_vtable}` in `src/abi/registry.c`. +- [ ] **Driver triple** — `arm-linux-gnueabihf` → `{KIT_ARCH_ARM_32, KIT_OS_LINUX, + float_abi=hard}`; runtime variant `arm-eabihf-thumb2` in `mk/rt.mk`. +- [ ] **ELF e_flags** — `EF_ARM_ABI_FLOAT_HARD` on hard-float objects; linker + rejects hard/soft mismatch. +- [ ] **Predefined macros** — `__ARM_FP=4` (FPv4-SP), `__ARM_NEON_FP=4` if NEON, + `__ARM_PCS_VFP=1`; remove `__SOFTFP__`. +- [ ] **Test lane + smoke** — `arm32-linux-hf` lane + `test/smoke/arm32_linux_hf.sh`. + +### §3.1 Dynamic linking design (the dominant item) + +The static-only `src/arch/arm32/link.c` currently provides `LinkArchDesc` with no +PLT. The hosted path follows the same structure as `src/arch/aa64/link.c` and +`src/arch/x64/link.c`: + +**PLT stub layout (Thumb-2, position-independent):** +``` + PUSH {r0–r3, lr} @ preserve arg regs + lr (per GNU ld Thumb PLT) + LDR r12, [pc, #off] @ load GOT entry address (literal pool or MOVW/MOVT) + LDR pc, [r12] @ tail-branch through GOT slot + .word <GOT entry abs addr> +``` +The resolver stub (PLT[0]) jumps to the dynamic linker; subsequent stubs are 12 +or 16 bytes depending on whether the GOT slot is PC-relative-reachable. Use the +Thumb-2 interworking BX for indirect branches since the dynamic linker may be in +ARM state. Mark each PLT entry `STT_FUNC` with the Thumb bit set. + +**Relocations to add** (`src/obj/obj.h` RelocKind + `src/obj/elf/reloc_arm.c`): + +| kit RelocKind | ELF R_ARM_* | Purpose | +|---|---|---| +| `R_ARM_JUMP_SLOT` | same | PLT GOT slot — patched by dynamic linker at load | +| `R_ARM_GLOB_DAT` | same | GOT data slot for a symbol | +| `R_ARM_RELATIVE` | same | base-relative fixup (for PIE / ASLR) | +| `R_ARM_COPY` | same | copy reloc for non-function symbols | +| `R_ARM_TLS_DTPMOD32` | same | TLS module index (dynamic TLS) | +| `R_ARM_TLS_DTPOFF32` | same | TLS offset within module | +| `R_ARM_TLS_TPOFF32` | same | TLS TP-relative offset (static TLS via LE) | + +**TLS models for A-profile hosted:** +- Local-exec (static, `-fno-pic`): `R_ARM_TLS_LE32`; thread pointer from + `MRC p15, 0, Rd, c13, c0, 3`; offset is `S + A − tp`. +- Initial-exec (GOT-indirect): `R_ARM_TLS_TPOFF32` in the GOT; two-instruction + sequence (`LDR r0, [tp, got_off]`). +- General-dynamic (cross-DSO): call `__tls_get_addr({module, offset})`; needs + `R_ARM_TLS_DTPMOD32` + `R_ARM_TLS_DTPOFF32` GOT pair. This is the default for + shared objects. + +Kit's driver should default to local-exec for executables and general-dynamic for +shared objects, matching GCC/clang defaults. + +**PIE:** `R_ARM_RELATIVE` applied at load time for all absolute data refs in a +position-independent executable. The linker emits a `.rel.dyn` section. This +enables ASLR on modern Linux/FreeBSD and is required for executables on hardened +distributions. + +### §3.2 A-profile vs M-profile divergence in the backend + +The Thumb-2 ISA is binary-identical across profiles. The divergences are +localized to: + +1. `arm_tls_addr_of` (`native.c`) — gate on `KIT_OS_FREESTANDING` for the static + path; emit `MRC p15, 0, Rd, c13, c0, 3` for Linux/FreeBSD. +2. `.ARM.attributes` (`obj/elf/emit.c:158`) — `Tag_CPU_arch_profile = 'A'` for + hosted; `'M'` for freestanding. Also `Tag_CPU_arch = 10` (ARMv7) stays the + same; `Tag_CPU_arch_profile` is the only field that changes. +3. `arch.c` predefined macros — `__ARM_ARCH_7A__` vs `__ARM_ARCH_7M__`; + `__linux__` / `__FreeBSD__` gated on OS. +4. `link.c` — PLT/GOT gated on `KIT_OS != KIT_OS_FREESTANDING`. + +No fork of `native.c` or the ABI vtable is needed for the soft-float hosted path. + +### §3.3 Test infrastructure + +**qemu-user-arm** is the right exec backend for arm32-linux. It runs A-profile +ELF32 user-space binaries directly on the host without a full system emulator, +intercepts Linux syscalls, and is widely available (`apt install qemu-user-static` +on Debian/Ubuntu). This is the same model used for other hosted cross targets +(`exec_target` seam in `test/lib/exec_target.sh`). + +**FreeBSD armv7:** options are (a) the FreeBSD armv7 QEMU system image (heavier, +already the model for FreeBSD aa64), or (b) `qemu-user-arm` + a FreeBSD armv7 +sysroot (lighter, but less battle-tested). The system-image path is more +consistent with the existing `exec_vm.sh` infrastructure. + +**armhf (gnueabihf):** `qemu-user-arm` handles hard-float ARMv7-A binaries; no +separate emulator is needed. The sysroot must be an armhf sysroot (e.g., Debian +armhf cross-compilation sysroot). + +The exec lane for arm32-linux slots naturally into the existing `exec_target` +harness alongside the other cross-hosted lanes. `make provision TARGET=arm32-linux` +installs `qemu-user-arm`, the cross sysroot, and any required binfmt_misc +registration. + +### §3.4 Sequencing rationale + +arm-linux-gnueabi first, because: +- Soft-float AAPCS32 is already implemented; no new ABI vtable needed. +- ELF32 output is already correct; work is limited to dynamic linking + TLS. +- arm-freebsd-eabi follows almost for free: same ISA, same ABI, same ELF32, same + CP15 TLS — only driver triples, the `.interp` path, and a test lane differ. +- arm-linux-gnueabihf is gated on the VFP codegen follow-on, so it cannot start + until that lands regardless of hosted progress. + +The dynamic linking work (§3.1) is the critical-path item for both Linux and +FreeBSD. It can be developed once and reused; the only FreeBSD-specific linker +concern is `.interp` and `DT_NEEDED` for `libc.so.7` vs `libc.so.6`. + +--- + ## Status — 2026-06-16 — Phase 2b — memory/frame/ABI correctness landed The arm32 backend is a working -O0/-O1 Cortex-M C toolchain: it compiles, links