You read a partly uninitialized struct object to return it, which is (arguably) Undefined Behaviour on the spot, even if the caller doesn't use the return value.
The 16-byte struct is returned in RDX:RAX in the x86-64 System V ABI (any larger and it would be returned by having the caller pass a pointer to a return-value object). GCC is zeroing the uninitialized parts, clang is leaving whatever garbage was there.
GCC loves to break dependencies any time there might be a risk of coupling a false dependency into something. (e.g. pxor xmm0,xmm0
before using the badly-designed cvtsi2sd xmm0, eax
). Clang is more "aggressive" in leaving that out, sometimes even when there's only a tiny code-size benefit for doing so, e.g. using mov al, 1
instead of mov eax,1
, or mov al, [rdi]
instead of movzx eax, byte ptr [rdi]
)
The simplest form of what you're seeing is returning an uninitialized plain int
,
same difference between GCC and clang code-gen:
int foo(){
int x;
return x;
}
(Godbolt)
# clang 11.0.1 -O2
foo:
# leaving EAX unwritten
ret
# GCC 10.2 -O2
foo:
xor eax, eax # return 0
ret
Here clang "gets to" leave out a whole instruction. Of course it's undefined behaviour (reading an uninitialized object), so the standard allows literally anything, including ud2
(guaranteed to raise an illegal instruction exception), or omitting even the ret
on the assumption that this code-path is unreachable, i.e. the function will never be called. Or to return 0xdeadbeef
, or call any other function, if you have a malicious DeathStation 9000 C implementation.