栈帧
- 栈帧就是利用EBP寄存器访问栈内局部变量、参数、函数返回地址等的手段。
- 栈帧对应汇编代码
PUSH EBP
MOV EBP,ESP
...
MOV ESP,EBP
POP EBP
RETN
调试示例:stack frame.exe
- Stack Frame.cpp
#include "stdio.h"
long add(long a, long b)
{
long x = a, y = b;
return (x + y);
}
int main(int argc, char* argv[])
{
long a = 1, b = 2;
printf("%d\n", add(a, b));
return 0;
}
- 使用OllyDbg调试工具打开Stack Frame.exe文件,按Ctrl+G快捷键转到401000地址处。
- 函数main是程序开始执行的地方,在main函数的起始地址(401020)处,按F2键设置一个断点,然后按F9运行程序,程序运行到main函数的断点处暂停。
- 此时ESP的值为0019FF2C,EBP的值为0019FF70,返回地址00401250保存在ESP(0019FF2C)中。
00401020 PUSH EBP
- 把EBP值压入栈,main函数中EBP为栈帧指针,用来把EBP之前的值备份到栈中。
00401021 MOV EBP,ESP
-
MOV是一条数据传送命令,上面这条MOV语句的命令是把ESP的值传送到EBP。
-
执行完上面这两条语句,函数main的栈帧就生成了。
-
进入OllyDbg的栈窗口,点击右键Address->Relative to EBP,把地址转换成相对EBP的偏移后,可以看到当前EBP的值为0019FF70,跟ESP的值一样。
- 下面开始分析变量声明及赋值语句。
long a = 1, b = 2;
- main函数中,上述语句用于在栈中为局部变量(a,b)分配空间,并赋初始值。
00401023 SUB ESP,8
- SUB是汇编语言中的一条减法指令,上面这条语句用来将ESP的值减去8个字节,减去之后ESP的值变为0019FF20,ESP减去8个字节,实质是为函数的局部变量(a与b)开辟空间,以便将它们保存在栈中,由于局部变量a与b都是long型,它们分别占据4个字节,所以需要在栈中开辟8个字节的空间来保存这2个变量。
00401026 |. C745 FC 010000>MOV DWORD PTR SS:[EBP-4],1
0040102D |. C745 F8 020000>MOV DWORD PTR SS:[EBP-8],2
- 把数据1与2分别保存到[EBP-4]与[EBP-8]中,即[EBP-4]代表局部变量a,[EBP-8]代表局部变量b。
- add函数参数传递与调用
printf("%d\n", add(a,b));
00401034 |. 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8]
00401037 |. 50 PUSH EAX ; /Arg2
00401038 |. 8B4D FC MOV ECX,DWORD PTR SS:[EBP-4] ; |
0040103B |. 51 PUSH ECX ; |Arg1
0040103C |. E8 BFFFFFFF CALL StackFra.00401000 ;
- 地址40103C处为”Call 401000“命令,该命令用于调用401000处的函数,而401000处的函数即为add()函数。
- 函数add()接收a、b这2个长整型参数,所以调用add()之前需要先把这2个参数压入栈,地址401034~40103B之间的代码即用于此。
- 执行完00401034~0040103B,栈内情况如下:
- 执行CALL指令进入被掉用的函数之前,CPU会先把函数的返回地址压入栈,用作函数执行完毕后的返回地址,在地址40103C处调用了add()函数,它的下一条命令的地址为401041,函数add()执行完毕后,程序执行流应该返回到401041地址处,该地址即被称为add()函数的返回地址。
- 开始执行add()函数&生成栈帧
- 函数开始执行时,栈中会单独生成与其对应的栈帧。
00401000 PUSH EBP
00401001 MOV EBP,ESP
- 代码与开始执行main()函数时的代码完全相同,先把EBP值保存到栈中,再把当前ESP存储到EBP中,这样函数add()的栈中就生成了。
- main函数使用的EBP的值(0019FF28)被备份到栈中,然后EBP的值被设置为一个新值0019FF10.
- 设置add()函数的局部变量(x,y)
00401003 SUB ESP,8
- 在栈内存中为局部变量x、y开辟8个字节的空间。
00401006 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8]
00401009 |. 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX
0040100C |. 8B4D 0C MOV ECX,DWORD PTR SS:[EBP+C]
0040100F |. 894D FC MOV DWORD PTR SS:[EBP-4],ECX
- [EBP+8]与[EBP+C]分别指向参数a与b,而[EBP-8]与[EBP-4]分别指向add函数的2个局部变量x与y。
- ADD运算
00401012 |. 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8]
- 变量x的值被传送到EAX中。
00401015 |. 0345 FC ADD EAX,DWORD PTR SS:[EBP-4]
- ADD指令为加法指令,上面这条语句中,变量y与EAX原值(x)相加,且运算结果被存储到EAX中,运算完成后EAX中的值为3.
- 删除函数add()的栈中&函数执行完毕
- 执行加法运算后,要返回函数add(),在此之前删除函数add()的栈帧。
00401018 MOV ESP,EBP
- 上面这条命令是把EBP的值赋给ESP,与地址40100处的MOV EBP,ESP命令向对应。
0040101A POP EBP
- 回复add函数开始执行时备份到栈中的EBP的值,它与401000地址处的PUSH EBP相对应,EBP值恢复为0019FF28,它是main函数的EBP值,到此,add函数的栈帧就被完全删除了。
- 此时ESP的值为00401041,它是执行CALL 401000命令时CPU存储到栈中的返回地址
0040101B RETN
- 执行RETN命令,存储在栈中的返回地址即被返回,调用栈已经返回调用add函数之前的状态。
- 从栈中删除函数add()的参数
- 程序执行流已经重新返回main函数中。
00401041 |. 83C4 08 ADD ESP,8
- 0019FF18与0019FF1C处存储的时传递给函数add的参数a与b,函数add执行完毕后,就不再需要参数a与b了,所以要把ESP+8把它们从栈中清理掉,执行完上述命令后,栈内情况。
- 调用printf()函数
printf("%d\n", add(a,b));
- 汇编代码如下:
00401044 |. 50 PUSH EAX
00401045 |. 68 84B34000 PUSH StackFra.0040B384 ; ASCII "%d"
0040104A |. E8 18000000 CALL StackFra.00401067
0040104F |. 83C4 08 ADD ESP,8
- 地址401044处的EAX寄存器中存储着add的返回值,它是执行加法运算后的结果值3,地址40104A处的CALL 401067命令中调用的401067地址处的函数,它是一个C标准库函数printf(),由于上面的printf()函数有2个参数,大小8个字节,所以在0040104F地址使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。
- 设置返回值
return 0;
- XOR命令用来进行异或运算,其特点时2个相同的值进行XOR运算,结果为0,XOR命令比MOV EAX,0命令执行速度块,常用于寄存器的初始化操作。
- 删除栈帧&main函数终止
- 最终主函数终止执行,同add()函数一样,其返回前要先从栈中删除与其对应的栈中。
00401020 /$ 55 PUSH EBP
00401021 |. 8BEC MOV EBP,ESP
- main函数的栈帧即被删除,且局部变量a、b也不再有效,执行至此,栈内情景如图.
00401057 \. C3 RETN
- 主函数执行完毕返回,程序执行流跳转返回地址00401250处,该地址执行Visual C++的启动函数区域,随后执行进程终止代码。