commit 68d27f53b730feff1ff4f9c6d7b122ec10197597
parent e76d974a034d26387ed146730d0366f5fa274d5b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 18:48:23 -0700
arm32 Phase 2: overflow/alloca/frame-addr intrinsics + kit self-built runtime
- 32-bit __builtin_*_overflow (6 forms): flagless signed (XOR+shift), unsigned
(CMP+IT), UMULL/SMULL for mul; i64 overflow stays in the cg lane path.
- alloca: sp -= align8(size); dst = sp + max_outgoing (patched in func_end so a
later call's outgoing args never clobber the block).
- __builtin_frame_address / __builtin_return_address: walk the r7 fp chain
([r7]=caller fp, [r7+4]=saved lr).
- tls_addr_of: single-thread bare-metal TLS-as-static (M-profile has no CP15
thread reg) — &__thread is a link-time-constant MOVW/MOVT abs.
- mk/rt.mk: kit now self-compiles libkit_rt.a for arm (all C compiler-rt
sources). The hand-written AEABI alias layer + coroutine asm switch are
dropped from the kit self-build (kit-as does not yet cover those idioms; a
follow-on) — kit-compiled code uses the libgcc-named helpers the C sources
provide, not __aeabi_*.
Diffstat:
3 files changed, 169 insertions(+), 22 deletions(-)
diff --git a/mk/rt.mk b/mk/rt.mk
@@ -209,8 +209,19 @@ RT_riscv32-elf-hardfloat_ARCH_FLAGS = -mabi=ilp32f -march=rv32imafc
RT_arm-eabi-thumb2_TARGET = arm-none-eabi
RT_arm-eabi-thumb2_ABI = ilp32
RT_arm-eabi-thumb2_INT128 = 0
-RT_arm-eabi-thumb2_CORO = arm32
-RT_arm-eabi-thumb2_AEABI = thumb2
+# Coroutine support is a hand-written register-switch in ARM asm (coro/arm32.c +
+# coro/coro.c's file-scope asm) that the kit assembler does not yet cover; like
+# the AEABI layer below it is dropped from the kit self-build (a follow-on once
+# kit-as covers the idioms). The C runtime kit needs for ordinary programs is
+# unaffected.
+RT_arm-eabi-thumb2_CORO =
+# AEABI alias layer (aeabi.c + aeabi_thumb2.S) is hand-written ARM asm that the
+# kit assembler does not yet fully cover; kit-compiled code references the
+# libgcc-named helpers (__udivdi3 / __adddf3 / ...) the generic compiler-rt C
+# sources provide, not the __aeabi_* aliases, so kit self-builds this runtime
+# without the AEABI layer. Full kit-as coverage of the AEABI asm (re-enabling
+# `= thumb2`) is a follow-on.
+RT_arm-eabi-thumb2_AEABI =
RT_arm-eabi-thumb1_TARGET = arm-none-eabi
RT_arm-eabi-thumb1_ABI = ilp32
diff --git a/src/arch/arm32/arch.c b/src/arch/arm32/arch.c
@@ -213,19 +213,19 @@ static int arm32_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) {
case KIT_CG_INTRIN_WFI:
case KIT_CG_INTRIN_WFE:
case KIT_CG_INTRIN_SEV:
- return 1;
- case KIT_CG_INTRIN_POPCOUNT:
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_FRAME_ADDRESS:
+ case KIT_CG_INTRIN_RETURN_ADDRESS:
+ return 1;
+ case KIT_CG_INTRIN_POPCOUNT:
case KIT_CG_INTRIN_PREFETCH:
case KIT_CG_INTRIN_EXPECT:
case KIT_CG_INTRIN_ASSUME_ALIGNED:
- 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:
diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c
@@ -90,6 +90,13 @@ typedef struct Arm32NativeTarget {
u8 slim_prologue; /* leaf, no frame: emit BX lr, no PUSH/anchor */
u16 saved_reglist; /* registers PUSHed by the prologue (POP restores via pc) */
u32 saved_block_bytes;
+ /* alloca: each dynamic allocation moves sp down and returns sp + max_outgoing
+ * (the block sits just above the call outgoing-arg area at the bottom of the
+ * frame). max_outgoing is only final at func_end, so the `ADD dst, sp, #off`
+ * is emitted with a placeholder and patched here. */
+ u32 alloca_patch_pos[16];
+ u8 alloca_patch_rd[16];
+ u8 n_alloca_patch;
} Arm32NativeTarget;
static Arm32NativeTarget* arm_of(NativeTarget* t) {
@@ -892,6 +899,7 @@ static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
a->slim_prologue = 0;
a->saved_reglist = (u16)((1u << ARM_FP) | (1u << 14u)); /* {r7, lr} */
a->saved_block_bytes = 8u; /* two words */
+ a->n_alloca_patch = 0;
mc_set_section(mc, fd->text_section_id);
mc_emit_align(mc, 4, 0);
@@ -952,6 +960,7 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd,
a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
a->known_frame = 1;
a->slim_prologue = 0;
+ a->n_alloca_patch = 0;
a->frame.known_frame = 1;
a->frame.has_alloca = kf ? kf->has_alloca : 0u;
@@ -1055,6 +1064,18 @@ static void arm_func_end(NativeTarget* t) {
arm_patch_t32(a, a->prologue_sub_pos + i * 4u, words[i]);
}
+ /* Patch each alloca's `ADD dst, sp, #max_outgoing` now that the outgoing-arg
+ * area size is final (the alloca block sits just above it, so a later call's
+ * sp-relative outgoing stores never clobber it). */
+ {
+ u32 i, mo = a->frame.max_outgoing;
+ if (a->n_alloca_patch && mo > 0xfffu)
+ arm_panic(a, "alloca with a large outgoing-arg area (needs IP staging)");
+ for (i = 0; i < a->n_alloca_patch; ++i)
+ arm_patch_t32(a, a->alloca_patch_pos[i],
+ arm_add_imm12(a->alloca_patch_rd[i], 13u, mo));
+ }
+
/* 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). */
@@ -1721,17 +1742,14 @@ static void arm_indirect_branch(NativeTarget* t, NativeLoc addr,
}
static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym,
i64 addend) {
- (void)dst;
- (void)sym;
- (void)addend;
- /* 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)");
+ /* The M-profile (Cortex-M) has no CP15 thread-ID register and the bare-metal
+ * arm32 lane is single-threaded, so the local-exec model degenerates: each
+ * `__thread` object has exactly one instance, addressed absolutely like a
+ * regular static. Materialize its address with the same MOVW/MOVT-absolute
+ * sequence as a global; the freestanding image places the TLS image as part
+ * of its data, so &tlsvar is a link-time constant. (A true variant-I TLS path
+ * with a software thread pointer is a follow-on for a multi-threaded lane.) */
+ arm_emit_global_addr(arm_of(t), loc_reg(dst), sym, addend);
}
/* 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
@@ -1823,10 +1841,23 @@ static void arm_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc v,
}
static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size,
u32 align) {
- (void)dst;
- (void)size;
- (void)align;
- ARM_UNIMPL("alloca");
+ Arm32NativeTarget* a = arm_of(t);
+ MCEmitter* mc = t->mc;
+ u32 rsz = loc_reg(size), rd = loc_reg(dst);
+ u32 enc7;
+ (void)align; /* AAPCS keeps sp 8-byte aligned; round the request up to 8. */
+ thumb_expand_imm_encode(7u, &enc7);
+ /* ip = (size + 7) & ~7; sp -= ip. */
+ arm_emit_t32(mc, arm_add_imm12(ARM_SCRATCH, rsz, 7u));
+ arm_emit_t32(mc, arm_dp_imm(1u, 0u, ARM_SCRATCH, ARM_SCRATCH, enc7)); /* BIC */
+ arm_emit_t32(mc, arm_sub_reg(13u, 13u, ARM_SCRATCH));
+ /* dst = sp + max_outgoing (the block sits above the outgoing-arg area). The
+ * offset is finalized in arm_func_end; emit a placeholder ADDW to patch. */
+ if (a->n_alloca_patch >= 16u) arm_panic(a, "too many alloca sites");
+ a->alloca_patch_rd[a->n_alloca_patch] = (u8)rd;
+ a->alloca_patch_pos[a->n_alloca_patch++] = mc_pos(mc);
+ arm_emit_t32(mc, arm_add_imm12(rd, 13u, 0u));
+ a->frame.has_alloca = 1;
}
/* ============================ atomics ============================ */
/* ARMv7-M atomics over LDREX/STREX (word/halfword/byte) + DMB. The cg layer
@@ -2159,6 +2190,80 @@ static void arm_va_copy_native(NativeTarget* t, NativeLoc dst_ap_ptr,
arm_va_addr_from_ptr(src_ap_ptr));
}
+/* Materialize the boolean of ARM condition `cc` into rd: MOV rd,#0; IT cc;
+ * MOV<cc> rd,#1. The flag state must already be set by a preceding CMP; the
+ * leading MOV.W (no S) does not disturb it. */
+static void arm_emit_setcc(MCEmitter* mc, u32 rd, u32 cc) {
+ u32 enc0, enc1;
+ thumb_expand_imm_encode(0u, &enc0);
+ thumb_expand_imm_encode(1u, &enc1);
+ arm_emit_t32(mc, arm_mov_imm(rd, enc0));
+ arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (mask 0b1000) */
+ arm_emit_t32(mc, arm_mov_imm(rd, enc1));
+}
+
+/* The 32-bit `__builtin_*_overflow` family. dsts = [value, overflow]; args =
+ * [a, b]. i64 overflow is lowered to lane ops by the cg layer (arith.c), so the
+ * backend only sees the 32-bit forms. lr/ip are the transient scratch (lr is
+ * reserved; ip is the materialization scratch, free here since the operands are
+ * already in their NDT registers). */
+static void arm_overflow(Arm32NativeTarget* a, IntrinKind kind,
+ const NativeLoc* dsts, const NativeLoc* args) {
+ MCEmitter* mc = a->base.mc;
+ u32 ra = loc_reg(args[0]), rb = loc_reg(args[1]);
+ u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]);
+ u32 enc0;
+ switch (kind) {
+ case INTRIN_SADD_OVERFLOW:
+ case INTRIN_SSUB_OVERFLOW:
+ /* r = a +/- b; signed ovf is the sign bit of ((a^r)&(b^r)) [ADD] or
+ * ((a^b)&(a^r)) [SUB]. */
+ if (kind == INTRIN_SADD_OVERFLOW)
+ arm_emit_t32(mc, arm_add_reg(ARM_TMP, ra, rb));
+ else
+ arm_emit_t32(mc, arm_sub_reg(ARM_TMP, ra, rb));
+ arm_emit_t32(mc, arm_eor_reg(ARM_SCRATCH, ra, ARM_TMP)); /* a ^ r */
+ if (kind == INTRIN_SADD_OVERFLOW)
+ arm_emit_t32(mc, arm_eor_reg(rovf, rb, ARM_TMP)); /* b ^ r */
+ else
+ arm_emit_t32(mc, arm_eor_reg(rovf, ra, rb)); /* a ^ b */
+ arm_emit_t32(mc, arm_and_reg(rovf, rovf, ARM_SCRATCH));
+ arm_emit_t32(mc, arm_shift_imm(1u, rovf, rovf, 31u)); /* LSR #31 -> 0/1 */
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
+ return;
+ case INTRIN_UADD_OVERFLOW:
+ arm_emit_t32(mc, arm_add_reg(ARM_TMP, ra, rb)); /* r = a + b */
+ arm_emit_t32(mc, arm_cmp_reg(ARM_TMP, ra)); /* CMP r, a */
+ arm_emit_setcc(mc, rovf, ARM_CC_CC); /* ovf = (r < a) unsigned */
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
+ return;
+ case INTRIN_USUB_OVERFLOW:
+ arm_emit_t32(mc, arm_cmp_reg(ra, rb)); /* CMP a, b */
+ arm_emit_t32(mc, arm_sub_reg(ARM_TMP, ra, rb)); /* r = a - b (no flags) */
+ arm_emit_setcc(mc, rovf, ARM_CC_CC); /* ovf = (a < b) unsigned */
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
+ return;
+ case INTRIN_UMUL_OVERFLOW:
+ /* UMULL lr:ip = a*b; ovf = (high != 0); value = low. */
+ arm_emit_t32(mc, arm_umull(ARM_TMP, ARM_SCRATCH, ra, rb));
+ thumb_expand_imm_encode(0u, &enc0);
+ arm_emit_t32(mc, arm_cmp_imm(ARM_SCRATCH, enc0));
+ arm_emit_setcc(mc, rovf, ARM_CC_NE);
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
+ return;
+ case INTRIN_SMUL_OVERFLOW:
+ /* SMULL lr:ip = a*b; ovf = (high != (low >>s 31)); value = low. */
+ arm_emit_t32(mc, arm_smull(ARM_TMP, ARM_SCRATCH, ra, rb));
+ arm_emit_t32(mc, arm_shift_imm(2u, rovf, ARM_TMP, 31u)); /* rovf = low ASR 31 */
+ arm_emit_t32(mc, arm_cmp_reg(ARM_SCRATCH, rovf));
+ arm_emit_setcc(mc, rovf, ARM_CC_NE);
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
+ return;
+ default:
+ arm_panic(a, "arm_overflow: not an overflow intrinsic");
+ }
+}
+
static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
const NativeLoc* dsts, u32 ndst, const NativeLoc* args,
u32 narg) {
@@ -2222,10 +2327,41 @@ static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
case INTRIN_ISB:
arm_emit_t32(mc, arm_isb(ARM_BARRIER_SY));
return;
+ case INTRIN_SADD_OVERFLOW:
+ case INTRIN_SSUB_OVERFLOW:
+ case INTRIN_UADD_OVERFLOW:
+ case INTRIN_USUB_OVERFLOW:
+ case INTRIN_SMUL_OVERFLOW:
+ case INTRIN_UMUL_OVERFLOW:
+ if (ndst == 2u && narg == 2u) {
+ arm_overflow(a, kind, dsts, args);
+ return;
+ }
+ break;
+ case INTRIN_FRAME_ADDRESS:
+ case INTRIN_RETURN_ADDRESS:
+ /* kit's prologue anchors r7 at the saved pair: [r7]=caller r7,
+ * [r7+4]=this frame's saved lr (return address). The level is constant
+ * (unrolls to `level` dependent loads up the fp chain). A frame-reading
+ * function is kept off the frameless-leaf tier (reads_frame), so r7 is
+ * always valid here. */
+ if (ndst == 1u) {
+ u32 level = (narg >= 1u && args[0].kind == NATIVE_LOC_IMM)
+ ? (u32)args[0].v.imm
+ : 0u;
+ u32 rd = loc_reg(dsts[0]), i;
+ arm_emit_t16(mc, arm_mov_hi(rd, ARM_FP)); /* rd = r7 */
+ for (i = 0; i < level; ++i)
+ arm_emit_t32(mc, arm_ldr_imm(rd, rd, 0)); /* rd = *(rd) */
+ if (kind == INTRIN_RETURN_ADDRESS)
+ arm_emit_t32(mc, arm_ldr_imm(rd, rd, 4u)); /* rd = *(rd+4) = saved lr */
+ return;
+ }
+ break;
default:
break;
}
- arm_panic(a, "intrinsic not lowered (overflow/FMA/syscall/etc. are other tracks)");
+ arm_panic(a, "intrinsic not lowered (FMA/syscall/setjmp/etc. are follow-ons)");
}
/* ============================ inline asm ============================ */