commit b4f2b7450e8b321b22ebed6233929eaecbae3203
parent bf9137427b2a9497d5c5436fdc159b9cc7a5e56a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 11:20:16 -0700
plan: ARM32 hosted
Diffstat:
| M | doc/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