1、调用函数流程(main函数调用print函数):
Step1. 保存main函数现场地址等信息
Step2. 跳转到print函数的位置
Step3. 执行print函数的指令
Step4. 返回main函数,执行下一条指令
Step1 保存main函数现场地址等信息
要存储的内容,主要有3个:
(1)传入print函数的形参【父函数中实现】
(2)原eip(main函数call指令的下一条指令的地址)【父函数中实现】
(3)原ebp(main函数的ebp地址,由于是main函数直接调用print函数)【子函数中实现】
实现方式:
在main函数中,依次执行以下指令:
- push eax; # 传入函数的形参arg
- call print; #作用是保存eip。当然,也有跳转的作用(见Step2)
复制代码 在print函数开头,依次执行以下指令:
- push ebp; # 保存父函数main函数的栈底地址,方便调用函数结束后pop回去
- mov ebp esp; # 指定print函数的栈帧基地址(print函数对应的栈底地址)
复制代码 Step2 跳转到print函数的位置
核心是修改eip寄存器,把eip寄存器的值,修改为print函数的入口地址。
实现方式: 在main函数中调用call指令
- call print.77c70888; # 相当于esp = esp - 4; push eip(此时的eip值,就是下面"main函数中的下一条指令"的地址); mov eip print函数的地址77c70888(跳转到新函数)
- main函数中的下一条指令;
复制代码 Step3 执行print函数的指令
子函数最开始,一样寻常要执行以下的命令:
- push ebp; # 保存原ebp
- mov ebp esp; # ebp指向当前函数的栈底
- ...
- sub esp, num; # 为子程序分配栈空间
- ...
- pop ebp; # 让ebp指向父函数的栈帧基地址
- ret n; # 让eip指向“main函数call指令的下一条指令的地址”;n是指平衡堆栈,即让最先push进去的形参args清空
复制代码 Step4 返回main函数,执行下一条指令
核心是修改eip寄存器的值,把eip寄存器的值,修改为main函数中下一条指令的地址。
实现方式:在print函数中,调用ret命令
- ret ; #相当于pop eip; esp = esp + 4
复制代码 留意,如果原先栈中还有其他数据,esp 没有归位会导致主函数引用栈中数据出错。在这种背景下,出现了堆栈均衡的概念。即,还需对esp 进行单独操作,才能将 esp 指向原函数栈顶。以常见的 c 语言,函数有好几种调用规则。比如 cdecl 方式和 stdcall 方式。
cdecl 方式中,由主程序执行 add esp, n 指令调整 esp,到达堆栈均衡。在 stdcall 方式中,由子程序在返回时,执行 ret n 均衡堆栈。n 着实就是函数的参数所占的空间巨细。
流程一连性总结
重要参考:《从汇编角度明白 ebp&esp 寄存器、函数调用过程、函数参数传递以及堆栈均衡》
函数调用
在一个函数中,调用另外一个函数,往往有以下几个步调:
汇编指令指令归属函数SP 变化作用push arg2主函数sp-4push arg1主函数sp-4call function主函数sp-4开始调用子程序,同时保存返回地址push ebp子函数sp-4mov ebp, esp子函数sp-4将当前esp 存入 ebp,目的是定位函数参数sub sp, #num子函数sp-num为子程序分配栈空间…子函数…函数的具体实现逻辑pop ebp子函数sp+4ret子函数sp+4 阐明:
- push arg 在调用一个函数之前,需要把传递的参数压入栈,因此需要有。每次 push 之后,栈多了一个字长(32 位系统 --> 4 字节),因此栈顶需要往上移动 4 字节,该指令暗含 sub sp, #4
- call call 指令用来调用某个函数,该指令有3个操作(1)sp = sp - 4(2)将返回地址压入栈; (3)修改eip
- push ebp, mov ebp, esp 如许的操作,你会在各个函数的开头见到,保存上一个函数栈的基址,并更新本函数的基址
- ret,即 return,此时 sp 应该指向 call 指令刚刚压入的返回地址;执行 ret 着实就是将此时栈中的数据弹出,存至 eip 寄存器。eip 存放的是下一条即将执行的指令的地址。 同时 sp = sp + 4
- ret 指令相称于 pop eip(esp = esp + 4)
- call 指令相称于 push eip(esp = esp - 4); mov eip 新函数的地址
下图左边是主函数调用子函数,右边是子函数返回主函数:
2、其他知识总结
1、esp寄存器,永远指向整个栈帧的栈顶(esp寄存器保存的值是堆栈的地址值);栈顶中的内容,可能是一个地址值,也可能是一个立即数等等。esp寄存器指向的堆栈地址始终有值!所以push是先移esp后压栈,pop是先弹栈后移esp。
2、ebp寄存器,永远指向当前函数地点栈帧的栈底(ebp寄存器保存的值是堆栈的地址值);栈底中的内容,永远保存的是上一个函数(父函数)的ebp地址!(方便函数执行完后pop回去,即修改ebp)
- ebp 的作用之一就是找到函数的形参(通过ebp + 偏移量),当然栈中的局部变量也是可以通过 ebp 来定位的(ebp - 偏移量)
3、父函数调用子函数,子函数的栈帧(低地址)是紧挨着父函数的栈帧(高地址)。
- 所以父函数调用子函数,一定是①args先压榨 ② 然后 原eip压栈(by call)③ 再 ebp压栈 ④ 末了函数的局部变量压榨
- 父函数栈帧 和 子函数栈帧,隔着的东西就是:args 和 eip
4、栈的增长方向,永远向着低地址的方向增长。
5、call 指令相称于:esp = esp - 4; push eip; mov eip 函数的地址.77c70888 (留意,push中已包含esp移动,这里只是表现先后)
6、ret指令相称于:pop eip; esp = esp + 4 (留意,pop中已包含esp移动,这里只是表现先后)
7、push eax指令,只会修改栈和esp:① esp = esp - 4 ② mov [esp], eax 这个[esp]表现esp指向的栈值(内存值)被赋值。
8、pop eax指令,不仅会修改栈和esp,而且还会修改eax(寄存器被赋值):① mov eax, [esp] ②esp = esp + 4
9、寄存器的地址值是不会变的,变的只是寄存器中装的堆栈地址值,以及这个堆栈地址中的内存内容值。这个雷同并区别于C语言的指针变量。C语言指针变量有2个地址值,一个是指针变量装的堆栈地址值,另一个是指针本身的地址值(也位于堆栈)。指针变量声明后,它存在于内存中(堆栈中),指针释放了,这个指针的地址值也就无了。区别点在于 指针有本身的地址值(可以通过&取出来),而寄存器没有本身的地址值的说法(取不出来,大概有也是固定的但不常用)。指针变量的常操作数据有3个:①p ②*p ③&p;esp寄存器常操作的数据有2个:①esp ②[esp]
- esp:esp寄存器的内容值,即栈/内存的某个地址
- [esp]:栈中的值 / 内存中的值
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |