【问题标题】:How does C return a structure? [duplicate]C如何返回一个结构? [复制]
【发布时间】:2011-07-18 09:50:55
【问题描述】:
(gdb) disas func
Dump of assembler code for function func:
0x00000000004004b8 <func+0>:    push   %rbp
0x00000000004004b9 <func+1>:    mov    %rsp,%rbp
0x00000000004004bc <func+4>:    movl   $0x64,0xfffffffffffffff0(%rbp)
0x00000000004004c3 <func+11>:   movb   $0x61,0xfffffffffffffff4(%rbp)
0x00000000004004c7 <func+15>:   mov    0xfffffffffffffff0(%rbp),%rax
0x00000000004004cb <func+19>:   leaveq
0x00000000004004cc <func+20>:   retq
End of assembler dump.


t_test func()
{
    t_test t;
    t.i = 100;
    t.c = 'a';
    return t;
}

所以看起来它返回的是局部变量t,但是这种工作是否保证可以工作,返回时不应该不引用任何局部变量吗??

【问题讨论】:

  • 你返回的是一个结构,因为你返回的东西的类型是一个结构。但是您实际上返回的是结构的值。所以int f() { ... 表示f 返回的事物类型是intreturn 5; 表示我们正在返回 value 5。类似地,int q = 3; return q; 表示我们正在返回 qvalue,即 3。我们实际上并没有返回q

标签: c struct x86-64 return-value calling-convention


【解决方案1】:

根据我的经验,C 如何返回结构没有标准方法。为了能够传递一个结构,编译器通常(对用户不可见)传递一个指向该结构的指针,函数可以将内容复制到该结构。该指针的传递方式(堆栈上的第一个或最后一个)取决于实现。一些编译器,如 32 位 MSVC++,在 EAX 和 EDX 等寄存器中返回小结构。显然,GCC 在 RAX 中以 64 位模式返回这样的结构。

但是,再一次,没有标准的方法是如何完成的。当使用函数的其余代码也由同一个编译器编译时,这没有问题,但如果该函数是 DLL 或 lib 的导出函数,则会出现问题。当使用来自不同语言(Delphi)或来自 C 和不同编译器的此类函数时,我已经被这个咬了几次。也见this link

【讨论】:

  • 是否有任何链接表明这是依赖于实现的行为?
  • 我知道它依赖于实现。每个实现都有自己的方式来做到这一点。我已经不止一次地与此作斗争,因为找出如何访问从 C 或 Delphi 程序返回结构的函数(在 DLL 中)并不总是那么容易。特别是如果函数在寄存器中返回,几乎没有直接的方法来处理它(汇编程序除外)。
  • @JeRog:您正在寻找的是关于调用约定的文档。在这种情况下,它被指定为 GCC 在为 x86-64 Linux、MacOS 或任何非 Windows 系统编译时所针对的 x86-64 System V ABI 的一部分。
【解决方案2】:

RAX 大到足以容纳整个结构。在 0x00000000004004c7 处,您正在加载整个结构(使用 mov),而不是其地址(您将使用 lea 代替)。

x86-64 System V ABI 的调用约定在 RDX:RAX 或 RAX 中返回最多 16 个字节的 C 结构。 C++ on x86-64: when are structs/classes passed and returned in registers?

对于较大的结构,调用者传递了一个“隐藏的”输出指针 arg。

【讨论】:

  • 如果rax不够大会怎么样?
  • @new_perl 创建一个更大的结构并自己看看。
  • 我猜它会复制堆栈上的结构。试试 ;)
  • @Rudy: movs 可能,但我完全不确定:P
  • 我知道如何复制结构。但是函数如何返回栈上的东西呢?该函数不知道应该将结果分配给局部变量还是全局变量。这就是为什么必须传递指向结构的指针,并且必须将返回值复制到该位置的原因。仅当调用者在堆栈上保留额外的内存并且函数知道这一点时,返回堆栈才有效。然后调用者可以执行分配。我不知道有任何编译器可以这样工作。
【解决方案3】:

返回东西的方式根本不是标准的,但通常在 RAX 中。在您的示例中,假设 t_test::i 和 t_test::c 是 t_test 的唯一成员,并且每个成员最多为 32 位,则整个结构可以放入 64 位寄存器,因此它只是通过 RAX 直接返回值, 并且通常可以放入 2 个寄存器的东西在 RAX:RDX (或 RDX:RAX,我忘记了常见的顺序)中返回。

