kit

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

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:
Mmk/rt.mk | 15+++++++++++++--
Msrc/arch/arm32/arch.c | 8++++----
Msrc/arch/arm32/native.c | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
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 ============================ */