这篇文章主要介绍x64平台下函数调用的过程。
主要内容包括caller如何完成到callee的转换,两者之间参数传递方式,函数的栈分配模型,以及callee如何返回到caller。
还是以一个例子来说明(为了简化说明过程,全部参数和局部变量均采用long类型,主要是因为其大小正好是8字节和寄存器大小一致,另外浮点的传参规范使用的是浮点寄存器,不在这篇文章里面讨论)。
long callee(long i, long j) {
long k;
long l;
i = 3333L;
j = 4444L;
k = 5555L;
l = 6666L;
return 9999L;
}
void caller() {
long r = foo(...);
}
caller函数调用callee的汇编代码:
long ret = foo(1111L, 2222L);
先介绍一点x64的通用寄存器集:
x64的寄存器集有16个通用寄存器,即rax, rbx, rcx, rdx, rbp, rsp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15。
乍一看这个寄存器排列毫无章法,命名也不整齐;了解RISC处理器的同学应该都比较喜欢RISC的寄存器命名,r0, r1, ..., r15或者r0, r1, ..., r31,简单直白。x64主要是借鉴的RISC处理器的一些特点,增加了通用寄存器的个数,然后又为了兼容历史版本,导致现在我们看到的通用寄存器命名不规范。早期x86处理器就没有这么多寄存器,也没有通用寄存器概念,基本都是专用寄存器即每个寄存器都有专门的用途,因为CISC不是采用load/store结构,大量指令都是直接操作内存运算的。
x64的函数传参规范:
- 对于整数和指针类型参数, x64使用6个寄存器传递前6个参数。
第一个参数使用rdi,第二个参数使用rsi,第三、四,五,六个参数依次使用rdx, rcx, r8, r9;从第七个开始通过栈传递,因此如果函数参数不超过6个,那么所有参数都是通过寄存器传递的。比如函数:
void callee(int a, int b, int c, int d, int e, int f);
param # | param name | register |
---|---|---|
1 | a | rdi |
2 | b | rsi |
3 | c | rdx |
4 | d | rcx |
5 | e | r8 |
6 | f | r9 |
这个传参过程是从RISC处理器里面借鉴过来的,RISC处理器一般采用寄存器传参,比如ARM就使用四个寄存器R0-R4传参,而早期的x86系统都是使用栈传参的。
至于为什么x64传参使用的寄存器命名这么没有规则,主要是为了和之前的x86处理器兼容,x86系统的ABI已经定义过一套寄存器使用规范。
先看caller生成的汇编指令:
movq $2222, %rsi
movq $1111, %rdi
call callee
movq %rax, -4(%rbp)
代表含义如下:
........instruction........ | description |
---|---|
movq $2222, %rsi | 把第二个参数值2222放在寄存器rsi,前面说过第二个参数使用rsi传递 |
movq $1111, %rdi | 把第一个参数值1111放在寄存器rdi,第一个参数使用rdi传递 |
call callee | call指令调用函数callee;call指令完成两件事情:把当前指令的下一条指令(即将来callee函数的返回地址)压栈,然后把pc指向callee函数的入口,开始执行callee函数代码 |
movq %rax, -4(%rbp) | 读取callee的返回值,函数返回值通过寄存器rax传递 |
再看callee的汇编代码:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq $3333, -24(%rbp)
movq $4444, -32(%rbp)
movq $5555, -16(%rbp)
movq $6666, -8(%rbp)
movq $9999, %rax
leave
ret
这些指令大致分为三大块,第一块入口指令,第二块函数功能代码,第三块返回指令;指令含义如下:
.........instruction......... | description |
---|---|
pushq %rbp | 保存caller的%rbp寄存器值,这个%rbp在函数返回给caller的时候需要恢复原来值,通过leave指令完成。 |
moveq %rsp, %rbp | 把当前的%rsp作为callee的%rbp值 |
moveq ..., offer(%rbp) | 这些moveq指令都是callee函数体的功能,不细说 |
movq $9999, %rax | 设置函数的返回值到%rax,函数返回值是通过寄存器%rax传递的 |
leave | leave完成两件事:把%rbp的值move到%rsp,然后从栈中弹出%rbp;这条指令的功能就是恢复到caller的frame结构,即把%rsp和%rbp恢复到caller函数的值 |
ret | 指令负责从栈中弹出返回地址,并且跳转的返回地址。 |
下面我们详细一步一步介绍函数调用过程中,寄存器和函数栈的变化过程:
按照习惯下面步骤中的图示代码段地址从上往下以递增的方式排列,栈地址从上往下以递减的方式排列。
-
call callee指令之前
此时pc指向call指令,需要传递的参数已经放到传参寄存器,栈是caller的frame。
1.jpg -
call callee指令之后
call指令完成两件事情,1: 把返回地址压栈,可以看到在栈顶0x4005f6正是call指令的下一个指令地址;2: pc指向函数callee的第一条指令。
2.jpg -
pushq %rbp指令之后
把当前rbp的值压入栈,并且pc向前移动到下一条指令。
3.jpg -
movq %rsp, %rbp指令之后
移动rbp到当前rsp地址, 此时rbp和rsp指向同一个地址;rbp就是callee的frame地址,后面callee函数内都将通过rbp加上偏移的方式来访问局部变量。例如:
movq $3333, -24(%rbp)
movq $4444, -32(%rbp)
4.jpg
5 执行函数体功能指令,例如:
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
这个时候我们可以清楚的看到,callee是如何分配栈空间的,rbp往下首先是局部变量,然后是参数预留空间。
5.jpg
6 movq $9999, %rax指令之后
这条指令就是把函数返回值放到寄存器rax,在这个例子中9999=0x270f;前面我们说过函数返回值都是通过rax返回的。
6.jpg
7 leave指令之后
leave指令完成两件事,1:把%rbp的值move到%rsp,在当前这个例子中,这个功能没有效果,因为 %rbp和%rsp的值始终相同。2:然后从栈中弹出%rbp。
7.jpg
8 ret指令之后
ret指令也是完成两件事情,1:栈中弹出返回地址,2:并且跳转的返回地址。
8.jpg
我们可以看到此时栈结构和函数进来之前是一样的,从而保证callee返回以后caller能够继续执行。
这个callee的代码其实有一点问题,不知道你有没有注意的,那就是callee只是调整了%rbp,但并没有调整%rsp,使得%rsp并没有真正指向栈顶,而是自始至终%rsp和%rbp指向同一个地址,按照前面的逻辑callee进来的时候保存了caller的%rbp和%rsp,并且在返回时需要恢复原来的值,而就是说%rbp和%rsp通常成对出现构成一个frame范围,那么这个callee为什么会这样呢?
原因是callee是一个叶子函数,它不再调用其他函数,就是说从进入这个函数到离开这个函数之间不会发生栈的操作,设置%rsp的操作就可以省略。
我们修改一下代码,添加一个子函数sub()让callee来使用:
void sub() {
}
long callee(long i, long j) {
long k;
long l;
i = 3333L;
j = 4444L;
k = 5555L;
l = 6666L;
sub();
return 99L;
}
生成的callee代码如下:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq $3333, -24(%rbp)
movq $4444, -32(%rbp)
movq $5555, -16(%rbp)
movq $6666, -8(%rbp)
movl $0, %eax
call sub
movl $9999, %eax
leave
ret
相比较前面的callee代码,此时多了一条指令:
subq $32, %rsp
这条指令就是调整函数callee的新的%rsp值,使得%rbp和%rsp之间构成一个标准的callee函数frame范围。栈结构如下:
其实栈的内容和前面没有call sub的栈内容是一样的,只是调整了%rsp的指针,因为callee已经不是叶子函数了,它需要调用sub函数,这个过程中是有栈的操作的,所以必须把%rsp指向正确的位置。然后在函数返回的时候leave指令能够再把%rsp重新调整到%rbp的位置。
网友评论