Kev*_*xie 2 c assembly gcc x86-64 calling-convention
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;
}
Run Code Online (Sandbox Code Playgroud)
汇编
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)
Run Code Online (Sandbox Code Playgroud)
我期望使用堆栈将结构传递给函数,但是上面的代码表明事实并非如此。
正如其他人所指出的那样 - 在大多数情况下通常不赞成按值传递结构,但C语言仍然允许这样做。我将讨论您确实使用过的代码,即使我不会这样做。
如何传递结构取决于 ABI/调用约定。目前使用的主要 64 位 ABI 有两个(可能还有其他的)。在64位微软ABI和X86-64 System V的ABI。64 位 Microsoft ABI 很简单,因为所有按值传递的结构都在堆栈上。在 x86-64 System V ABI(由 Linux/MacOS/BSD 使用)更复杂,因为有一种递归算法用于确定是否可以在通用寄存器/向量寄存器/X87 FPU 的组合中传递结构堆栈寄存器。如果它确定可以在寄存器中传递结构,则该对象不会出于调用函数的目的而放置在堆栈中。如果它不符合规则的寄存器,则将其传递到堆栈的内存中。
有一个迹象表明您的代码没有使用 64 位 Microsoft ABI,因为在进行函数调用之前编译器没有保留 32 字节的影子空间,因此这几乎可以肯定是针对 x86-64 System V 的编译器ABI。我可以使用在线 godbolt 编译器和禁用优化的 GCC 编译器在您的问题中生成相同的汇编代码。
通过传递聚合类型(如结构和联合)的算法超出了本答案的范围,但您可以参考第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 字节对齐)。在这种情况下,您的结构恰好是 32 字节大小,遵循自然对齐规则:
6fa: 55 push %rbp
6fb: 48 89 e5 mov %rsp,%rbp
6fe: 48 83 ec 20 sub $0x20,%rsp
Run Code Online (Sandbox Code Playgroud)
您的变量s将从 RBP-0x20 开始,到 RBP-0x01(含)结束。代码在堆栈上构建并初始化s变量 ( studentstruct)。该age字段的 32 位 int 0xa (10)位于结构的开头,位于 RBP-0x20。32 位枚举Man放在genRBP-0x1c 的字段中:
702: c7 45 e0 0a 00 00 00 movl $0xa,-0x20(%rbp)
709: c7 45 e4 00 00 00 00 movl $0x0,-0x1c(%rbp)
Run Code Online (Sandbox Code Playgroud)
常量值 1.30(类型double)由编译器存储在内存中。不能从存储器移动到存储器上的Intel x86处理器一个指令,因此编译器从移动存储器位置RIP双值1.30 + 0x100的矢量寄存器XMM0然后移动的低64位XMM0到height在字段堆栈上RBP-0x18:
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)
Run Code Online (Sandbox Code Playgroud)
值 3 被放置class在 RBP-0x10 字段的堆栈中:
71d: c7 45 f0 03 00 00 00 movl $0x3,-0x10(%rbp)
Run Code Online (Sandbox Code Playgroud)
最后,字符串的 64 位地址Tom(在程序的只读数据部分)被加载到RAX 中,然后最后移到name堆栈上的 RBP-0x08 字段中。尽管 for 的类型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)
Run Code Online (Sandbox Code Playgroud)
在这一点上,我们有一个完全建立在堆栈上的结构。然后编译器通过将结构的所有 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>
Run Code Online (Sandbox Code Playgroud)
然后是典型的堆栈清理和函数结语:
740: 48 83 c4 20 add $0x20,%rsp
744: b8 00 00 00 00 mov $0x0,%eax
749: c9 leaveq
Run Code Online (Sandbox Code Playgroud)
重要说明:在这种情况下,使用的寄存器不是为了传递参数,而是初始化s堆栈上的变量(结构)的代码的一部分。
这也取决于 ABI,但在这种情况下,我将重点关注 x86-64 System V ABI,因为这是您的代码所使用的。
通过引用:在RAX 中返回指向结构的指针。返回指向结构的指针是首选。
按值:C中按值返回的结构强制编译器为调用者中的返回结构分配额外的空间,然后将该结构的地址作为RDI 中隐藏的第一个参数传递给函数。被调用的函数将在RDI 中作为参数传递的地址在完成时作为返回值放入RAX。从函数返回时,RAX 中的值是指向存储返回结构的地址的指针,该地址始终与在隐藏的第一个参数RDI 中传递的地址相同。ABI在小标题下的第3.2.3节参数传递中对此进行了讨论价值的回归说:
- 如果类型具有类 MEMORY,则调用者为返回值提供空间并在 %rdi 中传递此存储的地址,就好像它是函数的第一个参数一样。实际上,这个地址变成了一个“隐藏的”第一个参数。此存储不得与通过此参数以外的其他名称对被调用者可见的任何数据重叠。返回时 %rax 将包含调用者在 %rdi 中传入的地址。
这取决于您系统的ABI。在x86_64上,大多数系统使用AMD64的SYSV ABI -Microsoft除外,他们使用自己的非标准ABI。
在这两个ABI中的任何一个上,此结构都将在代码中传递给堆栈-首先s在main的堆栈框架中构造,然后将其副本推入堆栈(4个pushq指令) 。