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:
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.
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:
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.
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.
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.
[email protected]
From: [email protected]
Date: 2025-05-17 14:45
To: tinycc-devel
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
```
[email protected]
_______________________________________________
Tinycc-devel mailing list
[email protected]
https://lists.nongnu.org/mailman/listinfo/tinycc-devel