kit

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

commit 5f1b0107a2365050f74e08713d520be2f8df9408
parent 4786aeb7126315cdd1bffdd91c6d85a7b25b8aca
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 22:28:34 -0700

arm32 §2: varargs at -O1 — GP-save spill, alignment, va_arg dst/cursor

Variadic functions miscompiled at -O1 (correct at -O0). Several coupled bugs:

- The -O1 known-frame prologue never emitted the variadic r0..r3 GP-save spill
  that the single-pass prologue does; add it.
- va_start anchored the GP-save area at the constant ARM_SAVED_PAIR_BYTES (8),
  but at -O1 the saved block also holds callee-saves — use saved_block_bytes.
- arm_frame_size didn't keep sp 8-byte aligned at calls (AAPCS): an odd-word
  saved-register block (e.g. {r4,r7,lr}) left sp 4-aligned, so a callee's
  variadic 8-byte va_arg alignment rounded the cursor PAST the first i64 (read
  the high half = 0). Pad the frame a word when the saved block is odd.
- arm_va_arg_native treated a wide (i64) result's dst as a pointer register, but
  the optimizer hands the result's storage (frame/stack); convert via arm_loc_addr
  (the riscv pattern) so the bytes land in the result, not aliasing ap.
- arm_va_arg_core used IP for the cursor, but at -O1 the scalar result is staged
  in IP too, so  clobbered the cursor and the +4 ran on the value;
  pick a cursor reg distinct from dst.

Fixes 133_varargs_mixed_types, 139_variadic_stack_arg_call_result,
23_cg_api_typed_varargs, 19_cg_api_variadic_asm at -O1.

Diffstat:
Msrc/arch/arm32/native.c | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 54 insertions(+), 14 deletions(-)

diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -906,7 +906,13 @@ static int arm_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot, } static u32 arm_frame_size(const Arm32NativeTarget* a) { - return align_up_u32(a->frame.cum_off + a->frame.max_outgoing, 8u); + u32 f = align_up_u32(a->frame.cum_off + a->frame.max_outgoing, 8u); + /* AAPCS: sp must be 8-byte aligned at public interfaces (calls). The total + * adjustment from entry sp is saved_block_bytes + (variadic ? 16 : 0) + f; the + * 16-byte GP-save area is 8-aligned, so only an odd-word saved-register block + * (e.g. {r4,r7,lr} = 12) breaks alignment — pad the frame by a word then. */ + if (a->saved_block_bytes & 4u) f += 4u; + return f; } /* r7-relative offset where the incoming stack-argument window begins. The saved @@ -1147,7 +1153,12 @@ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, return; } - /* Emit the final prologue: PUSH the save set, anchor r7, drop the frame. */ + /* Emit the final prologue. A variadic callee first spills the GP arg registers + * r0..r3 into a save area just above the saved pair (so they are contiguous + * below the named incoming stack args); va_start/va_arg walk forward across + * both. The frame teardown reclaims this 16-byte area (arm_emit_frame_restore). + * (The single-pass path does the same in arm_func_begin.) */ + if (a->is_variadic) arm_emit_t32(mc, arm_push_w(0xfu)); /* PUSH {r0-r3} */ arm_emit_t32(mc, arm_push_w(reglist)); arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ frame = arm_frame_size(a); @@ -2358,10 +2369,13 @@ static void arm_va_start_core(Arm32NativeTarget* a, NativeAddr ap) { ABIVaListInfo vai = abi_va_list_layout(t->c->abi); if (vai.kind != ABI_VA_LIST_POINTER) arm_panic(a, "unsupported va_list layout"); if (!a->is_variadic) arm_panic(a, "va_start: function not variadic"); - /* *ap = r7 + ARM_SAVED_PAIR_BYTES + next_param_int*4 (skip the named GP slots - * already consumed by the fixed params). lr (ARM_TMP) is the staging temp. */ + /* *ap = r7 + saved_block_bytes + next_param_int*4: the GP save area (r0..r3) + * sits just above the saved register block, so its base is saved_block_bytes + * (8 for the bare {r7,lr} single-pass prologue; larger when the -O1 known-frame + * prologue also PUSHes callee-saves), and next_param_int*4 skips the GP slots + * the fixed params already consumed. lr (ARM_TMP) is the staging temp. */ arm_emit_t32(mc, arm_add_imm12(ARM_TMP, ARM_FP, - ARM_SAVED_PAIR_BYTES + a->next_param_int * 4u)); + a->saved_block_bytes + a->next_param_int * 4u)); arm_emit_mem(a, 0, ptr, ap, native_mem_for_type(t, i32t, 4)); } @@ -2416,7 +2430,13 @@ static void arm_va_arg_core(Arm32NativeTarget* a, NativeLoc dst, NativeAddr ap, MCEmitter* mc = t->mc; KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); u32 sz = native_type_size(t, type); - NativeLoc cur = native_loc_reg(i32t, NATIVE_REG_INT, ARM_SCRATCH); + /* The cursor must survive the value load (dst = [cursor]) and the +4 update, so + * it cannot alias `dst` — at -O1 the result is staged in a reserved scratch + * (IP, or LR), so pick the other reserved reg for the cursor. */ + u32 cur_reg = (dst.kind == NATIVE_LOC_REG && loc_reg(dst) == ARM_SCRATCH) + ? ARM_TMP + : ARM_SCRATCH; + NativeLoc cur = native_loc_reg(i32t, NATIVE_REG_INT, cur_reg); NativeAddr from; ABIVaListInfo vai = abi_va_list_layout(t->c->abi); if (vai.kind != ABI_VA_LIST_POINTER) arm_panic(a, "unsupported va_list layout"); @@ -2425,10 +2445,10 @@ static void arm_va_arg_core(Arm32NativeTarget* a, NativeLoc dst, NativeAddr ap, arm_emit_mem(a, 1, cur, ap, native_mem_for_type(t, i32t, 4)); memset(&from, 0, sizeof from); from.base_kind = NATIVE_ADDR_BASE_REG; - from.base.reg = ARM_SCRATCH; + from.base.reg = cur_reg; from.base_type = type; arm_emit_mem(a, 1, dst, from, native_mem_for_type(t, type, sz)); - arm_emit_t32(mc, arm_add_imm12(ARM_SCRATCH, ARM_SCRATCH, 4u)); + arm_emit_t32(mc, arm_add_imm12(cur_reg, cur_reg, 4u)); arm_emit_mem(a, 0, cur, ap, native_mem_for_type(t, i32t, 4)); } @@ -2451,6 +2471,31 @@ static NativeAddr arm_va_addr_from_ptr(NativeLoc ap_ptr) { addr.base_type = ap_ptr.type; return addr; } +/* The destination memory of a value-location: a wide (i64/double) va_arg result + * is memory-resident, so the optimizer hands its storage (a frame/stack slot, or + * an explicit address), NOT a pointer in a register. Convert it to a NativeAddr + * the byte-copy can write through. (Mirrors riscv's rv_loc_addr.) */ +static NativeAddr arm_loc_addr(Arm32NativeTarget* a, NativeLoc loc) { + NativeAddr addr; + memset(&addr, 0, sizeof addr); + switch ((NativeLocKind)loc.kind) { + case NATIVE_LOC_FRAME: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.frame; + addr.base_type = loc.type; + return addr; + case NATIVE_LOC_STACK: + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = loc.v.stack.slot; + addr.base_type = loc.type; + addr.offset = loc.v.stack.offset; + return addr; + case NATIVE_LOC_ADDR: + return loc.v.addr; + default: + arm_panic(a, "va_arg destination is not addressable"); + } +} static void arm_va_start_native(NativeTarget* t, NativeLoc ap_ptr) { arm_va_start_core(arm_of(t), arm_va_addr_from_ptr(ap_ptr)); } @@ -2458,12 +2503,7 @@ static void arm_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr, KitCgTypeId type) { Arm32NativeTarget* a = arm_of(t); if (arm_va_arg_is_wide(t, type)) { - NativeAddr dstm; - memset(&dstm, 0, sizeof dstm); - dstm.base_kind = NATIVE_ADDR_BASE_REG; - dstm.base.reg = dst.v.reg; - dstm.base_type = type; - arm_va_arg_wide(a, dstm, arm_va_addr_from_ptr(ap_ptr), + arm_va_arg_wide(a, arm_loc_addr(a, dst), arm_va_addr_from_ptr(ap_ptr), native_type_size(t, type)); return; }