别人的图看着太累了,自己整理一下,简单干脆。
calling conventions,翻译过来可以叫“调用规则”,“调用约定”,“调用协议”等。
究其根底,不过是规定了多层函数嵌套时,caller与callee的有关参数的入栈方式。
因为是一个convention,所以你不必纠结为什么要这样,大家都这样了,世界就和平了。
我们约定
若一个函数A调用一个函数B,则A被称为调用者(Caller),B被称为被调用者(Callee)。
根据习惯,我们需要了解一个事实
在X86中,栈增长方向与内存编号增长方向相反。
你还需要知道2个指针的含义
ebp,当前函数栈区的基指针 (base pointer)
esp,当前函数栈区的栈顶指针(stack pointer)
下面正式介绍游戏规则。
Caller Rules
调用者规则包括一系列操作,描述如下:
1)在调用子程序之前,调用者应该保存一系列被设计为调用者保存的寄存器的值。调用者保存寄存器有eax,ecx,edx。由于被调用的子程序会修改这些寄存器,所以为了在调用子程序完成之后能正确执行,调用者必须在调用子程序之前将这些寄存器的值入栈。
2)在调用子程序之前,将参数入栈。参数入栈的顺序应该是从最后一个参数开始,如上图中parameter3先入栈。
3)利用call指令调用子程序。这条指令将返回地址放置在参数的上面,并进入子程序的指令执行。(子程序的执行将按照被调用者的规则执行)
当子程序返回时,调用者期望找到子程序保存在eax中的返回地址。为了恢复调用子程序执行之前的状态,调用者应该执行以下操作:
1)清除栈中的参数;
2)将栈中保存的eax值、ecx值以及edx值出栈,恢复eax、ecx、edx的值(当然,如果其它寄存器在调用之前需要保存,也需要完成类似入栈和出栈操作)
Callee Rules
被调用者应该遵循如下规则:
1)将ebp入栈,并将esp中的值拷贝到ebp中,其汇编代码如下:
push ebp mov ebp, esp
上述代码的目的是保存调用子程序之前的基址指针,基址指针用于寻找栈上的参数和局部变量。当一个子程序开始执行时,基址指针保存栈指针指示子程序的执行。为了在子程序完成之后调用者能正确定位调用者的参数和局部变量,ebp的值需要返回。
2)在栈上为局部变量分配空间。
3)保存callee-saved寄存器的值,callee-saved寄存器包括ebx,edi和esi,将ebx,edi和esi压栈。
4)在上述三个步骤完成之后,子程序开始执行,当子程序返回时,必须完成如下工作:
4.1)将返回的执行结果保存在eax中
4.2)弹出栈中保存的callee-saved寄存器值,恢复callee-saved寄存器的值(ESI和EDI)
4.3)收回局部变量的内存空间。实际处理时,通过改变EBP的值即可:mov esp, ebp。
4.4)通过弹出栈中保存的ebp值恢复调用者的基址寄存器值。
4.5)执行ret指令返回到调用者程序。
//来源:http://www.cnblogs.com/YukiJohnson/archive/2012/10/27/2741836.html
看不懂没关系,看完下面的内容等下再回来看规则就更好理解了。
网上有2种图,一种是把高地址放在上方的,比如头顶x1010,脚部0x1000,这样一来,以高地址为栈底,Stack向下扩张。
另一种是高地址在下方的。
所以你看不同的文章,不同的配图,会很头晕。
我总结下来,为了保持与MIT课程的统一,建议采用高地址在上方的版本。
MIT的图长这样:
+------------+ |
| arg 2 | \
+------------+ >- previous function's stack frame
| arg 1 | /
+------------+ |
| ret %eip | /
+============+
| saved %ebp | \
%ebp-> +------------+ |
| | |
| local | \
| variables, | >- current function's stack frame
| etc. | /
| | |
| | |
%esp-> +------------+ /
第二类图长这样:
ESP==>| : |
| . |
+-------------------------+
| 被调用者保存的寄存器现场 |
| EBX,ESI和EDI(根据需要) |
+-------------------------+
| 临时空间 |
+-------------------------+
| 局部变量#2 | [EBP - 8]
+-------------------------+
| 局部变量#1 | [EBP - 4]
+-------------------------+
EBP==>| 调用者的EBP |
+-------------------------+
| 返回地址 |
+-------------------------+
| 实际参数#1 | [EBP + 8]
+-------------------------+
| 实际参数#2 | [EBP + 12]
+-------------------------+
| 实际参数#3 | [EBP + 16]
+-------------------------+
| 调用者保存的寄存器现场 |
| EAX,ECX和EDX(根据需要)|
+-------------------------+
| : |
| . |
本文采用高地址在上方的版本。
依据caller规则,在函数调用时。
1. 参数从后向前入栈。
例如 myfunc(a,b,c)
入栈顺序为c -> b -> a
入栈完成后的结构是
2. caller将返回地址入栈保存
3.跳转到子函数起始地址
上述2和3两步,由call指令完成。
完成后,就已经来到了子函数的起始地址。
4.子函数将父函数栈帧起始地址(%epb) 入栈。
子函数第一条指令就是push %ebp,你可以认为这是固定的,这会有助于理解。
注意,栈区跟代码区是两个东西。我只画了栈区发生了什么。
比如上图中,push %ebp这条指令可能保存在0xf0ff这个位置。
但是执行完成之后,栈区0x1030这个位置就多出来一个父函数的%ebp里的值。
5.将 %ebp 的值设置为当前 %esp 的值,即将 %ebp 指向子函数栈帧的起始地址。
mov ebp, esp
这条指令只修改寄存器的值,不影响栈区。
注意,这一步之后,ebp里面就是当前esp的值。而当前esp指向的是父函数%ebp保存的地址!
所以你可以理解为,【在子程序运行期间,%ebp指向的都是父函数%ebp保存的地址】。 (有点绕口)
6.现在开始申请临时空间。
假设我们的myfunc里面,定义了2个int变量,一共8字节。
那么这步结束后,栈顶指针esp来到0x1024这个位置。【局部变量2占用从0x1028到0x1025共4个字节的内容】
现在我们可以正式运行子函数了。
7.运行结束之后,以防万一先把返回值存进%eax里。
现在该如何返回?
很简单,还记得上文说的,子程序运行期间,%ebp指向的是父函数%ebp保存的地址。
所以我们先用
mov esp,ebp
这样,esp就回到了保存父函数%ebp的地方。
pop ebp
执行指令后,会把%esp指向的地址里面的值弹出,赋值给ebp。
于是%ebp恢复到了父函数执行时的样子。
而栈顶指针esp再度退回一格。
8.现在我们可以用ret指令,或者leave指令。
效果都是一样的,弹出栈顶保存的返回地址(return address),并跳转到它(即赋值给EIP寄存器)。
现在,%ebp和%eip都回到了父函数,父函数取得控制权。
9.父函数在返回后有义务进行扫尾工作。
之前传入的3个参数ABC已经不需要了,需要退格。
esp += 12
这样esp就重新指向栈底,此时对计算机来说,为栈空。
上述过程我们可以看到,
如果所程序都遵守calling convention,那么就很容易回溯所有函数的栈区基指针ebp。
因为根据规则,在一个子函数中,ebp必然指向保存着父函数ebp的位置。所以可以不断的把当前ebp位置的值取出来赋值给ebp来回到上一级函数的栈区。
以上为个人理解版本。
若有误请指教。
附录:
袁春风书
//参考https://blog.csdn.net/cwcwj3069/article/details/10633493