St.Antario
St.Antario

Reputation: 27385

Returning a struct in SystemV ABI

There are only 2 return registers defined in SystemV ABI: rax and rdx, but structs can have a size much more than 16 bytes and have more than 2 members. So I considered the following example:

struct test{
    unsigned long a;
    char *b;
    unsigned long c;
};

struct test get_struct(void){
    return (struct test){.a = 10, .b = "abc", .c = 20};
}

void get_struct2(struct test *tst){
    struct test tmp = {.a = 10, .b = "abc", .c = 20};
    *tst = tmp;
}

The O3 compiled code with gcc for these functions looks almost identically:

Dump of assembler code for function get_struct:
   0x0000000000000820 <+0>:     lea    rdx,[rip+0x2f6]        # 0xb1d
   0x0000000000000827 <+7>:     mov    rax,rdi
   0x000000000000082a <+10>:    mov    QWORD PTR [rdi],0xa
   0x0000000000000831 <+17>:    mov    QWORD PTR [rdi+0x10],0x14
   0x0000000000000839 <+25>:    mov    QWORD PTR [rdi+0x8],rdx
   0x000000000000083d <+29>:    ret    
End of assembler dump.

and

Dump of assembler code for function get_struct2:
   0x0000000000000840 <+0>:     lea    rax,[rip+0x2d6]        # 0xb1d
   0x0000000000000847 <+7>:     mov    QWORD PTR [rdi],0xa
   0x000000000000084e <+14>:    mov    QWORD PTR [rdi+0x10],0x14
   0x0000000000000856 <+22>:    mov    QWORD PTR [rdi+0x8],rax
   0x000000000000085a <+26>:    ret    
End of assembler dump.

So the get_struct function signature was silently modified to accept a pointer to the struct and return that pointer.

QUESTION: In the example function returning the struct what is the reason behind returning the pointer passed as the first argument and the very first lea rdx,[rip+0x2f6] that is similar in both of the cases? Is such usage standardized in the ABI or it is compiler dependent?

The lea rdx,[rip+0x2f6] seems to stand for the loading of the char *, but its assembly looks a bit confusing to me since it uses the rip with disposition (I guess this is the issue with displaying the address of elements in the rodata section.)

In case if the struct contains 2 members that can be put in registers we can see the expected usage of rax and rdx.

Upvotes: 4

Views: 1799

Answers (1)

Margaret Bloom
Margaret Bloom

Reputation: 44066

Returning the pointer passed in rdi (an hidden pointer) is a convenience for the caller.
The callee, not being able to return the whole struct in the registers can only return the struct in memory.
However the callee cannot allocate a buffer for the struct as that would be not only inefficient but also problematic from the ownership point of view (how can the caller free a buffer it doesn't know how is allocated?), so it can only return the pointer given by the caller.
This is also useful to pass the value to other functions if compatible (e.g. f(g())) and the compiler already know how to deal with functions returning pointers to structs (namely, without any special action).

The use of the hidden pointer, as well as returning it in rax, is documented in the ABI:

Returning of Values

The returning of values is done according to the following algorithm:

  1. Classify the return type with the classification algorithm.
  2. If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function.
    In effect, this address becomes a “hidden” first argument. This storage must not overlap any data visible to the callee through other names than this argument.
    On return %rax will contain the address that has been passed in by the caller in %rdi.

The lea rax,[rip+0x2d6] is just the pointer to "abc", that's what PIEs (to not be confused with PIC) have to do to access their data (read-only or not).

Finally:

If the size of the aggregate exceeds two eightbytes and the first eight- byte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory.

The wording is not 100% correct IMO, a better version would be: "the whole argument has class MEMORY". But the effect is the same: a struct smaller than 16B can be passed and returned in the registers.

Here in practice.

Upvotes: 8

Related Questions