Stack frame
什么是栈帧
栈帧就是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量,参数,函数返回地址的手段,在IA-32寄存器中,ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行使栈帧指针的职能。
但是在程序运行的过程中,ESP寄存器的值随时发生变化,访问栈中函数的局部变量,参数时候,若以ESP值为基准编写程序会产生困难,并且难使CPU引用到准确的值。所以引入了栈帧。
在调用某些函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP,并维持在函数内部,这样无论ESP的值如何变化,以EBP的值作为基准(base)就能够安全访问到相关函数的局部变量,参数,返回地址。
分析栈帧
|
main() 函数产生栈帧
int main(int argc, char*argv[]) |
00401020 55 push ebp |
main() 函数局部变量
long a=1, b=2; |
00401023 83EC 08 sub esp,0x8 |
add()函数参数传递与调用
printf("%d/n",add(a,b)); |
00401034 8B45 F8 mov eax,dword ptr ss:[ebp-0x8] |
开始执行add()函数 & 生成栈帧
long add(long a,long b) |
00401000 55 push ebp |
设置 add()函数的局部变量(x,y)
long x=a, y=b; |
00401003 83EC 08 sub esp,0x8 |
注意:多个函数栈帧的生成:
主函数会参数栈帧,程序运行到其他函数的时候也会产生栈帧,栈帧生成之后 EBP 的值就会发生变化,所以每当程序运行到一个新函数的时候,EBP 都会发生变化。
主函数和其他函数中都存在局部变量,局部变量在引入的时候都要改变 栈中ESP (SS段中栈指针)的位置,为局部变量开辟内存空间。
在上述代码中,
main()
函数的栈帧先生成(ESP-4,栈中填充EBP地址),之后为局部变量 a,b 开辟空间(ESP-4,ESP-8),此时a,b 的实际地址是 [EBP-4] 和 [EBP-8];
add()
函数的栈帧后生成(ESP-4,栈中填充EBP地址),(此时的 EBP 已经发生变化,此时参数a,b 的实际地址是 [EBP+8] 和 [EBP+c], 多的那一部分就是进入栈的 EBP 所占据的内存单元的长度。) 之后为局部变量x,y 开辟内存空间(ESP-4,ESP-8)。此时 x,y 的实际地址是 [EBP-4] 和 [EBP-8]
add运算
return (x+y) |
00401012 8B45 F8 mov eax,dword ptr ss:[ebp-0x8] ;[ebp-8]=local x |
删除函数add()的栈帧 & 函数执行完毕(返回)
return (x+y) |
00401018 8BE5 mov esp,ebp |
图中红色矩形中的命令,在地址401001处,使用 MOV EBP,ESP 命令把函数 add()
开始执行时候的ESP值(12FF28)备份到EBP, 函数执行完毕后,使用地址401018处的 MOV ESP,EBP命令再把存储在EBP中的值恢复到ESP中。
注意:执行完上述命令后,地址401003处的 SUB ESP,8就会失效,函数 add()
的两个局部变量x,y ,不再有效。
从栈中删除函数 add()的参数(整理栈)
此时程序执行流已经回到了主函数main()
00401041 83C4 08 add esp,0x8 |
上述汇编指令的目的是清除在add()函数中给局部变量a,b开辟的内存空间,将他们从栈中清理掉。(由于a,b 都是长整型,各占4个字节,一共把八个字节,所以ESP+8)
调用printf() 函数
printf("%d/n") |
00401044 50 push eax |
printf() 函数有两个参数,大小都是八个字节,(32位寄存器+32位常量=64位=8字节),所以在40104F地址处使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。
此时栈内地址如下:
EBP-8 0012FF38 00000002 |
设置返回值
return 0; |
00401052 33C0 xor eax,eax |
汇编将数置零的操作:
-
MOV EAX,0
-
XOR EAX,EAX ;两个相同的值进行异或运算,,结果为零
异或运算的执行速度较快,常用于寄存器的初始化操作。
删除栈帧 & main() 函数终止
return 0; |
00401054 8BE5 mov esp,ebp |
此时主函数执行完毕并且返回,程序执行流程跳转到地址(401250),该地址指向 Visual C++ 的启动函数区域,随后执行进程终止代码。