Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect SystemV AMD64 ABI call when using aggregate types as parameters #22515

Open
mhcerri opened this issue Jan 17, 2025 · 3 comments
Open
Labels
bug Observed behavior contradicts documented or intended behavior

Comments

@mhcerri
Copy link

mhcerri commented Jan 17, 2025

Zig Version

0.14.0-dev.2649+77273103a

Steps to Reproduce and Observed Behavior

The zig compiler is generating wrong C ABI calls for amd64 linux when invoking functions with struct arguments.

Given the following source files:

main.c:

const std = @import("std");
const a = @import("a.zig");

pub fn main() !void {
    std.debug.print("begin\n", .{});

    const p1: ?*anyopaque = @ptrFromInt(1);
    const v1: a.vec = .{ .a = 2.0, .b = 3.0 };
    const v2: a.vec = .{ .a = 4.0, .b = 5.0 };
    const f1: f64 = 6.0;
    const s1: a.structure = .{ .a = 7, .b = 8, .c = 9 };
    const p2: ?*anyopaque = @ptrFromInt(10);
    const p3: ?*anyopaque = @ptrFromInt(11);

    a.func(p1, v1, v2, f1, s1, p2, p3);

    std.debug.print("end\n", .{});
}

a.h:

#include <stdint.h>
typedef struct { double a, b; } vec;
typedef struct { uintptr_t a; unsigned int b, c; } structure;
void func(void* p1, vec v1, vec v2, double f1, structure s1, void* p2, void* p3);

a.c:

#include <stdio.h>
#include "a.h"

void func(void *p1, vec v1, vec v2, double f1, structure s1, void *p2, void *p3) {
    printf("p1=%p\n", p1);
    printf("v1.a=%f\n", v1.a);
    printf("v1.b=%f\n", v1.b);
    printf("v2.a=%f\n", v2.a);
    printf("v2.b=%f\n", v2.b);
    printf("f1=%f\n", f1);
    printf("s1.a=%lu\n", s1.a);
    printf("s1.b=%u\n", s1.b);
    printf("s1.c=%u\n", s1.c);
    printf("p2=%p\n", p2);
    printf("p3=%p\n", p3);
}

The following commands produce:

$ zig translate-c ./a.h > ./a.zig
$ zig run ./main.zig ./a.c -lc
begin
p1=0x1
v1.a=2.000000
v1.b=3.000000
v2.a=4.000000
v2.b=5.000000
f1=6.000000
s1.a=10
s1.b=11
s1.c=0
p2=0x7
p3=0x6
end

However the expected output would be:

p1=0x1
v1.a=2.000000
v1.b=3.000000
v2.a=4.000000
v2.b=5.000000
f1=6.000000
s1.a=7
s1.b=8
s1.c=9
p2=0xa
p3=0xb

Since structure has 16 bytes, s1 should be passed via 2 separate 64 bit registers. However the zig compiler seems to be passing s1 via the stack instead. In practice, that causes the s1 fields in the C function to receive the values passed as p2 and p3 .

If I add an additional field to the structure, forcing it to be bigger than 16 bytes, then the SystemV amd64 ABI expects that argument to be passed via the stack and the program works fine.

You can find more information about aggregate types in the SystemV amd64 ABI in the following article:

https://yorickpeterse.com/articles/the-mess-that-is-handling-structure-arguments-and-returns-in-llvm/

Expected Behavior

Expected output:

p1=0x1
v1.a=2.000000
v1.b=3.000000
v2.a=4.000000
v2.b=5.000000
f1=6.000000
s1.a=7
s1.b=8
s1.c=9
p2=0xa
p3=0xb

The 3 fields from the structure should be passed to the C function via 2 separate 64 bit registers.

@mhcerri mhcerri added the bug Observed behavior contradicts documented or intended behavior label Jan 17, 2025
@190n
Copy link
Contributor

190n commented Jan 17, 2025

More minimal example on Godbolt. Zig:

const S = extern struct { a: usize, b: usize };
extern fn callee(f64, f64, f64, f64, f64, S) void;
export fn caller() void {
    callee(5, 6, 7, 8, 9, .{ .a = 1, .b = 2 });
}

C:

#include <stddef.h>
typedef struct { size_t a, b; } S;
void callee(double, double, double, double, double, S);
void caller(void) {
    callee(5, 6, 7, 8, 9, (S){ 1, 2 });
}

Same as in OP, you can see in the assembly that Zig passes S on the stack while gcc and clang pass it in registers.

If you reduce the number of floating-point arguments from 5 to 4, Zig behaves correctly. Perhaps the presence of 5 floating-point arguments passed in registers makes Zig think it has no registers left for passing arguments, even though it actually does still have integer registers that it should be using.

@190n
Copy link
Contributor

190n commented Jan 18, 2025

Interestingly, when I run your example with the native x86_64 backend (zig run ./main.zig ./a.c -lc -fno-llvm -fno-lld) it produces the expected output. Since Clang can compile an equivalent function call correctly, I would guess this is a bug in Zig's LLVM backend rather than a bug in LLVM itself.

@mhcerri
Copy link
Author

mhcerri commented Jan 18, 2025

@190n, I believe the problem is that the LLVM doesn't implement all the specific rules of the target ABI (as pointed in [1]) and it leaves for the frontends to deal with it. I'm not familiar with the zig compiler code, but there's a good chance the native zig backend is handling instead.

[1] https://yorickpeterse.com/articles/the-mess-that-is-handling-structure-arguments-and-returns-in-llvm/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Observed behavior contradicts documented or intended behavior
Projects
None yet
Development

No branches or pull requests

2 participants