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)
};
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.
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};
foo(s);
; 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
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.)