I would like to understand historical roots of the quirk in the SysV calling convention for the 32-bit x86, which was inherited by the ELF standard, and so remains used on Linux to this day. Consider this example:
struct S {
int i;
};
struct S f(int a)
{
struct S r = { a };
return r;
}
Optimized assembly output for i386 ELF target will be, in Intel syntax:
f:
mov eax, DWORD PTR [esp+4]
mov edx, DWORD PTR [esp+8]
mov DWORD PTR [eax], edx
ret 4
The return address of the function is at [esp]
, and the argument a
is at [esp+8]
, which is retrieved into edx
by the second instruction. The first instruction reads the 'hidden' extra argument at [esp+4]
into eax
.
The hidden argument is set up by the caller to indicate where the returned struct must be stored (it will be a location on the caller's stack). The third instruction writes a
into that location, and the last instruction, ret 4
, returns to the caller and also pops the hidden argument off the stack (i.e. it pops two words, the return address, and the extra argument, increasing esp
by 8 in total). This is the only instance where the use of return-with-immediate instruction is required by the i386 ELF psABI.
Essentially, the function is compiled as if it was internally transformed into
struct S *f(struct S *r, int a)
{
r->i = a;
return r;
}
... except for the automagic popping of the implicit first argument.
Before Linux adopted ELF, GCC's original calling convention (now known as the a.out
format) used a more natural approach where the caller was responsible for popping the implicit argument together with the rest, and short structures like in this example would be returned on registers. The calling convention for 64-bit x86 is efficient as well.
So, modern GCC ended up with an inefficient convention for struct return on 32-bit x86 because that's what was set in stone by the i386 processor supplement of the ELF standard. And the psABI document, presumably, documented how one of the important UNIX compilers of that time behaved. Was that the AT&T compiler? And why did it behave that way? What is the lineage of the compiler(s) that influenced the ELF standardization?
My speculation: it was not a deliberate decision, the i386 port of the AT&T compiler inherited that quirk from an older port, and nobody bothered to correct it. And that older port, in turn, evolved in a way where initially structures were returned via static locations rather than on stack, and the trick with popping a hidden argument allowed to bridge old and new code when the stack-based returning was implemented.
struct
is returned, it ends up being put to the memory anyway -- no matter whether it is done by caller or callee. Here the callee does that, in amd64 the caller would do.ret
andret n
take exactly the same number of cycles. Also note that in the case where there are no other arguments, the caller will not need to adjust the stack, so it is more efficient in this instance.ret
vs.ret 4
, compared to returning in EDX:EAX (or just EAX if narrow enough), as your example demonstrates, especially for the common case where the member(s) are 32-bit. x86-64 SysV returns structs up to 16 bytes in RDX:RAX, ready for the caller to store if it wants, or unpack with shifts. (Unfortunately x86 doesn't have very good bit-manipulation, not able to shift-and-mask in a single instruction to extract a bitfield, so it can get inefficient with multiple small members.)