x86-64函数调用参数传递
Posted rtoax
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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函数调用参数传递的主要内容,如果未能解决你的问题,请参考以下文章
如何计算程序(或函数调用)中两点之间执行的 x86-64 指令的数量?