在 SystemV ABI 中返回一个结构

Returning a struct in SystemV ABI

SystemV ABI 中只定义了 2 个 return 寄存器:raxrdx,但是 structs 的大小可以远远超过 16 个字节并且有超过2名成员。所以我考虑了以下示例:

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;
}

这些函数的 O3 编译代码与 gcc 看起来几乎相同:

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.

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.

因此 get_struct 函数签名被静默修改为接受指向结构的指针和 return 该指针。

问题: 在示例函数中 returning 结构 returning 背后的原因是什么作为第一个参数传递的指针和在这两种情况下都相似的第一个 lea rdx,[rip+0x2f6] ?这种用法在 ABI 中是标准化的还是依赖于编译器?

lea rdx,[rip+0x2f6] 似乎代表 char * 的加载,但它的装配对我来说有点混乱,因为它使用 rip with disposition(我猜这是在 rodata 部分显示元素地址的问题。)

如果结构包含 2 个可以放入寄存器的成员,我们可以看到 raxrdx 的预期用法。

返回在 rdi 中传递的指针(一个隐藏指针)对于调用者来说方便
被调用者不能 return 寄存器中的整个结构,只能 return 内存中的结构。
然而,被调用者不能为结构分配缓冲区,因为这不仅效率低下,而且从所有权的角度来看也是有问题的(调用者如何释放一个缓冲区,它不知道如何分配?),所以它只能 return调用者给的指针。
如果兼容(例如 f(g()))并且编译器已经知道如何处理函数 returning 指向结构的指针(即,没有任何特殊操作),这对于将值传递给其他函数也很有用。

隐藏指针的使用,以及 return 在 rax 中的使用,都记录在 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.

lea rax,[rip+0x2d6] 只是指向 "abc" 的指针,这就是 PIE(不要与 PIC 混淆)访问其数据(只读或非只读)所必须做的。

最后:

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.

IMO 的措辞并非 100% 正确,更好的版本是:"the whole argument has class MEMORY"。 但是效果是一样的:小于16B的struct可以passed并且returned in the registers.

Here in practice.