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:
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;
}