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指定,但这种机制很常见。
许多评论员留下了有用的链接,其中包含调用约定的文档,因此我将其中一些添加到此答案中:
的维基百科文章
Agner Fog 的优化资源合集,包括一个summary of calling conventions (Direct link to 57-page PDF document.)
的 Microsoft 开发人员网络 (MSDN) 文档
下面是一段代码的简单反汇编,示例了您所说的内容
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 位的概念完全相同,并且调用约定几乎相同
(这个问题是针对我机器的架构和调用约定的,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指定,但这种机制很常见。
许多评论员留下了有用的链接,其中包含调用约定的文档,因此我将其中一些添加到此答案中:
- 的维基百科文章
Agner Fog 的优化资源合集,包括一个summary of calling conventions (Direct link to 57-page PDF document.)
- 的 Microsoft 开发人员网络 (MSDN) 文档
下面是一段代码的简单反汇编,示例了您所说的内容
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 位的概念完全相同,并且调用约定几乎相同