kit

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

commit 3b67636bfc544336856a69b9a7f419d30b0f0858
parent 2c77a938603088abd0c636df6758674e9a10e723
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 16:24:59 -0700

arm32 Phase 1: 32-bit ARM (ARMv7-M Thumb-2) walking skeleton

`make test-cross TARGET=freestanding-arm32 DEPTH=smoke` is green: kit cc
compiles arm-none-eabi objects, kit ld links them with a clang-assembled
Cortex-M reset stub + linker script, qemu-system-arm (mps2-an385) runs the
image, and the ARM-semihosting SYS_EXIT_EXTENDED exit code matches.

Backend (src/arch/arm32/): arch (ArchImpl + Thumb-2 label-fixup patcher +
predefined macros/features), isa.h (header-only Thumb-2 encoders incl.
ThumbExpandImm), native.c (the -O0 NativeTarget spine: FP-anchored deferred-sub
frame, r0-r3 NDT pool + IP scratch + lr emit-temp, MOVW/MOVT immediates,
ALU/mul/sdiv-udiv-mls/shifts/extends, CMP+IT compare + B<cond>.W branches,
AAPCS calls/returns), reloc.c (THM_CALL/JUMP24 split-immediate patcher),
link.c, regs.c, variant.{h,c}, dbg/disasm/asm stubs.

ABI: src/abi/abi_aapcs32.c (AAPCS32 soft-float: r0-r3 + stack, i64/double GPR
pair, sret in r0, scalar_split_lane_size=4 for the wide8 path).

ELF: EM_ARM + EF_ARM_* + R_ARM_* (obj.h enum tail + elf.h), reloc_arm.c
mapper, obj/registry.c arch-ops row, EM_ARM e_flags synthesis.

Gating/registration: KIT_ARCH_ARM32_ENABLED + arch_impl_arm32 + aapcs32_vtable
across the arch/abi/obj registries, config_assert, mk/lib_srcs.mk, and the
disasm/dbg/link stub files.

Cross-test lane: scripts/hosted.sh (freestanding-arm32), test/lib/exec_bare.sh
(_bare_emit_arm32 + mps2-an385 semihosting oracle, kit-ld-compatible script),
cross_test.sh (arm32 smoke at -O0), mk/test.mk (test-smoke-arm32), plus
test/smoke/arm32.sh + check_arm32_env.sh.

Phase 2 (panic-stubbed/deferred per doc/plan/ARM32.md): -O1 known-frame,
i64/soft-double, atomics/TLS/varargs/inline-asm/structs, disassembler +
assembler, kit-built runtime, SRAM .data LMA.

Diffstat:
Mdoc/plan/ARM32.md | 31++++++++++++++++++++++++++-----
Minclude/kit/config.h | 1+
Mmk/lib_srcs.mk | 14++++++++++++++
Mmk/test.mk | 8++++++++
Mscripts/cross_test.sh | 8++++++--
Mscripts/hosted.sh | 8+++++---
Asrc/abi/abi_aapcs32.c | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/abi/abi_internal.h | 1+
Msrc/abi/registry.c | 8+++++++-
Asrc/arch/arm32/arch.c | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/arm32/arm32.h | 11+++++++++++
Asrc/arch/arm32/asm.c | 14++++++++++++++
Asrc/arch/arm32/dbg.c | 28++++++++++++++++++++++++++++
Asrc/arch/arm32/disasm.c | 13+++++++++++++
Asrc/arch/arm32/isa.h | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/arm32/link.c | 20++++++++++++++++++++
Asrc/arch/arm32/native.c | 1249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/arm32/regs.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/arm32/regs.h | 12++++++++++++
Asrc/arch/arm32/reloc.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/arch/arm32/variant.c | 18++++++++++++++++++
Asrc/arch/arm32/variant.h | 29+++++++++++++++++++++++++++++
Msrc/arch/dbg_stubs.c | 1+
Msrc/arch/disasm_stubs.c | 5+++++
Msrc/arch/dwarf.c | 6++++++
Msrc/arch/link_stubs.c | 1+
Msrc/arch/registry.c | 11+++++++++--
Msrc/core/config_assert.c | 6++++--
Msrc/obj/elf/elf.h | 36++++++++++++++++++++++++++++++++++++
Msrc/obj/elf/emit.c | 9+++++++++
Asrc/obj/elf/reloc_arm.c | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/obj/obj.c | 8++++++++
Msrc/obj/obj.h | 13+++++++++++++
Msrc/obj/registry.c | 28++++++++++++++++++++++++++--
Atest/lib/check_arm32_env.sh | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/lib/exec_bare.sh | 170++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atest/smoke/arm32.sh | 326+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
37 files changed, 3144 insertions(+), 18 deletions(-)

diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md @@ -1,10 +1,31 @@ # Plan: 32-bit ARM (`arm-none-eabi`, ARMv7-M / ARMv7E-M, Thumb-2) -## Status — 2026-06-16 — not started; scaffolding inventory + spec - -No `arm32` code backend exists yet (`src/arch/` has `aa64`, `riscv`, `x64`, -`wasm`, `c_target` — no `arm32`). This doc specs the ISA, the backend shape, and -the cross-test surface so the work is farmable. +## Status — 2026-06-16 — Phase 1 (walking skeleton) LANDED + +**Phase 1 is green:** `make test-cross TARGET=freestanding-arm32 DEPTH=smoke` +passes — `kit cc -target arm-none-eabi -mcpu=cortex-m3 -ffreestanding` compiles +the smoke set, `kit ld` links it with a clang-assembled Cortex-M reset stub + +linker script, `qemu-system-arm -M mps2-an385` runs it, and the ARM-semihosting +`SYS_EXIT_EXTENDED` exit code matches. The whole `src/arch/arm32/` backend +(`arch/isa/native/reloc/link/regs/variant/dbg/disasm/asm`), the AAPCS32 ABI +vtable (`src/abi/abi_aapcs32.c`), the ARM ELF relocations +(`src/obj/elf/reloc_arm.c` + `EM_ARM`/`EF_ARM_*`/`R_ARM_*`), the +`KIT_ARCH_ARM32_ENABLED` gate + `arch_impl_arm32` registration, and the +`freestanding-arm32` cross-test lane are in. Validated at -O0: return-const, ALU +(add/sub/mul/and/or/eor/shift), sdiv/udiv + mls remainder, signed/unsigned +compare + if/else, recursion, and ≤4-arg + >4-arg (stack) calls — all correct +under qemu; disassembly matches `llvm-objdump`. + +Scoped to Phase 2 (panic-stubbed or deferred today, per the plan below): the +`-O1` known-frame path (Phase 1 is -O0 only — the cross smoke lane forces -O0 +for arm32), i64/soft-double wide8 lowering, atomics/TLS/varargs/inline-asm/ +struct-by-value, the descriptor-driven disassembler + standalone assembler, the +runtime build with kit (the smoke set needs no `libkit_rt.a`), and a kit-ld +linker script with a real SRAM `.data` LMA (kit ld's script front end lacks +`AT>`, so the global-free Phase-1 lane keeps everything in flash). + +The "not started" inventory + ISA/backend/cross-test spec below remains the +source of truth for the remaining (Phase 2) work. The target is a freestanding **Cortex-M** toolchain: `kit cc/as/ld/objdump/disas` producing and consuming correct `arm-none-eabi` ELFCLASS32 objects and static diff --git a/include/kit/config.h b/include/kit/config.h @@ -26,6 +26,7 @@ /* Backend architectures. */ #define KIT_ARCH_AA64_ENABLED 1 #define KIT_ARCH_X64_ENABLED 1 +#define KIT_ARCH_ARM32_ENABLED 1 #define KIT_ARCH_RV32_ENABLED 1 #define KIT_ARCH_RV64_ENABLED 1 #define KIT_ARCH_WASM_ENABLED 1 diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk @@ -23,6 +23,7 @@ flatobjs = $(foreach s,$(1),$(BUILD_DIR)/$(3)/$(dir $(patsubst $(2)/%,%,$(s)))$( define arch-feature-off LIB_SRCS_ARCH_AA64 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_AA64)) LIB_SRCS_ARCH_X64 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_X64)) +LIB_SRCS_ARCH_ARM32 := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_ARM32)) LIB_SRCS_ARCH_RISCV := $$(filter-out %/$(1),$$(LIB_SRCS_ARCH_RISCV)) LIB_SRCS_NONARCH += $(2) endef @@ -57,6 +58,7 @@ LIB_SRCS_NONARCH = $(LIB_SRCS_ABI_CORE) \ # hundreds on every `make` invocation. LIB_SRCS_ARCH_AA64 := $(shell find src/arch/aa64 -name '*.c' 2>/dev/null) LIB_SRCS_ARCH_X64 := $(shell find src/arch/x64 -name '*.c' 2>/dev/null) +LIB_SRCS_ARCH_ARM32 := $(shell find src/arch/arm32 -name '*.c' 2>/dev/null) LIB_SRCS_ARCH_RISCV := $(shell find src/arch/riscv -name '*.c' 2>/dev/null) LIB_SRCS_ARCH_WASM := $(shell find src/arch/wasm -name '*.c' 2>/dev/null) LIB_SRCS_ARCH_C_TARGET := $(shell find src/arch/c_target -name '*.c' 2>/dev/null) @@ -97,6 +99,9 @@ endif ifeq ($(filter 1,$(KIT_ARCH_RV32_ENABLED) $(KIT_ARCH_RV64_ENABLED)),) LIB_SRCS_OBJ_ELF := $(filter-out %/reloc_riscv32.c %/reloc_riscv64.c,$(LIB_SRCS_OBJ_ELF)) endif +ifneq ($(KIT_ARCH_ARM32_ENABLED),1) +LIB_SRCS_OBJ_ELF := $(filter-out %/reloc_arm.c,$(LIB_SRCS_OBJ_ELF)) +endif ifneq ($(KIT_LINK_ENABLED),1) LIB_SRCS_OBJ_ELF := $(filter-out %/link.c %/link_dyn.c,$(LIB_SRCS_OBJ_ELF)) LIB_SRCS_OBJ_MACHO := $(filter-out %/link.c,$(LIB_SRCS_OBJ_MACHO)) @@ -159,6 +164,7 @@ LIB_SRC_ABI_SYSV_X64 = src/abi/abi_sysv_x64.c LIB_SRC_ABI_APPLE_X64 = src/abi/abi_apple_x64.c LIB_SRC_ABI_WIN64_X64 = src/abi/abi_win64_x64.c LIB_SRC_ABI_RV64 = src/abi/abi_rv64.c +LIB_SRC_ABI_AAPCS32 = src/abi/abi_aapcs32.c LIB_SRCS = $(LIB_SRCS_NONARCH) ifeq ($(KIT_OPT_ENABLED),1) @@ -211,6 +217,9 @@ endif ifeq ($(KIT_ARCH_X64_ENABLED),1) LIB_SRCS += $(LIB_SRCS_ARCH_X64) endif +ifeq ($(KIT_ARCH_ARM32_ENABLED),1) +LIB_SRCS += $(LIB_SRCS_ARCH_ARM32) +endif ifneq ($(filter 1,$(KIT_ARCH_RV32_ENABLED) $(KIT_ARCH_RV64_ENABLED)),) LIB_SRCS += $(LIB_SRCS_ARCH_RISCV) endif @@ -275,6 +284,11 @@ ifeq ($(KIT_OBJ_ELF_ENABLED),1) LIB_SRCS += $(LIB_SRC_ABI_RV64) endif endif +ifneq ($(filter 1,$(KIT_ARCH_ARM32_ENABLED) $(KIT_ARCH_C_TARGET_ENABLED)),) +ifeq ($(KIT_OBJ_ELF_ENABLED),1) +LIB_SRCS += $(LIB_SRC_ABI_AAPCS32) +endif +endif # Per-frontend source sets. Each is gated by its KIT_LANG_*_ENABLED flag # from mk/config.mk so the matching `#if` in src/api/lang_registry.c and diff --git a/mk/test.mk b/mk/test.mk @@ -124,6 +124,7 @@ TEST_TARGETS = \ test-rv64-tls-link \ test-smoke-rv64 \ test-smoke-rv32 \ + test-smoke-arm32 \ test-smoke-x64 \ test-smoke-x64-macos \ test-toy \ @@ -1203,6 +1204,13 @@ test-smoke-rv64: test-smoke-rv32: bash test/smoke/rv32.sh +# test-smoke-arm32: behavioral oracle for arm-none-eabi (Cortex-M3, Thumb-2) +# codegen under qemu-system-arm -machine mps2-an385 (single soft-float lane, +# i64 + soft-double + a <=4-int-arg call + a switch). Skips if the arm32 +# toolchain/qemu prerequisites are absent (see test/lib/check_arm32_env.sh). +test-smoke-arm32: + bash test/smoke/arm32.sh + # The whole-toolchain bare-metal smoke (compile + assemble reset stub + link + # boot under qemu-system, for aarch64/x86_64/riscv64/riscv32) is now driven by # make test-cross TARGET=freestanding diff --git a/scripts/cross_test.sh b/scripts/cross_test.sh @@ -184,9 +184,13 @@ smoke_build_case() { if [ "$os" = freestanding ]; then # Bare-metal: compile to an object, link with the reset stub now, and leave # only the qemu boot for the execution phase. - local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason + local obj="$cdir/$name.o" elf="$cdir/$name.elf" barch reason smopt barch="$(tok_arch "$t")" - if ! "$HOSTED" cc "$t" -O1 -ffreestanding -c "$src" -o "$obj" \ + # arm32 is a -O0-only Phase-1 walking skeleton (the -O1 known-frame path is + # a Phase-2 code-quality track, per doc/plan/ARM32.md); every other + # freestanding arch exercises the optimizer at -O1. + smopt=O1; [ "$barch" = arm32 ] && smopt=O0 + if ! "$HOSTED" cc "$t" -$smopt -ffreestanding -c "$src" -o "$obj" \ >"$cdir/$name.cc.out" 2>"$cdir/$name.cc.err"; then kit_fail "$label:cc" "cc failed"; sed 's/^/ | /' "$cdir/$name.cc.err" | head; return fi diff --git a/scripts/hosted.sh b/scripts/hosted.sh @@ -10,7 +10,7 @@ # Targets: <os>[-<libc>]-<arch> (canonical arch tokens: aa64/x64/rv64[/rv32]) # linux-glibc-{aa64,x64,rv64} linux-musl-{aa64,x64,rv64} # freebsd-{aa64,x64,rv64} windows-{aa64,x64} macos-aa64 -# freestanding-{aa64,x64,rv64,rv32} (no OS — cross only, bare-metal exec) +# freestanding-{aa64,x64,rv64,rv32,arm32} (no OS — cross only, bare-metal exec) # Long arch spellings (aarch64/amd64/x86_64/riscv64) are accepted as input # aliases; canon_token normalizes them to the short forms above. # @@ -44,7 +44,7 @@ linux-musl-aa64 linux-musl-x64 linux-musl-rv64 freebsd-aa64 freebsd-x64 freebsd-rv64 windows-aa64 windows-x64 macos-aa64 macos-x64 -freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32 +freestanding-aa64 freestanding-x64 freestanding-rv64 freestanding-rv32 freestanding-arm32 " die() { printf 'hosted: %s\n' "$*" >&2; exit 1; } @@ -90,6 +90,7 @@ parse_target() { x64|x86_64|amd64) T_ARCH=x64 ;; rv64|riscv64) T_ARCH=rv64 ;; rv32|riscv32) T_ARCH=rv32 ;; + arm32|arm|armv7m|armv7em|cortex-m3|cortex-m4|cortex-m7) T_ARCH=arm32 ;; *) die "bad freestanding arch '$rest' in '$t'" ;; esac ;; *) die "unknown os in target '$t'" ;; @@ -128,6 +129,7 @@ triple_of() { x64) echo x86_64-none-elf ;; rv64) echo riscv64-none-elf ;; rv32) echo riscv32-none-elf ;; + arm32) echo arm-none-eabi ;; esac ;; esac } @@ -153,7 +155,7 @@ canon_token() { local ea case "$T_ARCH" in aarch64) ea=aa64 ;; x64|amd64) ea=x64 ;; rv64|riscv64) ea=rv64 ;; - rv32) ea=rv32 ;; *) ea="$T_ARCH" ;; + rv32) ea=rv32 ;; arm32) ea=arm32 ;; *) ea="$T_ARCH" ;; esac case "$T_OS" in linux) printf 'linux-%s-%s' "$T_LIBC" "$ea" ;; diff --git a/src/abi/abi_aapcs32.c b/src/abi/abi_aapcs32.c @@ -0,0 +1,133 @@ +/* AAPCS32 / ARM EABI (AArch32, ELF, soft-float v1). + * + * Structurally the rv32 ILP32 classifier with the float-ABI pinned to soft: + * void -> IGNORE + * integer/ptr <= 4 -> DIRECT, one INT part (r0..r3 for args; r0 for return) + * i64 / double (8B) -> DIRECT, two 4-byte INT parts (the r0:r1 / r2:r3 pair). + * The 8-byte even-register / 8-byte-stack alignment that + * AAPCS requires is surfaced to the native backend via + * ABIArgPart.align; this layer just declares two lanes. + * aggregate <= 16B -> DIRECT, up to four word parts (r0..r3) + * aggregate > 16B -> INDIRECT (sret pointer in r0 / byval) + * + * The sret pointer is passed in r0 (the first integer arg register), consuming + * that slot, so sret_consumes_int_arg = 1 (like RISC-V, unlike AArch64's x8). + * + * Hard-float (FPv4-SP) + HFAs + __int128 are out of scope for v1. */ +#include <string.h> + +#include "abi/abi_internal.h" +#include "cg/type.h" +#include "core/arena.h" +#include "core/core.h" + +#define ARM32_GPR_BYTES 4u /* r0..r3 are 32-bit */ +#define ARM32_AGG_GPR_BYTES 16u /* aggregates <= 4 words pass in registers */ + +static void classify_scalar(TargetABI* a, KitCgTypeId t, ABIArgInfo* out) { + ABITypeInfo ti = abi_cg_type_info(a, t); + /* i64 / soft double -> even/odd GPR pair (r0:r1 or r2:r3). */ + if (ti.size == 2u * ARM32_GPR_BYTES && + (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT)) { + ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, 2); + memset(parts, 0, sizeof(ABIArgPart) * 2); + parts[0].cls = ABI_CLASS_INT; + parts[0].loc = ABI_LOC_REG; + parts[0].size = ARM32_GPR_BYTES; + parts[0].align = ARM32_GPR_BYTES; + parts[0].src_offset = 0; + parts[1].cls = ABI_CLASS_INT; + parts[1].loc = ABI_LOC_REG; + parts[1].size = ARM32_GPR_BYTES; + parts[1].align = ARM32_GPR_BYTES; + parts[1].src_offset = ARM32_GPR_BYTES; + out->kind = ABI_ARG_DIRECT; + out->flags = ABI_AF_NONE; + out->parts = parts; + out->nparts = 2; + out->indirect_align = 0; + return; + } + /* int / pointer / float<=4 -> one INT part (soft-float: floats in core regs). */ + abi_classify_scalar_reg_part(a, out, ti, /*is_fp=*/0); +} + +static u32 arm32_scalar_split_lane_size(TargetABI* a, KitCgTypeId t) { + ABITypeInfo ti = abi_cg_type_info(a, t); + if (ti.size == 2u * ARM32_GPR_BYTES && + (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT)) + return ARM32_GPR_BYTES; /* 4 -> drives the arch-neutral wide8 path */ + return 0; +} + +static void classify_aggregate(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, + int is_return) { + ABITypeInfo ti = abi_cg_type_info(a, t); + if (ti.size == 0) { + abi_classify_void(out); + return; + } + if (ti.size <= ARM32_AGG_GPR_BYTES) { + u32 nparts = (ti.size + ARM32_GPR_BYTES - 1u) / ARM32_GPR_BYTES; + ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, nparts); + u32 off = 0; + u32 i; + memset(parts, 0, sizeof(ABIArgPart) * nparts); + for (i = 0; i < nparts; ++i) { + u32 chunk = + (ti.size - off > ARM32_GPR_BYTES) ? ARM32_GPR_BYTES : (ti.size - off); + parts[i].cls = ABI_CLASS_INT; + parts[i].loc = ABI_LOC_REG; + parts[i].size = chunk; + parts[i].align = ARM32_GPR_BYTES; + parts[i].src_offset = off; + off += chunk; + } + out->kind = ABI_ARG_DIRECT; + out->flags = ABI_AF_NONE; + out->parts = parts; + out->nparts = (u16)nparts; + out->indirect_align = 0; + } else { + out->kind = ABI_ARG_INDIRECT; + out->flags = is_return ? ABI_AF_SRET : ABI_AF_BYVAL; + out->indirect_align = ti.align ? ti.align : ARM32_GPR_BYTES; + out->parts = NULL; + out->nparts = 0; + } +} + +static void classify_one(TargetABI* a, KitCgTypeId t, ABIArgInfo* out, + int is_return) { + const CgType* ty = cg_type_get(a->c, t); + if (!ty || ty->kind == KIT_CG_TYPE_VOID) { + abi_classify_void(out); + return; + } + switch (ty->kind) { + case KIT_CG_TYPE_RECORD: + classify_aggregate(a, t, out, is_return); + return; + default: + classify_scalar(a, t, out); + return; + } +} + +static ABIFuncInfo* arm32_compute_func_info(TargetABI* a, KitCgTypeId fn) { + return abi_compute_func_info_generic(a, fn, classify_one, + /*sret_consumes_int_arg=*/1); +} + +const ABIVtable aapcs32_vtable = { + .compute_func_info = arm32_compute_func_info, + .scalar_split_lane_size = arm32_scalar_split_lane_size, + /* AAPCS32 va_list is a plain 4-byte pointer; the variadic register-save + * area is r0..r3 spilled contiguously. No FP save area (soft-float). */ + .va_list_info = {4, 4, ABI_SC_PTR, 0, 0, 0}, + .va_list_layout = {.kind = ABI_VA_LIST_POINTER, + .gp_reg_count = 4, + .fp_reg_count = 0, + .gp_slot_size = 4, + .fp_slot_size = 0}, +}; diff --git a/src/abi/abi_internal.h b/src/abi/abi_internal.h @@ -35,6 +35,7 @@ extern const ABIVtable aapcs64_vtable; extern const ABIVtable sysv_x64_vtable; extern const ABIVtable rv64_vtable; extern const ABIVtable rv32_vtable; +extern const ABIVtable aapcs32_vtable; extern const ABIVtable wasm32_vtable; /* Apple Darwin variants — selected when (arch, os) matches. See * abi.c::select_vtable. */ diff --git a/src/abi/registry.c b/src/abi/registry.c @@ -17,6 +17,8 @@ typedef struct ABIImpl { (KIT_ARCH_RV64_ENABLED || KIT_ARCH_C_TARGET_ENABLED) #define KIT_ABI_RV32_ENABLED \ (KIT_ARCH_RV32_ENABLED || KIT_ARCH_C_TARGET_ENABLED) +#define KIT_ABI_ARM32_ENABLED \ + (KIT_ARCH_ARM32_ENABLED || KIT_ARCH_C_TARGET_ENABLED) static const ABIImpl abi_impls[] = { #if KIT_ABI_AA64_ENABLED && KIT_OBJ_ELF_ENABLED @@ -43,11 +45,14 @@ static const ABIImpl abi_impls[] = { #if KIT_ABI_RV32_ENABLED && KIT_OBJ_ELF_ENABLED {KIT_ARCH_RV32, KIT_OBJ_ELF, &rv32_vtable}, #endif +#if KIT_ABI_ARM32_ENABLED && KIT_OBJ_ELF_ENABLED + {KIT_ARCH_ARM_32, KIT_OBJ_ELF, &aapcs32_vtable}, +#endif #if KIT_ARCH_WASM_ENABLED && KIT_OBJ_WASM_ENABLED {KIT_ARCH_WASM, KIT_OBJ_WASM, &wasm32_vtable}, #endif #if !KIT_ABI_AA64_ENABLED && !KIT_ABI_X64_ENABLED && !KIT_ABI_RV64_ENABLED && \ - !KIT_ABI_RV32_ENABLED && !KIT_ARCH_WASM_ENABLED + !KIT_ABI_RV32_ENABLED && !KIT_ABI_ARM32_ENABLED && !KIT_ARCH_WASM_ENABLED {KIT_ARCH_WASM, KIT_OBJ_WASM, NULL}, #endif }; @@ -65,3 +70,4 @@ const ABIVtable* abi_vtable_lookup(KitArchKind arch, KitObjFmt obj) { #undef KIT_ABI_X64_ENABLED #undef KIT_ABI_RV64_ENABLED #undef KIT_ABI_RV32_ENABLED +#undef KIT_ABI_ARM32_ENABLED diff --git a/src/arch/arm32/arch.c b/src/arch/arm32/arch.c @@ -0,0 +1,269 @@ +/* arch_impl_arm32: the ARM32 (ARMv7-M Thumb-2, arm-none-eabi) ArchImpl. + * + * The backend is built on the shared NativeDirectTarget/MCEmitter substrate; + * arch.c wires the factories, register metadata, CFI defaults, predefined + * macros, target features, and the intra-function Thumb-2 branch label-fixup + * patcher. Soft-float v1; ARMv7-M baseline (Cortex-M3). See doc/plan/ARM32.md. */ +#include "arch/arch.h" + +#include <string.h> + +#include "arch/arm32/arm32.h" +#include "arch/arm32/regs.h" +#include "arch/arm32/variant.h" +#include "cg/native_direct_target.h" +#include "core/bytes.h" +#include "core/core.h" +#include "link/link_arch.h" +#include "obj/obj.h" + +extern const LinkArchDesc link_arch_arm32; +extern const ArchDbgOps arm32_dbg_ops; +extern const ArchDwarfOps arm32_dwarf_ops; +extern ArchAsm* arm32_arch_asm_new(Compiler*); +extern ArchDisasm* arm32_disasm_new(Compiler*); + +static int arm32_register_at_public(uint32_t idx, KitArchReg* out) { + return arch_register_at_public(idx, out, arm32_register_iter_get); +} + +static SrcLoc arm32_no_loc(void) { + SrcLoc l = {0, 0, 0}; + return l; +} + +/* Read/write a 32-bit Thumb-2 instruction stored as two little-endian + * half-words (high half-word first): the returned value has hw1 in [31:16], + * hw2 in [15:0]. */ +static u32 arm_rd_t32(const u8* p) { + return ((u32)rd_u16_le(p) << 16) | (u32)rd_u16_le(p + 2); +} +static void arm_wr_t32(u8* p, u32 instr) { + wr_u16_le(p, (u16)(instr >> 16)); + wr_u16_le(p + 2, (u16)(instr & 0xffffu)); +} + +/* Patch an intra-function Thumb-2 branch recorded by the MCEmitter label-fixup + * path: B.W (T4, R_ARM_THM_JUMP24, +-16MiB) and B<cond>.W (T3, + * R_ARM_THM_JUMP19, +-1MiB). fx->disp is the site-relative byte displacement + * (target - branch_start); the Thumb pipeline reads PC as branch_start + 4, so + * the encoded offset is fx->disp - 4. The opcode/cond bits the emitter wrote + * are preserved; only the split immediate fields are filled in. */ +static int arm32_apply_label_fixup(Compiler* c, const ArchLabelFixup* fx) { + const Section* s; + u8 cur[4]; + u32 instr; + i64 d; + + if (!fx) return 1; + if (fx->width != 4) return 1; + s = obj_section_get(fx->obj, fx->sec_id); + if (!s) return 0; + buf_read(&s->bytes, fx->offset, cur, 4); + instr = arm_rd_t32(cur); + d = (i64)fx->disp - 4; /* Thumb PC bias */ + + switch (fx->kind) { + case R_ARM_THM_JUMP24: { + u32 sbit, i1, i2, j1, j2, imm10, imm11; + if (d < -(i64)(1 << 24) || d >= (i64)(1 << 24)) + compiler_panic(c, arm32_no_loc(), "arm32: B.W out of range (+-16MiB)"); + sbit = (u32)((d >> 24) & 1u); + i1 = (u32)((d >> 23) & 1u); + i2 = (u32)((d >> 22) & 1u); + imm10 = (u32)((d >> 12) & 0x3ffu); + imm11 = (u32)((d >> 1) & 0x7ffu); + j1 = (~(i1 ^ sbit)) & 1u; + j2 = (~(i2 ^ sbit)) & 1u; + instr &= 0xf800d000u; /* keep opcode (11110), hw2[15:14]=10, hw2[12]=1 */ + instr |= (sbit << 26) | (imm10 << 16); + instr |= (j1 << 13) | (j2 << 11) | imm11; + break; + } + case R_ARM_THM_JUMP19: { + /* B<cond>.W T3: imm32 = SignExtend(S:J2:J1:imm6:imm11:0), no XOR-with-S. */ + u32 sbit, j1, j2, imm6, imm11; + if (d < -(i64)(1 << 20) || d >= (i64)(1 << 20)) + compiler_panic(c, arm32_no_loc(), "arm32: B.cond.W out of range (+-1MiB)"); + sbit = (u32)((d >> 20) & 1u); + j2 = (u32)((d >> 19) & 1u); + j1 = (u32)((d >> 18) & 1u); + imm6 = (u32)((d >> 12) & 0x3fu); + imm11 = (u32)((d >> 1) & 0x7ffu); + /* clear S(26), imm6(21:16), J1(13), J2(11), imm11(10:0); keep opcode, + * cond(25:22), hw2[15:14]=10, hw2[12]=0. */ + instr &= ~((1u << 26) | (0x3fu << 16) | (1u << 13) | (1u << 11) | 0x7ffu); + instr |= (sbit << 26) | (imm6 << 16) | (j1 << 13) | (j2 << 11) | imm11; + break; + } + default: + return 1; + } + + arm_wr_t32(cur, instr); + obj_patch(fx->obj, fx->sec_id, fx->offset, cur, 4); + return 0; +} + +/* Mirrors `clang --target=arm-none-eabi -march=armv7-m -mfloat-abi=soft -E -dM` + * for the in-scope ARMv7-M (Cortex-M3) soft-float profile. Arch-identity + + * feature macros only; the ILP32 size/type macros are emitted by the + * preprocessor from target.ptr_size=4. Static (ptr,count) table consumed + * without a Target, so it reflects this one default profile (soft-float + * Cortex-M3); -mcpu/-march cannot retune it (cf. rv32's identical limitation). + * Soft-float codegen correctness is driven by c->target.float_abi. */ +static const KitPredefinedMacro arm32_predefined_macros[] = { + {KIT_SLICE_LIT("__arm__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARMEL__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__thumb__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__thumb2__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__THUMBEL__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_ARCH"), KIT_SLICE_LIT("7")}, + {KIT_SLICE_LIT("__ARM_ARCH_7M__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_ARCH_PROFILE"), KIT_SLICE_LIT("'M'")}, + {KIT_SLICE_LIT("__ARM_ARCH_ISA_THUMB"), KIT_SLICE_LIT("2")}, + {KIT_SLICE_LIT("__ARM_ARCH_ISA_ARM"), KIT_SLICE_LIT("0")}, + {KIT_SLICE_LIT("__ARM_32BIT_STATE"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_EABI__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_PCS"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__SOFTFP__"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_FP"), KIT_SLICE_LIT("0")}, + {KIT_SLICE_LIT("__ARM_FEATURE_CLZ"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_FEATURE_IDIV"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_FEATURE_UNALIGNED"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ARM_FEATURE_QBIT"), KIT_SLICE_LIT("1")}, + {KIT_SLICE_LIT("__ORDER_LITTLE_ENDIAN__"), KIT_SLICE_LIT("1234")}, + {KIT_SLICE_LIT("__ORDER_BIG_ENDIAN__"), KIT_SLICE_LIT("4321")}, + {KIT_SLICE_LIT("__BYTE_ORDER__"), KIT_SLICE_LIT("__ORDER_LITTLE_ENDIAN__")}, + {KIT_SLICE_LIT("__LITTLE_ENDIAN__"), KIT_SLICE_LIT("1")}, +}; + +static const ArchTargetFeature arm32_target_features[] = { + {"dsp"}, /* ARMv7E-M saturating/SIMD (follow-on) */ + {"vfp"}, /* hard-float FPv4-SP (follow-on) */ +}; + +static void arm32_target_feature_defaults(const Target* t, u64* words, + u32 nwords) { + (void)t; + (void)words; + (void)nwords; /* baseline ARMv7-M soft: no features default-on. */ +} + +static KitStatus arm32_target_feature_apply_isa(const Target* t, KitSlice isa, + u64* words, u32 nwords) { + (void)t; + (void)words; + (void)nwords; + if (kit_slice_eq_cstr(isa, "armv7-m") || kit_slice_eq_cstr(isa, "armv7e-m") || + kit_slice_eq_cstr(isa, "thumbv7m") || kit_slice_eq_cstr(isa, "thumbv7em")) + return KIT_OK; + return KIT_UNSUPPORTED; +} + +static int arm32_supports_call_conv(const Compiler* c, KitCgCallConv cc) { + (void)c; + return cc == KIT_CG_CC_TARGET_C; +} + +/* Kept in lockstep with arm32_intrinsic in native.c: returning 1 for an + * intrinsic this backend does not lower panics at emit. No default: case so + * -Wswitch enforces the twin-sync invariant. Phase 1 lowers only TRAP. */ +static int arm32_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { + (void)c; + switch (intrin) { + case KIT_CG_INTRIN_TRAP: + return 1; + case KIT_CG_INTRIN_CLZ: + case KIT_CG_INTRIN_CTZ: + case KIT_CG_INTRIN_POPCOUNT: + case KIT_CG_INTRIN_BSWAP: + case KIT_CG_INTRIN_SADD_OVERFLOW: + case KIT_CG_INTRIN_UADD_OVERFLOW: + case KIT_CG_INTRIN_SSUB_OVERFLOW: + case KIT_CG_INTRIN_USUB_OVERFLOW: + case KIT_CG_INTRIN_SMUL_OVERFLOW: + case KIT_CG_INTRIN_UMUL_OVERFLOW: + case KIT_CG_INTRIN_PREFETCH: + case KIT_CG_INTRIN_EXPECT: + case KIT_CG_INTRIN_ASSUME_ALIGNED: + case KIT_CG_INTRIN_CPU_NOP: + case KIT_CG_INTRIN_CPU_YIELD: + case KIT_CG_INTRIN_ISB: + case KIT_CG_INTRIN_DMB: + case KIT_CG_INTRIN_DSB: + case KIT_CG_INTRIN_WFI: + case KIT_CG_INTRIN_WFE: + case KIT_CG_INTRIN_SEV: + case KIT_CG_INTRIN_FRAME_ADDRESS: + case KIT_CG_INTRIN_RETURN_ADDRESS: + case KIT_CG_INTRIN_READCYCLECOUNTER: + case KIT_CG_INTRIN_SYSCALL: + case KIT_CG_INTRIN_SETJMP: + case KIT_CG_INTRIN_LONGJMP: + case KIT_CG_INTRIN_FMA: + case KIT_CG_INTRIN_IRQ_SAVE: + case KIT_CG_INTRIN_IRQ_RESTORE: + case KIT_CG_INTRIN_IRQ_DISABLE: + case KIT_CG_INTRIN_IRQ_ENABLE: + case KIT_CG_INTRIN_DCACHE_CLEAN: + case KIT_CG_INTRIN_DCACHE_INVALIDATE: + case KIT_CG_INTRIN_DCACHE_CLEAN_INVALIDATE: + case KIT_CG_INTRIN_ICACHE_INVALIDATE: + case KIT_CG_INTRIN_CORO_SWITCH: + return 0; + } + return 0; +} + +static CgTarget* arm32_backend_make(Compiler* c, ObjBuilder* o, + const KitCodeOptions* opts) { + return native_direct_backend_make(c, o, opts, arm32_native_target_new, + arm32_native_direct_ops()); +} + +static CgTarget* arm32_semantic_target_new(Compiler* c, ObjBuilder* o, + MCEmitter* mc) { + return native_direct_semantic_target_new(c, o, mc, arm32_native_target_new, + arm32_native_direct_ops()); +} + +const ArchImpl arch_impl_arm32 = { + .backend = {.name = "arm32", .make = arm32_backend_make}, + .kind = KIT_ARCH_ARM_32, + .name = "arm32", + .cgtarget_new = arm32_semantic_target_new, + .asm_new = arm32_arch_asm_new, + .disasm_new = arm32_disasm_new, + .apply_label_fixup = arm32_apply_label_fixup, + /* .decode / .emu omitted (NULL) for Phase 1 — like aa64. */ + .link = &link_arch_arm32, + .dwarf = &arm32_dwarf_ops, + .dbg = &arm32_dbg_ops, + .asm_ops = NULL, /* numeric operand printing until the asm frontend lands */ + .predefined_macros = arm32_predefined_macros, + .npredefined_macros = + (u32)(sizeof arm32_predefined_macros / sizeof arm32_predefined_macros[0]), + .target_features = arm32_target_features, + .ntarget_features = + (u32)(sizeof arm32_target_features / sizeof arm32_target_features[0]), + .target_feature_defaults = arm32_target_feature_defaults, + .target_feature_apply_isa = arm32_target_feature_apply_isa, + .register_name = arm32_register_name, + .register_index = arm32_register_index, + .register_count = arm32_register_iter_size, + .register_at = arm32_register_at_public, + /* AAPCS32 / Thumb-2: LR = r14, SP = r13 (legacy AArch32 DWARF numbering). + * 2-byte minimum insn width -> code_align 2; word stack stride -> + * data_align -4. CFA = sp at entry. */ + .cfi_return_addr_reg = 14u, + .cfi_code_align_factor = 2, + .cfi_data_align_factor = -4, + .cfi_cfa_init_reg = 13u, + .cfi_cfa_init_offset = 0, + .backend_features = KIT_CG_BACKEND_STRICT_ALIGNMENT, + .atomic_lock_free_max = 4u, /* no LDREXD/STREXD on M-profile -> 8B spinlock */ + .supports_call_conv = arm32_supports_call_conv, + .supports_intrinsic = arm32_supports_intrinsic, + /* .resolve_float_abi omitted (NULL) for Phase 1 (soft-float only). */ +}; diff --git a/src/arch/arm32/arm32.h b/src/arch/arm32/arm32.h @@ -0,0 +1,11 @@ +#ifndef KIT_ARCH_ARM32_H +#define KIT_ARCH_ARM32_H + +#include "arch/native_target.h" +#include "cg/native_direct_target.h" + +/* Backend entry points consumed by arch.c (mirrors riscv/rv64.h). */ +NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* o, MCEmitter* mc); +const NativeOps* arm32_native_direct_ops(void); + +#endif diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c @@ -0,0 +1,14 @@ +/* ARM32 (Thumb-2) textual-assembler frontend. + * + * Phase 1 (the walking skeleton) ships no standalone assembler: the qemu + * smoke lane's reset stub is clang-assembled, and the smoke C set uses no + * inline asm. The `.syntax unified` operand parser + IT-block syntax + + * byte-golden lane are a Phase-2 deliverable (ARM32.md "Standalone assembler + * frontend" / "Inline asm"). arm32_arch_asm_new yields no assembler until + * then; the ArchImpl leaves .asm_ops NULL (numeric operand printing). */ +#include "arch/arch.h" + +ArchAsm* arm32_arch_asm_new(Compiler* c) { + (void)c; + return NULL; +} diff --git a/src/arch/arm32/dbg.c b/src/arch/arm32/dbg.c @@ -0,0 +1,28 @@ +/* ARM32 (Thumb-2) debugger ops. Phase 1 provides the breakpoint primitive + * (BKPT #0 = 0xBE00, a 16-bit Thumb instruction) and the instruction-length + * bounds; displaced-step and decode are host-gated follow-ons (no AArch32 + * execution on the arm64 dev host). */ +#include <string.h> + +#include "arch/arch.h" + +static KitStatus arm32_dbg_breakpoint_patch(u8* out, u32 cap, u32* len_out) { + /* BKPT #0, Thumb T1 encoding 0xBE00, little-endian half-word. */ + static const u8 bkpt[2] = {0x00u, 0xBEu}; + if (!out || !len_out) return KIT_INVALID; + if (cap < sizeof bkpt) return KIT_INVALID; + memcpy(out, bkpt, sizeof bkpt); + *len_out = (u32)sizeof bkpt; + return KIT_OK; +} + +static u64 arm32_dbg_breakpoint_addr_from_fault_pc(u64 fault_pc) { + return fault_pc; +} + +const ArchDbgOps arm32_dbg_ops = { + .min_insn_len = 2u, + .max_insn_len = 4u, + .breakpoint_patch = arm32_dbg_breakpoint_patch, + .breakpoint_addr_from_fault_pc = arm32_dbg_breakpoint_addr_from_fault_pc, +}; diff --git a/src/arch/arm32/disasm.c b/src/arch/arm32/disasm.c @@ -0,0 +1,13 @@ +/* ARM32 (Thumb-2) disassembler. + * + * Phase 1 (the walking skeleton) ships no descriptor-driven decode — the + * codegen gate is run-correctness under qemu, not byte-golden disassembly. + * Table-driven decode (the 16/32-bit Thumb-2 families + IT-state tracking) + * is a Phase-2 op-group deliverable; until then arm32_disasm_new yields no + * disassembler (objdump/disas report unsupported for arm32). */ +#include "arch/arch.h" + +ArchDisasm* arm32_disasm_new(Compiler* c) { + (void)c; + return NULL; +} diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -0,0 +1,239 @@ +/* ARM32 Thumb-2 instruction encoders (header-only, inline). + * + * Phase 1 (the walking skeleton) needs only the encoders the -O0 native + * backend emits for the smoke set: MOVW/MOVT + modified-immediate constants, + * the data-processing reg/imm families, multiply/divide, shifts, extends, + * compare, branches, BL, push/pop, and LDR/STR. A descriptor-driven table + + * disassembler is a Phase-2 deliverable. + * + * Every 32-bit Thumb-2 instruction is returned as a single u32 with the FIRST + * half-word (hw1) in bits [31:16] and the SECOND (hw2) in [15:0]; native.c's + * arm_emit_t32 writes them as two little-endian half-words (hw1 first). 16-bit + * instructions are returned as a u16 and emitted via arm_emit_t16. */ +#ifndef KIT_ARCH_ARM32_ISA_H +#define KIT_ARCH_ARM32_ISA_H + +#include "core/core.h" + +/* Core register numbers (AAPCS roles). */ +enum { + ARM_R0 = 0, + ARM_R1 = 1, + ARM_R2 = 2, + ARM_R3 = 3, + ARM_R4 = 4, + ARM_R12 = 12, + ARM_IP = 12, /* intra-procedure scratch */ + ARM_SP = 13, + ARM_LR = 14, + ARM_PC = 15, +}; + +/* Thumb condition codes (for B<cond> / IT). */ +enum { + ARM_CC_EQ = 0x0, + ARM_CC_NE = 0x1, + ARM_CC_CS = 0x2, /* HS, unsigned >= */ + ARM_CC_CC = 0x3, /* LO, unsigned < */ + ARM_CC_MI = 0x4, + ARM_CC_PL = 0x5, + ARM_CC_VS = 0x6, + ARM_CC_VC = 0x7, + ARM_CC_HI = 0x8, /* unsigned > */ + ARM_CC_LS = 0x9, /* unsigned <= */ + ARM_CC_GE = 0xa, + ARM_CC_LT = 0xb, + ARM_CC_GT = 0xc, + ARM_CC_LE = 0xd, + ARM_CC_AL = 0xe, +}; + +static inline u32 arm_t32(u32 hw1, u32 hw2) { + return ((hw1 & 0xffffu) << 16) | (hw2 & 0xffffu); +} + +/* --------- ThumbExpandImm (rotated-8-bit modified immediate) --------- */ +/* Encode `v` as a 12-bit i:imm3:imm8 modified immediate. Returns 1 + sets + * *out12 on success, 0 if `v` is not representable (caller materializes it). */ +static inline int thumb_expand_imm_encode(u32 v, u32* out12) { + u32 b0 = v & 0xffu, b1 = (v >> 8) & 0xffu, b2 = (v >> 16) & 0xffu, + b3 = (v >> 24) & 0xffu; + u32 rot; + if (b1 == 0 && b2 == 0 && b3 == 0) { + *out12 = b0; + return 1; /* 0x000000XY */ + } + if (b0 == b2 && b0 != 0 && b1 == 0 && b3 == 0) { + *out12 = 0x100u | b0; + return 1; /* 0x00XY00XY */ + } + if (b1 == b3 && b1 != 0 && b0 == 0 && b2 == 0) { + *out12 = 0x200u | b1; + return 1; /* 0xXY00XY00 */ + } + if (b0 == b1 && b1 == b2 && b2 == b3 && b0 != 0) { + *out12 = 0x300u | b0; + return 1; /* 0xXYXYXYXY */ + } + for (rot = 8; rot < 32; ++rot) { + u32 base = (v << rot) | (v >> (32u - rot)); /* rol(v, rot) == base8 */ + if (base <= 0xffu && (base & 0x80u)) { + *out12 = (rot << 7) | (base & 0x7fu); + return 1; + } + } + return 0; +} + +/* --------- constant / address materialization --------- */ +static inline u32 arm_movw(u32 rd, u32 imm16) { + u32 i = (imm16 >> 11) & 1u, imm4 = (imm16 >> 12) & 0xfu, imm3 = (imm16 >> 8) & 7u, + imm8 = imm16 & 0xffu; + return arm_t32(0xf240u | (i << 10) | imm4, (imm3 << 12) | (rd << 8) | imm8); +} +static inline u32 arm_movt(u32 rd, u32 imm16) { + u32 i = (imm16 >> 11) & 1u, imm4 = (imm16 >> 12) & 0xfu, imm3 = (imm16 >> 8) & 7u, + imm8 = imm16 & 0xffu; + return arm_t32(0xf2c0u | (i << 10) | imm4, (imm3 << 12) | (rd << 8) | imm8); +} +/* MOV.W rd, #modimm (out12 from thumb_expand_imm_encode). */ +static inline u32 arm_mov_imm(u32 rd, u32 out12) { + u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu; + return arm_t32(0xf04fu | (i << 10), (imm3 << 12) | (rd << 8) | imm8); +} +/* MVN.W rd, #modimm. */ +static inline u32 arm_mvn_imm(u32 rd, u32 out12) { + u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu; + return arm_t32(0xf06fu | (i << 10), (imm3 << 12) | (rd << 8) | imm8); +} + +/* --------- data processing (register, no shift) --------- */ +/* op4: AND=0,BIC=1,ORR=2,ORN=3,EOR=4,ADD=8,ADC=10,SBC=11,SUB=13,RSB=14. */ +static inline u32 arm_dp_reg(u32 op4, u32 setflags, u32 rd, u32 rn, u32 rm) { + return arm_t32(0xea00u | (op4 << 5) | (setflags << 4) | rn, + (rd << 8) | rm); +} +static inline u32 arm_add_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(8u, 0u, rd, rn, rm); } +static inline u32 arm_adds_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(8u, 1u, rd, rn, rm); } +static inline u32 arm_sub_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(13u, 0u, rd, rn, rm); } +static inline u32 arm_and_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(0u, 0u, rd, rn, rm); } +static inline u32 arm_orr_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(2u, 0u, rd, rn, rm); } +static inline u32 arm_eor_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(4u, 0u, rd, rn, rm); } +/* MOV.W rd, rm (ORR rd, 1111, rm). */ +static inline u32 arm_mov_reg(u32 rd, u32 rm) { return arm_dp_reg(2u, 0u, rd, 0xfu, rm); } +/* MVN.W rd, rm (ORN rd, 1111, rm). */ +static inline u32 arm_mvn_reg(u32 rd, u32 rm) { return arm_dp_reg(3u, 0u, rd, 0xfu, rm); } +/* CMP.W rn, rm (SUB S=1, Rd=1111). */ +static inline u32 arm_cmp_reg(u32 rn, u32 rm) { return arm_dp_reg(13u, 1u, 0xfu, rn, rm); } + +/* --------- data processing (modified immediate) --------- */ +static inline u32 arm_dp_imm(u32 op4, u32 setflags, u32 rd, u32 rn, u32 out12) { + u32 i = (out12 >> 11) & 1u, imm3 = (out12 >> 8) & 7u, imm8 = out12 & 0xffu; + return arm_t32(0xf000u | (i << 10) | (op4 << 5) | (setflags << 4) | rn, + (imm3 << 12) | (rd << 8) | imm8); +} +static inline u32 arm_add_imm12(u32 rd, u32 rn, u32 rn_is_imm12) { + /* ADDW rd, rn, #imm12 (raw 12-bit, no flags). rn_is_imm12 is the imm12. */ + u32 imm12 = rn_is_imm12 & 0xfffu; + u32 i = (imm12 >> 11) & 1u, imm3 = (imm12 >> 8) & 7u, imm8 = imm12 & 0xffu; + return arm_t32(0xf200u | (i << 10) | rn, (imm3 << 12) | (rd << 8) | imm8); +} +static inline u32 arm_sub_imm12(u32 rd, u32 rn, u32 imm12) { + /* SUBW rd, rn, #imm12 (raw 12-bit, no flags). */ + u32 v = imm12 & 0xfffu; + u32 i = (v >> 11) & 1u, imm3 = (v >> 8) & 7u, imm8 = v & 0xffu; + return arm_t32(0xf2a0u | (i << 10) | rn, (imm3 << 12) | (rd << 8) | imm8); +} +/* CMP.W rn, #modimm (SUB S=1, Rd=1111). */ +static inline u32 arm_cmp_imm(u32 rn, u32 out12) { return arm_dp_imm(13u, 1u, 0xfu, rn, out12); } + +/* --------- shifts --------- */ +/* MOV.W rd, rm, <type> #sh : type LSL=0,LSR=1,ASR=2,ROR=3 (immediate). */ +static inline u32 arm_shift_imm(u32 type, u32 rd, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xea4fu, (imm3 << 12) | (rd << 8) | (imm2 << 6) | (type << 4) | rm); +} +/* LSL/LSR/ASR (register): Rd = Rn shifted by Rm. */ +static inline u32 arm_shift_reg(u32 type, u32 rd, u32 rn, u32 rm) { + u32 hw1 = 0xfa00u | (type << 5) | rn; /* LSL=0xFA00, LSR=0xFA20, ASR=0xFA40 */ + return arm_t32(hw1, 0xf000u | (rd << 8) | rm); +} + +/* --------- multiply / divide --------- */ +static inline u32 arm_mul(u32 rd, u32 rn, u32 rm) { + return arm_t32(0xfb00u | rn, 0xf000u | (rd << 8) | rm); +} +static inline u32 arm_mls(u32 rd, u32 rn, u32 rm, u32 ra) { + return arm_t32(0xfb00u | rn, (ra << 12) | (rd << 8) | 0x10u | rm); +} +static inline u32 arm_sdiv(u32 rd, u32 rn, u32 rm) { + return arm_t32(0xfb90u | rn, 0xf0f0u | (rd << 8) | rm); +} +static inline u32 arm_udiv(u32 rd, u32 rn, u32 rm) { + return arm_t32(0xfbb0u | rn, 0xf0f0u | (rd << 8) | rm); +} + +/* --------- sign/zero extends --------- */ +static inline u32 arm_sxtb(u32 rd, u32 rm) { return arm_t32(0xfa4fu, 0xf080u | (rd << 8) | rm); } +static inline u32 arm_sxth(u32 rd, u32 rm) { return arm_t32(0xfa0fu, 0xf080u | (rd << 8) | rm); } +static inline u32 arm_uxtb(u32 rd, u32 rm) { return arm_t32(0xfa5fu, 0xf080u | (rd << 8) | rm); } +static inline u32 arm_uxth(u32 rd, u32 rm) { return arm_t32(0xfa1fu, 0xf080u | (rd << 8) | rm); } + +/* --------- loads / stores (immediate, positive 12-bit offset, T3) --------- */ +static inline u32 arm_ldr_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf8d0u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_str_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf8c0u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_ldrb_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf890u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_strb_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf880u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_ldrh_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf8b0u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_strh_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf8a0u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_ldrsb_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf990u | rn, (rt << 12) | (imm12 & 0xfffu)); +} +static inline u32 arm_ldrsh_imm(u32 rt, u32 rn, u32 imm12) { + return arm_t32(0xf9b0u | rn, (rt << 12) | (imm12 & 0xfffu)); +} + +/* LDR/STR (immediate) T4: [Rn, #+/-imm8], offset addressing (P=1, W=0). + * `add` = 1 for +imm8, 0 for -imm8. hw1 base picks the op + width: + * LDR=0xF850 STR=0xF840 LDRB=0xF810 STRB=0xF800 LDRH=0xF830 STRH=0xF820 + * LDRSB=0xF910 LDRSH=0xF930. */ +static inline u32 arm_ldst_t4(u32 hw1_base, u32 rt, u32 rn, u32 imm8, u32 add) { + return arm_t32(hw1_base | rn, + (rt << 12) | 0xc00u | (add << 9) | (imm8 & 0xffu)); +} + +/* 16-bit MOV (register, high-reg form T1): handles all of r0..r15 incl. sp. */ +static inline u16 arm_mov_hi(u32 rd, u32 rm) { + return (u16)(0x4600u | ((rd >> 3) << 7) | (rm << 3) | (rd & 7u)); +} + +/* --------- branches (placeholders; immediate filled by reloc/label-fixup) --- */ +static inline u32 arm_b_w(void) { return arm_t32(0xf000u, 0x9000u); } /* B.W (T4) */ +static inline u32 arm_b_cond_w(u32 cond) { /* B<cond>.W (T3) */ + return arm_t32(0xf000u | (cond << 6), 0x8000u); +} +static inline u32 arm_bl(void) { return arm_t32(0xf000u, 0xd000u); } /* BL (T1) */ + +/* --------- 16-bit instructions --------- */ +static inline u16 arm_bx(u32 rm) { return (u16)(0x4700u | (rm << 3)); } +static inline u16 arm_blx_reg(u32 rm) { return (u16)(0x4780u | (rm << 3)); } +static inline u16 arm_bkpt(u32 imm8) { return (u16)(0xbe00u | (imm8 & 0xffu)); } +static inline u16 arm_nop16(void) { return (u16)0xbf00u; } + +/* --------- push / pop (32-bit, STMDB sp! / LDMIA sp!) --------- */ +static inline u32 arm_push_w(u32 reglist) { return arm_t32(0xe92du, reglist & 0xdfffu); } +static inline u32 arm_pop_w(u32 reglist) { return arm_t32(0xe8bdu, reglist & 0xffffu); } + +#endif diff --git a/src/arch/arm32/link.c b/src/arch/arm32/link.c @@ -0,0 +1,20 @@ +/* ARM32 link-time arch descriptor. See link_arch.h for the contract. + * + * Static-only for v1: no PLT/GOT/IPLT, no dynamic linking (mirrors rv32's + * freestanding decision). ELF TLS is variant I (TCB ahead of .tdata), so + * tls_variant_ii = 0 (the struct's zero-init default). The only live fields + * are the per-arch reloc descriptor + the Thumb-2 split-immediate patcher; + * everything else (PLT geometry, stub emitters, JIT relax) is zero/NULL. */ +#include "core/core.h" +#include "link/link_arch.h" + +/* Defined in src/arch/arm32/reloc.c. */ +const RelocDesc* arm32_reloc_desc(RelocKind k); +int arm32_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, + u64 P); + +const LinkArchDesc link_arch_arm32 = { + .reloc_desc = arm32_reloc_desc, + .reloc_apply_insn = arm32_reloc_apply_insn, + /* .tls_variant_ii = 0 (variant I) — implicit zero. */ +}; diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -0,0 +1,1249 @@ +/* ARM32 (ARMv7-M Thumb-2, soft-float) NativeTarget backend. + * + * Built on the shared NativeDirectTarget/MCEmitter/NativeFrame substrate. This + * is the Phase-1 "walking skeleton" backend: it lowers the smoke set (return, + * ALU, compare + if/else, and a <=4-int-arg call) correctly; the op-group + * expansion (i64 lanes, bitfields, atomics, varargs, inline asm, the disasm + * tables, code-density tiers) is Phase 2. + * + * Frame model (single-pass, FP-anchored): + * prologue: PUSH {r7, lr} ; save the frame pointer + return address + * MOV r7, sp ; r7 = frame anchor (just below the saved pair) + * SUB sp, sp, #N ; reserve locals + outgoing args (patched at end) + * slots: addressed [r7, #-off] (off fixed at allocation, anchor fixed) + * epilogue: MOV sp, r7 ; drop the frame (size-independent) + * POP {r7, pc} ; restore r7 + return (Thumb bit from stacked lr) + * + * The NDT value-cache pool is the caller-saved arg registers r0..r3 (Lever 1: + * args materialize directly into their ABI registers); r12 (IP) is the emit + * scratch; r7 is the frame pointer. Callee-saved r4..r11 are reserved from the + * -O0 pool (the optimizer allocates over them at -O1). */ +#include <string.h> + +#include "abi/abi.h" +#include "arch/arm32/arm32.h" +#include "arch/arm32/isa.h" +#include "arch/arm32/regs.h" +#include "arch/arm32/variant.h" +#include "arch/mc.h" +#include "arch/native_target.h" +#include "cg/cgir.h" +#include "cg/native_argmove.h" +#include "cg/native_asm.h" +#include "cg/native_direct_target.h" +#include "cg/native_frame.h" +#include "cg/type.h" +#include "core/bytes.h" +#include "core/core.h" +#include "obj/obj.h" + +extern void debug_emit_row(Debug*, ObjSecId text_section, u32 offset, SrcLoc); +extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs, + u32 end_ofs); + +#define ARM_FP 7u /* frame pointer (Thumb convention) */ +#define ARM_SCRATCH 12u /* IP: NDT operand-materialization / cycle-break scratch */ +/* Backend emit-internal temp for multi-step sequences (div/mod). lr is RESERVED + * (never holds an NDT operand) and dead in the body — its live value is saved on + * the stack by the prologue and restored via `pop {..,pc}`, so clobbering the lr + * register between calls is free and cannot collide with an NDT operand in IP. */ +#define ARM_TMP 14u +#define ARM_MAX_REG_ARG_MOVES 16u + +/* ============================ state ============================ */ + +typedef struct Arm32NativeTarget { + NativeTarget base; /* MUST be first member (arm_of is a reinterpret cast) */ + const Arm32Variant* variant; + SrcLoc loc; + const CGFuncDesc* func; + NativeFrame frame; + u32 next_param_int; /* incoming param cursor: r0..r3 */ + u32 next_param_stack; + u8 has_sret; + u8 is_variadic; + NativeFrameSlot sret_ptr_slot; + u32 func_start; + u32 prologue_sub_pos; /* file offset of the deferred SUB sp (patched at end) */ + MCLabel epilogue_label; +} Arm32NativeTarget; + +static Arm32NativeTarget* arm_of(NativeTarget* t) { + return (Arm32NativeTarget*)t; +} + +static _Noreturn void arm_panic(Arm32NativeTarget* a, const char* msg) { + compiler_panic(a->base.c, a->loc, "arm32 native target: %s", msg); +} + +static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0xfu; } + +/* ============================ emit ============================ */ + +static void arm_emit_t16(MCEmitter* mc, u16 hw) { + u8 b[2]; + u32 ofs = obj_pos(mc->obj, mc->section_id); + b[0] = (u8)(hw & 0xffu); + b[1] = (u8)((hw >> 8) & 0xffu); + mc_emit_bytes(mc, b, sizeof b); + if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc); +} + +/* A 32-bit Thumb-2 instruction (passed as (hw1<<16)|hw2) is two LE half-words, + * hw1 first in memory. NOT a single LE word — never route through mc_emit32. */ +static void arm_emit_t32(MCEmitter* mc, u32 instr) { + u8 b[4]; + u32 ofs = obj_pos(mc->obj, mc->section_id); + u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu; + b[0] = (u8)(hw1 & 0xffu); + b[1] = (u8)((hw1 >> 8) & 0xffu); + b[2] = (u8)(hw2 & 0xffu); + b[3] = (u8)((hw2 >> 8) & 0xffu); + mc_emit_bytes(mc, b, sizeof b); + if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc); +} + +static void arm_patch_t32(Arm32NativeTarget* a, u32 pos, u32 instr) { + u8 b[4]; + u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu; + b[0] = (u8)(hw1 & 0xffu); + b[1] = (u8)((hw1 >> 8) & 0xffu); + b[2] = (u8)(hw2 & 0xffu); + b[3] = (u8)((hw2 >> 8) & 0xffu); + obj_patch(a->base.obj, a->base.mc->section_id, pos, b, 4); +} + +/* ============================ register tables ============================ */ + +#define ARM_PHYS_ARG(r, idx) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = (idx), \ + .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \ + ((idx) < 2u ? NATIVE_REG_RET : 0), \ + .spill_cost = 1u, \ + .copy_cost = 1u} +#define ARM_PHYS_CALLEE(r) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \ + .spill_cost = 4u, \ + .copy_cost = 1u} +#define ARM_PHYS_RESERVED(r) \ + {.reg = (r), \ + .cls = NATIVE_REG_INT, \ + .abi_index = 0xffu, \ + .flags = NATIVE_REG_RESERVED, \ + .spill_cost = 0u, \ + .copy_cost = 0u} + +/* NDT (-O0) value-cache pool: the caller-saved arg registers r0..r3, fronted so + * a producer materializes a call's args directly into their ABI registers + * (Lever 1 via api_pack_call_args_in_order). The -O0 cache flushes at every + * call/branch/return, so caller-saved suffices and the prologue needs no + * callee-save spills. */ +static const Reg arm_int_allocable[] = {0u, 1u, 2u, 3u}; +static const Reg arm_int_scratch[] = {ARM_SCRATCH}; + +static const NativePhysRegInfo arm_int_phys[] = { + ARM_PHYS_ARG(0u, 0u), ARM_PHYS_ARG(1u, 1u), + ARM_PHYS_ARG(2u, 2u), ARM_PHYS_ARG(3u, 3u), + ARM_PHYS_CALLEE(4u), ARM_PHYS_CALLEE(5u), + ARM_PHYS_CALLEE(6u), ARM_PHYS_RESERVED(7u), /* fp */ + ARM_PHYS_CALLEE(8u), ARM_PHYS_CALLEE(9u), + ARM_PHYS_CALLEE(10u), ARM_PHYS_CALLEE(11u), + ARM_PHYS_RESERVED(12u), /* ip = scratch */ + ARM_PHYS_RESERVED(13u), /* sp */ + ARM_PHYS_RESERVED(14u), /* lr */ + ARM_PHYS_RESERVED(15u), /* pc */ +}; + +static int arm_resolve_name(const NativeRegInfo* ri, Slice name, Reg* out, + NativeAllocClass* cls_out) { + char buf[16]; + uint32_t dwarf; + (void)ri; + if (!name.s || !name.len || name.len >= sizeof buf) return 1; + memcpy(buf, name.s, name.len); + buf[name.len] = '\0'; + if (arm32_register_index(buf, &dwarf) != 0) return 1; + if (dwarf <= 15u) { + *cls_out = NATIVE_REG_INT; + *out = (Reg)dwarf; + return 0; + } + return 1; +} + +static int arm_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls, + Reg reg) { + (void)ri; + if (cls != NATIVE_REG_INT) return 0; + if (reg <= 3u) return 1; /* r0..r3 */ + if (reg >= 4u && reg <= 11u && reg != 7u) return 1; /* r4..r11 except fp */ + return 0; +} + +static int arm_asm_constraint_reg(const NativeRegInfo* ri, const char* body, + NativeAllocClass* cls_out, Reg* fixed_out, + u32* allowed_mask_out) { + (void)ri; + if (!body || !body[0]) return 0; + if (fixed_out) *fixed_out = REG_NONE; + if (allowed_mask_out) *allowed_mask_out = 0; + if ((body[0] == 'r' || body[0] == 'l') && body[1] == '\0') { + if (cls_out) *cls_out = NATIVE_REG_INT; + return 1; + } + return 0; +} + +static const NativeAllocClassInfo arm_classes[] = { + {.cls = NATIVE_REG_INT, + .allocable = arm_int_allocable, + .nallocable = sizeof arm_int_allocable / sizeof arm_int_allocable[0], + .scratch = arm_int_scratch, + .nscratch = sizeof arm_int_scratch / sizeof arm_int_scratch[0], + .phys = arm_int_phys, + .nphys = sizeof arm_int_phys / sizeof arm_int_phys[0], + /* r0..r3 + r12 (lr/ip caller-saved; lr reserved) */ + .caller_saved_mask = 0x0000100fu, + /* r4..r11 */ + .callee_saved_mask = 0x00000ff0u, + .arg_mask = 0x0000000fu, + .ret_mask = 0x00000003u, + /* r7 (fp), r12 (ip), r13 (sp), r14 (lr), r15 (pc) */ + .reserved_mask = 0x0000f080u}, +}; + +static const NativeRegInfo arm_reg_info = { + .classes = arm_classes, + .nclasses = sizeof arm_classes / sizeof arm_classes[0], + .ndt_caller_saved_only = 1u, + .ndt_result_reg_stable = 1u, + .resolve_name = arm_resolve_name, + .asm_operand_reg_ok = arm_asm_operand_reg_ok, + .asm_constraint_reg = arm_asm_constraint_reg, +}; + +/* ============================ legality ============================ */ + +static int arm_imm_legal(NativeTarget* t, NativeImmUse use, u32 op, + KitCgTypeId type, i64 imm) { + u32 enc; + (void)t; + (void)type; + switch (use) { + case NATIVE_IMM_MOVE: + return 1; /* MOVW/MOVT materializes any 32-bit value */ + case NATIVE_IMM_CMP: + return thumb_expand_imm_encode((u32)imm, &enc); + case NATIVE_IMM_BINOP: + switch ((BinOp)op) { + case BO_IADD: + case BO_ISUB: + if (thumb_expand_imm_encode((u32)(op == BO_ISUB ? -imm : imm), &enc)) + return 1; + return imm >= 0 && imm <= 0xfff; /* ADDW/SUBW raw 12-bit */ + case BO_AND: + case BO_OR: + case BO_XOR: + return thumb_expand_imm_encode((u32)imm, &enc); + case BO_SHL: + case BO_SHR_S: + case BO_SHR_U: + return imm >= 0 && imm < 32; + default: + return 0; + } + case NATIVE_IMM_ADDR_OFFSET: + return imm >= -255 && imm <= 4095; + } + return 0; +} + +static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr, + MemAccess mem) { + (void)t; + (void)mem; + if (!addr) return 0; + /* Base + 12-bit positive or 8-bit signed offset, no scaled index in v1. */ + if (addr->index_kind != NATIVE_ADDR_INDEX_NONE) return 0; + return addr->offset >= -255 && addr->offset <= 4095; +} + +/* ============================ memory ============================ */ + +/* Resolve a NativeAddr to a (base reg, signed byte offset) pair. */ +static u32 arm_addr_base(Arm32NativeTarget* a, const NativeAddr* addr, + i32* off_out) { + switch (addr->base_kind) { + case NATIVE_ADDR_BASE_FRAME: { + NativeFrameSlotEntry* s = + native_frame_slot_at(&a->frame, addr->base.frame); + *off_out = -(i32)s->off + addr->offset; /* [r7, #-(off) + extra] */ + return ARM_FP; + } + case NATIVE_ADDR_BASE_REG: + *off_out = addr->offset; + return addr->base.reg & 0xfu; + default: + arm_panic(a, "unsupported addressing mode"); + } +} + +/* Emit a load (is_load=1) or store of `reg` to [base, #off], dispatching the + * width from mem.size. Loads zero-extend (LDR/LDRB/LDRH); signed narrowing is a + * separate convert. */ +static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg, + NativeAddr addr, MemAccess mem) { + MCEmitter* mc = a->base.mc; + i32 off; + u32 base = arm_addr_base(a, &addr, &off); + u32 rt = loc_reg(reg); + u32 size = mem.size ? mem.size : native_type_size(&a->base, reg.type); + u32 t3hw1, t4hw1; + if (size >= 8u) arm_panic(a, "8-byte memory access not lowered (wide8 path)"); + /* pick the op (T3 positive, T4 +/-) for this width. */ + if (size == 1u) { + t3hw1 = is_load ? 0xf890u : 0xf880u; + t4hw1 = is_load ? 0xf810u : 0xf800u; + } else if (size == 2u) { + t3hw1 = is_load ? 0xf8b0u : 0xf8a0u; + t4hw1 = is_load ? 0xf830u : 0xf820u; + } else { + t3hw1 = is_load ? 0xf8d0u : 0xf8c0u; + t4hw1 = is_load ? 0xf850u : 0xf840u; + } + if (off >= 0 && off <= 4095) { + arm_emit_t32(mc, arm_t32(t3hw1 | base, (rt << 12) | (u32)off)); + } else if (off < 0 && off >= -255) { + arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)(-off), 0u)); + } else if (off >= 0 && off <= 255) { + arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)off, 1u)); + } else { + arm_panic(a, "frame/memory offset out of range (needs IP fallback)"); + } +} + +/* ============================ moves / immediates ============================ */ + +static void arm_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) { + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst); + u32 v = (u32)imm; + u32 enc; + if (thumb_expand_imm_encode(v, &enc)) { + arm_emit_t32(mc, arm_mov_imm(rd, enc)); + return; + } + if (thumb_expand_imm_encode(~v, &enc)) { + arm_emit_t32(mc, arm_mvn_imm(rd, enc)); + return; + } + arm_emit_t32(mc, arm_movw(rd, v & 0xffffu)); + if ((v >> 16) != 0u) arm_emit_t32(mc, arm_movt(rd, (v >> 16) & 0xffffu)); +} + +static void arm_move(NativeTarget* t, NativeLoc dst, NativeLoc src) { + u32 rd = loc_reg(dst), rs = loc_reg(src); + if (rd == rs) return; + arm_emit_t16(t->mc, arm_mov_hi(rd, rs)); +} + +static void arm_load(NativeTarget* t, NativeLoc dst, NativeAddr addr, + MemAccess mem) { + arm_emit_mem(arm_of(t), 1, dst, addr, mem); +} +static void arm_store(NativeTarget* t, NativeAddr addr, NativeLoc src, + MemAccess mem) { + arm_emit_mem(arm_of(t), 0, src, addr, mem); +} +static void arm_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot, + MemAccess mem) { + NativeAddr addr; + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = slot; + addr.base_type = src.type; + arm_emit_mem(arm_of(t), 0, src, addr, mem); +} +static void arm_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot, + MemAccess mem) { + NativeAddr addr; + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = slot; + addr.base_type = dst.type; + arm_emit_mem(arm_of(t), 1, dst, addr, mem); +} + +/* Load the address of a frame slot / reg base into a register (ADD rd, base, + * #off via the modified-immediate or ADDW form; small offsets only in v1). */ +static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + i32 off; + u32 base = arm_addr_base(a, &addr, &off); + u32 rd = loc_reg(dst); + u32 enc; + if (off == 0) { + arm_emit_t16(mc, arm_mov_hi(rd, base)); + } else if (off > 0 && off <= 0xfff) { + arm_emit_t32(mc, arm_add_imm12(rd, base, (u32)off)); + } else if (off < 0 && thumb_expand_imm_encode((u32)(-off), &enc)) { + arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, base, enc)); /* SUB rd, base, #-off */ + } else { + arm_panic(a, "load_addr offset out of range"); + } +} + +/* ============================ ALU ============================ */ + +static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc, + NativeLoc b) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst), ra = loc_reg(a_loc); + int b_imm = b.kind == NATIVE_LOC_IMM; + u32 rb = b_imm ? 0u : loc_reg(b); + u32 enc; + switch (op) { + case BO_IADD: + if (b_imm) { + if (thumb_expand_imm_encode((u32)b.v.imm, &enc)) + arm_emit_t32(mc, arm_dp_imm(8u, 0u, rd, ra, enc)); + else + arm_emit_t32(mc, arm_add_imm12(rd, ra, (u32)b.v.imm & 0xfffu)); + } else { + arm_emit_t32(mc, arm_add_reg(rd, ra, rb)); + } + return; + case BO_ISUB: + if (b_imm) { + if (thumb_expand_imm_encode((u32)b.v.imm, &enc)) + arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, ra, enc)); + else + arm_emit_t32(mc, arm_sub_imm12(rd, ra, (u32)b.v.imm & 0xfffu)); + } else { + arm_emit_t32(mc, arm_sub_reg(rd, ra, rb)); + } + return; + case BO_IMUL: + arm_emit_t32(mc, arm_mul(rd, ra, rb)); + return; + case BO_SDIV: + arm_emit_t32(mc, arm_sdiv(rd, ra, rb)); + return; + case BO_UDIV: + arm_emit_t32(mc, arm_udiv(rd, ra, rb)); + return; + case BO_SREM: + arm_emit_t32(mc, arm_sdiv(ARM_TMP, ra, rb)); + arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra)); /* rd = ra - (ra/rb)*rb */ + return; + case BO_UREM: + arm_emit_t32(mc, arm_udiv(ARM_TMP, ra, rb)); + arm_emit_t32(mc, arm_mls(rd, ARM_TMP, rb, ra)); + return; + case BO_AND: + if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc)) + arm_emit_t32(mc, arm_dp_imm(0u, 0u, rd, ra, enc)); + else + arm_emit_t32(mc, arm_and_reg(rd, ra, rb)); + return; + case BO_OR: + if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc)) + arm_emit_t32(mc, arm_dp_imm(2u, 0u, rd, ra, enc)); + else + arm_emit_t32(mc, arm_orr_reg(rd, ra, rb)); + return; + case BO_XOR: + if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc)) + arm_emit_t32(mc, arm_dp_imm(4u, 0u, rd, ra, enc)); + else + arm_emit_t32(mc, arm_eor_reg(rd, ra, rb)); + return; + case BO_SHL: + if (b_imm) + arm_emit_t32(mc, arm_shift_imm(0u, rd, ra, (u32)b.v.imm & 31u)); + else + arm_emit_t32(mc, arm_shift_reg(0u, rd, ra, rb)); + return; + case BO_SHR_U: + if (b_imm) + arm_emit_t32(mc, arm_shift_imm(1u, rd, ra, (u32)b.v.imm & 31u)); + else + arm_emit_t32(mc, arm_shift_reg(1u, rd, ra, rb)); + return; + case BO_SHR_S: + if (b_imm) + arm_emit_t32(mc, arm_shift_imm(2u, rd, ra, (u32)b.v.imm & 31u)); + else + arm_emit_t32(mc, arm_shift_reg(2u, rd, ra, rb)); + return; + default: + arm_panic(a, "binop not lowered (FP / wide8 is Phase 2)"); + } +} + +static void arm_unop(NativeTarget* t, UnOp op, NativeLoc dst, NativeLoc src) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst), rs = loc_reg(src); + u32 enc; + switch (op) { + case UO_NEG: + thumb_expand_imm_encode(0u, &enc); + arm_emit_t32(mc, arm_dp_imm(14u, 0u, rd, rs, enc)); /* RSB rd, rs, #0 */ + return; + case UO_BNOT: + arm_emit_t32(mc, arm_mvn_reg(rd, rs)); + return; + default: + arm_panic(a, "unop not lowered (FNEG/NOT are Phase 2)"); + } +} + +/* Map a CmpOp to its ARM condition code (taken-when-true). */ +static u32 arm_cond_for(Arm32NativeTarget* a, CmpOp op) { + switch (op) { + case CMP_EQ: return ARM_CC_EQ; + case CMP_NE: return ARM_CC_NE; + case CMP_LT_S: return ARM_CC_LT; + case CMP_LE_S: return ARM_CC_LE; + case CMP_GT_S: return ARM_CC_GT; + case CMP_GE_S: return ARM_CC_GE; + case CMP_LT_U: return ARM_CC_CC; + case CMP_LE_U: return ARM_CC_LS; + case CMP_GT_U: return ARM_CC_HI; + case CMP_GE_U: return ARM_CC_CS; + default: + arm_panic(a, "FP compare not lowered (Phase 2)"); + } +} + +static void arm_emit_cmp_operands(Arm32NativeTarget* a, NativeLoc x, + NativeLoc y) { + MCEmitter* mc = a->base.mc; + u32 enc; + if (y.kind == NATIVE_LOC_IMM && thumb_expand_imm_encode((u32)y.v.imm, &enc)) + arm_emit_t32(mc, arm_cmp_imm(loc_reg(x), enc)); + else + arm_emit_t32(mc, arm_cmp_reg(loc_reg(x), loc_reg(y))); +} + +/* Materialize the boolean (a OP b) into dst: MOV dst,#0; CMP; IT cc; MOV dst,#1. */ +static void arm_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc a_loc, + NativeLoc b) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst); + u32 cc = arm_cond_for(a, op); + u32 enc0, enc1; + thumb_expand_imm_encode(0u, &enc0); + thumb_expand_imm_encode(1u, &enc1); + arm_emit_t32(mc, arm_mov_imm(rd, enc0)); /* dst = 0 */ + arm_emit_cmp_operands(a, a_loc, b); /* CMP a, b */ + arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (1 insn, mask 0b1000) */ + arm_emit_t32(mc, arm_mov_imm(rd, enc1)); /* MOV<cc> dst, #1 */ +} + +static void arm_convert(NativeTarget* t, ConvKind op, NativeLoc dst, + NativeLoc src) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst), rs = loc_reg(src); + u32 dsz = native_type_size(t, dst.type); + u32 ssz = native_type_size(t, src.type); + switch (op) { + case CV_SEXT: + if (ssz == 1u) arm_emit_t32(mc, arm_sxtb(rd, rs)); + else if (ssz == 2u) arm_emit_t32(mc, arm_sxth(rd, rs)); + else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs)); + return; + case CV_ZEXT: + if (ssz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs)); + else if (ssz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs)); + else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs)); + return; + case CV_TRUNC: + if (dsz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs)); + else if (dsz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs)); + else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs)); + return; + case CV_BITCAST: + if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs)); + return; + default: + arm_panic(a, "convert not lowered (FP conversions are Phase 2)"); + } +} + +/* ============================ _rr narrow hooks ============================ */ + +static void arm_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + arm_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} +static void arm_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) { + arm_move(t, native_loc_from_reg(dst), native_loc_from_reg(src)); +} +static void arm_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst, + NativeRegLoc a, NativeRegLoc b) { + arm_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a), + native_loc_from_reg(b)); +} +static void arm_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst, + NativeRegLoc src) { + arm_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src)); +} + +/* ============================ control flow ============================ */ + +static MCLabel arm_label_new(NativeTarget* t) { return mc_label_new(t->mc); } +static void arm_label_place(NativeTarget* t, MCLabel l) { + mc_label_place(t->mc, l); +} +static void arm_jump(NativeTarget* t, MCLabel l) { + arm_emit_t32(t->mc, arm_b_w()); + mc_emit_label_ref(t->mc, l, R_ARM_THM_JUMP24, 4, 0); +} +static void arm_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a_loc, + NativeLoc b, MCLabel target) { + Arm32NativeTarget* a = arm_of(t); + u32 cc = arm_cond_for(a, op); + arm_emit_cmp_operands(a, a_loc, b); + arm_emit_t32(t->mc, arm_b_cond_w(cc)); + mc_emit_label_ref(t->mc, target, R_ARM_THM_JUMP19, 4, 0); +} + +/* ============================ frame lifecycle ============================ */ + +static NativeFrameSlot arm_frame_slot(NativeTarget* t, + const NativeFrameSlotDesc* d) { + return native_frame_slot_alloc(&arm_of(t)->frame, d); +} +static void arm_release_frame_slot(NativeTarget* t, NativeFrameSlot slot) { + native_frame_release_slot(&arm_of(t)->frame, slot); +} +static int arm_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot, + CGDebugLoc* out) { + Arm32NativeTarget* a = arm_of(t); + NativeFrameSlotEntry* s; + if (!out) return 0; + memset(out, 0, sizeof *out); + if (slot == NATIVE_FRAME_SLOT_NONE || slot > a->frame.nslots) return 0; + s = native_frame_slot_at(&a->frame, slot); + out->kind = CG_DEBUG_LOC_FRAME; + out->v.frame_ofs = -(i32)s->off; /* r7-relative */ + return 1; +} + +static u32 arm_frame_size(const Arm32NativeTarget* a) { + return align_up_u32(a->frame.cum_off + a->frame.max_outgoing, 8u); +} + +static void arm_reserve_callee_saves(NativeTarget* t, const u32* used_by_class, + u32 nclasses) { + native_frame_set_callee_saves(&arm_of(t)->frame, used_by_class, nclasses, + NULL, 0, 0); +} + +static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); + a->func = fd; + a->loc = fd->loc; + native_frame_reset(&a->frame); + a->next_param_int = 0; + a->next_param_stack = 0; + a->has_sret = (abi && abi->has_sret) ? 1u : 0u; + a->is_variadic = (abi && abi->variadic) ? 1u : 0u; + a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE; + + mc_set_section(mc, fd->text_section_id); + mc_emit_align(mc, 4, 0); + a->func_start = mc_pos(mc); + mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start); + mc_cfi_startproc(mc); + a->epilogue_label = mc_label_new(mc); + + /* Prologue: save fp+lr, set the frame anchor, reserve the (deferred) frame. */ + arm_emit_t32(mc, arm_push_w((1u << ARM_FP) | (1u << 14u))); /* PUSH {r7, lr} */ + arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ + a->prologue_sub_pos = mc_pos(mc); + arm_emit_t32(mc, arm_sub_imm12(13u, 13u, 0u)); /* SUB sp, sp, #0 (patched) */ + + /* sret: spill the incoming destination pointer (r0) to a hidden home. */ + if (a->has_sret) { + NativeFrameSlotDesc sd; + NativeAddr addr; + KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); + memset(&sd, 0, sizeof sd); + sd.type = i32t; + sd.size = 4; + sd.align = 4; + sd.kind = NATIVE_FRAME_SLOT_SAVE; + a->sret_ptr_slot = arm_frame_slot(t, &sd); + a->next_param_int = 1; + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = a->sret_ptr_slot; + addr.base_type = i32t; + arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0), addr, + native_mem_for_type(t, i32t, 4)); + } +} + +static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, + const NativeKnownFrameDesc* kf, + NativeFrameSlot* out_slots) { + (void)fd; + (void)kf; + (void)out_slots; + arm_panic(arm_of(t), "known-frame (-O1) path not implemented in Phase 1"); +} + +static void arm_func_end(NativeTarget* t) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 frame; + /* Place the epilogue and emit the teardown. */ + mc_label_place(mc, a->epilogue_label); + arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 */ + arm_emit_t32(mc, arm_pop_w((1u << ARM_FP) | (1u << 15u))); /* POP {r7, pc} */ + + native_frame_set_final(&a->frame); + frame = arm_frame_size(a); + if (frame > 0xfffu) + arm_panic(a, "frame too large for Phase 1 (needs IP-staged SUB)"); + arm_patch_t32(a, a->prologue_sub_pos, arm_sub_imm12(13u, 13u, frame)); + + /* Publish the function symbol. ARM marks Thumb STT_FUNC symbols with the + * low bit set (the Thumb bit), so &fn and indirect BLX reach Thumb state; + * direct-branch relocs mask it off (S & ~1). */ + { + u32 end = mc_pos(mc); + ObjSecId sec = a->func->text_section_id; + obj_symbol_define(t->obj, a->func->sym, sec, (u64)a->func_start | 1u, + (u64)(end - a->func_start)); + if (a->func->atomize) + obj_atom_define(t->obj, sec, a->func_start, end - a->func_start, + a->func->sym, 0); + if (mc->debug) debug_func_pc_range(mc->debug, sec, a->func_start, end); + } + + mc_cfi_endproc(mc); + mc_end_function(mc); + a->func = NULL; +} + +/* ============================ param binding ============================ */ + +static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p, + NativeLoc dst) { + Arm32NativeTarget* a = arm_of(t); + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type); + const ABIArgInfo* ai = p->index < abi->nparams ? &abi->params[p->index] : NULL; + int to_reg = dst.kind == NATIVE_LOC_REG; + u32 i; + if (!ai || ai->kind == ABI_ARG_IGNORE) return; + if (ai->kind == ABI_ARG_INDIRECT) + arm_panic(a, "indirect (by-ref aggregate) params are Phase 2"); + for (i = 0; i < ai->nparts; ++i) { + const ABIArgPart* part = &ai->parts[i]; + NativeLoc src; + if (a->next_param_int < 4u) { + src = native_loc_reg(p->type, NATIVE_REG_INT, (Reg)(a->next_param_int++)); + } else { + /* Incoming stack arg: above the saved {r7,lr} pair = [r7 + 8 + k]. */ + NativeAddr sa; + src = native_loc_reg(p->type, NATIVE_REG_INT, ARM_SCRATCH); + memset(&sa, 0, sizeof sa); + sa.base_kind = NATIVE_ADDR_BASE_REG; + sa.base.reg = ARM_FP; + sa.base_type = p->type; + sa.offset = (i32)(8u + a->next_param_stack); + arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, p->type, part->size)); + a->next_param_stack += 4u; + } + if (dst.kind == NATIVE_LOC_NONE) { + /* unused parameter; cursor already advanced */ + } else if (to_reg) { + NativeLoc d = native_loc_reg(dst.type ? dst.type : p->type, + (NativeAllocClass)dst.cls, (Reg)dst.v.reg); + if (!(src.kind == NATIVE_LOC_REG && loc_reg(src) == loc_reg(d))) + arm_move(t, d, src); + } else { + NativeAddr home; + memset(&home, 0, sizeof home); + home.base_kind = NATIVE_ADDR_BASE_FRAME; + home.base.frame = dst.v.frame; + home.base_type = p->type; + home.offset = (i32)part->src_offset; + arm_emit_mem(a, 0, src, home, native_mem_for_type(t, p->type, part->size)); + } + } +} + +/* ============================ calls / returns ============================ */ + +/* Outgoing stack-argument bytes for a call: int parts beyond r0..r3. */ +static u32 arm_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) { + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); + u32 next_int = (abi && abi->has_sret) ? 1u : 0u; + u32 stack = 0, i, p; + if (!abi) return 0; + for (i = 0; i < desc->nargs && i < abi->nparams; ++i) { + const ABIArgInfo* ai = &abi->params[i]; + if (ai->kind == ABI_ARG_IGNORE) continue; + if (ai->kind == ABI_ARG_INDIRECT) { + if (next_int < 4u) next_int++; + else stack += 4u; + continue; + } + for (p = 0; p < ai->nparts; ++p) { + if (next_int < 4u) next_int++; + else stack += 4u; + } + } + return align_up_u32(stack, 8u); +} + +static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, + int* variadic, u32* nparams) { + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type); + u32 next_int = (abi && abi->has_sret) ? 1u : 0u; + u32 stack = 0, i, p; + if (variadic) *variadic = abi && abi->variadic; + if (nparams) *nparams = abi ? abi->nparams : 0u; + if (!abi) return 0; + for (i = 0; i < abi->nparams; ++i) { + const ABIArgInfo* ai = &abi->params[i]; + if (ai->kind == ABI_ARG_IGNORE) continue; + if (ai->kind == ABI_ARG_INDIRECT) { + if (next_int < 4u) next_int++; + else stack += 4u; + continue; + } + for (p = 0; p < ai->nparts; ++p) { + if (next_int < 4u) next_int++; + else stack += 4u; + } + } + return align_up_u32(stack, 8u); +} + +static u32 arm_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) { + return arm_call_stack_size(t, desc); +} + +static void arm_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src, + u32 src_offset, u32 size) { + /* Load `size` bytes at src_offset of `src` into register `dst`. */ + if (src.kind == NATIVE_LOC_REG) { + arm_move(t, dst, src); + return; + } + { + NativeAddr addr; + MemAccess mem; + memset(&addr, 0, sizeof addr); + if (src.kind == NATIVE_LOC_FRAME) { + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = src.v.frame; + addr.offset = (i32)src_offset; + } else if (src.kind == NATIVE_LOC_STACK) { + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = src.v.stack.slot; + addr.offset = src.v.stack.offset + (i32)src_offset; + } else { + arm_panic(arm_of(t), "unsupported arg source location"); + } + addr.base_type = dst.type; + memset(&mem, 0, sizeof mem); + mem.type = dst.type; + mem.size = size; + arm_emit_mem(arm_of(t), 1, dst, addr, mem); + } +} + +static void arm_store_outgoing(NativeTarget* t, u32 stack_off, NativeLoc src, + u32 size) { + NativeAddr addr; + MemAccess mem; + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_REG; + addr.base.reg = 13u; /* sp */ + addr.offset = (i32)stack_off; + addr.base_type = src.type; + memset(&mem, 0, sizeof mem); + mem.type = src.type; + mem.size = size; + arm_emit_mem(arm_of(t), 0, src, addr, mem); +} + +static void arm_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) { + if (m->is_addr) + arm_load_addr(t, m->dst, m->src.v.addr); /* unused in smoke set */ + else + arm_load_part(t, m->dst, m->src, m->src_offset, m->size); +} + +static void arm_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves, + u32 n) { + NativeArgShuffle s; + if (n > ARM_MAX_REG_ARG_MOVES) arm_panic(arm_of(t), "too many register args"); + memset(&s, 0, sizeof s); + s.t = t; + s.emit_one = arm_emit_one_arg_move; + s.reg_move = arm_move; + s.scratch[NATIVE_REG_INT] = ARM_SCRATCH; + native_arg_shuffle(&s, moves, n); +} + +static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, + NativeCallPlan* plan) { + Arm32NativeTarget* a = arm_of(t); + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); + NativeCallPlanRet* rets; + u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) + ? abi->ret.nparts + : ((!abi && desc->nresults) ? 1u : 0u); + memset(plan, 0, sizeof *plan); + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; + plan->callee = desc->callee; + plan->rets = rets; + plan->flags = desc->flags; + plan->has_sret = abi && abi->has_sret; + plan->is_variadic = abi && abi->variadic; + plan->stack_arg_size = arm_call_stack_size(t, desc); + if (plan->stack_arg_size > a->frame.max_outgoing) + a->frame.max_outgoing = plan->stack_arg_size; + + /* Stage an indirect callee out of the arg registers (arg loads clobber them). */ + if (plan->callee.kind == NATIVE_LOC_REG && + (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT && + plan->callee.v.reg <= 3u) { + NativeLoc scratch = + native_loc_reg(plan->callee.type, NATIVE_REG_INT, ARM_SCRATCH); + arm_move(t, scratch, plan->callee); + plan->callee = scratch; + } + { + u32 next_int = (abi && abi->has_sret) ? 1u : 0u; + u32 stack = 0, nmoves = 0, i, p; + NativeArgMove moves[ARM_MAX_REG_ARG_MOVES]; + KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); + for (i = 0; i < desc->nargs; ++i) { + const ABIArgInfo* ai = i < abi->nparams ? &abi->params[i] : NULL; + if (!ai || ai->kind == ABI_ARG_IGNORE) continue; + if (ai->kind == ABI_ARG_INDIRECT) + arm_panic(a, "indirect (by-ref aggregate) args are Phase 2"); + for (p = 0; p < ai->nparts; ++p) { + const ABIArgPart* part = &ai->parts[p]; + if (next_int < 4u) { + NativeArgMove* m = &moves[nmoves++]; + m->dst = native_loc_reg(desc->args[i].type, NATIVE_REG_INT, + (Reg)(next_int++)); + m->src = desc->args[i]; + m->src_offset = part->src_offset; + m->size = part->size; + m->is_addr = 0; + } else { + NativeLoc tmp = native_loc_reg(desc->args[i].type, NATIVE_REG_INT, + ARM_SCRATCH); + arm_load_part(t, tmp, desc->args[i], part->src_offset, part->size); + stack = align_up_u32(stack, 4u); + arm_store_outgoing(t, stack, tmp, part->size); + stack += 4u; + } + } + } + arm_emit_reg_arg_moves(t, moves, nmoves); + if (abi && abi->has_sret && desc->nresults) { + NativeLoc r0 = native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0); + arm_load_addr(t, r0, (NativeAddr){.base_kind = NATIVE_ADDR_BASE_FRAME, + .base.frame = desc->results[0].v.frame, + .base_type = i32t}); + } + } + if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) { + u32 nr = 0, ni = 0, p; + for (p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + KitCgTypeId pty = desc->results[0].type; + rets[nr].src = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++)); + rets[nr].dst = desc->results[0]; + if (rets[nr].dst.kind == NATIVE_LOC_FRAME) + rets[nr].dst = native_loc_stack(pty, desc->results[0].v.frame, + (i32)part->src_offset); + rets[nr].mem = native_mem_for_type(t, pty, part->size); + nr++; + } + plan->nrets = nr; + } else if (!abi && desc->nresults) { + rets[0].src = native_loc_reg(desc->results[0].type, NATIVE_REG_INT, ARM_R0); + rets[0].dst = desc->results[0]; + rets[0].mem = native_mem_for_type(t, desc->results[0].type, 0); + plan->nrets = 1; + } +} + +static void arm_emit_call(NativeTarget* t, const NativeCallPlan* plan) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + if (plan->flags & CG_CALL_TAIL) + arm_panic(a, "tail calls are Phase 2"); + if (plan->callee.kind == NATIVE_LOC_GLOBAL) { + u32 pos = mc_pos(mc); + arm_emit_t32(mc, arm_bl()); + mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_CALL, + plan->callee.v.global.sym, plan->callee.v.global.addend, 0, + 0); + return; + } + if (plan->callee.kind == NATIVE_LOC_REG) { + arm_emit_t16(mc, arm_blx_reg(loc_reg(plan->callee))); + return; + } + arm_panic(a, "unsupported call target"); +} + +static void arm_plan_ret(NativeTarget* t, const CGFuncDesc* fd, + const NativeLoc* value, NativeCallPlanRet** out_rets, + u32* out_nrets) { + Arm32NativeTarget* a = arm_of(t); + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); + NativeCallPlanRet* rets = NULL; + u32 nr = 0; + if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); + if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { + arm_panic(a, "indirect (by-ref aggregate) return is Phase 2"); + } + if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) { + u32 ni = 0, p; + for (p = 0; p < abi->ret.nparts; ++p) { + const ABIArgPart* part = &abi->ret.parts[p]; + KitCgTypeId pty = value->type; + rets[nr].src = *value; + if (rets[nr].src.kind == NATIVE_LOC_FRAME) + rets[nr].src = + native_loc_stack(pty, value->v.frame, (i32)part->src_offset); + rets[nr].dst = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++)); + rets[nr].mem = native_mem_for_type(t, pty, part->size); + nr++; + } + } else if (value) { + rets[0].src = *value; + rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, ARM_R0); + rets[0].mem = native_mem_for_type(t, value->type, 0); + nr = 1; + } + *out_rets = rets; + *out_nrets = nr; +} + +static void arm_ret(NativeTarget* t) { + Arm32NativeTarget* a = arm_of(t); + arm_jump(t, a->epilogue_label); +} + +/* ============================ misc / stubs ============================ */ + +static void arm_trap(NativeTarget* t) { arm_emit_t16(t->mc, arm_bkpt(0u)); } +static void arm_set_loc(NativeTarget* t, SrcLoc loc) { + arm_of(t)->loc = loc; + mc_set_loc(t->mc, loc); +} + +#define ARM_UNIMPL(name) arm_panic(arm_of(t), name " not implemented in Phase 1") + +static void arm_load_const(NativeTarget* t, NativeLoc dst, ConstBytes c) { + (void)dst; + (void)c; + ARM_UNIMPL("load_const"); +} +static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) { + (void)dst; + (void)l; + ARM_UNIMPL("load_label_addr"); +} +static void arm_indirect_branch(NativeTarget* t, NativeLoc addr, + const MCLabel* targets, u32 n) { + (void)addr; + (void)targets; + (void)n; + ARM_UNIMPL("indirect_branch (switch table)"); +} +static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym, + i64 addend) { + (void)dst; + (void)sym; + (void)addend; + ARM_UNIMPL("tls_addr_of"); +} +static void arm_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src, + AggregateAccess acc) { + (void)dst; + (void)src; + (void)acc; + ARM_UNIMPL("copy_bytes (aggregate)"); +} +static void arm_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc v, + AggregateAccess acc) { + (void)dst; + (void)v; + (void)acc; + ARM_UNIMPL("set_bytes (memset)"); +} +static void arm_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr addr, + BitFieldAccess bf) { + (void)dst; + (void)addr; + (void)bf; + ARM_UNIMPL("bitfield_load"); +} +static void arm_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc v, + BitFieldAccess bf) { + (void)addr; + (void)v; + (void)bf; + ARM_UNIMPL("bitfield_store"); +} +static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, + u32 align) { + (void)dst; + (void)size; + (void)align; + ARM_UNIMPL("alloca"); +} +static void arm_atomic_load(NativeTarget* t, NativeLoc dst, NativeAddr addr, + MemAccess mem, KitCgMemOrder order) { + (void)dst; + (void)addr; + (void)mem; + (void)order; + ARM_UNIMPL("atomic_load"); +} +static void arm_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc v, + MemAccess mem, KitCgMemOrder order) { + (void)addr; + (void)v; + (void)mem; + (void)order; + ARM_UNIMPL("atomic_store"); +} +static void arm_intrinsic(NativeTarget* t, IntrinKind kind, + const NativeLoc* dsts, u32 ndst, const NativeLoc* args, + u32 narg) { + (void)dsts; + (void)ndst; + (void)args; + (void)narg; + if (kind == INTRIN_TRAP) { + arm_trap(t); + return; + } + ARM_UNIMPL("intrinsic"); +} + +/* ============================ construction ============================ */ + +NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* obj, + MCEmitter* mc) { + Arm32NativeTarget* a = arena_znew(c->tu, Arm32NativeTarget); + NativeTarget* t; + if (!a) return NULL; + t = &a->base; + t->c = c; + t->obj = obj; + t->mc = mc; + a->variant = arm32_variant_for_kind(c->target.arch); + native_frame_init(&a->frame, c); + t->regs = &arm_reg_info; + t->class_for_type = native_class_for_type_fp_le8; + t->imm_legal = arm_imm_legal; + t->addr_legal = arm_addr_legal; + t->func_begin = arm_func_begin; + t->func_begin_known_frame = arm_func_begin_known_frame; + t->reserve_callee_saves = arm_reserve_callee_saves; + t->signature_stack_bytes = arm_signature_stack_bytes; + t->call_stack_bytes = arm_call_stack_bytes; + t->has_store_zero_reg = 0; /* ARM has no zero register */ + t->func_end = arm_func_end; + t->frame_slot = arm_frame_slot; + t->release_frame_slot = arm_release_frame_slot; + t->frame_slot_debug_loc = arm_frame_slot_debug_loc; + t->bind_param = arm_bind_native_param; + t->label_new = arm_label_new; + t->label_place = arm_label_place; + t->jump = arm_jump; + t->cmp_branch = arm_cmp_branch; + t->indirect_branch = arm_indirect_branch; + t->load_label_addr = arm_load_label_addr; + t->move = arm_move; + t->load_imm = arm_load_imm; + t->load_const = arm_load_const; + t->load_addr = arm_load_addr; + t->load = arm_load; + t->store = arm_store; + t->tls_addr_of = arm_tls_addr_of; + t->copy_bytes = arm_copy_bytes; + t->set_bytes = arm_set_bytes; + t->bitfield_load = arm_bitfield_load; + t->bitfield_store = arm_bitfield_store; + t->binop = arm_binop; + t->unop = arm_unop; + t->cmp = arm_cmp; + t->convert = arm_convert; + t->binop_rr = arm_binop_rr; + t->move_rr = arm_move_rr; + t->cmp_rr = arm_cmp_rr; + t->convert_rr = arm_convert_rr; + t->alloca_ = arm_alloca; + t->spill = arm_spill; + t->reload = arm_reload; + t->plan_call = arm_plan_call; + t->emit_call = arm_emit_call; + t->plan_ret = arm_plan_ret; + t->ret = arm_ret; + t->atomic_load = arm_atomic_load; + t->atomic_store = arm_atomic_store; + t->intrinsic = arm_intrinsic; + t->file_scope_asm = native_file_scope_asm; + t->trap = arm_trap; + t->set_loc = arm_set_loc; + t->finalize = native_finalize; + return t; +} + +/* ============================ NativeOps (-O0) ============================ */ + +static void arm_bind_param(NativeDirectTarget* d, const CGParamDesc* p, + CGLocal local, NativeDirectLocal* l) { + NativeLoc dst; + (void)local; + memset(&dst, 0, sizeof dst); + dst.kind = NATIVE_LOC_FRAME; + dst.type = p->type; + dst.v.frame = l->home; + arm_bind_native_param(d->native, p, dst); +} + +static const char* arm_no_tail(NativeDirectTarget* d, const CGCallDesc* call) { + (void)d; + (void)call; + return "arm32 tail calls not implemented in Phase 1"; +} + +static const NativeOps arm_direct_ops = { + .bind_param = arm_bind_param, + .tail_call_unrealizable_reason = arm_no_tail, +}; + +const NativeOps* arm32_native_direct_ops(void) { return &arm_direct_ops; } diff --git a/src/arch/arm32/regs.c b/src/arch/arm32/regs.c @@ -0,0 +1,101 @@ +/* ARM32 (AArch32) register name table -- DWARF index <-> AAPCS name. + * + * "DWARF for the ARM Architecture" numbers the core registers r0..r15 as + * 0..15 and the VFP single-precision registers s0..s31 as 64..95. Canonical + * names are rN; sp/lr/pc/ip/fp aliases are accepted by lookup. v1 is + * soft-float, but the s-registers are listed for disassembly/debug. */ + +#include "arch/arm32/regs.h" + +#include <stdint.h> + +#include "core/core.h" +#include "core/slice.h" + +typedef struct Arm32Reg { + uint32_t dwarf_idx; + const char* name; +} Arm32Reg; + +static const Arm32Reg ARM32_REGS[] = { + {0, "r0"}, {1, "r1"}, {2, "r2"}, {3, "r3"}, {4, "r4"}, + {5, "r5"}, {6, "r6"}, {7, "r7"}, {8, "r8"}, {9, "r9"}, + {10, "r10"}, {11, "r11"}, {12, "r12"}, {13, "sp"}, {14, "lr"}, + {15, "pc"}, + + {64, "s0"}, {65, "s1"}, {66, "s2"}, {67, "s3"}, {68, "s4"}, + {69, "s5"}, {70, "s6"}, {71, "s7"}, {72, "s8"}, {73, "s9"}, + {74, "s10"}, {75, "s11"}, {76, "s12"}, {77, "s13"}, {78, "s14"}, + {79, "s15"}, {80, "s16"}, {81, "s17"}, {82, "s18"}, {83, "s19"}, + {84, "s20"}, {85, "s21"}, {86, "s22"}, {87, "s23"}, {88, "s24"}, + {89, "s25"}, {90, "s26"}, {91, "s27"}, {92, "s28"}, {93, "s29"}, + {94, "s30"}, {95, "s31"}, +}; + +static const uint32_t ARM32_REGS_N = + (uint32_t)(sizeof ARM32_REGS / sizeof ARM32_REGS[0]); + +static int parse_num_suffix(const char* name, char prefix, uint32_t max, + uint32_t* out) { + uint32_t v = 0; + const char* p; + if (!name || name[0] != prefix || name[1] == '\0') return 1; + p = name + 1; + while (*p) { + if (*p < '0' || *p > '9') return 1; + v = v * 10u + (uint32_t)(*p - '0'); + if (v > max) return 1; + ++p; + } + if (out) *out = v; + return 0; +} + +const char* arm32_register_name(uint32_t dwarf_idx) { + uint32_t i; + for (i = 0; i < ARM32_REGS_N; ++i) { + if (ARM32_REGS[i].dwarf_idx == dwarf_idx) return ARM32_REGS[i].name; + } + return NULL; +} + +int arm32_register_index(const char* name, uint32_t* idx_out) { + uint32_t i; + uint32_t n; + Slice q; + if (!name) return 1; + q = slice_from_cstr(name); + for (i = 0; i < ARM32_REGS_N; ++i) { + if (slice_eq_cstr(q, ARM32_REGS[i].name)) { + if (idx_out) *idx_out = ARM32_REGS[i].dwarf_idx; + return 0; + } + } + if (!parse_num_suffix(name, 'r', 15, &n)) { + if (idx_out) *idx_out = n; + return 0; + } + if (!parse_num_suffix(name, 's', 31, &n)) { + if (idx_out) *idx_out = 64u + n; + return 0; + } + if (slice_eq_cstr(q, "ip")) { /* r12 */ + if (idx_out) *idx_out = 12u; + return 0; + } + if (slice_eq_cstr(q, "fp")) { /* r11 (Thumb FP) */ + if (idx_out) *idx_out = 11u; + return 0; + } + return 1; +} + +uint32_t arm32_register_iter_size(void) { return ARM32_REGS_N; } + +int arm32_register_iter_get(uint32_t i, uint32_t* dwarf_out, + const char** name_out) { + if (i >= ARM32_REGS_N) return 1; + if (dwarf_out) *dwarf_out = ARM32_REGS[i].dwarf_idx; + if (name_out) *name_out = ARM32_REGS[i].name; + return 0; +} diff --git a/src/arch/arm32/regs.h b/src/arch/arm32/regs.h @@ -0,0 +1,12 @@ +#ifndef KIT_ARCH_ARM32_REGS_H +#define KIT_ARCH_ARM32_REGS_H + +#include <stdint.h> + +const char* arm32_register_name(uint32_t dwarf_idx); +int arm32_register_index(const char* name, uint32_t* idx_out); +uint32_t arm32_register_iter_size(void); +int arm32_register_iter_get(uint32_t i, uint32_t* dwarf_out, + const char** name_out); + +#endif diff --git a/src/arch/arm32/reloc.c b/src/arch/arm32/reloc.c @@ -0,0 +1,83 @@ +/* ARM32 (Thumb-2) relocation descriptors (width + classification) + the + * split-immediate instruction byte patcher. + * + * Reached through LinkArchDesc.reloc_desc / .reloc_apply_insn (wired in + * link.c) and the arch-aware reloc_desc() / link_reloc_apply() dispatchers. + * Wire encoding + diagnostic name live in src/obj/elf/reloc_arm.c. + * + * Data words (the vector table, data pointers) use the neutral R_ABS32 / + * R_REL32 kinds, claimed by reloc_apply_neutral before this hook runs — so + * only the instruction-embedded Thumb-2 branch kinds appear here. + * + * Thumb-2 byte order: a 32-bit instruction is two independently-little-endian + * 16-bit half-words, the HIGH half-word FIRST in memory. rd_u32_le/wr_u32_le + * would swap them, so arm_rd_t32/arm_wr_t32 read/write half-word-pairs. */ + +#include "obj/reloc.h" + +#include "core/bytes.h" +#include "core/core.h" +#include "link/link_arch.h" +#include "obj/obj.h" + +static const RelocDescRow arm32_rows[] = { + {R_ARM_THM_CALL, {4, RELOC_IS_BRANCH}}, /* BL (T1), +-16 MiB */ + {R_ARM_THM_JUMP24, {4, RELOC_IS_BRANCH}}, /* B.W (T4), +-16 MiB */ +}; + +const RelocDesc* arm32_reloc_desc(RelocKind k) { + return reloc_desc_row_find(arm32_rows, + (u32)(sizeof arm32_rows / sizeof arm32_rows[0]), k); +} + +/* Read/write a 32-bit Thumb-2 instruction stored as two little-endian + * half-words, HIGH half-word first in memory (the Thumb byte order). The + * returned/accepted 32-bit value has hw1 in the high 16 bits, hw2 in the low + * 16 bits. */ +static u32 arm_rd_t32(const u8* p) { + return ((u32)rd_u16_le(p) << 16) | (u32)rd_u16_le(p + 2); +} +static void arm_wr_t32(u8* p, u32 instr) { + wr_u16_le(p, (u16)(instr >> 16)); + wr_u16_le(p + 2, (u16)(instr & 0xffffu)); +} + +int arm32_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, + u64 P) { + switch (k) { + case R_ARM_THM_CALL: + case R_ARM_THM_JUMP24: { + /* BL/B.W T1/T4 25-bit field {S,I1,I2,imm10,imm11}, in 2-byte units: + * instr[26]=Sbit, instr[25:16]=imm10, instr[13]=J1, instr[11]=J2, + * instr[10:0]=imm11, with I1=NOT(J1^S), I2=NOT(J2^S). + * Thumb-only on Cortex-M: BL never relaxes to BLX, so the callee's + * Thumb bit (S|1) is masked out of the displacement. The hardware PC + * base is the instruction address + 4 (Thumb pipeline), so the encoded + * byte offset is (S&~1) + A - P - 4. (The field is overwritten; a + * non-zero in-field addend, i.e. `bl sym+N`, is a Phase-2 refinement.) */ + i64 disp = (i64)(S & ~(u64)1) + A - (i64)P - 4; + u32 instr, sbit, i1, i2, j1, j2, imm10, imm11; + if (disp & 1) + compiler_panic(c, SRCLOC_NONE, + "link: THM_CALL misaligned displacement"); + if (disp < -(i64)(1 << 24) || disp >= (i64)(1 << 24)) + compiler_panic(c, SRCLOC_NONE, + "link: THM_CALL out of range (need +-16MiB)"); + sbit = (u32)((disp >> 24) & 1u); + i1 = (u32)((disp >> 23) & 1u); + i2 = (u32)((disp >> 22) & 1u); + imm10 = (u32)((disp >> 12) & 0x3ffu); + imm11 = (u32)((disp >> 1) & 0x7ffu); + j1 = (~(i1 ^ sbit)) & 1u; /* I1 = NOT(J1^S) -> J1 = NOT(I1^S) */ + j2 = (~(i2 ^ sbit)) & 1u; + instr = arm_rd_t32(P_bytes); + instr &= 0xf800d000u; /* keep opcode (11110), hw2[15:14] (BL/B), hw2[12] */ + instr |= (sbit << 26) | (imm10 << 16); + instr |= (j1 << 13) | (j2 << 11) | imm11; + arm_wr_t32(P_bytes, instr); + return 1; + } + default: + return 0; + } +} diff --git a/src/arch/arm32/variant.c b/src/arch/arm32/variant.c @@ -0,0 +1,18 @@ +/* The single immutable ARM32 variant. See variant.h for the contract. */ +#include "arch/arm32/variant.h" + +const Arm32Variant arm32_variant_v7m = { + .kind = KIT_ARCH_ARM_32, + .name = "arm32", + .profile = "armv7-m", + .ptr_bytes = 4u, + .has_thumb2 = 1u, + .has_idiv = 1u, + .has_dsp = 0u, + .frame_save_size = 8u, /* 2 * ptr_bytes */ +}; + +const Arm32Variant* arm32_variant_for_kind(KitArchKind kind) { + (void)kind; + return &arm32_variant_v7m; +} diff --git a/src/arch/arm32/variant.h b/src/arch/arm32/variant.h @@ -0,0 +1,29 @@ +/* ARM32 backend variant descriptor — an immutable per-profile table threaded + * through the otherwise stateless backend so a single backend can later serve + * ARMv7-M and (future) ARMv6-M / A-profile variants. It is always reached + * through a context (Arm32NativeTarget.variant) or arm32_variant_for_kind(), + * never as ambient global state (CLAUDE.md: hang off a context struct). + * + * v1 ships exactly one variant: ARMv7-M Thumb-2, soft-float. */ +#ifndef KIT_ARCH_ARM32_VARIANT_H +#define KIT_ARCH_ARM32_VARIANT_H + +#include "core/core.h" + +typedef struct Arm32Variant { + KitArchKind kind; /* KIT_ARCH_ARM_32 */ + const char* name; /* "arm32" */ + const char* profile; /* "armv7-m" / (future) "armv7e-m" / "armv6-m" */ + u8 ptr_bytes; /* 4 */ + u8 has_thumb2; /* 1 on v7-M (32-bit Thumb-2, MOVW/MOVT, IT) */ + u8 has_idiv; /* 1 on v7-M (hardware SDIV/UDIV) */ + u8 has_dsp; /* 0 on v7-M, 1 on v7E-M (saturating/SIMD; follow-on) */ + u32 frame_save_size; /* saved lr + fp pair = 2 * ptr_bytes */ +} Arm32Variant; + +extern const Arm32Variant arm32_variant_v7m; + +/* Returns the variant for KIT_ARCH_ARM_32. v1 has a single variant. */ +const Arm32Variant* arm32_variant_for_kind(KitArchKind kind); + +#endif diff --git a/src/arch/dbg_stubs.c b/src/arch/dbg_stubs.c @@ -2,5 +2,6 @@ const ArchDbgOps aa64_dbg_ops = {0}; const ArchDbgOps x64_dbg_ops = {0}; +const ArchDbgOps arm32_dbg_ops = {0}; const ArchDbgOps rv64_dbg_ops = {0}; const ArchDbgOps rv32_dbg_ops = {0}; diff --git a/src/arch/disasm_stubs.c b/src/arch/disasm_stubs.c @@ -15,4 +15,9 @@ ArchDisasm* rv64_disasm_new(Compiler* c) { return NULL; } +ArchDisasm* arm32_disasm_new(Compiler* c) { + (void)c; + return NULL; +} + const ArchDecodeOps rv64_decode_ops = {0}; diff --git a/src/arch/dwarf.c b/src/arch/dwarf.c @@ -14,3 +14,9 @@ const ArchDwarfOps rv64_dwarf_ops = { .min_inst_len = 4u, .max_ops_per_inst = 1u, }; + +/* Thumb-2 mixes 16- and 32-bit instructions; the minimum width is 2. */ +const ArchDwarfOps arm32_dwarf_ops = { + .min_inst_len = 2u, + .max_ops_per_inst = 1u, +}; diff --git a/src/arch/link_stubs.c b/src/arch/link_stubs.c @@ -2,5 +2,6 @@ const LinkArchDesc link_arch_aa64 = {0}; const LinkArchDesc link_arch_x64 = {0}; +const LinkArchDesc link_arch_arm32 = {0}; const LinkArchDesc link_arch_rv64 = {0}; const LinkArchDesc link_arch_rv32 = {0}; diff --git a/src/arch/registry.c b/src/arch/registry.c @@ -30,6 +30,9 @@ extern const ArchImpl arch_impl_rv64; #if KIT_ARCH_X64_ENABLED extern const ArchImpl arch_impl_x64; #endif +#if KIT_ARCH_ARM32_ENABLED +extern const ArchImpl arch_impl_arm32; +#endif #if KIT_ARCH_C_TARGET_ENABLED extern const CGBackend cg_backend_c_target; #endif @@ -50,6 +53,9 @@ static const ArchImpl* const arch_impls[] = { #if KIT_ARCH_X64_ENABLED &arch_impl_x64, #endif +#if KIT_ARCH_ARM32_ENABLED + &arch_impl_arm32, +#endif #if KIT_ARCH_RV32_ENABLED &arch_impl_rv32, #endif @@ -59,8 +65,9 @@ static const ArchImpl* const arch_impls[] = { #if KIT_ARCH_WASM_ENABLED &arch_impl_wasm, #endif -#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \ - !KIT_ARCH_RV32_ENABLED && !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_WASM_ENABLED +#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \ + !KIT_ARCH_ARM32_ENABLED && !KIT_ARCH_RV32_ENABLED && \ + !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_WASM_ENABLED NULL, #endif }; diff --git a/src/core/config_assert.c b/src/core/config_assert.c @@ -4,8 +4,9 @@ #include "kit/config.h" _Static_assert(KIT_ARCH_AA64_ENABLED + KIT_ARCH_X64_ENABLED + - KIT_ARCH_RV32_ENABLED + KIT_ARCH_RV64_ENABLED + - KIT_ARCH_WASM_ENABLED + KIT_ARCH_C_TARGET_ENABLED >= + KIT_ARCH_ARM32_ENABLED + KIT_ARCH_RV32_ENABLED + + KIT_ARCH_RV64_ENABLED + KIT_ARCH_WASM_ENABLED + + KIT_ARCH_C_TARGET_ENABLED >= 1, "at least one backend architecture must be enabled"); @@ -19,6 +20,7 @@ _Static_assert(KIT_OBJ_ELF_ENABLED + KIT_OBJ_MACHO_ENABLED + KIT_ASSERT_BOOL(KIT_ARCH_AA64_ENABLED); KIT_ASSERT_BOOL(KIT_ARCH_X64_ENABLED); +KIT_ASSERT_BOOL(KIT_ARCH_ARM32_ENABLED); KIT_ASSERT_BOOL(KIT_ARCH_RV32_ENABLED); KIT_ASSERT_BOOL(KIT_ARCH_RV64_ENABLED); KIT_ASSERT_BOOL(KIT_ARCH_WASM_ENABLED); diff --git a/src/obj/elf/elf.h b/src/obj/elf/elf.h @@ -61,6 +61,7 @@ #define EM_X86_64 0x3E #define EM_AARCH64 0xB7 #define EM_RISCV 0xF3 +#define EM_ARM 0x28 /* ---- header sizes (also literal e_*size fields) ---- * On-disk sizes of the ELF64 records the linker emits. Wire-format @@ -207,6 +208,14 @@ static inline u8 elf_st_other(u8 vis /* SymVis */) { #define PF_W 0x2u #define PF_R 0x4u +/* ---- e_flags (ARM EABI bits, EM_ARM) ---- + * EABI version is the top byte; the float-ABI is two independent flag bits + * (a soft object sets SOFT, a hard object sets HARD, an old object neither). */ +#define EF_ARM_EABIMASK 0xFF000000u +#define EF_ARM_EABI_VER5 0x05000000u +#define EF_ARM_ABI_FLOAT_SOFT 0x00000200u +#define EF_ARM_ABI_FLOAT_HARD 0x00000400u + /* ---- e_flags (RISC-V ABI bits, EM_RISCV) ---- */ #define EF_RISCV_RVC 0x0001u #define EF_RISCV_FLOAT_ABI_SOFT 0x0000u @@ -470,6 +479,33 @@ const char* elf_riscv_reloc_name(u32 elf_type); KitFloatAbi elf_riscv_float_abi_from_e_flags(u32 e_flags); u32 elf_riscv_float_abi_to_e_flags(KitFloatAbi abi); +/* ---- ARM (32-bit, Thumb-2) relocation types (EM_ARM, ELFCLASS32) ---- + * + * Canonical numbers from "ELF for the ARM Architecture" (ARM IHI 0044). + * Static-only subset: data words (ABS32/REL32/ABS16/ABS8) reuse the neutral + * RelocKinds; only the instruction-embedded Thumb-2 kinds get arch entries. */ +#define ELF_R_ARM_NONE 0 +#define ELF_R_ARM_ABS32 2 +#define ELF_R_ARM_REL32 3 +#define ELF_R_ARM_ABS16 5 +#define ELF_R_ARM_ABS8 8 +#define ELF_R_ARM_THM_CALL 10 +#define ELF_R_ARM_THM_JUMP24 30 +#define ELF_R_ARM_MOVW_ABS_NC 43 +#define ELF_R_ARM_MOVT_ABS 44 +#define ELF_R_ARM_THM_MOVW_ABS_NC 47 +#define ELF_R_ARM_THM_MOVT_ABS 48 +#define ELF_R_ARM_THM_MOVW_PREL_NC 49 +#define ELF_R_ARM_THM_MOVT_PREL 50 +#define ELF_R_ARM_THM_JUMP19 51 +#define ELF_R_ARM_TLS_LE32 108 + +u32 elf_arm_reloc_to(u32 kind /* RelocKind */); +u32 elf_arm_reloc_from(u32 elf_type); +const char* elf_arm_reloc_name(u32 elf_type); +KitFloatAbi elf_arm_float_abi_from_e_flags(u32 e_flags); +u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi); + /* ---- little-endian byte writers (Writer-based) ---- * Writes go through the shared writer_u*_le helpers (core/bytes.h); the * elf_wr_* aliases keep the ELF spelling at existing call sites. Reads diff --git a/src/obj/elf/emit.c b/src/obj/elf/emit.c @@ -653,6 +653,15 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { e_flags = (e_flags & ~(u32)EF_RISCV_FLOAT_ABI_MASK) | fa; } } + /* ARM: keep the EABI version (top byte) from the descriptor and override + * the two float-ABI flag bits from -mfloat-abi (float_abi). */ + if (e_machine == EM_ARM) { + Compiler* ec = obj_compiler(ob); + u32 fa = ec ? elf_arm_float_abi_to_e_flags(ec->target.float_abi) + : EF_ARM_ABI_FLOAT_SOFT; + e_flags = (e_flags & ~(u32)(EF_ARM_ABI_FLOAT_SOFT | EF_ARM_ABI_FLOAT_HARD)) | + fa; + } } kit_writer_seek(w, 0); diff --git a/src/obj/elf/reloc_arm.c b/src/obj/elf/reloc_arm.c @@ -0,0 +1,130 @@ +/* RelocKind <-> ARM (Thumb-2) ELF reloc-type mapping (ELFCLASS32). + * + * Self-contained (ARM has no 64-bit ELF sibling). Data words reuse the neutral + * R_ABS32 / R_REL32 / R_ABS16 / R_ABS8 kinds; only the instruction-embedded + * Thumb-2 kinds get arch entries. The split-immediate byte patcher lives in + * src/arch/arm32/reloc.c. */ +#include "obj/elf/elf.h" +#include "obj/obj.h" + +u32 elf_arm_reloc_to(u32 kind /* RelocKind */) { + switch (kind) { + case R_NONE: + return ELF_R_ARM_NONE; + case R_ABS32: + return ELF_R_ARM_ABS32; + case R_REL32: + case R_PC32: + return ELF_R_ARM_REL32; /* PC-rel data collapses to REL32 */ + case R_ABS16: + return ELF_R_ARM_ABS16; + case R_ABS8: + return ELF_R_ARM_ABS8; + case R_ARM_THM_CALL: + return ELF_R_ARM_THM_CALL; + case R_ARM_THM_JUMP24: + return ELF_R_ARM_THM_JUMP24; + case R_ARM_THM_JUMP19: + return ELF_R_ARM_THM_JUMP19; + case R_ARM_THM_MOVW_ABS_NC: + return ELF_R_ARM_THM_MOVW_ABS_NC; + case R_ARM_THM_MOVT_ABS: + return ELF_R_ARM_THM_MOVT_ABS; + case R_ARM_THM_MOVW_PREL_NC: + return ELF_R_ARM_THM_MOVW_PREL_NC; + case R_ARM_THM_MOVT_PREL: + return ELF_R_ARM_THM_MOVT_PREL; + case R_ARM_TLS_LE32: + return ELF_R_ARM_TLS_LE32; + default: + return ELF_R_ARM_NONE; + } +} + +u32 elf_arm_reloc_from(u32 elf_type) { + switch (elf_type) { + case ELF_R_ARM_NONE: + return R_NONE; + case ELF_R_ARM_ABS32: + return R_ABS32; + case ELF_R_ARM_REL32: + return R_REL32; + case ELF_R_ARM_ABS16: + return R_ABS16; + case ELF_R_ARM_ABS8: + return R_ABS8; + case ELF_R_ARM_THM_CALL: + return R_ARM_THM_CALL; + case ELF_R_ARM_THM_JUMP24: + return R_ARM_THM_JUMP24; + case ELF_R_ARM_THM_JUMP19: + return R_ARM_THM_JUMP19; + case ELF_R_ARM_THM_MOVW_ABS_NC: + return R_ARM_THM_MOVW_ABS_NC; + case ELF_R_ARM_THM_MOVT_ABS: + return R_ARM_THM_MOVT_ABS; + case ELF_R_ARM_THM_MOVW_PREL_NC: + return R_ARM_THM_MOVW_PREL_NC; + case ELF_R_ARM_THM_MOVT_PREL: + return R_ARM_THM_MOVT_PREL; + case ELF_R_ARM_TLS_LE32: + return R_ARM_TLS_LE32; + default: + return (u32)-1; /* reader diagnoses unsupported wire types */ + } +} + +const char* elf_arm_reloc_name(u32 elf_type) { + switch (elf_type) { + case ELF_R_ARM_NONE: + return "R_ARM_NONE"; + case ELF_R_ARM_ABS32: + return "R_ARM_ABS32"; + case ELF_R_ARM_REL32: + return "R_ARM_REL32"; + case ELF_R_ARM_ABS16: + return "R_ARM_ABS16"; + case ELF_R_ARM_ABS8: + return "R_ARM_ABS8"; + case ELF_R_ARM_THM_CALL: + return "R_ARM_THM_CALL"; + case ELF_R_ARM_THM_JUMP24: + return "R_ARM_THM_JUMP24"; + case ELF_R_ARM_THM_JUMP19: + return "R_ARM_THM_JUMP19"; + case ELF_R_ARM_THM_MOVW_ABS_NC: + return "R_ARM_THM_MOVW_ABS_NC"; + case ELF_R_ARM_THM_MOVT_ABS: + return "R_ARM_THM_MOVT_ABS"; + case ELF_R_ARM_THM_MOVW_PREL_NC: + return "R_ARM_THM_MOVW_PREL_NC"; + case ELF_R_ARM_THM_MOVT_PREL: + return "R_ARM_THM_MOVT_PREL"; + case ELF_R_ARM_TLS_LE32: + return "R_ARM_TLS_LE32"; + default: + return NULL; + } +} + +/* Decode the float ABI from ARM ELF e_flags (EF_ARM_ABI_FLOAT_*). The two + * flags are independent bits, not a mask field. */ +KitFloatAbi elf_arm_float_abi_from_e_flags(u32 e_flags) { + if (e_flags & EF_ARM_ABI_FLOAT_HARD) return KIT_FLOAT_ABI_SINGLE; + if (e_flags & EF_ARM_ABI_FLOAT_SOFT) return KIT_FLOAT_ABI_SOFT; + return KIT_FLOAT_ABI_DEFAULT; +} + +u32 elf_arm_float_abi_to_e_flags(KitFloatAbi abi) { + /* Match the ARM toolchain convention (clang/gas): only hard-float objects + * set a float-ABI bit; soft/agnostic objects leave both unset (soft is the + * default). Setting EF_ARM_ABI_FLOAT_SOFT would make every kit object + * mismatch the ecosystem's soft objects under the linker's e_flags check. */ + switch (abi) { + case KIT_FLOAT_ABI_SINGLE: + case KIT_FLOAT_ABI_DOUBLE: + return EF_ARM_ABI_FLOAT_HARD; + default: + return 0u; /* soft / agnostic: no float-ABI bit */ + } +} diff --git a/src/obj/obj.c b/src/obj/obj.c @@ -1474,6 +1474,14 @@ const char* reloc_kind_name(RelocKind k) { _CASE(R_COFF_AARCH64_SECREL_LOW12A); _CASE(R_COFF_AARCH64_SECREL_HIGH12A); _CASE(R_COFF_AARCH64_SECREL_LOW12L); + _CASE(R_ARM_THM_CALL); + _CASE(R_ARM_THM_JUMP24); + _CASE(R_ARM_THM_JUMP19); + _CASE(R_ARM_THM_MOVW_ABS_NC); + _CASE(R_ARM_THM_MOVT_ABS); + _CASE(R_ARM_THM_MOVW_PREL_NC); + _CASE(R_ARM_THM_MOVT_PREL); + _CASE(R_ARM_TLS_LE32); #undef _CASE } return "UNKNOWN"; diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -308,6 +308,19 @@ typedef enum RelocKind { /* AArch64 Windows SECREL low-12 load/store form. Appended at the enum tail * to keep existing public relocation values stable. */ R_COFF_AARCH64_SECREL_LOW12L, + /* 32-bit ARM (Thumb-2, M-profile) ELF relocations. Static-only, Thumb-only: + * BL never relaxes to BLX (everything is Thumb on Cortex-M). The split- + * immediate byte patcher lives in src/arch/arm32/reloc.c. Data words reuse + * the neutral R_ABS32/R_REL32/R_ABS16/R_ABS8 kinds. Appended at the enum + * tail to keep existing public relocation values stable. */ + R_ARM_THM_CALL, /* BL/BLX (T1): 25-bit {S,I1,I2,imm10,imm11}, +-16MB */ + R_ARM_THM_JUMP24, /* B.W (T4): same 25-bit field */ + R_ARM_THM_JUMP19, /* B<cond>.W (T3): 21-bit {S,J2,J1,imm6,imm11}, +-1MB */ + R_ARM_THM_MOVW_ABS_NC, /* MOVW (T3): imm16 = (S+A)[15:0], imm4:i:imm3:imm8 */ + R_ARM_THM_MOVT_ABS, /* MOVT (T3): imm16 = (S+A)[31:16] */ + R_ARM_THM_MOVW_PREL_NC, /* PC-rel MOVW (PIC; follow-on) */ + R_ARM_THM_MOVT_PREL, /* PC-rel MOVT (PIC; follow-on) */ + R_ARM_TLS_LE32, /* TLS local-exec: S + A - tp (variant I; follow-on) */ } RelocKind; typedef struct Section { diff --git a/src/obj/registry.c b/src/obj/registry.c @@ -152,8 +152,32 @@ static const ObjElfArchOps obj_elf_arch_ops[] = { .float_abi_from_e_flags = elf_riscv_float_abi_from_e_flags, }, #endif -#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \ - !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_RV32_ENABLED +#if KIT_ARCH_ARM32_ENABLED + { + /* arm-none-eabi: ELFCLASS32, EABI version 5, soft-float default. + * Static-only freestanding — no musl interp; the dyn r_* fields are + * unused on the static path. EM_ARM is unique (no 64-bit ARM here), + * so obj_elf_machine_class matches on e_machine alone. */ + .arch = KIT_ARCH_ARM_32, + .e_machine = EM_ARM, + /* EABI v5; soft-float objects carry no float-ABI bit (the ecosystem + * convention — clang/gas mark only hard-float). */ + .e_flags = EF_ARM_EABI_VER5, + .default_musl_interp = NULL, + .r_relative = ELF_R_ARM_NONE, + .r_glob_dat = ELF_R_ARM_NONE, + .r_jump_slot = ELF_R_ARM_NONE, + .r_irelative = ELF_R_ARM_NONE, + /* ARM EABI TLS variant-I: tp points at an 8-byte (2-word) TCB. */ + .tls_tp_bias = 8u, + .reloc_to = elf_arm_reloc_to, + .reloc_from = elf_arm_reloc_from, + .reloc_name = elf_arm_reloc_name, + .float_abi_from_e_flags = elf_arm_float_abi_from_e_flags, + }, +#endif +#if !KIT_ARCH_AA64_ENABLED && !KIT_ARCH_X64_ENABLED && \ + !KIT_ARCH_RV64_ENABLED && !KIT_ARCH_RV32_ENABLED && !KIT_ARCH_ARM32_ENABLED {.arch = KIT_ARCH_WASM}, #endif }; diff --git a/test/lib/check_arm32_env.sh b/test/lib/check_arm32_env.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# test/lib/check_arm32_env.sh — kit arm32 "doctor". +# +# Prerequisite check for the arm32 behavioral-oracle lane (test/smoke/arm32.sh). +# Like rv32, the arm32 target is freestanding `arm-none-eabi`: it runs as a +# bare-metal Cortex-M3 (Thumb-2) image under qemu-system-arm -machine +# mps2-an385, with a startup stub at flash 0x0 (M-profile vector table: initial +# MSP + reset handler) that copies .data flash->SRAM, zeroes .bss, calls the +# program, and reports its result through ARM semihosting SYS_EXIT_EXTENDED +# (BKPT #0xAB) — qemu's own process exit then == the program's exit code. +# +# arm32 v1 is soft-float only (Cortex-M3 has no FPU): -mfloat-abi=soft routes +# float + double through the AEABI helpers. So, unlike rv32, there is no +# FPU-enable step in the stub and only a single ABI lane. +# +# Each checked tool is reported as a one-liner (OK / MISSING) with what was +# looked for and how to install it. Source it and call check_arm32_env to +# populate the ARM32_* globals; run it directly for a standalone report. +# +# After check_arm32_env returns, these globals are set: +# ARM32_HAVE_CLANG_TARGET 0/1 — clang accepts --target=arm-none-eabi (M3) +# ARM32_HAVE_LLD 0/1 — ld.lld on PATH (bare-metal link fallback) +# ARM32_HAVE_QEMU_SYSTEM 0/1 — qemu-system-arm on PATH +# ARM32_QEMU_SYSTEM_BIN path or empty +# ARM32_READY 0/1 — clang target + qemu-system both usable + +_arm32_os_tag() { + case "$(uname -s 2>/dev/null)" in + Darwin) echo darwin ;; + Linux) + if [ -r /etc/os-release ]; then + . /etc/os-release + case "${ID:-}:${ID_LIKE:-}" in + *alpine*) echo alpine ;; + *debian*|*ubuntu*) echo debian ;; + *fedora*|*rhel*) echo fedora ;; + *) echo linux ;; + esac + else echo linux; fi ;; + *) echo other ;; + esac +} + +_arm32_hint_clang() { + case "$(_arm32_os_tag)" in + darwin) echo "brew install llvm (clang ships the arm-none-eabi target)" ;; + debian) echo "apt install clang lld" ;; + fedora) echo "dnf install clang lld" ;; + alpine) echo "apk add clang lld" ;; + *) echo "install an LLVM/clang with the arm (Cortex-M) target" ;; + esac +} + +_arm32_hint_qemu_system() { + case "$(_arm32_os_tag)" in + darwin) echo "brew install qemu" ;; + debian) echo "apt install qemu-system-arm" ;; + fedora) echo "dnf install qemu-system-arm" ;; + alpine) echo "apk add qemu-system-arm" ;; + *) echo "install qemu (qemu-system-arm)" ;; + esac +} + +check_arm32_env() { + ARM32_HAVE_CLANG_TARGET=0 + ARM32_HAVE_LLD=0 + ARM32_HAVE_QEMU_SYSTEM=0 + ARM32_QEMU_SYSTEM_BIN="" + + # clang with the arm-none-eabi (Cortex-M3) target: probe by compiling an + # empty TU. The smoke lane uses --target=arm-none-eabi; the canonical Thumb-2 + # M-profile spelling thumbv7m-none-eabi is equivalent for this probe. + if command -v clang >/dev/null 2>&1; then + if echo 'int _e(void){return 0;}' | \ + clang --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -mfloat-abi=soft \ + -ffreestanding -nostdlib -c -x c - -o /dev/null >/dev/null 2>&1; then + ARM32_HAVE_CLANG_TARGET=1 + echo " OK clang --target=arm-none-eabi -mcpu=cortex-m3" + else + echo " MISSING clang arm-none-eabi target — install: $(_arm32_hint_clang)" + fi + else + echo " MISSING clang — install: $(_arm32_hint_clang)" + fi + + if command -v ld.lld >/dev/null 2>&1; then + ARM32_HAVE_LLD=1 + echo " OK ld.lld" + else + echo " MISSING ld.lld — install: $(_arm32_hint_clang)" + fi + + if command -v qemu-system-arm >/dev/null 2>&1; then + ARM32_HAVE_QEMU_SYSTEM=1 + ARM32_QEMU_SYSTEM_BIN="$(command -v qemu-system-arm)" + echo " OK qemu-system-arm ($ARM32_QEMU_SYSTEM_BIN)" + else + echo " MISSING qemu-system-arm — install: $(_arm32_hint_qemu_system)" + fi + + ARM32_READY=0 + if [ "$ARM32_HAVE_CLANG_TARGET" -eq 1 ] && [ "$ARM32_HAVE_QEMU_SYSTEM" -eq 1 ]; then + ARM32_READY=1 + echo " READY arm32 behavioral oracle available" + else + echo " BLOCKED arm32 behavioral oracle needs clang arm-none-eabi target + qemu-system-arm" + fi +} + +# Run standalone: report and exit non-zero if blocked. +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + check_arm32_env + [ "${ARM32_READY:-0}" -eq 1 ] +fi diff --git a/test/lib/exec_bare.sh b/test/lib/exec_bare.sh @@ -1,7 +1,7 @@ # test/lib/exec_bare.sh — bare-metal (freestanding) execution backend for # test/lib/exec_target.sh, and the single owner of kit's per-arch boot # scaffolding (reset stub + linker script + exit-code oracle + qemu-system -# invocation) for aarch64 / x86_64 / riscv64 / riscv32. +# invocation) for aarch64 / x86_64 / riscv64 / riscv32 / arm32 (Cortex-M). # # This consolidates what used to be three scripts: the rv32 corpus runner # (exec_rv32_bare.sh), the aa64/rv64 ready-image runner (exec_kernel.sh, now a @@ -28,6 +28,7 @@ # aa64 ARM semihosting SYS_EXIT_EXTENDED → qemu rc = guest code # rv* SiFive test finisher MMIO @0x100000 → qemu rc = guest code # x64 isa-debug-exit @0x501 → qemu rc = (code<<1)|1, decoded here +# arm32 ARM semihosting BKPT #0xAB + SYS_EXIT_EXTENDED → qemu rc = guest code # # Corpus depth (exec_bare_setup/run) is fully wired for rv32 only; the aa64/x64/ # rv64 stubs here are smoke-capable (single object, no TLS/rt). Hardening them @@ -48,6 +49,7 @@ _bare_canon() { x64|x86_64|amd64) echo x64 ;; rv64|riscv64) echo rv64 ;; rv32|riscv32) echo rv32 ;; + arm32|arm|armv7m|armv7em|cortex-m3|cortex-m4|cortex-m7) echo arm32 ;; *) return 1 ;; esac } @@ -57,6 +59,7 @@ _bare_qemu() { x64) echo qemu-system-x86_64 ;; rv64) echo qemu-system-riscv64 ;; rv32) echo qemu-system-riscv32 ;; + arm32) echo qemu-system-arm ;; esac } _bare_triple() { @@ -65,6 +68,7 @@ _bare_triple() { x64) echo x86_64-none-elf ;; rv64) echo riscv64-none-elf ;; rv32) echo riscv32-none-elf ;; + arm32) echo arm-none-eabi ;; esac } @@ -94,6 +98,11 @@ _bare_qemu_run() { $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine virt -bios none \ -kernel "$elf" -nographic -no-reboot \ >"$out" 2>"$err"; RUN_RC_RAW=$? ;; + arm32) + $EXEC_BARE_TIMEOUT "$EXEC_BARE_TO" "$q" -machine mps2-an385 -cpu cortex-m3 \ + -kernel "$elf" -nographic -monitor none \ + -semihosting-config enable=on,target=native -no-reboot \ + >"$out" 2>"$err"; RUN_RC_RAW=$? ;; *) RUN_RC_RAW=127 ;; esac } @@ -131,6 +140,7 @@ exec_bare_setup_build() { # arch work [entry] -> build per-arch stub x64) _bare_emit_x64 "$dir" || return 1 ;; rv64) _bare_emit_rv64 "$dir" || return 1 ;; rv32) _bare_emit_rv32 "$dir" "$entry" || return 1 ;; + arm32) _bare_emit_arm32 "$dir" "$entry" || return 1 ;; esac : > "$dir/.ok" # Cache the stub dir per arch so a caller may pass a different work dir to @@ -159,6 +169,25 @@ exec_bare_link() { # arch obj work elf -> echoes reason; 0 linked / 2 failed "$dir/start.o" "$dir/wrap.o" "$obj" "$rt" -o "$elf" 2>"$lderr"; then echo "kit ld (rv32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2 fi + elif [ "$arch" = arm32 ]; then + # The Cortex-M reset stub IS the image entry (Reset_Handler == _start); + # i64 mul/div/shift and soft-double pull __aeabi_*/__*di3 helpers from the + # arm-eabi-thumb2 runtime variant (built on demand by _bare_emit_arm32). + rt="$EXEC_BARE_ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a" + if [ -f "$rt" ]; then + if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \ + "$dir/start.o" "$obj" "$rt" -o "$elf" 2>"$lderr"; then + echo "kit ld (arm32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2 + fi + else + # No runtime variant present: link the stub + corpus object alone. + # Inline-only i64/double cases still resolve; helper-needing cases + # surface as unresolved-symbol link failures (caller decides). + if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \ + "$dir/start.o" "$obj" -o "$elf" 2>"$lderr"; then + echo "kit ld (arm32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2 + fi + fi else if ! "$EXEC_BARE_KIT" ld -T "$dir/link.ld" -e _start \ "$dir/start.o" "$obj" -o "$elf" 2>"$lderr"; then @@ -408,6 +437,145 @@ EOF -O1 -ffreestanding -c "$dir/wrap.c" -o "$dir/wrap.o" 2>"$dir/wrap.err" || return 1 } +# arm32 (Cortex-M3, Thumb-2) bare-metal reset stub for qemu-system-arm +# -machine mps2-an385. The M-profile boot contract is a vector table at flash +# address 0x0: word[0] = initial MSP (top of SRAM), word[1] = reset handler. +# The reset handler copies .data flash->SRAM, zeroes .bss, calls the corpus +# entry (default `main`), and reports its return through the ARM semihosting +# SYS_EXIT_EXTENDED oracle (r0=0x20, r1=&{ADP_Stopped_ApplicationExit, code}, +# BKPT #0xAB) — qemu's own process exit then == the guest exit code. +# +# Phase-1 bring-up: the startup stub uses M-profile system mnemonics the kit +# assembler does not yet name, so it is CLANG-assembled (--target=arm-none-eabi +# -mcpu=cortex-m3 -mthumb). The corpus object itself is produced by kit cc and +# linked in by exec_bare_link via `kit ld`. There is no TLS image to seed +# (freestanding arm32 v1 is non-TLS); the soft-float i64/double helpers come +# from the arm-eabi-thumb2 runtime variant when exec_bare_link finds it. +# +# `Reset_Handler` is the true M-profile reset entry (named in the vector table); +# `_start` is aliased to it so the shared `kit ld -e _start` link path resolves. +_bare_emit_arm32() { + local dir="$1" entry="${2:-main}" + local cc="${EXEC_BARE_ARM32_CC:-clang}" + command -v "$cc" >/dev/null 2>&1 || return 1 + # Build the arm-eabi-thumb2 runtime variant on demand for the i64/double + # helpers (best-effort; exec_bare_link degrades gracefully if absent). + local rt="$EXEC_BARE_ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a" + if [ ! -f "$rt" ]; then + make -C "$EXEC_BARE_ROOT" rt-arm-eabi-thumb2 >/dev/null 2>&1 || true + fi + cat > "$dir/start.S" <<EOF + .syntax unified + .thumb + .cpu cortex-m3 + +/* ---- Vector table: must be FIRST, placed at address 0x0 ---- */ + .section .vectors, "a" + .align 2 + .global _vectors +_vectors: + .word _estack /* word[0]: initial MSP = top of SRAM */ + .word Reset_Handler /* word[1]: reset handler (Thumb bit auto-set by + .thumb_func; do NOT add +1 or it HardFaults) */ + +/* ---- Reset handler (== _start for the shared kit-ld entry) ---- */ + .section .text.start, "ax" + .thumb_func + .align 2 + .global Reset_Handler + .global _start +Reset_Handler: +_start: + /* Set SP explicitly (the vector table also provides it). */ + ldr r0, =_estack + mov sp, r0 + + /* Copy .data from flash LMA (_sidata) to SRAM VMA (_sdata.._edata). */ + ldr r0, =_sidata + ldr r1, =_sdata + ldr r2, =_edata +1: + cmp r1, r2 + bcs 2f + ldr r3, [r0], #4 + str r3, [r1], #4 + b 1b +2: + + /* Zero .bss (_sbss.._ebss). */ + ldr r0, =_sbss + ldr r1, =_ebss + movs r2, #0 +3: + cmp r0, r1 + bcs 4f + str r2, [r0], #4 + b 3b +4: + + /* Call the corpus entry; its return value lands in r0 == exit code. */ + bl ${entry} + bl _exit_semihost +5: b 5b + +/* ---- Semihosting SYS_EXIT_EXTENDED (0x20): r0 = exit code on entry. ---- */ + .thumb_func + .align 2 + .global _exit_semihost +_exit_semihost: + mov r2, r0 /* save exit code */ + sub sp, sp, #8 /* 2-word parameter block on the stack */ + ldr r3, =0x20026 /* ADP_Stopped_ApplicationExit */ + str r3, [sp, #0] /* block[0] = reason */ + str r2, [sp, #4] /* block[1] = exit code */ + mov r1, sp /* r1 -> parameter block */ + movs r0, #0x20 /* SYS_EXIT_EXTENDED */ + bkpt 0xAB /* semihosting call */ + add sp, sp, #8 +6: b 6b +EOF + # mps2-an385: code in FLASH @ 0x0 (vector table first), data in SRAM @ + # 0x20000000 with the .data init image at a flash LMA (AT> FLASH); the stub + # copies it across at reset. kit ld's linker-script front end supports + # MEMORY / AT> / LOADADDR / ORIGIN / LENGTH; the one unsupported GNU form is + # a *top-level* `sym = expr`, so _estack is assigned inside SECTIONS. + cat > "$dir/link.ld" <<'EOF' +ENTRY(_start) +MEMORY { + FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M + SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4M +} +SECTIONS { + .text : { + *(.vectors) + *(.text.start) + *(.text*) + *(.rodata*) + . = ALIGN(4); + } > FLASH + _sidata = LOADADDR(.data); + .data : ALIGN(4) { + _sdata = .; + *(.data*) + . = ALIGN(4); + _edata = .; + } > SRAM AT> FLASH + .bss (NOLOAD) : ALIGN(4) { + _sbss = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > SRAM + _estack = ORIGIN(SRAM) + LENGTH(SRAM); + /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.ARM.attributes) + *(.comment) *(.note*) } +} +EOF + "$cc" --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib \ + -fno-pic -fno-pie -c "$dir/start.S" -o "$dir/start.o" 2>"$dir/as.err" || return 1 +} + # x86_64 long-mode reset: a multiboot header, a 32-bit entry that sets up # identity-mapped paging + long mode, then calls main and reports its return via # isa-debug-exit (port 0x501). Ported from freestanding_system.sh. diff --git a/test/smoke/arm32.sh b/test/smoke/arm32.sh @@ -0,0 +1,326 @@ +#!/usr/bin/env bash +# test/smoke/arm32.sh — behavioral oracle for kit's arm-none-eabi (Cortex-M3, +# Thumb-2) codegen. +# +# Like rv32 (freestanding `-none-elf`), arm32 is bare-metal: kit compiles the +# app, we link a Cortex-M image with FLASH (code) at 0x00000000 and SRAM (data) +# at 0x20000000, fronted by a startup stub whose M-profile vector table at 0x0 +# carries the initial MSP (top of SRAM) + the reset handler. The reset handler +# copies .data flash->SRAM, zeroes .bss, calls the app, and reports the result +# through the ARM semihosting SYS_EXIT_EXTENDED oracle (r0=0x20, +# r1=&{ADP_Stopped_ApplicationExit=0x20026, code}, BKPT #0xAB) — qemu's own +# process exit then == the program's exit code (no stdout parsing). Run under +# qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -semihosting. +# +# Scope: the verified codegen surface — 32-bit integer + pointer, control flow, +# a ≤4-int-arg call to a second function, a switch, AND 64-bit-value +# legalization: long long carry/borrow/bitwise/compare/convert inline as GPR +# pairs, i64 mul/div/shift via __*di3 / __aeabi_* runtime calls, and soft +# `double` arith/compare/convert via __*df3 / __aeabi_* calls. arm32 v1 is a +# single soft-float lane (Cortex-M3 has no FPU): -mfloat-abi=soft routes both +# float and double through the AEABI helpers. The i64-mul/div/shift and double +# cases link kit's freestanding runtime (libkit_rt.a, the arm-eabi-thumb2 +# variant) for the helpers; the inline i64 cases need no runtime. +# +# Phase 1 bring-up: kit compiles app.c (the code under test); the startup stub +# uses M-profile system mnemonics the kit assembler does not yet name, so it is +# clang-assembled. The final bare-metal link uses ld.lld; a separate kit-ld +# lane (kitld_lane) re-links with `kit ld` to exercise kit's own static ELF +# base-addr control + runtime auto-resolution. The .eh_frame/.ARM.exidx kit +# emits is discarded by the bare-metal link script (no unwinder in a +# freestanding image). +# +# Skipped (per the shared kit_exit convention) if clang lacks the arm-none-eabi +# target, ld.lld is missing, or qemu-system-arm is unavailable. + +set -u +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="$ROOT/build/test/smoke-arm32" +mkdir -p "$BUILD_DIR" + +KIT_KIT_DIR="$ROOT/test/lib" +# shellcheck source=../lib/kit_sh_kit.sh +. "$ROOT/test/lib/kit_sh_kit.sh" +kit_report_init +KIT_SKIP_IS_FAILURE=1 + +# ---- prerequisites (the arm32 doctor) ------------------------------------- +# shellcheck source=../lib/check_arm32_env.sh +. "$ROOT/test/lib/check_arm32_env.sh" +check_arm32_env +if [ "${ARM32_HAVE_CLANG_TARGET:-0}" -eq 0 ]; then + skip_test "smoke-arm32" "clang arm-none-eabi target unavailable"; kit_summary test-smoke-arm32; kit_exit +fi +if [ "${ARM32_HAVE_LLD:-0}" -eq 0 ]; then + skip_test "smoke-arm32" "ld.lld unavailable"; kit_summary test-smoke-arm32; kit_exit +fi +if [ "${ARM32_HAVE_QEMU_SYSTEM:-0}" -eq 0 ]; then + skip_test "smoke-arm32" "qemu-system-arm unavailable"; kit_summary test-smoke-arm32; kit_exit +fi + +KIT="$ROOT/build/kit" +QEMU="${ARM32_QEMU_SYSTEM_BIN:-qemu-system-arm}" +CLANG="${ARM32_CLANG:-clang}" + +# Cortex-M startup stub: M-profile vector table at 0x0 (word[0]=initial MSP, +# word[1]=reset handler), reset handler copies .data flash->SRAM, zeroes .bss, +# calls cmain. The semihosting exit lives in app.c (__semihost_exit, an +# assembler-op call), so the stub just hands control to cmain. The stub is +# clang-assembled (uses M-profile mnemonics kit-as does not yet name). +cat > "$BUILD_DIR/start.S" <<'EOF' + .syntax unified + .thumb + .cpu cortex-m3 + + .section .vectors, "a" + .align 2 + .global _vectors +_vectors: + .word _estack /* word[0]: initial MSP = top of SRAM */ + .word Reset_Handler /* word[1]: reset handler (Thumb bit auto-set) */ + + .section .text.start, "ax" + .thumb_func + .align 2 + .global Reset_Handler + .global _start +Reset_Handler: +_start: + ldr r0, =_estack + mov sp, r0 + + /* copy .data flash LMA -> SRAM VMA */ + ldr r0, =_sidata + ldr r1, =_sdata + ldr r2, =_edata +1: cmp r1, r2 + bcs 2f + ldr r3, [r0], #4 + str r3, [r1], #4 + b 1b +2: + /* zero .bss */ + ldr r0, =_sbss + ldr r1, =_ebss + movs r2, #0 +3: cmp r0, r1 + bcs 4f + str r2, [r0], #4 + b 3b +4: + bl cmain +5: b 5b +EOF + +cat > "$BUILD_DIR/link.ld" <<'EOF' +/* mps2-an385 (Cortex-M3): FLASH (code) @ 0x0, SRAM (data) @ 0x20000000. */ +ENTRY(_start) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 4M + SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 4M +} + +_estack = ORIGIN(SRAM) + LENGTH(SRAM); + +SECTIONS +{ + .text : + { + KEEP(*(.vectors)) + *(.text.start) + *(.text*) + *(.rodata*) + . = ALIGN(4); + _etext = .; + } > FLASH + + _sidata = LOADADDR(.data); + + .data : ALIGN(4) + { + _sdata = .; + *(.data*) + . = ALIGN(4); + _edata = .; + } > SRAM AT> FLASH + + .bss (NOLOAD) : ALIGN(4) + { + _sbss = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > SRAM + + /DISCARD/ : { *(.eh_frame) *(.eh_frame_hdr) + *(.ARM.exidx*) *(.ARM.extab*) *(.ARM.attributes) + *(.comment) *(.note*) } +} +EOF + +# The app under test. Every check returns a distinct nonzero code on failure so +# a qemu exit pinpoints the broken case. __semihost_exit reports the result via +# ARM semihosting SYS_EXIT_EXTENDED (BKPT #0xAB). +cat > "$BUILD_DIR/app.c" <<'EOF' +__attribute__((noreturn)) static void __semihost_exit(int code){ + /* SYS_EXIT_EXTENDED (0x20): r1 -> {ADP_Stopped_ApplicationExit, code}. */ + volatile unsigned block[2]; + block[0] = 0x20026u; /* ADP_Stopped_ApplicationExit */ + block[1] = (unsigned)code; + register unsigned r0 __asm__("r0") = 0x20u; + register const volatile unsigned *r1 __asm__("r1") = block; + __asm__ volatile ("bkpt 0xAB" :: "r"(r0), "r"(r1) : "memory"); + for(;;){} +} +/* A second function with <=4 int args, called from compute(). */ +static int sum4(int a, int b, int c, int d){ return a + b + c + d; } +static int classify(int n){ + switch (n){ /* switch -> TBB/TBH or branch table */ + case 0: return 100; + case 1: return 101; + case 2: return 102; + case 3: return 103; + default: return 199; + } +} +static int compute(void){ + /* return-const / 32-bit integer / control flow */ + int acc = 0; for (int i = 0; i < 10; i++) acc += i; + if (acc != 45) return 1; + /* basic ALU */ + volatile unsigned u = 0x12345678u; u ^= 0x0F0F0F0Fu; + if (u != (0x12345678u ^ 0x0F0F0F0Fu)) return 2; + int p = 6, q = 7; + if (p * q + (p << 2) - (q >> 1) != 42 + 24 - 3) return 3; + /* if/else */ + int sel = (acc > 40) ? 1 : 0; + if (sel) { if (u == 0) return 4; } else return 5; + /* call to a second function with <=4 int args */ + if (sum4(1, 2, 3, 4) != 10) return 6; + if (sum4(acc, p, q, sel) != 45 + 6 + 7 + 1) return 7; + /* switch */ + if (classify(2) != 102) return 8; + if (classify(9) != 199) return 9; + /* i64 inline: carry / borrow / bitwise / compare / convert (no runtime) */ + volatile unsigned long long a = 0xFFFFFFFFull; a += 1; + if (a != 0x100000000ull) return 10; + volatile unsigned long long b = 0x100000000ull; b -= 1; + if (b != 0x0FFFFFFFFull) return 11; + volatile unsigned long long c = 0x1122334455667788ull; + if ((c ^ 0xFFFFFFFFFFFFFFFFull) != 0xEEDDCCBBAA998877ull) return 12; + volatile long long x = 0x1234567800000000ll, y = 0x1234567700000001ll; + if (!(x > y) || !(y < x) || (x == y)) return 13; + volatile int s32 = -7; volatile long long s64 = s32; + if (s64 != -7) return 14; + volatile long long big = 0x00000000FAFAFAFAll; + if ((unsigned)(int)big != 0xFAFAFAFAu) return 15; + if (a) { } else return 16; /* truthiness (hi word set) */ + /* i64 runtime: mul / udiv / umod / shifts (__muldi3/__udivdi3/__aeabi_*) */ + volatile unsigned long long m = 0x0000000100000001ull; + if (m * 3ull != 0x0000000300000003ull) return 17; + volatile unsigned long long d = 0xFFFFFFFFFFFFFFFFull; + if (d / 0xFFFFFFFFull != 0x0000000100000001ull) return 18; + if (d % 7ull != (0xFFFFFFFFFFFFFFFFull % 7ull)) return 19; + volatile unsigned long long sh = 1ull; + if ((sh << 40) != 0x0000010000000000ull) return 20; + volatile long long sar = -0x4000000000000000ll; + if ((sar >> 36) != (-0x4000000000000000ll >> 36)) return 21; + /* soft double: arith / compare / convert (__adddf3/__muldf3/__aeabi_*) */ + volatile double dp = 1.5, dq = 2.25; + if (dp + dq != 3.75) return 22; + if (dp * dq != 3.375) return 23; + if (!(dp < dq) || (dp >= dq)) return 24; + volatile int iv = 7; volatile double dv = iv; + if (dv != 7.0) return 25; + volatile double dd = 3.75; if ((int)dd != 3) return 26; + volatile long long L = 5000000000ll; volatile double dL = L; + if (dL != 5000000000.0) return 27; + volatile double dbig = 5000000000.0; + if ((long long)dbig != 5000000000ll) return 28; + return 0; +} +void cmain(void){ __semihost_exit(compute()); } +EOF + +# Build kit's freestanding runtime archive for the helpers (i64 mul/div/shift + +# soft double). Built on demand; skip the runtime-dependent lanes if it cannot +# be produced (e.g. a partial checkout) rather than failing spuriously. +RT="$ROOT/build/rt/arm-eabi-thumb2/libkit_rt.a" +make -C "$ROOT" rt-arm-eabi-thumb2 >/dev/null 2>&1 || true + +# Assemble the startup stub once (clang; M-profile mnemonics). +START_O="$BUILD_DIR/start.o" +"$CLANG" --target=arm-none-eabi -mcpu=cortex-m3 -mthumb -ffreestanding \ + -nostdlib -fno-pic -fno-pie -c "$BUILD_DIR/start.S" -o "$START_O" 2>/dev/null + +run_lane() { # <name> + local name="$1" + if [ ! -f "$RT" ]; then + skip_test "$name" "runtime archive $RT missing"; return; fi + local O + for O in -O0 -O1; do + local o="$BUILD_DIR/$name$O.o" elf="$BUILD_DIR/$name$O.elf" + if ! "$KIT" cc -target arm-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft $O \ + -ffreestanding -c "$BUILD_DIR/app.c" -o "$o" 2>"$BUILD_DIR/$name$O.cc.err"; then + not_ok "$name $O (kit cc)" "$BUILD_DIR/$name$O.cc.err"; continue; fi + if ! ld.lld -T "$BUILD_DIR/link.ld" --gc-sections -static -no-pie \ + "$START_O" "$o" "$RT" -o "$elf" 2>"$BUILD_DIR/$name$O.ld.err"; then + not_ok "$name $O (link)" "$BUILD_DIR/$name$O.ld.err"; continue; fi + local rc=0 + timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$elf" \ + -nographic -monitor none -semihosting-config enable=on,target=native \ + -no-reboot >/dev/null 2>&1 || rc=$? + if [ "$rc" -eq 0 ]; then ok "$name $O (qemu rc=0)"; + else not_ok "$name $O" "expected exit 0, got $rc (failed check #$rc)"; fi + done +} + +# Single soft-float lane (no FPU on Cortex-M3): float + double + i64 all via the +# AEABI helpers. +run_lane "soft" + +# Full kit toolchain, runtime auto-linked: re-link the -O1 object with `kit ld` +# (not ld.lld) and WITHOUT naming a runtime archive. kit ld resolves the +# float-ABI / arch axis from the object's ELF EABI header, builds the matching +# libkit_rt.a on demand (build/rt/arm-eabi-thumb2), and links it in. A +# freestanding arm32 target defaults to non-PIE; -T places .text at the +# Cortex-M flash base (0x0). The i64-mul/div/shift and soft-double helpers come +# from the auto-linked runtime, so a clean qemu exit also proves runtime +# resolution. +kitld_lane() { # <name> + local name="$1" + local appo="$BUILD_DIR/$name-O1.o" + local elf="$BUILD_DIR/kitld-$name.elf" + [ -f "$START_O" ] && [ -f "$appo" ] || return + if "$KIT" ld -T "$BUILD_DIR/link.ld" -e _start "$START_O" "$appo" \ + -o "$elf" 2>"$BUILD_DIR/kitld-$name.ld.err"; then + local rc=0 + timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$elf" \ + -nographic -monitor none -semihosting-config enable=on,target=native \ + -no-reboot >/dev/null 2>&1 || rc=$? + if [ "$rc" -eq 0 ]; then ok "kit-ld $name -O1 auto-rt (qemu rc=0)"; + else not_ok "kit-ld $name -O1 auto-rt" "expected exit 0, got $rc"; fi + else + not_ok "kit-ld $name -O1 auto-rt (link)" "$BUILD_DIR/kitld-$name.ld.err" + fi +} +kitld_lane "soft" + +# Negative control: a deliberately wrong result must produce a nonzero exit. +sed 's/if (acc != 45) return 1;/if (acc != 45) return 1; return 99;/' "$BUILD_DIR/app.c" > "$BUILD_DIR/bad.c" +if "$KIT" cc -target arm-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft -O1 \ + -ffreestanding -c "$BUILD_DIR/bad.c" -o "$BUILD_DIR/bad.o" 2>/dev/null \ + && ld.lld -T "$BUILD_DIR/link.ld" --gc-sections -static -no-pie \ + "$START_O" "$BUILD_DIR/bad.o" "$RT" -o "$BUILD_DIR/bad.elf" 2>/dev/null; then + rc=0; timeout 20 "$QEMU" -machine mps2-an385 -cpu cortex-m3 -kernel "$BUILD_DIR/bad.elf" \ + -nographic -monitor none -semihosting-config enable=on,target=native -no-reboot \ + >/dev/null 2>&1 || rc=$? + if [ "$rc" -eq 99 ]; then ok "negative-control (qemu rc=99)"; + else not_ok "negative-control" "expected exit 99, got $rc"; fi +fi + +kit_summary test-smoke-arm32 +kit_exit