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

(这个问题特定于我的机器的架构和调用约定,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函数的反汇编,以及调用它的主函数

主要:

 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然后是主要开始的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][ebp - 28]偏移可能是由结构对齐引起的,所以我猜结构的大小在这里应该是28个字节而不是预期的25个字节。 正如我们在foo的本地myA创建并填充参数和立即值之后在foo中看到的那样,它被复制并重新写入从main传递的myA的地址(这是按值返回的实际含义)

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

[ebp + 8]main::myA的地址存储的地方(内存地址向上,因此ebp + old 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 

将该值放在edx中foo::myA.c的地址中,并将该值放在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] 

这基本上certificate了当用val返回结构时,不能作为参数放置,所发生的是返回值应该驻留的地址作为参数传递给函数并且在被称为函数的函数内。返回的struct的值被复制到作为参数传递的地址中…

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

编辑

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

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

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

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

  • 关于x86调用约定的维基百科文章

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

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

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