miscompilation of `{i16, i16}` passed via the stack
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
I'm seeing a miscompilation when passing `{i16, i16}` via the stack on at least `powerpc64`, `powerpc64le` , `aarch64_be` and `sparc64`.
The symptom is that part of the value goes missing in the callee. I think (see below) that the `alloca` is incorrectly elided: the logic believes there is a `{i16, i16}` on the stack, but really it's two `i16` values passed in separate stack slots.
Consider:
https://godbolt.org/z/bsr6ajf1f
```llvm
declare void @use(ptr)
; 8 arguments to exhaust r3-r10, so %a8 is forced into the parameter save area.
define void @callee_i16(i32 %a0, i32 %a1, i32 %a2, i32 %a3, i32 %a4, i32 %a5, i32 %a6, i32 %a7, {i16,i16} %a8) {
%p = alloca {i16,i16}, align 2
store {i16,i16} %a8, ptr %p, align 2
call void @use(ptr %p)
ret void
}
define void @caller_i16({i16,i16} %v) {
call void @callee_i16(i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, {i16,i16} %v)
ret void
}
```
In caller the arguments are in r3 and r4. The upper 48 bits are cleared (like `v & 0xFFFF`):
```
clrldi 7, 4, 48
clrldi 11, 3, 48
```
and then stored at a stack offset
```
std 7, 120(1)
std 11, 112(1)
```
Then the `callee` is called. Its stack frame is 112 bytes so SP shifts by that amount:
```
stdu 1, -112(1)
```
Hence the first `i16` starts at offset 112 + 112 = 224, and it's a 64-bit slot spans 224..=232. Due to BE the actual value bytes are at 230, but the code computes:
```
addi 3, 1, 228
```
which is 2 bytes too early, and it just finds zeros.
---
Looking at the result of `-stop-after=finalize-isel`
https://godbolt.org/z/j1Y4b7boq
```
localFrameSize: 0
- { id: 0, type: default, offset: 124, size: 4, alignment: 4, stack-id: default,
isImmutable: true, isAliased: false, callee-saved-register: '', callee-saved-restored: true,
debug-info-variable: '', debug-info-expression: '', debug-info-location: '' }
- { id: 1, type: default, offset: 116, size: 4, alignment: 4, stack-id: default,
isImmutable: false, isAliased: true, callee-saved-register: '', callee-saved-restored: true,
debug-info-variable: '', debug-info-expression: '', debug-info-location: '' }
```
I noticed that the two entries, corresponding to the same logical argument, have different flags for `isImmutable` and `isAliased`. That seems weird?
This seems to confirm that the issue is that the optimizer thinks there is a `{i16, i16 }` struct on the stack, while really it's two separate `i16` values. Eliding the `alloca` is hence incorrect.
---
The same issue occurs on some other targets:
- `aarch64_be` https://godbolt.org/z/8baahefW8: stores at 4, stack pointer changes by 16, so the value is at 20, but the actual data bytes are at 22
- `sparc64` https://godbolt.org/z/qj3WcG8oY annoying due to the stack bias, but the argument is at 2239, the read is at 2243, but this is a 64-bit target so the actual i16 is at 2245.
This can be reproduced in practice with (with qemu and gcc as the linker)
```c
// main.c
#include
void go(short, short);
void use(void *p) {
unsigned char *b = p;
printf("callee sees : %02X %02X %02X %02X\n", b[0], b[1], b[2], b[3]);
}
int main(void) {
// This came up while implementing `_Complex` in rustc.
short re = 0x1234, im = 0x5678;
unsigned char *r = (unsigned char *)&re;
unsigned char *i = (unsigned char *)&im;
printf("caller sent : %02X %02X %02X %02X\n", r[0], r[1], i[0], i[1]);
go(re, im);
return 0;
}
```
```llvm
; repro.ll
declare void @use(ptr)
define void @callee(i32 %a0, i32 %a1, i32 %a2, i32 %a3, i32 %a4, i32 %a5, i32 %a6, i32 %a7, { i16, i16 } %a8) {
%p = alloca { i16, i16 }, align 2
store { i16, i16 } %a8, ptr %p, align 2
call void @use(ptr %p)
ret void
}
define void @go(i16 %1, i16 %2) {
%a = insertvalue { i16, i16 } poison, i16 %1, 0
%b = insertvalue { i16, i16 } %a, i16 %2, 1
call void @callee(i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, i32 0, { i16, i16 } %b)
ret void
}
```
and (note: `-O0` is required)
```sh
echo "===== powerpc64"
clang -target powerpc64-linux-gnu -O0 -c repro.ll -o repro-ppc.o -Wno-override-module
powerpc64-linux-gnu-gcc -O2 -o run-ppc main.c repro-ppc.o
QEMU_LD_PREFIX=/usr/powerpc64-linux-gnu qemu-ppc64 ./run-ppc
echo "===== powerpc64le"
clang -target powerpc64le-linux-gnu -O0 -c repro.ll -o repro-ppc.o -Wno-override-module
powerpc64le-linux-gnu-gcc -O2 -o run-ppc main.c repro-ppc.o
QEMU_LD_PREFIX=/usr/powerpc64le-linux-gnu qemu-ppc64le ./run-ppc
echo "===== sparc64"
clang -target sparc64-linux-gnu -O0 -c repro.ll -o repro-aa.o -Wno-override-module -Wno-gcc-install-dir-libstdcxx
sparc64-linux-gnu-gcc -O2 -o run-aa main.c repro-aa.o
QEMU_LD_PREFIX=/usr/sparc64-linux-gnu qemu-sparc64 ./run-aa
echo "===== aarch64"
clang -target aarch64-linux-gnu -O0 -c repro.ll -o repro-aa.o -Wno-override-module
aarch64-linux-gnu-gcc -O2 -o run-aa main.c repro-aa.o
QEMU_LD_PREFIX=/usr/aarch64-linux-gnu qemu-aarch64 ./run-aa
```
(locally I also have aarch64_be)
emits
```
===== powerpc64
caller sent : 12 34 56 78
callee sees : 00 00 12 34
===== powerpc64le
caller sent : 34 12 78 56
callee sees : 34 12 00 00
===== sparc64
caller sent : 12 34 56 78
callee sees : 00 00 12 34
===== aarch64
caller sent : 34 12 78 56
callee sees : 34 12 78 56
===== aarch64_be
caller sent : 12 34 56 78
callee sees : 00 00 12 34
```
Contributor guide
Research direction
Start with the minimal IR in repro.ll and reproduce the failure using the listed clang, cross-linker, and QEMU commands for the affected targets. Inspect the -stop-after=finalize-isel output and the alloca-elision path, then add a regression test showing that both i16 values remain intact in the callee across the affected ABIs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100