x86-64 规定只有6个寄存器来存参数,那 C 函数为什么还能超过6个参数呢?

梦康 2019-09-13 00:00:00 1052

#include <stdio.h>

int test(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10) {
    return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8 + arg9 + arg10;
}

int main() {
    printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    return 0;
}
000000000040052d <test>:
  40052d:    55                       push   %rbp
  40052e:    48 89 e5                 mov    %rsp,%rbp
  400531:    89 7d fc                 mov    %edi,-0x4(%rbp)
  400534:    89 75 f8                 mov    %esi,-0x8(%rbp)
  400537:    89 55 f4                 mov    %edx,-0xc(%rbp)
  40053a:    89 4d f0                 mov    %ecx,-0x10(%rbp)
  40053d:    44 89 45 ec              mov    %r8d,-0x14(%rbp)
  400541:    44 89 4d e8              mov    %r9d,-0x18(%rbp)
  400545:    8b 45 f8                 mov    -0x8(%rbp),%eax
  400548:    8b 55 fc                 mov    -0x4(%rbp),%edx
  40054b:    01 c2                    add    %eax,%edx
  40054d:    8b 45 f4                 mov    -0xc(%rbp),%eax
  400550:    01 c2                    add    %eax,%edx
  400552:    8b 45 f0                 mov    -0x10(%rbp),%eax
  400555:    01 c2                    add    %eax,%edx
  400557:    8b 45 ec                 mov    -0x14(%rbp),%eax
  40055a:    01 c2                    add    %eax,%edx
  40055c:    8b 45 e8                 mov    -0x18(%rbp),%eax
  40055f:    01 c2                    add    %eax,%edx
  400561:    8b 45 10                 mov    0x10(%rbp),%eax
  400564:    01 c2                    add    %eax,%edx
  400566:    8b 45 18                 mov    0x18(%rbp),%eax
  400569:    01 c2                    add    %eax,%edx
  40056b:    8b 45 20                 mov    0x20(%rbp),%eax
  40056e:    01 c2                    add    %eax,%edx
  400570:    8b 45 28                 mov    0x28(%rbp),%eax
  400573:    01 d0                    add    %edx,%eax
  400575:    5d                       pop    %rbp
  400576:    c3                       retq

0000000000400577 <main>:
  400577:    55                       push   %rbp
  400578:    48 89 e5                 mov    %rsp,%rbp
  40057b:    48 83 ec 20              sub    $0x20,%rsp
  40057f:    c7 44 24 18 0a 00 00     movl   $0xa,0x18(%rsp)
  400586:    00
  400587:    c7 44 24 10 09 00 00     movl   $0x9,0x10(%rsp)
  40058e:    00
  40058f:    c7 44 24 08 08 00 00     movl   $0x8,0x8(%rsp)
  400596:    00
  400597:    c7 04 24 07 00 00 00     movl   $0x7,(%rsp)
  40059e:    41 b9 06 00 00 00        mov    $0x6,%r9d
  4005a4:    41 b8 05 00 00 00        mov    $0x5,%r8d
  4005aa:    b9 04 00 00 00           mov    $0x4,%ecx
  4005af:    ba 03 00 00 00           mov    $0x3,%edx
  4005b4:    be 02 00 00 00           mov    $0x2,%esi
  4005b9:    bf 01 00 00 00           mov    $0x1,%edi
  4005be:    e8 6a ff ff ff           callq  40052d <test>
  4005c3:    89 c6                    mov    %eax,%esi
  4005c5:    bf 70 06 40 00           mov    $0x400670,%edi
  4005ca:    b8 00 00 00 00           mov    $0x0,%eax
  4005cf:    e8 3c fe ff ff           callq  400410 <printf@plt>
  4005d4:    b8 00 00 00 00           mov    $0x0,%eax
  4005d9:    c9                       leaveq
  4005da:    c3                       retq
  4005db:    0f 1f 44 00 00           nopl   0x0(%rax,%rax,1)

关于函数的调用栈,https://www.cnblogs.com/clover-toeic/p/3755401.html 这篇文章写得挺好。

push %rbp

把 caller(相对于当前函数的调用函数)的函数栈的栈底的内存地址(存在 rbp 寄存器中)压栈(写入到栈上),注意会占用8字节的内存长度。
因为C程序的入口函数是_start,然后再经过一系列初始化函数,才到main,所以这里main里面也需要执行push %rbp,又比如test函数里面的push %rbp就是main函数的%rbp里记录的地址。
栈是由高位向低位生长,所以此时是在调用函数的rsp 基础上向低位移动,又因为x86-64机器上,地址占8字节,所以push %rbp的背后还有一个隐含的操作就是sub $0x8 %rsp(如果是32机器就是向下移动4字节)。

mov    %rsp,%rbp

把 caller 的栈顶的内存地址(存在 rsp 寄存器中)赋值到 rbp 寄存器。

sub    $0x20,%rsp

rsp再减去0x20,也就是地址再向下移动32个单元(字节),用于main函数局部变量和参数的存储。这里预留少空间,在编译期就计算好了。当前实验是10个参数,如果我们改为13个参数,则会是sub $0x50,%rsp所以生成的汇编代码,在运行之前就已经确定了rsp偏移量

movl   $0xa,0x18(%rsp)
movl   $0x9,0x10(%rsp)
movl   $0x8,0x8(%rsp)
movl   $0x7,(%rsp)

参数的压栈是从右向左的,最后面的参数最先处理,我们知道寄存器里存储函数参数的只有6个,所以还需要使用栈上的空间来存储其他参数。把0xa放在rsp+0x18的地址上,然后把0x9放在rsp+0x10的地址上,以此类推。

mov    $0x6,%r9d
mov    $0x5,%r8d
mov    $0x4,%ecx
mov    $0x3,%edx
mov    $0x2,%esi
mov    $0x1,%edi

把参数从右向左进行处理,剩余的6个则有专门的寄存器来存储。

callq  40052d <test>

调用test函数,背后隐含的一个操作是把调用test(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)下一条指令mov %eax,%esi的地址压栈,这样rsp的地址会向下偏移8字节。

mov    %eax,%esi

test的返回值,放入esi作为printf的第二个参数

mov    $0x400670,%edi

把 0x400670 作为第一个参数传入printf0x400670 则是%d字符串常量,在只读取的地址

具体的之前在 https://mengkang.net/1383.html 的实验中做过说明

leaveq

这个指令是函数开头的push %rbpmov %rsp,%rbp的逆操作。把rbp赋值给rsp,然后把rsp+0x8

retq

retq指令,它是callq指令的逆操作。在上面执行leaveq之后,再把0x8(rsp)里保存的调用函数的下一行代码的地址取出来赋值给riprip程序计数器,负责从哪里开始执行代码),然后再次rsp+0x8,就从test恢复到main里面了。

test函数的操作就比较简单了,就是把寄存器的值压栈,然后相加计算,返回。

x86-64 规定只有6个寄存器来存参数,那 C 函数为什么还能超过6个参数呢?