【问题标题】:C/C++ returning struct by value under the hoodC/C++ 在后台按值返回结构
【发布时间】:2016-08-21 20:19:10
【问题描述】:

(这个问题是针对我机器的架构和调用约定,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);


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

【问题讨论】:

  • 当您需要有关返回内容的信息时,您从示例函数中省略了返回类型。
  • 您似乎在谈论 return value optimization,这是 C++ 特有的。我不知道 C 中有任何此类功能。
  • 这实际上取决于 C++ 编译器,以及您用于构建的优化级别。有很多针对 x86-64 win64 的 C++ 编译器。
  • @mike:不,它是由平台 ABI 定义的。如果允许在优化级别之间变化,您将无法使用外部库。如果编译器之间可能有所不同,则每个编译器都需要不同的库。这就是为什么必须为每个平台定义调用约定的原因,也是存在平台 ABI 的原因。
  • 是的。不仅适用于返回值,也适用于参数。确切的细节非常复杂且记录不充分。 Agner Fog 辛苦了,chapter 7 拥有您想知道的一切以及更多。

标签: c++ c assembly x86-64 win64


【解决方案1】:

这是一个简单的反汇编代码示例,说明你在说什么

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

富:

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 并填充参数和立即值后,它被复制并重新写入从myA 传递的地址main(这个是按值返回的实际含义)

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 位是完全相同的,并且调用约定几乎相同

【讨论】:

  • 使用 gcc.godbolt.org 查看 x86、x86-64、ARM、MIPS、PowerPC 或 AVR 的编译器输出,使用 clang 或 gcc。此外,您可以使用A myA = { b, c, 5, 6, 7, 8, 9, 10} 来缩短示例。并且可能会删除几个结构成员以缩短 asm。只要它大于 128 位,x86-64 SysV 调用约定就不会将其打包到 RDX:RAX 中。
  • 此外,如果您在启用优化的情况下编译 foo,则 asm 会更容易阅读。将其存储到堆栈上的本地,然后将其复制到返回值指针,如果您还不知道它在做什么,那么真的很吵而且很难理解。
  • 最初这段代码在我测试它的反汇编时稍微复杂了一点,我已经快速编辑了它而没有改变它,但你是对的,虽然它更具可读性,不会'在实际的代码开发中不这样做:),我同意你的看法,我只是想证明实际的复制是在幕后发生的@PeterCordes
【解决方案2】:

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

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

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

【讨论】:

  • 更具体地说,请参阅x86 tag wiki 以获取指向 ABI 文档的链接,这些文档指定了何时将结构打包到一个或两个寄存器中或何时将指针作为隐藏的第一个参数传递的规则。对于 32 位和 64 位,ABI 之间的规则有所不同。另见structs in calling conventions on SO docs
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-26
  • 1970-01-01
  • 1970-01-01
  • 2020-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多