kit

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

commit a607d9e0169a971697a9073770e075477e079853
parent b05d4004256585236bb5b1aee44b177aea3cdb32
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 17:19:18 -0700

arm32 Phase 2: global address materialization (MOVW/MOVT + ABS relocs)

Make `kit cc -target arm-none-eabi` compile programs that reference
globals/statics. The house strategy is MOVW/MOVT absolute (Cortex-M has no
literal pools and the bare-metal lane is non-PIC): a symbol address is
`MOVW rd,#:lower16:sym` + `MOVT rd,#:upper16:sym` with R_ARM_THM_MOVW_ABS_NC
then R_ARM_THM_MOVT_ABS, the byte offset folded into the explicit reloc addend.

Works (verified under qemu-system-arm mps2-an385, -O0, both ld.lld and kit ld):
- global int read+write (.data) and .bss scalar (zero-init, write, read)
- global array indexed by a runtime value (.data and .bss) — load_addr now
  folds the scaled index (ADD.W rd, rd, idx, LSL #scale)
- global struct field read+write
- static const table in .rodata read by index
- pointer to a global: deref + store through it; &global into a local pointer
- function pointer call through a global (.data / .rodata table)
- &function taken as data keeps the Thumb bit (odd) so an indirect BLX reaches
  Thumb — the MOVW/MOVT patcher does NOT strip bit 0 (unlike the branch relocs)
- scalar load_const (<=4 bytes) via MOV.W/MVN.W/MOVW+MOVT (arm_load_imm)
- &&label address-take (load_label_addr) via MOVW/MOVT ABS vs the label's
  per-block local symbol

native.c:
- arm_emit_global_addr: shared MOVW/MOVT-ABS materializer.
- arm_addr_base: GLOBAL base materializes into ARM_TMP (lr), NOT the IP scratch
  (r12) the NDT uses for operands — in `str rt,[base]` the transfer reg may be
  r12; lr is reserved and dead in the body so it never aliases rt.
- arm_load_addr: GLOBAL straight into rd (offset folds into the addend);
  FRAME/REG via base+offset; folds any scaled index for all base kinds (the NDT
  routes addr_legal-rejected indexed addresses through here).
- arm_load_const / arm_load_label_addr implemented; arm_tls_addr_of left a
  clean panic.

isa.h: arm_add_reg_lsl (ADD.W rd, rn, rm, LSL #sh) for scaled-index addressing.

reloc.c: R_ARM_THM_MOVW_ABS_NC / R_ARM_THM_MOVT_ABS patchers (imm4:i:imm3:imm8
scatter) + R_ARM_THM_JUMP19 patcher for completeness; rows added (width 4,
MOV* not a branch). kit ld now links all of the above end-to-end.

Clean documented leave-red:
- arm_tls_addr_of: TLS local-exec needs a thread pointer read from the CP15
  thread-ID register, which the M-profile (Cortex-M) does not implement. The
  bare-metal arm32 lane is non-TLS; a correct app-profile path is a follow-on,
  so this stays a clean panic rather than emitting a wrong sequence.
- arm_load_const for size>4 panics ("wide8 track") — the NDT pre-lowers 8-byte
  constants to two 32-bit lanes, so this is a backstop for the -O1 emit path.

Diffstat:
Msrc/arch/arm32/isa.h | 6++++++
Msrc/arch/arm32/native.c | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/arch/arm32/reloc.c | 48++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 146 insertions(+), 17 deletions(-)

diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -114,6 +114,12 @@ static inline u32 arm_dp_reg(u32 op4, u32 setflags, u32 rd, u32 rn, u32 rm) { (rd << 8) | rm); } static inline u32 arm_add_reg(u32 rd, u32 rn, u32 rm) { return arm_dp_reg(8u, 0u, rd, rn, rm); } +/* ADD.W rd, rn, rm, LSL #sh (T3, no flags): scaled-index base+offset for + * indexed addressing. type=00 (LSL); sh in imm3:imm2. */ +static inline u32 arm_add_reg_lsl(u32 rd, u32 rn, u32 rm, u32 sh) { + u32 imm3 = (sh >> 2) & 7u, imm2 = sh & 3u; + return arm_t32(0xeb00u | rn, (imm3 << 12) | (rd << 8) | (imm2 << 6) | 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); } diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -275,7 +275,36 @@ static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr, /* ============================ memory ============================ */ -/* Resolve a NativeAddr to a (base reg, signed byte offset) pair. */ +/* Materialize the runtime address of `sym` (+ addend) into register `rd`. + * + * arm32's house strategy is MOVW/MOVT absolute (Cortex-M has no literal pools + * and the bare-metal lane is non-PIC): + * MOVW rd, #:lower16:sym ; R_ARM_THM_MOVW_ABS_NC -> (S+addend)[15:0] + * MOVT rd, #:upper16:sym ; R_ARM_THM_MOVT_ABS -> (S+addend)[31:16] + * The 16-bit imm placeholders the encoders write are overwritten by the + * patcher (src/arch/arm32/reloc.c); the addend rides in the reloc record (an + * explicit addend), so the symbol's full address+addend lands in `rd` and no + * post-add is emitted. (A GOT-indirect variant would belong here if a PIC + * arm32 lane were ever added; bare-metal ELF resolves these absolutely.) */ +static void arm_emit_global_addr(Arm32NativeTarget* a, u32 rd, ObjSymId sym, + i64 addend) { + MCEmitter* mc = a->base.mc; + u32 pos = mc_pos(mc); + arm_emit_t32(mc, arm_movw(rd, 0u)); + mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_MOVW_ABS_NC, sym, addend, + 1, 0); + pos = mc_pos(mc); + arm_emit_t32(mc, arm_movt(rd, 0u)); + mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_MOVT_ABS, sym, addend, 1, + 0); +} + +/* Resolve a NativeAddr to a (base reg, signed byte offset) pair. A GLOBAL base + * is materialized via MOVW/MOVT into ARM_TMP (lr) — NOT the IP scratch (r12), + * which the NDT uses to materialize operands: in a store `str rt, [base]` the + * transfer reg rt may itself be r12 (an NDT-materialized value or scratch), + * which the IP global-address would clobber. lr is reserved (never an NDT + * operand) and dead in the body, so it can never alias rt. */ static u32 arm_addr_base(Arm32NativeTarget* a, const NativeAddr* addr, i32* off_out) { switch (addr->base_kind) { @@ -288,6 +317,11 @@ static u32 arm_addr_base(Arm32NativeTarget* a, const NativeAddr* addr, case NATIVE_ADDR_BASE_REG: *off_out = addr->offset; return addr->base.reg & 0xfu; + case NATIVE_ADDR_BASE_GLOBAL: + arm_emit_global_addr(a, ARM_TMP, addr->base.global.sym, + addr->base.global.addend); + *off_out = addr->offset; + return ARM_TMP; default: arm_panic(a, "unsupported addressing mode"); } @@ -379,17 +413,12 @@ static void arm_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot, 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); +/* Materialize a full address into `rd`: base + signed byte offset. */ +static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off) { + MCEmitter* mc = a->base.mc; u32 enc; if (off == 0) { - arm_emit_t16(mc, arm_mov_hi(rd, base)); + if (rd != base) 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)) { @@ -399,6 +428,34 @@ static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { } } +/* Materialize the address of a NativeAddr into the destination register. + * + * The NDT routes a base+index (or any addr the addr_legal hook rejects) + * through here and then treats `rd` as a plain BASE_REG, so this MUST fold any + * scaled index: rd = base + offset + (index << log2_scale). A GLOBAL base's + * byte offset folds into the MOVW/MOVT reloc addend; FRAME/REG bases add the + * offset explicitly. `rd` is a fresh NDT scratch, distinct from the pinned + * index register, so no aliasing post-add is needed. */ +static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) { + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + i32 off; + u32 base, rd = loc_reg(dst); + if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL) { + arm_emit_global_addr(a, rd, addr.base.global.sym, + addr.base.global.addend + addr.offset); + } else { + base = arm_addr_base(a, &addr, &off); + arm_emit_base_off(a, rd, base, off); + } + if (addr.index_kind == NATIVE_ADDR_INDEX_REG) { + u32 idx = addr.index.reg & 0xfu; + arm_emit_t32(mc, arm_add_reg_lsl(rd, rd, idx, addr.log2_scale)); + } else if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) { + arm_panic(a, "unsupported address index kind in load_addr"); + } +} + /* ============================ ALU ============================ */ static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc, @@ -1064,14 +1121,25 @@ static void arm_set_loc(NativeTarget* t, SrcLoc 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"); + Arm32NativeTarget* a = arm_of(t); + u32 v = 0, i; + /* Scalar constant <=4 bytes: reinterpret the ABI bytes (little-endian on + * arm32) as a u32 and materialize via arm_load_imm (MOV.W/MVN.W/MOVW+MOVT). + * 8-byte constants are the wide8 track's concern. */ + if (c.size > 4u) + arm_panic(a, "8-byte load_const is the wide8 track"); + for (i = 0; i < c.size && i < 4u; ++i) v |= (u32)c.bytes[i] << (i * 8u); + arm_load_imm(t, dst, (i64)(i32)v); } static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) { - (void)dst; - (void)l; - ARM_UNIMPL("load_label_addr"); + /* `&&label` address-take (computed goto / jump-table base): materialize the + * label's address via MOVW/MOVT ABS against its per-block local symbol — the + * same form arm_emit_global_addr uses for a global, so an encoding-divergent + * assembler recomputes the split immediate. (Setting the Thumb bit for a + * BX/BLX through this address is the indirect-branch consumer's job.) */ + Arm32NativeTarget* a = arm_of(t); + ObjSymId sym = mc_label_symbol(t->mc, l); + arm_emit_global_addr(a, loc_reg(dst), sym, 0); } static void arm_indirect_branch(NativeTarget* t, NativeLoc addr, const MCLabel* targets, u32 n) { @@ -1085,7 +1153,14 @@ static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym, (void)dst; (void)sym; (void)addend; - ARM_UNIMPL("tls_addr_of"); + /* TLS local-exec needs a thread pointer. The ARM EABI reads it from the + * CP15 thread-ID register (MRC p15,0,rd,c13,c0,3), which the M-profile + * (Cortex-M) does not implement — the bare-metal arm32 lane is non-TLS. A + * correct application-profile TLS path (R_ARM_TLS_LE32 + tp read) is a + * follow-on; emitting a wrong sequence here would silently miscompile, so + * this stays a clean panic. */ + arm_panic(arm_of(t), + "tls_addr_of: TLS unsupported on M-profile (no CP15 thread-id reg)"); } /* copy_bytes / set_bytes: aggregate (struct/array, and the wide8 i64/double) * memory ops. Both keep the materialized src/dst NativeAddr bases as-is and bake diff --git a/src/arch/arm32/reloc.c b/src/arch/arm32/reloc.c @@ -23,6 +23,9 @@ 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 */ + {R_ARM_THM_JUMP19, {4, RELOC_IS_BRANCH}}, /* B<cond>.W (T3), +-1 MiB */ + {R_ARM_THM_MOVW_ABS_NC, {4, 0}}, /* MOVW (T3) low-16 of S+A; not a branch */ + {R_ARM_THM_MOVT_ABS, {4, 0}}, /* MOVT (T3) high-16 of S+A; not a branch */ }; const RelocDesc* arm32_reloc_desc(RelocKind k) { @@ -77,6 +80,51 @@ int arm32_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, arm_wr_t32(P_bytes, instr); return 1; } + case R_ARM_THM_JUMP19: { + /* B<cond>.W T3 21-bit field {S,J2,J1,imm6,imm11} (no XOR-with-S, unlike + * THM_CALL). PC base = instruction + 4 (Thumb pipeline). The branch + * targets code, so the callee's Thumb bit (S|1) is masked out. Mirrors + * arch.c:arm32_apply_label_fixup's JUMP19 encoder. */ + i64 disp = (i64)(S & ~(u64)1) + A - (i64)P - 4; + u32 instr, sbit, j1, j2, imm6, imm11; + if (disp & 1) + compiler_panic(c, SRCLOC_NONE, + "link: THM_JUMP19 misaligned displacement"); + if (disp < -(i64)(1 << 20) || disp >= (i64)(1 << 20)) + compiler_panic(c, SRCLOC_NONE, + "link: THM_JUMP19 out of range (need +-1MiB)"); + sbit = (u32)((disp >> 20) & 1u); + j2 = (u32)((disp >> 19) & 1u); + j1 = (u32)((disp >> 18) & 1u); + imm6 = (u32)((disp >> 12) & 0x3fu); + imm11 = (u32)((disp >> 1) & 0x7ffu); + instr = arm_rd_t32(P_bytes); + /* 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; + arm_wr_t32(P_bytes, instr); + return 1; + } + case R_ARM_THM_MOVW_ABS_NC: + case R_ARM_THM_MOVT_ABS: { + /* MOVW/MOVT (T3): a 16-bit immediate scattered as imm4:i:imm3:imm8 -> + * hw1[3:0]=imm4, hw1[10]=i, hw2[14:12]=imm3, hw2[7:0]=imm8. + * MOVW fills (S+A)[15:0], MOVT fills (S+A)[31:16]. The Thumb bit is NOT + * stripped: a code symbol taken as data (`&fn`) must stay odd so a later + * BLX reaches Thumb, and a data symbol's bit 0 is part of its address. */ + u32 instr = arm_rd_t32(P_bytes); + u32 full = (u32)((u64)S + (u64)A); + u32 imm16 = (k == R_ARM_THM_MOVT_ABS) ? (full >> 16) & 0xffffu + : full & 0xffffu; + u32 imm4 = (imm16 >> 12) & 0xfu, i = (imm16 >> 11) & 1u, + imm3 = (imm16 >> 8) & 7u, imm8 = imm16 & 0xffu; + /* clear the four immediate fields, keep the opcode + Rd. */ + instr &= ~((0xfu << 16) | (1u << 26) | (7u << 12) | 0xffu); + instr |= (imm4 << 16) | (i << 26) | (imm3 << 12) | imm8; + arm_wr_t32(P_bytes, instr); + return 1; + } default: return 0; }