C 结构如何传递给汇编中的函数?

How C structures get passed to function in assembly?

1)C 结构如何在汇编中传递给函数。我的意思是按值传递,而不是按引用传递。 2)顺便问一下,如何将calleesreturn结构传给它的callers? 由于我的母语不是英语,因此表达不佳,我感到非常抱歉。

我写了一个简单的程序来证明 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    [=11=]x20,%rsp
 702:   c7 45 e0 0a 00 00 00    movl   [=11=]xa,-0x20(%rbp)
 709:   c7 45 e4 00 00 00 00    movl   [=11=]x0,-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   [=11=]x3,-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    [=11=]x20,%rsp
 744:   b8 00 00 00 00          mov    [=11=]x0,%eax
 749:   c9                      leaveq 
 74a:   c3                      retq   
 74b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)   

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

您的问题没有通用的答案 - 每个编译器的工作方式都不同,并且可以根据您的优化来做不同的事情 select。您观察到的是一种常见的优化 - 合适类型的前几个参数在寄存器中传递,额外的 and/or 复杂的参数在堆栈中传递。

这取决于您系统的 ABI。在 x86_64,大多数系统使用 SYSV ABI fo AMD64 -- the exception being Microsoft, who use their own non-standard ABI

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

正如其他人所指出的那样 - 在大多数情况下,按值传递结构通常不受欢迎,但 C 语言仍然允许这样做。我将讨论您使用的代码,即使它不是我的做法。


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

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

遍历 algorithm for passing aggregate types(如结构和联合)超出了这个答案的范围,但您可以参考 3.2.3 参数传递 部分,但我可以说这个结构是在堆栈上传递的,因为 post 清理规则说:

If the size of the aggregate exceeds two eightbytes and the first eightbyte isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory.

碰巧您的结构会尝试将前两个 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    [=10=]x20,%rsp

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

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

常量值 1.30(类型 double)由编译器存储在内存中。您不能在 Intel x86 处理器上使用一条指令从内存移动到内存,因此编译器将双精度值 1.30 从内存位置 RIP+0x100 移动到向量寄存器 XMM0 然后移动较低的 64- 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)

RBP-0x10:

处的 class 字段的值 3 被放置在堆栈上
 71d:   c7 45 f0 03 00 00 00    movl   [=13=]x3,-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    [=16=]x20,%rsp
 744:   b8 00 00 00 00          mov    [=16=]x0,%eax
 749:   c9                      leaveq 

重要说明:在这种情况下,使用的寄存器不是为了传递参数,而是初始化 s 变量(结构) 在堆栈上。


返回结构

这也取决于 ABI,但在本例中我将重点关注 x86-64 System V ABI,因为这是您的代码所使用的。

参考:指向结构的指针在RAX中被return编辑。返回指向结构的指针是首选。

按值C中按值return的结构强制编译器分配额外的space 用于 t 中的 return 结构e 调用者,然后将该结构的地址作为隐藏的第一个参数传递给 RDI 中的函数。被调用的函数完成后会将RDI作为参数传入的地址放入RAX作为return值。根据函数的 return,RAX 中的值是指向存储 return 结构的地址的指针,该地址始终与首先传递给隐藏层的地址相同参数 RDI。 ABI 在 3.2.3 参数传递 部分的副标题 返回值 下讨论了这一点,它说:

  1. If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument. This storage must not overlap any data visible to the callee through other names than this argument. On return %rax will contain the address that has been passed in by the caller in %rdi.