对于大于两个的寄存器,它通常涉及作为第一个参数传递的隐藏指针参数,该参数指向调用函数中的一个对象(通常是直接获得返回值的对象)。然后在从被调用函数返回之前写入该对象(通常从被调用函数中使用的本地结构复制),并且通常在 RAX 中返回相同的指针。

EAX/EDX 可以在 32 位 x86 系统上替代 RAX/RDX。

对于在堆栈上传递“this”指针的约定(如标准 x86 GCC 约定),返回值指针通常作为隐藏的第二个参数而不是第一个参数传递。

【讨论】:

  • 那么隐藏指针指向被调用函数的栈帧,逻辑上已经过期了?
  • 调用函数为其分配空间(通常它已经是该函数中的本地结构,而不是专门为函数调用分配的东西),然后将其写入在返回之前由 called 函数(返回大结构的那个)。附带说明一下,您提到的内容可能仍然“正常”,因为刚刚返回的函数的堆栈帧尚未被覆盖。
  • 您所描述的对于 Windows 上的某些 C 和 C++ 编译器可能是正确的,但对所有编译器来说甚至都不是正确的。绝对不是这样普遍的(尤其是因为不是每个处理器都有这些寄存器)。
  • 我刚刚在 gcc 上测试了这个。我采用了一个 4 字节的结构。我创建了一个函数并在那里创建了一个局部结构,并将其返回到 main 中创建的结构中。然后查看生成的汇编代码。一切都如你所说。可能编译器使用了 thiscall 约定。
【解决方案4】:

您的原始代码返回的是在函数中创建的结构的副本 - 因为您返回的是结构类型,而不是指向结构的指针。看起来整个结构是用rax 值传递的。一般来说,编译器可以为此生成各种汇编代码,这取决于调用者和被调用者的行为和调用约定。

处理结构的正确方法是将它们用作输出参数:

void func(t_test* t)
{
    t->i = 100;
    t->c = 'a';
}

【讨论】:

  • 其实正确的做法是按值返回。这种方法要求任何阅读代码的人都考虑非局部效应,以便了解正在发生的事情。可重入性和幂等性更容易证明你的程序是正确的。谷歌“价值语义”。
  • @spraff - 我来自嵌入式背景,对我来说按值返回结构是一个坏习惯。对于参数(数组、类等)的任何隐式复制也可以这样说。这主要是由于性能原因,以及由于编译器错误或通用硬件伏都教可能发生的各种错误(蹩脚的借口,但它发生的次数比我关心的要多)。
  • 一些编译器在这个 mannar 中实现了按值返回。关键是,你不应该在这个级别做出决定。按值返回更易于优化,因为可以省略副本并且可能会受到某些其他转换的影响。更喜欢修复你的编译器而不是破坏你的应用程序。
  • @spraff - 我明白你的意思。然而,由于嵌入式应用程序的静态特性(至少在我的领域),更常见的情况是您已经分配了内存并且您希望填充它。对于这种情况,我相信没有什么比通过引用传递更好的了。
  • 很公平。有时好的设计和好的工程不是一回事;-)
【解决方案5】:

堆栈指针在函数开始时没有改变,因此t_test 的分配不是在函数内进行的,因此不会被函数释放。如何处理取决于使用的调用约定。如果你看看函数是如何被调用的,就会更容易看到它是如何完成的。

【讨论】:

  • 我不确定你是否可以说 t_test 没有在函数内部分配。它没有明确分配,但例如Win64 调用约定要求调用者在调用之前(即在调用者内存中)保留一个特殊的内存块来保存寄存器,并且这里似乎使用了类似的东西(RBP-16 是使用的地址)。 IOW,毕竟它可能是本地的,即使它没有从堆栈中显式分配。
  • 为了记录,这是 x86-64 System V,使用红色区域 below RSP。 Windows x64 阴影空间将高于 RSP,但是是的,函数可以以与暂存空间相同的方式使用它。
猜你喜欢
  • 2021-09-09
  • 2015-12-16
  • 1970-01-01
  • 2016-07-02
  • 1970-01-01
  • 1970-01-01
  • 2015-05-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多