【问题标题】:How C structures get passed to function in assembly?C 结构如何在汇编中传递给函数?
【发布时间】:2019-09-03 07:13:48
【问题描述】:

1) C 结构如何在汇编中传递给函数。我的意思是按值传递,而不是按引用传递。 2)顺便说一句,被调用者如何将结构返回给调用者? 由于我的母语不是英语,所以我很抱歉表达不佳。

我编写了一个简单的程序来证明 C 结构是如何传递给函数的。但结果非常令人惊讶。一些值是通过寄存器传递的,但一些值是通过将它们推入堆栈来传递的。这是代码。

源代码

#include <stdio.h>

typedef struct {
        int age;
        enum {Man, Woman} gen;
        double height;
        int class;
        char *name;
} student;

void print_student_info(student s) {
        printf("age: %d, gen: %s, height: %f, name: %s\n", 
                        s.age,
                        s.gen == Man? "Man":"Woman",
                        s.height, s.name);
}

int main() {
        student s;
        s.age = 10;
        s.gen = Man;
        s.height = 1.30;
        s.class = 3;
        s.name = "Tom";
        print_student_info(s);
        return 0;
}

asm

 6fa:   55                      push   %rbp
 6fb:   48 89 e5                mov    %rsp,%rbp
 6fe:   48 83 ec 20             sub    $0x20,%rsp
 702:   c7 45 e0 0a 00 00 00    movl   $0xa,-0x20(%rbp)
 709:   c7 45 e4 00 00 00 00    movl   $0x0,-0x1c(%rbp)
 710:   f2 0f 10 05 00 01 00    movsd  0x100(%rip),%xmm0        # 818 <_IO_stdin_used+0x48>
 717:   00 
 718:   f2 0f 11 45 e8          movsd  %xmm0,-0x18(%rbp)
 71d:   c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%rbp)
 724:   48 8d 05 e5 00 00 00    lea    0xe5(%rip),%rax        # 810 <_IO_stdin_used+0x40>
 72b:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 72f:   ff 75 f8                pushq  -0x8(%rbp)
 732:   ff 75 f0                pushq  -0x10(%rbp)
 735:   ff 75 e8                pushq  -0x18(%rbp)
 738:   ff 75 e0                pushq  -0x20(%rbp)
 73b:   e8 70 ff ff ff          callq  6b0 <print_student_info>
 740:   48 83 c4 20             add    $0x20,%rsp
 744:   b8 00 00 00 00          mov    $0x0,%eax
 749:   c9                      leaveq 
 74a:   c3                      retq   
 74b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)   

我预计结构是使用堆栈传递给函数的,但上面的代码显示它不是。

【问题讨论】:

  • 一般来说,一旦编译的结构就不再存在了。存在的是一个内存区域,并为结构的成员偏移到该内存中。如果没有打包(省略成员之间的填充),您可以确定的唯一偏移量是第一个成员,它将始终具有偏移量0。结构如何传递给函数是编译器之间可能不同的实现细节。
  • 如果我很笨,请原谅我(自从我从事汇编工作很久以来)-但是如果不使用堆栈,那 4 个pushq 指令在做什么?
  • @Adrian 我也想知道同样的事情。我见过在 ARM 的寄存器中传递的结构,但这些结构通常要小得多,最多 8 个字节。在这里,在我看来,结构确实是使用堆栈传递的,但我可能弄错了!
  • 调用约定由 ABI 为特定系统指定,它应该提到结构。如果不考虑特定的 CPU 和编译器,讨论的意义不大。此外,首先,按值传递结构被认为是非常糟糕的做法。
  • @MichaelPetch 是的。根据程序集标签的标签用法,我们应该始终为目标 ISA 添加一个标签。

标签: c gcc assembly x86-64 calling-convention


【解决方案1】:

正如其他人所指出的 - 在大多数情况下通常不赞成按值传递结构,但 C 语言仍然允许这样做。我会讨论你确实使用的代码,即使我不会这样做。


