C/C++ 在幕后按值返回结构

C/C++ returning struct by value under the hood

(这个问题是针对我机器的架构和调用约定的,Windows x86_64)

我不记得我在哪里读过这篇文章,或者我是否记得正确,但我听说,当一个函数应该 return 一些结构或对象的值时,它要么将其填充到 rax 中(如果该对象可以容纳 64 位的寄存器宽度)或传递一个指向结果对象所在位置的指针(我猜是在调用函数的堆栈帧中分配的)在 rcx,它会在那里进行所有通常的初始化,然后是 return 行程的 mov rax, rcx。也就是说,像

extern some_struct create_it(); // implemented in assembly

真的会有一个像

这样的秘密参数
extern some_struct create_it(some_struct* secret_param_pointing_to_where_i_will_be);


我的记忆正确还是错误?大对象(即比寄存器宽度更宽)return如何由函数值编辑?

完全正确。调用者传递一个额外的参数,它是 return 值的地址。通常它会在调用者的堆栈帧上,但不能保证。

精确的机制由平台ABI指定,但这种机制很常见。

许多评论员留下了有用的链接,其中包含调用约定的文档,因此我将其中一些添加到此答案中:

下面是一段代码的简单反汇编,示例了您所说的内容

typedef struct 
{
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    char x;
} A;

A foo(int b, int c)
{
    A myA = {b, c, 5, 6, 7, 8, 10};
    return myA; 
}

int main()
{   
    A myA = foo(5,9);   
    return 0;
}

下面是 foo 函数的反汇编,以及调用它的主函数

主要内容:

push    ebp
mov     ebp, esp
and     esp, 0FFFFFFF0h
sub     esp, 30h
call    ___main
lea     eax, [esp+20]        ; placing the addr of myA in eax
mov     dword ptr [esp+8], 9 ; param passing 
mov     dword ptr [esp+4], 5 ; param passing
mov     [esp], eax           ; passing myA addr as a param
call    _foo
mov     eax, 0
leave
retn

foo:

push    ebp
mov     ebp, esp
sub     esp, 20h
mov     eax, [ebp+12]  
mov     [ebp-28], eax
mov     eax, [ebp+16]
mov     [ebp-24], eax
mov     dword ptr [ebp-20], 5
mov     dword ptr [ebp-16], 6
mov     dword ptr [ebp-12], 7
mov     dword ptr [ebp-8], 9
mov     byte ptr [ebp-4], 0Ah
mov     eax, [ebp+8]
mov     edx, [ebp-28]
mov     [eax], edx     
mov     edx, [ebp-24]
mov     [eax+4], edx
mov     edx, [ebp-20]
mov     [eax+8], edx
mov     edx, [ebp-16]
mov     [eax+0Ch], edx
mov     edx, [ebp-12]
mov     [eax+10h], edx
mov     edx, [ebp-8]
mov     [eax+14h], edx
mov     edx, [ebp-4]
mov     [eax+18h], edx
mov     eax, [ebp+8]
leave
retn

现在让我们回顾一下刚刚发生的事情,所以当调用 foo 时,参数是按以下方式传递的,9 是最高地址,然后是 5,然后是 main 中 myA 开始的地址

lea     eax, [esp+20]        ; placing the addr of myA in eax
mov     dword ptr [esp+8], 9 ; param passing 
mov     dword ptr [esp+4], 5 ; param passing
mov     [esp], eax           ; passing myA addr as a param

foo中有一些局部的myA存放在栈帧中,由于栈是向下的,myA的最低地址从[=21=开始],-28 偏移量可能是由结构对齐引起的,所以我猜测结构的大小在这里应该是 28 个字节,而不是预期的 25 个字节。正如我们在 foo 中看到的那样,在 foo 的本地 myA 创建并填充参数和立即值后,它被复制并重新写入 [=19= 的地址]从main传过来(这才是return传值的实际意义)

mov     eax, [ebp+8]
mov     edx, [ebp-28]

[ebp + 8] 是存储 main::myA 地址的地方(内存地址向上因此 ebp + old ebp(4 字节)+ return 地址(4 字节)) ebp + 8 到达 main::myA 的第一个字节,如前所述 foo::myA 随着堆栈向下

存储在 [ebp-28]
mov     [eax], edx     

foo::myA.b放在main::myA的第一个数据成员的地址中,即main::myA.b

mov     edx, [ebp-24]
mov     [eax+4], edx

将驻留在foo::myA.c地址中的值放入edx,并将该值放入main::myA.b地址+4字节即main::myA.c

如您所见,此过程在整个函数中不断重复

mov     edx, [ebp-20]
mov     [eax+8], edx
mov     edx, [ebp-16]
mov     [eax+0Ch], edx
mov     edx, [ebp-12]
mov     [eax+10h], edx
mov     edx, [ebp-8]
mov     [eax+14h], edx
mov     edx, [ebp-4]
mov     [eax+18h], edx
mov     eax, [ebp+8]

这基本上证明了当 return 通过 val 构造一个结构时,它不能作为参数放入,发生的情况是 return 值应该驻留的地址是作为参数传递给函数,在被调用的函数中,returned 结构的值被复制到作为参数传递的地址中...

希望这个例子能帮助您更好地想象引擎盖下发生的事情:)

编辑

我希望您注意到我的示例使用的是 32 位汇编器,并且我知道您询问过有关 x86-64 的问题,但我目前无法反汇编在 64 位机器上编写代码,所以我希望你相信我的话,64 位和 32 位的概念完全相同,并且调用约定几乎相同