函数调用的过程理解_汇编角度

目录

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函数中,依次执行以下指令:

bash 复制代码
push eax; # 传入函数的形参arg
call print; #作用是保存eip。当然,也有跳转的作用(见Step2)

print函数开头,依次执行以下指令:

bash 复制代码
push ebp; # 保存父函数main函数的栈底地址,方便调用函数结束后pop回去
mov ebp esp; # 指定print函数的栈帧基地址(print函数对应的栈底地址)

Step2 跳转到print函数的位置

核心是修改eip寄存器,把eip寄存器的值,修改为print函数的入口地址。

实现方式:main函数中调用call指令

bash 复制代码
call print.77c70888; # 相当于esp = esp - 4; push eip(此时的eip值,就是下面"main函数中的下一条指令"的地址); mov eip print函数的地址77c70888(跳转到新函数)
main函数中的下一条指令;

Step3 执行print函数的指令

子函数最开始,一般要执行以下的命令:

bash 复制代码
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命令

bash 复制代码
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-4
push arg1 主函数 sp-4
call function 主函数 sp-4 开始调用子程序,同时保存返回地址
push ebp 子函数 sp-4
mov ebp, esp 子函数 sp-4 将当前esp 存入 ebp,目的是定位函数参数
sub sp, #num 子函数 sp-num 为子程序分配栈空间
... 子函数 ... 函数的具体实现逻辑
pop ebp 子函数 sp+4
ret 子函数 sp+4

说明:

  • push arg 在调用一个函数之前,需要把传递的参数压入栈,因此需要有。每次 push 之后,栈多了一个字长(32 位系统 --> 4 字节),因此栈顶需要往上移动 4 字节,该指令暗含 sub sp, #4
  • call call 指令用来调用某个函数,该指令有3个操作(1)sp = sp - 4(2)将返回地址压入栈; (3)修改eip
  • push ebpmov 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]:栈中的值 / 内存中的值
相关推荐
tyler_download3 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
小小小~3 分钟前
qt5将程序打包并使用
开发语言·qt
hlsd#3 分钟前
go mod 依赖管理
开发语言·后端·golang
小春学渗透5 分钟前
Day107:代码审计-PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·安全·web安全·php·mvc
杜杜的man7 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
亦世凡华、8 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
测试界的酸菜鱼22 分钟前
C# NUnit 框架:高效使用指南
开发语言·c#·log4j
GDAL22 分钟前
lua入门教程 :模块和包
开发语言·junit·lua
李老头探索24 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
CSXB9925 分钟前
三十四、Python基础语法(文件操作-上)
开发语言·python·功能测试·测试工具