结构的传递方式取决于 ABI / 调用约定。目前有两个主要的 64 位 ABI 正在使用(可能还有其他)。 64-bit Microsoft ABIx86-64 System V ABI。 64 位 Microsoft ABI 很简单,因为所有按值传递的结构都在堆栈上。在 x86-64 System V ABI(Linux/MacOS/BSD 使用)中更复杂,因为有一个递归算法用于确定是否可以在通用寄存器/向量寄存器/X87 FPU 的组合中传递结构堆栈寄存器。如果它确定结构可以在寄存器中传递,则该对象不会出于调用函数的目的而放置在堆栈上。如果根据规则它不适合寄存器,则它会在堆栈上的内存中传递。

有一个迹象表明您的代码没有使用 64 位 Microsoft ABI,因为在进行函数调用之前编译器没有保留 32 字节的影子空间,因此这几乎可以肯定是针对 x86- 的编译器64 系统 V ABI。我可以使用在线godbolt 编译器和禁用优化的GCC 编译器在您的问题中生成same assembly code

通过algorithm for passing aggregate types(如结构和联合)超出了此答案的范围,但您可以参考3.2.3 参数传递部分,但我可以说这个结构已通过由于后清理规则说:

如果聚合的大小超过两个 8 字节,并且第一个 8 字节不是 SSE 或任何其他 8 字节不是 SSEUP,则整个参数在内存中传递。

碰巧您的结构会尝试将前两个 32 位 int 值打包在 64 位寄存器中,然后将 double 放在向量寄存器中,然后将 int 放在在一个 64 位寄存器中(由于对齐规则),并且指针在另一个 64 位寄存器中传递。您的结构将超过两个八字节(64 位)寄存器,而第一个八字节(64 位)寄存器不是 SSE 寄存器,因此编译器将结构传递到堆栈上。

您有未优化的代码,但我们可以将代码分解成块。首先是构建堆栈框架并为局部变量分配空间。如果不启用优化(这里就是这种情况),结构变量 s 将在堆栈上构建,然后该结构的副本将被推入堆栈以调用 print_student_info

这会构建堆栈帧并为局部变量分配 32 字节 (0x20)(并保持 16 字节对齐)。在这种情况下,natural alignment rules 后面的结构恰好是 32 字节:

 6fa:   55                      push   %rbp
 6fb:   48 89 e5                mov    %rsp,%rbp
 6fe:   48 83 ec 20             sub    $0x20,%rsp

您的变量 s 将从 RBP-0x20 开始并在 RBP-0x01(包括)结束。代码构建并初始化堆栈上的s 变量(student 结构)。 age 字段的 32 位 int 0xa (10) 放置在结构的开头 RBP-0x20。 Man 的 32 位枚举位于 RBP-0x1c 的 gen 字段中:

 702:   c7 45 e0 0a 00 00 00    movl   $0xa,-0x20(%rbp)
 709:   c7 45 e4 00 00 00 00    movl   $0x0,-0x1c(%rbp)

常量值 1.30(double 类型)由编译器存储在内存中。您不能在 Intel x86 处理器上使用一条指令从内存移动到内存,因此编译器将双精度值 1.30 从内存位置 RIP+0x100 移动到向量寄存器 XMM0 然后移动 XMM0 的低 64 位em>XMM0 到 RBP-0x18 堆栈上的 height 字段:

 710:   f2 0f 10 05 00 01 00    movsd  0x100(%rip),%xmm0        # 818 <_IO_stdin_used+0x48>
 717:   00 
 718:   f2 0f 11 45 e8          movsd  %xmm0,-0x18(%rbp)

值 3 被放置在 RBP-0x10 处的 class 字段的堆栈中:

 71d:   c7 45 f0 03 00 00 00    movl   $0x3,-0x10(%rbp)

