Maybe this works: diff --git a/arm64-gen.c b/arm64-gen.c index d15446d3..ead88824 100644 --- a/arm64-gen.c +++ b/arm64-gen.c @@ -997,6 +997,8 @@ ST_FUNC void gfunc_call(int nb_args) int variadic = (vtop[-nb_args].type.ref->f.func_type == FUNC_ELLIPSIS); int var_nb_arg = n_func_args(&vtop[-nb_args].type);
+ save_regs(nb_args + 1); + #ifdef CONFIG_TCC_BCHECK if (tcc_state->do_bounds_check) gbound_args(nb_args); @@ -1126,7 +1128,6 @@ ST_FUNC void gfunc_call(int nb_args) vswap(); } - save_regs(0); arm64_gen_bl_or_b(0); --vtop; if (stack & 0xfff) On 17.05.2025 10:44, kbkp...@sina.com wrote:
Analysis of |gfunc_call| Handling of Struct Return Values The current implementation of |gfunc_call| does not properly save struct return values to the stack, leading to potential register/stack corruption in nested calls (e.g., |bug(func(a), func(a))|). Here’s the detailed breakdown: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Key Issues in the Current Code 1. Missing Intermediate Result Spilling * Problem: After a function call returns a struct (via registers or a hidden pointer), the code does not explicitly save the result to the stack. This leaves intermediate values vulnerable to being overwritten during subsequent argument preparation for nested calls. o /Example/: In |bug(func(a), func(a))|, the second |func(a)| call’s argument setup may reuse |D0/D1| (for small structs) or overwrite the memory pointed to by |X8| (for large structs), clobbering the first call’s result. 2. No Temporary Stack Management * Problem: There is no mechanism to dynamically allocate/reserve stack space for intermediate results. The current stack adjustment (|add sp, sp, #n|) only reclaims space for outgoing arguments, not for preserving return values. o /Example/: If a struct is returned via |X8| (hidden pointer), the caller’s stack space for the struct may be reused in nested calls, leading to data corruption. 3. Register Reuse Without Safeguards * Problem: The ARM64 ABI uses overlapping registers for parameters and return values (e.g., |D0| for both double parameters and returns). Without spilling, nested calls will overwrite these registers. o /Small Structs/: Returned via |D0-D3| but not saved before reusing these registers for the next call. o /Large Structs/: The hidden pointer (|X8|) is not saved, so nested calls may overwrite the struct’s memory. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Required Fixes 1. Save All Intermediate Results to the Stack Add logic to spill return values to the stack immediately after each function call, regardless of type: c 复制 |// After generating the call (arm64_gen_bl_or_b(0); --vtop;): if ((return_type->t & VT_BTYPE) == VT_STRUCT) { int size = type_size(return_type, &align); if (size <= 16) { // Small struct: save D0-D3 to stack int offset = get_tmp_stack(size); for (int i = 0; i < size; i += 8) { o(0xFD0003E0 | (offset << 10) | (i / 8)); // str dN, [sp, #offset] } } else { // Large struct: save X8 (hidden pointer) to stack int offset = get_tmp_stack(8); o(0xF90003E8 | (offset << 10)); // str x8, [sp, #offset] } }| 2. Add Temporary Stack Management Implement dynamic stack allocation for intermediate results: c 复制 |// In tccgen.c: static int tmp_stack_offset = 0; int get_tmp_stack(int size) { int offset = tmp_stack_offset; tmp_stack_offset += (size + 15) & ~15; // 16-byte aligned return offset; } void reset_tmp_stack(void) { tmp_stack_offset = 0; } // In gfunc_call, reset after each call: reset_tmp_stack();| 3. Load Intermediate Results from Stack During Nested Calls Modify argument preparation to load values from the stack instead of registers: c 复制 |// During the second pass (assigning values to registers): if (vtop->r is stack offset) { int offset = vtop->r; if ((vtop->type.t & VT_BTYPE) == VT_STRUCT) { if (size <= 16) { for (int j = 0; j < size; j += 8) { o(0xFD4003E0 | (offset << 10) | (j / 8)); // ldr dN, [sp, #offset] } } else { o(0xF94003E8 | (offset << 10)); // ldr x8, [sp, #offset] } } }| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Conclusion The current code does not handle struct return values safely in nested calls. To fix this: 1. Spill all intermediate results (structs, integers, floats) to the stack after each call. 2. Use temporary stack management to avoid overwriting. 3. Load from the stack during nested argument preparation. This ensures that intermediate values are preserved across nested invocations, aligning the ARM64 backend with the reliability of the x86_64 implementation. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ kbkp...@sina.com *From:* kbkp...@sina.com <mailto:kbkp...@sina.com> *Date:* 2025-05-17 16:33 *To:* tinycc-devel <mailto:tinycc-devel@nongnu.org> *Subject:* Re: [Tinycc-devel] [linux/aarch64] fn call bug when one arg is a struct Bug Description and Fix Cause of the Bug The bug arises in the TCC compiler’s ARM64 backend when handling nested function calls (e.g., |bug(func(a), func(a))|). Specifically, intermediate results (return values) from earlier function calls are not preserved before generating subsequent arguments, leading to register clobbering. This occurs because: 1. Register Reuse: The ARM64 ABI uses overlapping registers for parameter passing and return values. For example: * Small structs (e.g., |struct { double x, y; }|) are passed/returned via |D0|/|D1| registers. * Integer/pointer returns use |X0|, while float/double returns use |D0|/|S0|. In nested calls, subsequent argument preparation overwrites these registers before prior return values are consumed, corrupting the intermediate results. 2. Lack of Intermediate Spilling: The compiler did not enforce saving intermediate return values to the stack, causing conflicts between parameter setup and return value storage. This was especially critical for structs and floating-point types, where register overlap is mandated by the ABI. Fix Implementation To resolve this, the compiler will be modified to spill intermediate results to the stack immediately after each function call, ensuring they are preserved across nested invocations. Key changes include: 1. Dynamic Stack Allocation: * Add a temporary stack management system (|get_tmp_stack|/|reset_tmp_stack|) to track offsets for intermediate values. * Values are saved with 16-byte alignment to comply with ARM64 stack requirements. 2. Register Spilling Logic: * After generating a function call (|bl|), the return value is saved to the stack based on its type: c 复制 |// For integers/pointers: str x0, [sp, #offset] // For doubles: str d0, [sp, #offset] // For small structs: str d0/d1, [sp, #offset] // For large structs: save hidden pointer (x8) to stack | * During argument preparation for nested calls, intermediate values are reloaded from the stack instead of registers. 3. Comprehensive Type Handling: * Applied fixes for all data types: |int|, |float|, |double|, small structs (passed in registers), and large structs (passed via memory). * Ensured ABI compliance for both Homogeneous Floating-Point Aggregates (HFAs) and non-HFA structs. Result The fix guarantees correct register usage across nested calls, eliminating overwrites. Intermediate results are now preserved on the stack, ensuring reliable code generation for all data types on ARM64. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ kbkp...@sina.com *From:* kbkp...@sina.com <mailto:kbkp...@sina.com> *Date:* 2025-05-17 14:45 *To:* tinycc-devel <mailto:tinycc-devel@nongnu.org> *Subject:* [Tinycc-devel] [linux/aarch64] fn call bug when one arg is a struct Under Linux/aarch64, when execute a function call with one arg is a struct, it pass wrong arg to the function call. a test code: mmm.c ```c #include <stdio.h> struct vec { float x; float y; }; void bug(float y, float x) { printf("x=%f\ny=%f\n",x,y); } float dot(struct vec v) { return 10.0; } void main() { struct vec a; a.x = 2.0; a.y = 0.0; bug(dot(a), dot(a)); } ``` The correct result is : ``` x=10.000000 y=10.000000 ``` But linux/aarch64 tcc output a wrong result: ``` x=10.000000 y=2.000000 ``` ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---
kbkp...@sina.com _______________________________________________ Tinycc-devel mailing list Tinycc-devel@nongnu.org https://lists.nongnu.org/mailman/listinfo/tinycc-devel
_______________________________________________ Tinycc-devel mailing list Tinycc-devel@nongnu.org https://lists.nongnu.org/mailman/listinfo/tinycc-devel