x86-64函数调用参数传递
公众号原文链接:https://mp.weixin.qq.com/s/qXYnIezKNuXhOBN7nZouZw
问题引入
不多解释,先写个小程序:
void func1(a,b,c)
int a,b,c;
{}
void func2(a,b,c,d,e,f)
int a,b,c,d,e,f;
{}
void func3(a,b,c,d,e,f,g,h)
int a,b,c,d,e,f,g,h;
{}
int main()
{
int a,b,c,d,e,f,g,h;
func1(a,b,c);
func2(a,b,c,d,e,f);
func3(a,b,c,d,e,f,g,h);
}
从程序中可以看出,函数分别带有3个,6个,8个入参,那么这些入参是如何做的呢?如果站在汇编的角度,实际上可以采用两种方案:
采用通用寄存器传参;
采用栈传参;
为了证明函数如何传参,首先用GCC生成汇编代码:
gcc arg-pass.c -S
这样,我们就获的C代码对应的x86平台的汇编代码(为了显示清晰,我删除了以"."开头的标记性语句):
func1:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
popq %rbp
ret
func2:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
popq %rbp
ret
func3:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
popq %rbp
ret
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movl -12(%rbp), %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
movl %ecx, %esi
movl %eax, %edi
movl $0, %eax
call func1
movl -24(%rbp), %r8d
movl -20(%rbp), %edi
movl -16(%rbp), %ecx
movl -12(%rbp), %edx
movl -8(%rbp), %esi
movl -4(%rbp), %eax
movl %r8d, %r9d
movl %edi, %r8d
movl %eax, %edi
movl $0, %eax
call func2
movl -24(%rbp), %r9d
movl -20(%rbp), %r8d
movl -16(%rbp), %ecx
movl -12(%rbp), %edx
movl -8(%rbp), %esi
movl -4(%rbp), %eax
movl -32(%rbp), %edi
movl %edi, 8(%rsp)
movl -28(%rbp), %edi
movl %edi, (%rsp)
movl %eax, %edi
movl $0, %eax
call func3
leave
ret
接下来,我们就可以进行一次分析。
x86寄存器
首先,我需要先给出X86相关寄存器,又例如我们分析上面的汇编代码。
因为x86都是向后兼容的,所以在x86-64架构下,32bit和16bit的寄存器都是可以使用的,32bit通用寄存器如下:
寄存器 | 描述 |
eax | 操作数的运算、结果 |
ebx | 指向DS段中的数据的指针 |
ecx | 字符串操作或者循环计数器 |
edx | 输入输出指针 |
esi | 指向DS寄存器所指示的段中某个数据的指针,字符串操作中的复制源 |
edi | 指向ES寄存器所指示的段中某个数据的指针,字符串操作中的目的 |
esp | SP寄存器 |
ebp | 指向栈上数据的指针 |
x86-64架构寄存器如下:
寄存器 | 描述 |
rdi | 传递第一个参数 |
rsi | 传递第二个参数 |
rdx | 传递第三个参数或者第二个返回值 |
rcx | 传递第四个参数 |
r8 | 传递第五个参数 |
r9 | 传递第六个参数 |
rax | 临时寄存器或者第一个返回值 |
rsp | sp寄存器 |
rbp | 栈帧寄存器 |
在了解了寄存器之后,我们就可以分析上面说的函数调用过程了。
栈结构分析
首先我们看函数1:
void func1(a,b,c)
int a,b,c;
{}
调用方式为:
func1(a,b,c);
上面对应的在main中调用汇编为:
movq %rsp, %rbp
subq $48, %rsp
movl -12(%rbp), %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
movl %ecx, %esi
movl %eax, %edi
movl $0, %eax
call func1
上面做了什么?注意栈指针rsp的移动就能理解了。
栈的起始状态为:
给rsp减值48,这很好玩,就这么个操作,直接在栈上申请了48字节的空间,是不是比brk()系统调用还快。
为啥这里会有
movl -12(%rbp), %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
到底谁才是abc?说实话,我也糊涂了,所以说,我们还是给他们初始值吧。
int main()
{
int a,b,c,d,e,f,g,h;
a = 1;
b = 2;
c = 3;
d = 4;
e = 5;
f = 6;
g = 7;
h = 8;
func1(a,b,c);
func2(a,b,c,d,e,f);
func3(a,b,c,d,e,f,g,h);
}
反汇编为:
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movl $3, -12(%rbp)
movl $4, -16(%rbp)
movl $5, -20(%rbp)
movl $6, -24(%rbp)
movl $7, -28(%rbp)
movl $8, -32(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
movl %ecx, %esi
movl %eax, %edi
movl $0, %eax
call func1
movl -24(%rbp), %r8d
movl -20(%rbp), %edi
movl -16(%rbp), %ecx
movl -12(%rbp), %edx
movl -8(%rbp), %esi
movl -4(%rbp), %eax
movl %r8d, %r9d
movl %edi, %r8d
movl %eax, %edi
movl $0, %eax
call func2
movl -24(%rbp), %r9d
movl -20(%rbp), %r8d
movl -16(%rbp), %ecx
movl -12(%rbp), %edx
movl -8(%rbp), %esi
movl -4(%rbp), %eax
movl -32(%rbp), %edi
movl %edi, 8(%rsp)
movl -28(%rbp), %edi
movl %edi, (%rsp)
movl %eax, %edi
movl $0, %eax
call func3
leave
ret
那我们重新分析,main是如何调用func1的吧!还是这个代码:
movq %rsp, %rbp
subq $48, %rsp
movl $1, -4(%rbp)
movl $2, -8(%rbp)
movl $3, -12(%rbp)
movl $4, -16(%rbp)
movl $5, -20(%rbp)
movl $6, -24(%rbp)
movl $7, -28(%rbp)
movl $8, -32(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %ecx
movl -4(%rbp), %eax
movl %ecx, %esi
movl %eax, %edi
movl $0, %eax
call func1
函数调用前的栈
根据上面的汇编代码分析,实际上就变成了这样:
上面的流程实际上只是main函数中的局部变量的初始化,接下来的汇编就要开始调用func1了,首先使用通用寄存器存放abc:
下面就要像我在描述通用寄存器的时候说的
寄存器 | 描述 |
rdi | 传递第一个参数 |
rsi | 传递第二个参数 |
rdx | 传递第三个参数或者第二个返回值 |
rax | 临时寄存器或者第一个返回值 |
分别使用通用寄存器edi、esi、edx保存了三个参数a、b、c,eax会保存函数的返回值,所以需要置零。后续就可以调用func1了。
函数调用的栈
首先看下func1的汇编代码:
func1:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
popq %rbp
ret
在刚进入func1时,栈空间是下面这样的:
首先将rbp压栈,实际上保存的不是rbp,而是保存的rbp的值:
为了更加清楚动人,还是给之前的rbp设定个数值(0x40000)吧:
接下来,需要把通用寄存器存至函数栈:
为什么要做上面这一步?我直接用寄存器不好吗?为什么非要存?
答案很简单,因为我这里写的函数为:
void func1(a,b,c)
int a,b,c;
{}
函数体为空,也就是完全没有任何其他的操作,在复杂的函数体中,通用寄存器的使用还有很多,所以编译器必须这么做。【这里抛出一个问题,如果编译器能知道当前函体为空,那么我使用gcc -O3优化编译,会省略上图的压栈操作吗?感兴趣可以试一试】。
执行完上面的操作,函数可以返回了,在返回前(调用ret前),需要恢复栈空间为调用func1之前:
接着就可以返回了。下面我们分析超过6个参数的函数调用。
超过6个入参的函数调用
下面我们分析超过6个参数的函数调用。假定func2已经返回【因为func2位六个入参,流程基本上和func1函数相同,直接看超过6个入参的情况】,此时的栈空间为:
直接看main函数中的函数调用部分汇编代码:
movl -24(%rbp), %r9d
movl -20(%rbp), %r8d
movl -16(%rbp), %ecx
movl -12(%rbp), %edx
movl -8(%rbp), %esi
movl -4(%rbp), %eax
movl -32(%rbp), %edi
movl %edi, 8(%rsp)
movl -28(%rbp), %edi
movl %edi, (%rsp)
movl %eax, %edi
movl $0, %eax
call func3
首先,先给寄存器赋值:
接着使用栈上的空闲空间存储edi【8】:
接着,在将【7】入栈:
然后,将【1】保存到edi寄存器,这与我们上面表格中给出的rdi保存第一个参数是一致的。
在这一波操作之后,就可以调用func3了,当然,还是先看一眼func3的汇编代码:
func3:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
popq %rbp
ret
函数原型为:
void func3(a,b,c,d,e,f,g,h)
int a,b,c,d,e,f,g,h;
{}
首先还是先将rbp入栈:
然后分别将通用寄存器入栈:
然后将rbp出栈,恢复到main调用func3之前的栈结构:
结论
在x86平台上,函数调用过程中,如果没有超过6个入参,那么直接使用寄存器就可以了,如果超过6个入参,那么超出6个的参数将通过栈传递。
x86-64函数调用参数传递相关推荐
- 第19部分- Linux x86 64位汇编GDB单步调试
第19部分- Linux x86 64位汇编GDB单步调试 本篇我们使用gdb来调试上篇中的汇编代码. gdb调试 使用gdb进行调试. #gdb ./addsum_arg 设置参数: (gdb) s ...
- 第41部分-Linux x86 64位汇编MMX使用
第41部分-Linux x86 64位汇编MMX使用 使用MMX架构需要一下步骤 从整数值创建打包整数值 把打包整数值加载到MMX寄存器中 对打包整数值执行MMX数学操作. 从MMX寄存器获得结果放到 ...
- 第77部分- Linux x86 64位汇编 优化编译器代码
第77部分- Linux x86 64位汇编 优化编译器代码-O1/-O2/-O3 仅仅使用汇编语言代码替换C或者C++不会必然使得程序执行的更好,因为编译器已经把所有高级语言代码都转化成了汇编语言. ...
- linux的x64与x86_在Linux x86 64机器上链接
linux的x64与x86 Linking is the process of combining various pieces of code and files in order to const ...
- 64位x86的函数调用栈布局
作者:gfree.wind@gmail.com 博客:blog.focus-linux.net linuxfocus.blog.chinaunix.net 在看本文之前,如果不了解x86的32位 ...
- qemu+linux+x86+64,qemu以64位跟32位的方式跑busybox
qemu以64位和32位的方式跑busybox 两种方式x86_64 和32位的i386方式 -----------x86_64------------------------------------ ...
- 阿里云发布全新开源操作系统『龙蜥』,支持 X86 64 和 ARM 64 架构及鲲鹏、飞腾等芯片...
公众号关注 「奇妙的 Linux 世界」 设为「星标」,每天带你玩转 Linux ! 近日,2021 云栖大会上,阿里云发布了全新操作系统 "龙蜥"(Anolis OS),并宣布开 ...
- X86 64位和32位
不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Architecture),也可以称为指令集(instruction set).Intel将x86系列CP ...
- 栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限(二)shellcode
shellcode 是一组指令opcode, 是可以被程序运行,因为shellcode是要直接操作寄存器和函数,所以opcode 必须是十六进制的形式. 既然是攻击,那shellcode 主要的目的是 ...
最新文章
- python招聘笔试题_滴滴2020年春招笔试题分析(Python)
- Android camera开发总结
- JVM(六)为什么新生代有两个Survivor分区?
- java 假设当前时间_Java如何比较当前时间是否在两个时间范围内
- 《数据科学与大数据分析——数据的发现 分析 可视化与表示》一2.3 第2阶段:数据准备...
- 其实,人的核心职场时间是有限的,一定要和高手玩
- 南京:第三届软博会“外包”将唱主角
- Linux C++ 简单爬虫
- 今天,给我妈打电话聊了我爸
- @codeforces - 786E@ ALT
- STM32 F103 时钟树详解
- unreal ue4 虚幻 websocket Server websocket服务 插件使用及下载 非官方自己写的
- 用例图、功能模块图和数据库的区别
- 微信Windows版无法备份聊天记录
- 小白都能懂的设计模式 java版 抽象工厂模式 实战练习(超详细)
- Flowable API 瞬时变量
- C++的errorC2039和C2679的解决
- 百慕大永中为何有权继续开发集成Office?
- Schrodinger软件学习计算机辅助药物设计——基本操作以及分子对接
- android开发对Webview的应用