栈帧就是利用EBP(栈帧指针)寄存器访问栈内局部变量、参数、函数返回地址等的手段。通过前面关于IA-32寄存器的学习可知,ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行使栈帧指针的职能。程序运行中,ESP寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以ESP值为基准编写程序会十分困难,并且也很难使CPU引用到准确的地址。所以,调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到ESP,并维持在函数内部。这样,无论ESP的值如何变化,以EBP的值为基准(base)能够安全访问到相关的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。借助栈帧技术管理函数调用时,无论函数调用的深度有多深、多复杂,调用栈都能得到好的管理和维护。
提示:1.最新的编译器中都带有一个“优化”(Optimization)选项,使用该选项编译简单的函数将不会生成栈帧。
2.在栈中保存函数返回地址是系统安全隐患之一,攻击者使用缓冲区溢出技术能够把保存在栈内存的返回地址更改为其他地址。
调试示例:stackframe.exe
设置局部变量
分析两条MOV命令,其含义为“将数据1与2分别保存到[EBP-4]与[EBP-8]中”,即[EBP-4]代表局部变量a,[EBP-8]代表局部变量b。执行完上面两条语句后,函数栈内的情况如图所示。
add()函数参数传递与调用
地址40103C处为“Call 401000”命令,该命令用于调用401000处的函数即为add()函数。代码中函数add()接收a、b这2个长整型参数,所以调用add()之前需要先把2个参数压入栈,地址401034~40103B之间的代码即用于此。这一过程中需要注意,参数入栈的顺序与C语言源码中的参数顺序恰好相反(函数参数的逆向存储)。换言之,变量b([EBP-8])首先入栈,接着变量a([EBP-4])再入栈。执行完地址401034~40103B之间的代码后,栈内情况如图。
![}N]{G~R~17SBAIJNW(3SA5.png
返回地址
执行CALL命令进入被调用的函数之前,CPU会先把函数的返回地址压入栈,用作函数执行完毕后的返回地址。地址40103C处调用了add()函数,它下一条命令的地址为401041.函数add()执行完毕后,程序执行流应该返回到401041地址处,改地址即被称为add()函数的返回地址。执行完40103C地址处的CALL命令后进入函数,栈内情况如图。
删除函数add()的栈帧&函数执行完毕(返回)
执行完加法运算后,要返回函数add(),在此之前先删除函数add()的栈帧。
执行上述RETN命令,存储在栈中的返回地址即被返回,此时栈内情形如图。由图可知,调用栈已经完全返回到调用add()函数之前的状态。
![U@XKQQ{E3U$N4_(05B5MAS.png
应用程序采用上述方式管理栈,不论有多少函数嵌套调用,栈都能得到比较好的维护,不会崩溃。但是由于函数的局部变量、参数、返回地址等是一次性保存到栈中的,利用字符串函数的漏洞等很容易一起栈缓冲区溢出,最终导致程序或系统崩溃。
从栈中删除函数add()的参数(整理栈)
调用printf()函数
![BQKQ1}I700FIL%_AICPILE.png
由于上面的printf()函数有2个参数,大小为8个字节(32位寄存器+32位常量=64位=8字节),所以在4010F地址处使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。
设置返回值
![XS$DKTN~6VUE6JTT7$7V%W.png
XOR命令用来进行Exclusive OR bit(异或)运算,其特点为“2个相同的值进行XOR运算,结果为0”。XOR命令比MOV EAX,0命令执行速度快,常用于寄存器的初始化操作。
提示:利用相同的值连续进行两次XOR运算即变为原值,这个特征被大量应用于编码与解码。
删除栈帧&main()函数终止
最终主函数终止执行,同add()函数一样,其返回前要先从栈中删除与其对应的栈帧。
与main()函数开始执行时栈内情形完全一致。
![A9YZQ%$(}`KQD{NJ(_EN4O.png
执行完上面命令后,主函数执行完毕并返回,程序执行流跳转到返回地址处(401250),该地址指向Visual C++的启动函数区域。随后执行进程终止代码。