Intel x86 Assembly Language & Microarchitecture Calling Conventions 32-bit, cdecl — Dealing with Structs



Remember, members of a struct are usually padded to ensure they are aligned on their natural boundary:

struct t
    int a, b, c, d;    // a is at offset 0, b at 4, c at 8, d at 0ch
    char e;            // e is at 10h
    short f;           // f is at 12h (naturally aligned)
    long g;            // g is at 14h
    char h;            // h is at 18h
    long i;            // i is at 1ch (naturally aligned)

As parameters (pass by reference)

When passed by reference, a pointer to the struct in memory is passed as the first argument on the stack. This is equivalent to passing a natural-sized (32-bit) integer value; see 32-bit cdecl for specifics.

As parameters (pass by value)

When passed by value, structs are entirely copied on the stack, respecting the original memory layout (i.e., the first member will be at the lower address).

int __attribute__((cdecl)) foo(struct t a);

struct t s = {0, -1, 2, -3, -4, 5, -6, 7, -8};
; Assembly call

push DWORD 0fffffff8h    ; i (-8)
push DWORD 0badbad07h    ; h (7), pushed as DWORD to naturally align i, upper bytes can be garbage
push DWORD 0fffffffah    ; g (-6)
push WORD 5              ; f (5)
push WORD 033fch         ; e (-4), pushed as WORD to naturally align f, upper byte can be garbage
push DWORD 0fffffffdh    ; d (-3)
push DWORD 2             ; c (2)
push DWORD 0ffffffffh    ; b (-1)
push DWORD 0             ; a (0)
call foo
add esp, 20h

As return value

Unless they are trivial1, structs are copied into a caller-supplied buffer before returning. This is equivalent to having an hidden first parameter struct S *retval (where struct S is the type of the struct).

The function must return with this pointer to the return value in eax; The caller is allowed to depend on eax holding the pointer to the return value, which it pushed right before the call.

struct S
    unsigned char a, b, c;

struct S foo();         // compiled as struct S* foo(struct S* _out)

The hidden parameter is not added to the parameter count for the purposes of stack clean-up, since it must be handled by the callee.

sub esp, 04h        ; allocate space for the struct

; call to foo
push esp            ; pointer to the output buffer
call foo
add esp, 00h        ; still as no parameters have been passed

In the example above, the structure will be saved at the top of the stack.

struct S foo()
    struct S s;
    s.a = 1; s.b = -2; s.c = 3;
    return s;
; Assembly code
push ebx
mov eax, DWORD PTR [esp+08h]   ; access hidden parameter, it is a pointer to a buffer
mov ebx, 03fe01h               ; struct value, can be held in a register
mov DWORD [eax], ebx           ; copy the structure into the output buffer 
pop ebx
ret 04h                        ; remove the hidden parameter from the stack
                               ; EAX = pointer to the output buffer

1 A "trivial" struct is one that contains only one member of a non-struct, non-array type (up to 32 bits in size). For such structs, the value of that member is simply returned in the eax register. (This behavior has been observed with GCC targeting Linux)

The Windows version of cdecl is different from the System V ABI's calling convention: A "trivial" struct is allowed to contain up to two members of a non-struct, non-array type (up to 32 bits in size). These values are returned in eax and edx, just like a 64-bit integer would be. (This behavior has been observed for MSVC and Clang targeting Win32.)