32位汇编:实验11子程序调用
程序示例

堆栈图
调试梳理
我们在OB中观察我们堆栈和ESP寄存器的变化情况
初始状态下,我们先准备了参数,执行后第一次调用 call AddProc1,我们让CPU走到这里的时候停下。
我们画出初始状态下的堆栈图

执行到相关位置后,我们发现没有执行cal命令之前堆栈没有发生变化。
执行 call AddProc1,call 会:压入返回地址(call 下一条指令的地址)。
F7单步进入,观察call瞬间的堆栈变化

ESP ← ESP - 4

接着在 AddProc1 内部执行接下来的命令

我们可以发现在执行return之前没有变化。
而我们知道return相当于pop EIP,从[ESP]弹出返回地址到EIP,*ESP+4*

结束后堆栈相当于回到最开始的状态

下面程序完成保存结果并准备第二次调用。

第二次调用call AddProc2

call 压入返回地址:

进入到AddProc2
push eax

将EAX当前值(0000001Eh)压栈,ESP减4

接着实现我们的函数功能后**pop eax**

从[ESP]弹出数据到EAX,ESP加4

恢复堆栈后,执行完毕,return弹出返回地址,回到调用处。

堆栈变为初始状态。

同样我们发现因为少了一次平衡,堆栈其实并没有完全恢复。
emm……中间程序卡了一下,重启后堆栈信息发生了变化,不过不影响我们的分析思路。我们下面画图中用新的堆栈表示
接着cal调指令后,我们到了用 printf 函数部分。

这时的堆栈为下图状态

输出寄存器传参的计算结果,四次push后:

push ESP -= 4,我们的堆栈图也变为了

第二次 printf 函数部分输出内存变量传参的计算结果

add esp,10,堆栈指针回到初始位置,栈顶数据被标记为"已释放",下次调用必须重新压入参数。

下面就是我们第二次调用printf的四次压栈

第二次printf调用时栈顶

同理add esp,10,堆栈指针回到初始位置,栈顶数据被标记为"已释放"。
最后return跳出程序

程序堆栈图片变化如下

也算是把当年看滴水画堆栈的功拾回来了。
都写了这么多了,我们顺便回顾一下实验里的知识点吧。
1. 汇编程序结构
.686 ; 使用686指令集 .model flat,stdcall ; 平坦内存模型,stdcall调用约定 option casemap:none ; 大小写敏感
-
平坦内存模型:所有段(代码、数据、堆栈)都在同一个4GB地址空间
-
stdcall调用约定:被调用函数负责清理堆栈参数
2. 函数声明与调用
printf PROTO C :dword,:vararg ; 声明C函数原型 invoke printf, offset szFmt, esi, edi, eax ; 调用函数
-
PROTO:函数原型声明,指定参数类型
-
invoke:高级函数调用指令,自动处理参数压栈
3. 数据定义
szFmt byte '%d + %d = %d', 0ah, 0 ; 字符串以0结尾 X dword ? ; 未初始化双字变量
-
byte/dword:定义字节/双字(4字节)数据
-
?:未初始化变量
-
0ah:换行符(LF)
-
0:字符串结束符
4. 寄存器使用
mov esi, 10 ; 源索引寄存器存放参数 mov edi, 20 ; 目的索引寄存器存放参数
通用寄存器用途:
-
EAX:累加器,常用于算术运算和函数返回值
-
ESI:源索引,常用于字符串/数组操作
-
EDI:目的索引,常用于字符串/数组操作
-
ESP:堆栈指针,指向栈顶
-
EBP:基址指针,指向当前栈帧
5. 算术运算指令
add eax, edi ; eax = eax + edi
算术指令:
-
add dest, src:加法 -
sub dest, src:减法 -
inc reg:加1 -
dec reg:减1
6. 堆栈操作指令
push eax ; ESP-4, 将EAX压栈 pop eax ; ESP+4, 从堆栈弹出到EAX call proc ; ESP-4, 压入返回地址后跳转 ret ; ESP+4, 弹出返回地址并跳转
堆栈变化规律:
调用前: ESP = 1000 call指令: ESP = 1000-4 = 0FFC, [0FFC]=返回地址 push: ESP = 0FFC-4 = 0FF8 pop: ESP = 0FF8+4 = 0FFC ret: ESP = 0FFC+4 = 1000
7. 函数调用约定
寄存器传参(AddProc1):
; 调用前准备参数 mov esi, 10 mov edi, 20 call AddProc1 ; 结果在EAX中
全局变量传参(AddProc2):
; 调用前设置全局变量 mov X, 50 mov Y, 60 call AddProc2 ; 结果在Z中
8. 内存访问
mov eax, X ; 直接寻址,从变量X读取值 mov Z, eax ; 直接寻址,将值存入变量Z
9. 程序控制流
start: ; 程序入口标签 call AddProc1 ; 函数调用 ret ; 程序结束返回 end start ; 指定程序入口点

