函数调用过程
主要流程:
调用前的准备 —— 参数入栈,eip 入栈,ebp 入栈,eip跳转
函数执行
恢复到调用前状态 —— 返回值 eax,恢复ebp,恢复 eip
#include <stdio.h>
int f(){
int a=1;
return a;
}
int main()
{
int b=f();//汇编 call f (0B711D6h)
return 0;
}
call f (0B711D6h)
pushl %ebp
movl %esp, %ebp
subl $16, %esp
。。。
movl %ebp %esp
pop %ebp
ret
这些是函数调用的必经步骤!
看懂这个之前还得先了解计算机的堆栈结构
一:计算机一般是小端——储存顺序和常识相反(仅供理解)
即 高地址(栈底)—————— 低地址(栈顶)
ebp 和 esp 是两个寄存器,分别保存栈底指针和栈顶指针(地址)
二:栈——相当于往有底的杯子里倒水
那么,上面的汇编语句就可以开始解析了
首先:
call f (0B711D6h)
零:call 语句 —— eip 入栈,eip跳转
(eip —— 保存了下一条即将要执行语句的地址)
eip 入栈——记录当前语句执行到哪了,之后好恢复
eip跳转 ——跳转到要调用的函数处
(通过括号内的地址实现跳转,这个地址是编译器给的)
一:push ebp 保存原先栈底
入栈前:
然后 push 指令—— esp 往低地址移动4个字节
( esp 的值为4字节)
然后把 ebp 的值存到 esp 所指的空间
(指针所指的是数据的起始地址,从低地址开始)
Ps:
因为计算机是小端,所以才会有东西从栈底进的感觉
其实,在栈顶存入新元素,这其实就是入栈 push
这里一定要好好理解!!!!
二:movl %esp, %ebp 得到新栈底
把 esp 的值赋给ebp
这时 esp 和 ebp 指向同一地址
( 这里是寄存器之间的赋值!)
三:subl $16, %esp 得到新栈顶
esp 减16 的实际意义
—— 把esp从高地址往低地址移动,从而有:
栈顶和栈顶分开,形成了新的栈
——他们之间就可以做好多事情了
Ps:
他们之间的并不是函数内容
函数语句转成的汇编语句存在内存的其他地方
他们之间存的一般是我们口中的局部变量~
正是因为调用函数时用的栈是临时的
函数执行完之后,ebp 和 esp 恢复到原先的值
局部变量的生命周期也就结束了~
(在内存中的值可能不变,但是已经访问不了了)
这里举个例子吧:
int f(){
int a=1;
return a;
}
// 赋值语句对应汇编语句: movl $1, -4(%ebp)
栈中存的 1 就是我们说的局部变量啦
(函数结束后,ebp esp 转移,局部变量就无效咯~)
最后是函数调用的结束处理
int f(){
int a=1;
return a;
}
//return a 对应的汇编语句
movl -4(%ebp), %eax
//把a的值赋到eax寄存器中
return 返回值其实就保存在 eax 寄存器中
接下来,把 esp 和 ebp 的值还原回去
movl %ebp %esp
pop %ebp
ret
把 ebp 的值赋给 esp
此时,esp 和 ebp 都指向栈底那唯一的 —— “ ebp的值 ”
然后,pop 指令 —— 把栈顶指向的值(即上图的“ ebp 的值”)
赋到ebp 寄存器中
同时,esp 往高地址移动4字节(出栈了一个4字节的值嘛)
此时,ebp 和 esp 都回到了原本的状态
然后
ret
把返回地址出栈,并且赋值给 eip
从而返回到调用函数之前的状态
这里再提一下带参的函数调用
(在 call语句 调用函数之前先把这些参数入栈就行啦~)
(之后以 ebp 为基准就可以访问这些传进的参数了)
(参数表为: 参数1,参数2 时先参数2入栈,再参数1入栈)
后面的参数放高地址嘛,就先入栈啦~
(然后再 eip 入栈)