栈帧就是利用EBP(栈帧指针)寄存器访问栈内局部变量、参数、函数返回地址等的手段。通过前面关于IA-32寄存器的学习可知,ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行使栈帧指针的职能。程序运行中,ESP寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以ESP值为基准编写程序会十分困难,并且也很难使CPU引用到准确的地址。所以,调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到ESP,并维持在函数内部。这样,无论ESP的值如何变化,以EBP的值为基准(base)能够安全访问到相关的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。借助栈帧技术管理函数调用时,无论函数调用的深度有多深、多复杂,调用栈都能得到好的管理和维护。
提示:1.最新的编译器中都带有一个“优化”(Optimization)选项,使用该选项编译简单的函数将不会生成栈帧。
2.在栈中保存函数返回地址是系统安全隐患之一,攻击者使用缓冲区溢出技术能够把保存在栈内存的返回地址更改为其他地址。

调试示例:stackframe.exe

OJ9`_I{FG2T~${KZ)@2%9GF.png
I}Q80AG17}U]RTX8{]{%]8Y.png

设置局部变量

[7I[7%}U{IYRQTETM@75SC7.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581598993366-df4bd184-a71d-4e48-b738-e3f9bb9bf456.png#align=left&display=inline&height=61&name=%5B7I%5B7%25%7DU%7BIYRQTETM%4075SC7.png&originHeight=61&originWidth=488&size=14484&status=done&style=none&width=488)<br />![])H([V{(LM8K{FMS1BZ7}Y4.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581599192942-70d61069-fd7e-49b4-9ce2-d00b03a4d3e8.png#align=left&display=inline&height=20&name=%5D%29H%28%5BV%7B%28LM8K%7BFMS1BZ7%7DY4.png&originHeight=20&originWidth=173&size=4047&status=done&style=none&width=173)<br />执行该命令前,ESP的值为12FF40,减去8个字节后变为12FF38.从ESP减去8个字节,实质是为函数的局部变量(a与b)开辟空间,以便将其保存在栈中。由于局部变量a与b都是long型,它们分别占据4个字节大小,所以需要在栈中开辟8个字节的空间来保存这2个常量。<br />使用SUB指令为2个函数变量开辟好栈空间后,在main()内部,无论ESP的值如何变化,变量a与b的栈空间都不会受到损坏。由于EBP的值在main()函数内部是固定不变的,我们就能以它为基准来访问函数的局部变量。<br />![~T]0X5)P$5}CI`PAF%CDZ_J.png
分析两条MOV命令,其含义为“将数据1与2分别保存到[EBP-4]与[EBP-8]中”,即[EBP-4]代表局部变量a,[EBP-8]代表局部变量b。执行完上面两条语句后,函数栈内的情况如图所示。
DO5XW@S}_7_8SB5ACP$MPG5.png

add()函数参数传递与调用

77VRSKM`{OXK~`LK1(P_WK2.png
地址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命令后进入函数,栈内情况如图。
YNC@3Y~ZB(0AF3D_@8EB9JH.png

删除函数add()的栈帧&函数执行完毕(返回)

OF(JVRQ}04P_P_V%ND~P~`6.png
执行完加法运算后,要返回函数add(),在此之前先删除函数add()的栈帧。
AQG_F{9P$7V$2STZ46M~[@D.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581603893014-09811d08-0c8f-443d-80cc-667c45c506a8.png#align=left&display=inline&height=26&name=AQG_F%7B9P%247V%242STZ46M~%5B%40D.png&originHeight=26&originWidth=237&size=5154&status=done&style=none&width=237)<br />此命令将当前EBP的值赋给ESP,与地址401001处的MOV EBP,ESP命令相对应。在地址401001处,MOV EBP,ESP命令把函数add()开始执行时的ESP值(12FF28)放入EBP,函数执行完毕时,使用401018处的MOV ESP,EBP命令再把存储到EBP中的值恢复到ESP中。<br />提示:执行完上面的命令后,地址401003处的SUB EBP,8就会失效,即函数add()的2个局部变量x、y不再有效。<br />![4[CIA`RAQ$@[E)HA_[$__I3.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581605260232-7c5214e6-abe8-4b41-8bbd-b94891f64204.png#align=left&display=inline&height=23&name=4%5BCIA%60RAQ%24%40%5BE%29HA_%5B%24__I3.png&originHeight=23&originWidth=197&size=4553&status=done&style=none&width=197)<br />该命令用于恢复函数add()开始执行时备份到栈中的EBP值,它与401000地址处的PUSH EBP命令对应。EBP值恢复为12FF40,是main()函数的EBP值。到此add()函数的栈帧就被删除了。执行完上述命令后栈内情况如图。<br />![XE0F}5M`7SOF7_Y30UV028M.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581605762335-5932608f-1d8b-4a19-ad50-a795c6cabc07.png#align=left&display=inline&height=222&name=XE0F%7D5M%607SOF7_Y30UV028M.png&originHeight=222&originWidth=352&size=51224&status=done&style=none&width=352)<br />由图可见,ESP的值为12FF2C,改地址的值为401041,它是执行CALL 401000命令时CPU存储到栈中的返回地址。<br />![G5JG](FHN893YVH]ESD}IZK.png
执行上述RETN命令,存储在栈中的返回地址即被返回,此时栈内情形如图。由图可知,调用栈已经完全返回到调用add()函数之前的状态。
![U@XKQQ{E3U$N4_(05B5MAS.png
应用程序采用上述方式管理栈,不论有多少函数嵌套调用,栈都能得到比较好的维护,不会崩溃。但是由于函数的局部变量、参数、返回地址等是一次性保存到栈中的,利用字符串函数的漏洞等很容易一起栈缓冲区溢出,最终导致程序或系统崩溃。

从栈中删除函数add()的参数(整理栈)

现在程序执行流已经重新返回main()函数中。
AI)J3GX4LEDXA[SMLT}%@1G.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581754070548-ae720bd6-da9f-434a-ac30-46e357c76557.png#align=left&display=inline&height=23&name=AI%29J3GX4LEDXA%5BSMLT%7D%25%401G.png&originHeight=23&originWidth=201&size=4392&status=done&style=none&width=201)<br />上述语句使用ADD命令将ESP加上8,地址12FF30与12FF34处存储的是传递给add()函数的参数a与b。函数add()执行完毕后,就不再需要参数a和b了,所以要把ESP加上8,将他们从栈中清理掉。执行后栈内情况如图。<br />![671D(~721L_W0U1])`)_GCD.png

调用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()函数终止

]10VZQQ4_GZT)@LH85M}(5Q.png
最终主函数终止执行,同add()函数一样,其返回前要先从栈中删除与其对应的栈帧。
AWL[}%8~SSPKT2[}P[LW(PL.png](https://cdn.nlark.com/yuque/0/2020/png/554486/1581756040931-abd81997-b083-4bcc-b492-6e465f29b423.png#align=left&display=inline&height=34&name=AWL%5B%7D%258~SSPKT2%5B%7DP%5BLW%28PL.png&originHeight=34&originWidth=222&size=9132&status=done&style=none&width=222)<br />执行以上命令后,main()函数的栈帧即被删除,且其局部变量a、b也不再有效。执行至此,栈内情形如图。<br />![K{GA4Z]70_Z%ZE1XBLXRSI7.png
与main()函数开始执行时栈内情形完全一致。
![A9YZQ%$(}`KQD{NJ(_EN4O.png
执行完上面命令后,主函数执行完毕并返回,程序执行流跳转到返回地址处(401250),该地址指向Visual C++的启动函数区域。随后执行进程终止代码。