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 中的任何一个上,这个结构将在堆栈上传递,这就是代码中发生的事情——首先 s
在 main
的堆栈框架中构造,然后它的一个副本被压入堆栈(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 参数传递 部分的副标题 返回值 下讨论了这一点,它说:
- 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.
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 中的任何一个上,这个结构将在堆栈上传递,这就是代码中发生的事情——首先 s
在 main
的堆栈框架中构造,然后它的一个副本被压入堆栈(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 参数传递 部分的副标题 返回值 下讨论了这一点,它说:
- 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.