欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

函数调用过程

程序员文章站 2024-03-06 17:04:50
...

主要流程:

调用前的准备 —— 参数入栈,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 入栈)

函数调用过程

相关标签: 原理