栈帧就是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量、参数、函数返回地址等的手段。
调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP中,并维持在函数内部。
这样无论ESP的值如何变化,以EBP的值作为基准(base)能够安全访问到相关函数的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。
栈帧对应的汇编代码:
PUSH EBP ;函数开始(使用EBP前先把已有值保存到栈中)
MOV EBP,ESP ;保存到当前ESP到EBP中
... ;函数体
;无论ESP值如何变化,EBP都保持不变,可以安全访问函数的局部变量、参数
MOV ESP, EBP ;将函数的起始地址返回到ESP中
POP EBP ;函数返回前弹出保存在栈中的EBP值
RETN ;函数终止
#include<stdio.h> long add(long a ,long b) { long x = a , y = b; return (x+y); } int main(int argc, char* arg[]) { long a = 1 , b = 2 ; printf("%d ",add(a,b)); return 0; }
0x1:开始执行main()函数&生成栈帧
int main(int argc , char* argv[]) {
函数main()是程序开始执行的地方。开始执行main()函数时栈的状态如图所示:
切记地址401279保存在ESP(0012FF84)中,它是main函数执行完毕后要返回的地址。
main()函数一开始运行就生成与其对应的函数栈帧。
PUSH是一条压栈指令。“把EBP值压入栈中”。
main()函数中,EBP为栈帧指针,用来把EBP之前的值备份到栈中(main()函数执行完毕,返回之前,该值会再次恢复)。
MOV是一条传送数据指令,上面这条MOV语句的命令是“把ESP值传送到EBP”。
从这条命令开始,EBP就持有与当前ESP相同的值,并且直到main()函数执行完毕,EBP的值始终保持不变。
执行完这两条命令后,main()函数的栈帧就生成了(设置好EBP了)。
进入OllyDbg的栈窗口,单击鼠标右键,选择Adress-Relative to EBP
当前EBP值为12FF80,与ESP值一致,12FF80地址处保存着12FFC0,它是main()函数开始执行时EBP持有的初始值。
0x2:
汇编代码详解:
00401063 |. 83EC 48 sub esp,0x48 //为函数的局部变量申请一段空间 00401066 |. 53 push ebx 00401067 |. 56 push esi //寄存器压栈,保存现场 00401068 |. 57 push edi 00401069 |. 8D7D B8 lea edi,dword ptr ss:[ebp - 0x48] //将局部变量的堆栈中开始地址保存到edi寄存器 0040106C |. B9 12000000 mov ecx,0x12 //将重复执行指令的次数放到ecx 00401071 |. B8 CCCCCCCC mov eax,0xCCCCCCCC //初始化eax 00401076 |. F3:AB rep stos dword ptr es:[edi] //用eax中的值初始化到es:[edi]指向的地址,长度为dword,循环执行次数为ecx中的值
详解:http://blog.csdn.net/ypist/article/details/8467163
rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.
REP的作用是根据ecx的值,重复执行后面的串传送指令。
REP能够引发其后的字符串指令被重复, 只要ecx的值不为0, 重复就会继续.
每一次字符串指令执行后, ecx的值都会减小.
如果设置了direction flag, 那么edi会在该指令执行后减小,
如果没有设置direction flag, 那么edi的值会增加.
stos((store into String),意思是把eax的内容拷贝到目的地址。
用法:stos dst,dst是一个目的地址,例如:stos dword ptr es:[edi]。dword ptr前缀告诉stos,一次拷贝双字(4个字节)的数据到目的地址。为什么一次非要拷贝双字呢?这和eax寄存器有关。
执行stos之前必须往eax(32为寄存器)放入要拷贝的数据。上图中,eax的内容是cccccccc,意思是int3中断。
这段代码是初始化堆栈和分配局部变量用的,往分配好的局部变量空间放入int3中断的原因是:防止该空间被意外执行。这样发生意外时执行堆栈里面的内容会引发调试中断。
0x12*4 (字节)刚好是为局部变量申请的那段空间的大小。
执行完后栈中的情况:
0x3:设置局部变量
long a = 1 , b = 2;
上面两条MOV指令的含义“把数据1与2分别保存到[ebp - 0x4]与[ebp - 0x8]中”,即[[ebp - 0x4]代表局部变量a,[ebp - 0x8]代表局部变量b。
执行完上面两条语句后,函数栈内的情况如下:
【提示】:
DWORD PTR SS:[EBP-0x4]语句中,SS是Stack Segment 的缩写,表示栈段。由于Windows中使用的是段内存模型(Segment Memory Model),使用时需要指出相关内存属于哪一个段区。其实,32位的Windows OS中,SS、DS、ES 的值均为0,所以采用这种方式附上区段并没有什么意义。因EBP与ESP是指向栈的寄存器,所以添加了SS寄存器。
0x4:add()函数参数传递与调用
printf(“%d ”,add(a,b));
0040108E 处的CALL 401005命令,该命令用于调用401005处的函数,而401005处的函数即为add()函数。函数add()接收a、b这两个长整型参数,所以调用add()之前需要把2个参数压入栈。
参数入栈的顺序与C语言源码中的参数顺序恰好相反。换言之,变量b([EBP - 0x8])首先入栈,接着变量a([EBP - 0x4])再入栈。
执行完地址00401086-0040108D之间的行代码后,栈内情况:
接下来进入add()函数(00401005)内部,分析整个函数调用过程。
返回地址
执行CALL命令进入被调用的函数之前,CPU会先把函数的返回地址压入栈,用作函数执行完毕后的返回地址。
由图中可知,在地址0040108E处掉调用了add()函数,他的下一条命令的地址为00401093。函数add()执行完毕后,程序执行流应该返回到00401093地址处,该地址即被被称为add()函数的返回地址。执行完0040108E地址处的CALL命令后进入该函数,栈内情况如图:
间接调用
00401093地址处的CALL 00401005命令用于调用add()函数,不是直接转到add()函数,而是通过通过中间地址00401005地址处的JMP命令跳转。
0x5:开始执行add()函数&生成栈帧
long add(long a, long b) {
函数开始执行时,栈中会单独生成与其对应的栈帧。
上面2行代码与开始执行main()函数时的代码完全相同,先把EBP值(main()函数的基址指针)保存到栈中,再把当前ESP存储到EBP中,这样函数add()的栈帧就生成了。
可以看到main()函数使用的EBP值(12FF80)被备份到栈中,然后EBP的值被设置为一个新值0012FF1C。
0x6:设置add()函数内部的局部变量(x,y)
long x = a, y = b;
上面一行语句声明了2个长整型的局部变量(x,y),并使用2个形式参数(a,b)分别为他们赋初始值。密切关注形参与局部变量在函数内部以何种方式表示。
00401023 |. 83EC 48 sub esp,0x48 //为局部变量申请一段空间 00401026 |. 53 push ebx 00401027 |. 56 push esi //寄存器压栈,保存现场 00401028 |. 57 push edi 00401029 |. 8D7D B8 lea edi,dword ptr ss:[ebp - 0x48] 0040102C |. B9 12000000 mov ecx,0x12 00401031 |. B8 CCCCCCCC mov eax,0xCCCCCCCC 00401036 |. F3:AB rep stos dword ptr es:[edi] //将 为局部变量开辟的这段空间设置为CC (int 3 中断)
密切关注形式参数与局部变量在函数内部以何种方式表示
add函数的栈帧生成之后,EBP的值发生了变化,[EBP+8]与[EBP+C]分别指向参数a 和 参数b,而[EBP - 4] 与 [EBP - 8] 则分别指向add()函数的2个局部变量x、y。
执行完上述语句后栈内情况如图所示
0x7:ADD运算
return (x + y);
上述MOV语句中,局部变量x的值被传送到eax中。
上面这条语句中,变量y([EBP - 8 ] = 2)与 原EAX值(x)相加,且运算结果被存储在EAX中,运算完成后EAX中的值为3。
EAX是一种通用寄存器,在算数运算中存储输入输出数据,为函数提供返回值。函数返回时,若像EAX中输入某个值,该值就会原封不动的返回。执行运算的过程中栈内情况保持不变。
0x8: 删除函数add()的栈帧&函数执行完毕(返回)
return (x + y) }
//恢复现场
这三条指令与00401026 到 00401028之间的4条指令相对应
执行完加运算后,要返回函数add(),在此之前先删除函数add()的栈帧。
上面这条命令把当前EBP的值赋给ESP,与地址00401021处的MOV EBP,ESP命令相对应。在地址00401021处MOV EBP,ESP命令把函数add()开始执行时的ESP值(12FF1C)放入EBP,函数执行完毕时,使用0040104D处的MOV ESP,EBP命令再把存储到EBP中的值恢复到ESP中。
【提示】:执行完上面的命令后,地址00401023处的SUB ESP,0X48 命令就会失效,即函数add()的两个局部变量x,y不再有效。
上面这条命令用于恢复函数add()开始执行时备份到栈中的EBP值,他与00401020处的PUSH EBP命令对应。EBP的值恢复12FF80,它是main()函数的EBP值。到此,add()函数的栈就被删除了。
执行完上述命令后,栈内情形如图所示:
可以看到,ESP的值为12FF20,该地址的值为00401093,它是执行CALL 401005的命令时CPU存储到栈中的返回地址。
执行上述RETN命令,存储在栈中的返回地址即被返回,此时栈内的情形如图所示:
从图中可以看到,调用栈已经完全返回到调用add()函数之前的状态。可以对比0X4中的第一个栈内图。
0x9:从栈中删除函数add()的参数(整理栈)
现在程序执行流已经重新返回main()函数中。
上面语句使用ADD命令将ESP加上8。看上上图,此图中的栈窗口,地址12FF24与12FF28存储的是传递给函数add()的参数a与b。函数add()执行完毕后,就不再需要参数a与b了,所以要把ESP加上8,将他们从栈中清理掉(参数a与b都是长整型,合占4个字节,合起来共8个字节)。
【提示】:调用add()函数之前先使用PUSH命令把参数a、b压入栈。
执行完上述命令后,栈内情况如图所示
0x10:调用printf()函数
printf("%d ",add(a , b));
地址0401096处的EAX寄存器中存储着函数add()的返回值,它是执行假发运算后的结果值3,。地址0040109C处的CALL 004010D0命令中调用的是004010D0地址处的函数,它是一个c标准库函数printf(),所有C标准库函数都有Visual C++编写而成。由于上面的printf()函数有2个参数,大小为8个字节(32位寄存器+32位常量=64位=8字节),所以在004010A1地址处使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。函数printf()执行完毕后通过ADD命令删除参数后,栈内的情形如图所示:
0x11:设置返回值
return 0;
main()函数使用该语句设置返回值(0)。
两个相同的值进行XOR运算结果为0。XOR命令比MOV EAX,0命令执行速度快,常用与寄存器的初始化操作。
0x12:删除栈帧&main()函数终止
return 0 }
最终主函数终止执行,同add()函数一样,其返回前要先从栈中删除与其对应的栈帧。
004010A6 |. 5F pop edi 004010A7 |. 5E pop esi //寄存器出栈,恢复现场 004010A8 |. 5B pop ebx 004010A9 |. 83C4 48 add esp,0x48 //释放局部变量,平衡堆栈 004010AC |. 38EC cmp ebp.esp //检查堆栈是否平衡 004010AE |. EB 9D000000 call StackTra.00401150
执行完上面2两条命令后,main()函数的栈帧即被删除,且其局部变量a、b也不再有效。执行至此,栈内情形如图所示:
执行完毕上面的命令后,主函数执行完毕并返回,程序执行流跳转到返回地址处(00401279),该函数指向Visual C++的启动函数区域。随后执行进程终止代码。