C语言函数入参和返回值是结构体时的汇编分析

时间:2021-12-07 01:01:01

在C语言程序中,一般不会直接传一个结构体给一个函数,也不会让函数的返回值直接返回一个结构体,这样会拷贝过多影响效率。但是这样也是合法的,有时候也会使用,并且有时候效率也并不会变得太差。

  • C函数传参:参数少或者传入的结构体小只借助寄存器即可,否则借助栈。
  • C函数返回值:如果返回一个比较小的结构体,借助寄存器即可,否则依旧借助栈。按调用约定,当返回值是较大的结构体时,会在caller栈里产生一个临时变量,并将其首地址传给callee,callee返回值会修改此变量做到将返回值返回给caller。

看一个例子, x64的Linux系统下:

#include <stdio.h>

struct Cord {
int x;
int y;
};

struct Cord add(struct Cord b)
{
b.x++;
b.y++;
return b;
}

int main()
{
struct Cord a = {2, 5};
struct Cord re = add(a);
printf("re (%d, %d)\n", re.x, re.y);
return 0;
}

对于上面的代码,我们从汇编角度看一下如何实现结构体做函数入参和返回值的:

(gdb) disass main
Dump of assembler code for function main:
0x000000000040054d <+0>: push %rbp
0x000000000040054e <+1>: mov %rsp,%rbp
0x0000000000400551 <+4>: sub $0x20,%rsp
0x0000000000400555 <+8>: movl $0x2,-0x20(%rbp)
0x000000000040055c <+15>: movl $0x5,-0x1c(%rbp)
0x0000000000400563 <+22>: mov -0x20(%rbp),%rax //结构体8字节,从首地址拷贝,直接拷贝到 %rax
0x0000000000400567 <+26>: mov %rax,%rdi //调用约定 %rdi, 可直接把这个8字节的结构体传入
0x000000000040056a <+29>: callq 0x40052d <add> // 打断点 1,看后面分析
0x000000000040056f <+34>: mov %rax,-0x10(%rbp)
0x0000000000400573 <+38>: mov -0xc(%rbp),%edx
0x0000000000400576 <+41>: mov -0x10(%rbp),%eax
0x0000000000400579 <+44>: mov %eax,%esi
0x000000000040057b <+46>: mov $0x400624,%edi
0x0000000000400580 <+51>: mov $0x0,%eax
0x0000000000400585 <+56>: callq 0x400410 <printf@plt>
0x000000000040058a <+61>: mov $0x0,%eax // 断点2
0x000000000040058f <+66>: leaveq
0x0000000000400590 <+67>: retq
End of assembler dump.
(gdb) disass add
Dump of assembler code for function add:
0x000000000040052d <+0>: push %rbp
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: mov %rdi,-0x10(%rbp)
0x0000000000400535 <+8>: mov -0x10(%rbp),%eax
0x0000000000400538 <+11>: add $0x1,%eax
0x000000000040053b <+14>: mov %eax,-0x10(%rbp)
0x000000000040053e <+17>: mov -0xc(%rbp),%eax
0x0000000000400541 <+20>: add $0x1,%eax
0x0000000000400544 <+23>: mov %eax,-0xc(%rbp)
0x0000000000400547 <+26>: mov -0x10(%rbp),%rax //返回值 %rax可以直接把这个8字节的结构带出
0x000000000040054b <+30>: pop %rbp //打断点2,看后面分析
0x000000000040054c <+31>: retq
End of assembler dump.
(gdb) r
Starting program: /tmp/a.out
Breakpoint 1, 0x000000000040056a in main ()
(gdb) p $edi //可以看出,这个 %rdi 8字节寄存器正好放的是入参结构体
$3 = 2
(gdb) p $rdi >> 32
$4 = 5
Breakpoint 2, 0x000000000040054b in add ()
(gdb) p $eax
$1 = 3
(gdb) p $rax >> 32
$2 = 6
(gdb)

从上面的例子就很容易看出,C程序是如何用结构体作为入参和返回值的,编译后的汇编指令是没有类型概念的,结构体也就是对一块连续的内存的布局的解释而已,结合x64平台的C calling convention,就很好理解这些内容了。