C/ c++在底层按值返回结构体

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

本文关键字:返回 结构体 在底层 c++      更新时间:2023-10-16

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

我完全不记得我读过这个,或者如果我回忆它正确,但我曾听说,当一个函数应该返回一些结构或对象的值,它要么东西在rax(如果该对象可以在64位的寄存器的宽度)或被传递到生成的对象的指针会(我猜分配在调用函数的堆栈帧)在rcx,哪里会做所有常见的初始化,然后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);


我的记忆是正确的,还是我错了?如何从函数的值返回大对象(即比寄存器宽度宽)?

下面是一个简单的代码反汇编示例

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函数的反汇编,以及调用它的main函数

主:

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的最低地址开始于[ebp - 28], -28偏移量可能是由结构体对齐引起的,所以我猜结构体的大小应该是28字节,而不是预期的25。从foo中我们可以看到,在创建了foo的本地myA并填充了参数和直接值之后,它被复制并重写到从main传递过来的myA的地址中(这就是按值返回的实际含义)

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

[ebp + 8]main::myA的地址存储的地方(内存地址向上,因此ebp +旧ebp(4字节)+返回地址(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]

这基本上证明了当通过val返回一个结构体时,它不能作为参数放置,所发生的是返回值应该驻留的地址作为参数传递给函数,并且在被调用的函数中,返回结构体的值被复制到作为参数传递的地址中…

希望这个例子能帮助你更好地理解下面发生的事情:)

编辑

我希望你已经注意到我的例子是使用32位汇编器和我知道你已经问过关于x86-64,但我目前无法在64位机器上反汇编代码,所以我希望你相信我的话,这个概念对于64位和32位是完全相同的,调用约定几乎是相同的

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

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

许多评论者都留下了一些有用的调用约定文档链接,所以我将其中的一些提升到这个答案中:

  • Wikipedia关于x86调用约定的文章

  • Agner Fog的优化资源集合,包括调用约定的摘要(直接链接到57页的PDF文档)

  • Microsoft Developer Network (MSDN)调用约定文档。

  • StackOverflow x86标签wiki有很多有用的链接。