C/C++在引擎盖下按值返回struct

Mon*_*nad 3 c c++ assembly win64 x86-64

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

我不记得我在哪里读过这个,或者我是否正确地回忆过它,但我听说过,当一个函数应该按值返回一些结构或对象时,它会填充它rax(如果对象可以适合在寄存器宽度为64位)或传递一个指针,指向结果对象将在哪里(我猜测在调用函数的堆栈帧中分配)rcx,在那里它将执行所有通常的初始化,然后a mov rax, rcx为返回行程.就是这样的

extern some_struct create_it(); // implemented in assembly
Run Code Online (Sandbox Code Playgroud)

真的会有一个秘密的参数

extern some_struct create_it(some_struct* secret_param_pointing_to_where_i_will_be);
Run Code Online (Sandbox Code Playgroud)


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

ric*_*ici 5

这是完全正确的.调用者传递一个额外的参数,该参数是返回值的地址.通常它会在调用者的堆栈框架上,但没有任何保证.

精确的机制由平台ABI指定,但这种机制非常普遍.

各种评论员都为调用约定留下了有用的链接,因此我将其中的一些内容提升到这个答案中:


DrP*_*tay 5

这是对代码的简单反汇编,例如你所说的

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;
}
Run Code Online (Sandbox Code Playgroud)

这里是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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

现在让我们来看看刚刚发生的事情,所以当调用foo时,参数通过以下方式传递,9是最高地址,然后是5然后是主要开始的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
Run Code Online (Sandbox Code Playgroud)

在堆栈框架内存foo在一些局部myA,因为堆栈向下,最低地址myA开始[ebp - 28],-28偏移可能是由结构对齐引起的,所以我猜测结构的大小应该是28这里的字节数不是预期的25.并且我们可以看到在foo后当地myAfoo创建和填充参数和立即值,它被复制并重新写入的地址myA从传递main(这是价值回归的实际意义)

mov     eax, [ebp+8]
mov     edx, [ebp-28]
Run Code Online (Sandbox Code Playgroud)

[ebp + 8]main::myA存储地址的地方(内存地址向上,因此ebp +旧ebp(4字节)+返回地址(4字节))在整个ebp + 8处到达第一个字节main::myA,如前所述foo::myA存储在[ebp-28]堆栈中向下走

mov     [eax], edx     
Run Code Online (Sandbox Code Playgroud)

放置foo::myA.b在第一数据成员的地址的main::myAmain::myA.b

mov     edx, [ebp-24]
mov     [eax+4], edx
Run Code Online (Sandbox Code Playgroud)

放置驻留在foo::myA.cedx 地址中的值,并将该值放在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]
Run Code Online (Sandbox Code Playgroud)

这基本上证明了当用val返回结构时,无法作为参数放置,所发生的是返回值应该驻留的地址作为参数传递给函数并在函数内被调用返回的struct的值被复制到作为参数传递的地址中...

希望这个例子帮助你想象一下发动机罩下发生了什么更好:)

编辑

我希望您注意到我的示例是使用32位汇编程序而且我知道您已经询问了x86-64,但我目前无法在64位计算机上反汇编代码,所以我希望您接受我的意见64位和32位的概念完全相同,并且调用约定几乎相同