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

简介: x86-64 寄存器只有6个寄存器来存参数,那 C 函数为什么还能超过6个参数
#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函数的操作就比较简单了,就是把寄存器的值压栈,然后相加计算,返回。

WX20190914-071635@2x.png

目录
相关文章
|
8月前
|
机器学习/深度学习 数据采集 人工智能
Phi-3 技术报告:手机本地运行的高能力语言模型
Phi-3系列模型通过高质量数据训练与架构创新,实现小体积、高性能。38亿参数的phi-3-mini在手机端可达GPT-3.5水平,支持长上下文、多模态与高效推理,推动AI普惠化。
904 1
|
2月前
|
人工智能 API 网络安全
什么是OpenClaw?OpenClaw能干什么?OpenClaw详细介绍及部署喂饭级图文教程及避坑指南
OpenClaw(前身为 Clawdbot/Moltbot)是一款开源、本地优先、可执行任务的 AI 自动化代理引擎,遵循 MIT 开源协议。它以自然语言指令为核心驱动,能在本地或私有云环境中完成文件操作、流程编排、浏览器自动化、多IM平台交互等各类实际任务,实现了从传统AI“对话式建议”到“自动化执行”的关键跨越,是面向个人开发者与企业团队的自托管式AI数字员工。
2260 17
|
IDE 编译器 开发工具
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
在本文中,我们系统地讲解了常见的 `#pragma` 指令,包括其基本用法、编译器支持情况、示例代码以及与传统方法的对比。`#pragma` 指令是一个强大的工具,可以帮助开发者精细控制编译器的行为,优化代码性能,避免错误,并确保跨平台兼容性。然而,使用这些指令时需要特别注意编译器的支持情况,因为并非所有的 `#pragma` 指令都能在所有编译器中得到支持。
1701 41
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
|
存储 算法 编译器
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用(一)
【C++ 引用 】C++深度解析:引用成员变量的初始化及其在模板编程中的应用
2143 0
|
SQL 安全 数据处理
Web 测试神器:HackBar 保姆级教程
Web 测试神器:HackBar 保姆级教程
|
Linux UED iOS开发
|
数据采集
IQR法(四分位距法)
IQR法(四分位距法)
|
安全 数据安全/隐私保护 虚拟化
win11家庭版怎么升级专业版
Windows 11家庭版用户常需升级到专业版以解锁远程桌面、组策略和BitLocker等高级功能。在升级前,备份数据、确保系统更新至最新。购买正版密钥后,通过“设置”-&gt;“系统”-&gt;“激活”输入密钥进行升级。遵循提示完成升级过程,系统会自动应用专业版特性。如有问题,参考官方文档或寻求技术支持。
win11家庭版怎么升级专业版
|
存储 机器学习/深度学习 并行计算
Pytorch NCHW/NHWC 的理解
Pytorch NCHW/NHWC 的理解
1287 1