在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,就很好理解这些内容了。

Logo

更多推荐