最后将字符串Tom(在程序的只读数据部分)的64位地址加载到RAX中,然后最终移入堆栈上的name字段在 RBP-0x08。尽管 class 的类型只有 32 位(int 类型),但它被填充为 8 个字节,因为以下字段 name 必须在 8 字节边界上自然对齐,因为指针的大小为 8 字节.

 724:   48 8d 05 e5 00 00 00    lea    0xe5(%rip),%rax        # 810 <_IO_stdin_used+0x40>
 72b:   48 89 45 f8             mov    %rax,-0x8(%rbp)

此时,我们有一个完全建立在堆栈上的结构。然后编译器通过将结构的所有 32 个字节(使用 4 个 64 位推送)压入堆栈来复制它以进行函数调用:

 72f:   ff 75 f8                pushq  -0x8(%rbp)
 732:   ff 75 f0                pushq  -0x10(%rbp)
 735:   ff 75 e8                pushq  -0x18(%rbp)
 738:   ff 75 e0                pushq  -0x20(%rbp)
 73b:   e8 70 ff ff ff          callq  6b0 <print_student_info>

然后是典型的堆栈清理和函数尾声:

 740:   48 83 c4 20             add    $0x20,%rsp
 744:   b8 00 00 00 00          mov    $0x0,%eax
 749:   c9                      leaveq 

重要提示:在这种情况下,使用的寄存器不是为了传递参数,而是在堆栈上初始化s 变量(结构)的代码的一部分。 p>


返回结构

这也取决于 ABI,但在这种情况下我将重点关注 x86-64 System V ABI,因为您的代码正在使用它。

通过引用:在RAX 中返回一个指向结构的指针。首选返回指向结构的指针。

按值C 中按值返回的结构强制编译器为调用者中的返回结构分配额外空间,然后分配该结构的地址作为 RDI 中隐藏的第一个参数传递给函数。被调用的函数将把 RDI 中作为参数传递的地址放入 RAX 作为完成时的返回值。从函数返回时,RAX 中的值是指向存储返回结构的地址的指针,该地址始终与隐藏的第一个参数 RDI 中传递的地址相同。 ABI 在 3.2.3 参数传递 部分的子标题 Returning of Values 下讨论了这一点:

  1. 如果该类型具有类 MEMORY,则调用者为返回提供空间 值并在 %rdi 中传递此存储的地址,就好像它是第一个一样 函数的参数。实际上,这个地址变成了一个“隐藏的”第一个参数。此存储不得与被调用方可见的任何数据重叠 此参数以外的其他名称。 返回时 %rax 将包含由 %rdi 中的调用者。

【讨论】:

【解决方案2】:

这取决于您系统的 ABI。在 x86_64 上,大多数系统使用SYSV ABI fo AMD64——微软例外,它使用their own non-standard ABI

在这些 ABI 中的任何一个上,这个结构都将在堆栈上传递,这就是代码中发生的事情——首先smain 的堆栈框架中构造,然后它的副本被推送在堆栈上(4 个 pushq 指令)。

【讨论】:

  • 你的回答说“这个结构将在堆栈上传递”,但我建议你强调 这个 结构,因为这个问题一般都询问结构。其他的,特别是小的结构,可以在寄存器中传递。您链接到的 SYSV 文档在第 18-20 页讨论了这一点。此外,该问题询问有关返回结构的问题,此答案未涵盖。
【解决方案3】:

您的问题没有一般的答案 - 每个编译器的工作方式都不同,并且可以根据您选择的优化进行不同的处理。您观察到的是一种常见的优化 - 合适类型的前几个参数在寄存器中传递,额外和/或复杂的参数在堆栈中传递。

【讨论】:

  • 此代码未优化。使用的寄存器用于在堆栈上构建s 变量(学生结构)。在此代码的情况下,根本没有使用寄存器来传递参数。然后将在堆栈上创建的整个结构复制到堆栈上,作为参数传递给函数。
猜你喜欢
  • 1970-01-01
  • 2015-01-13
  • 1970-01-01
  • 2021-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-06
相关资源
最近更新 更多