什么是栈帧

栈帧就是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量,参数,函数返回地址的手段,在IA-32寄存器中,ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行使栈帧指针的职能。

但是在程序运行的过程中,ESP寄存器的值随时发生变化,访问栈中函数的局部变量,参数时候,若以ESP值为基准编写程序会产生困难,并且难使CPU引用到准确的值。所以引入了栈帧。

在调用某些函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP,并维持在函数内部,这样无论ESP的值如何变化,以EBP的值作为基准(base)就能够安全访问到相关函数的局部变量,参数,返回地址。

分析栈帧

# 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;
}

main() 函数产生栈帧

int main(int argc, char*argv[])
00401020         55                    push ebp
00401021 8BEC mov ebp,esp

main() 函数局部变量

long a=1, b=2;
00401023         83EC 08               sub esp,0x8
00401026 C745 FC 01000000 mov dword ptr ss:[ebp-0x4],0x1
0040102D C745 F8 02000000 mov dword ptr ss:[ebp-0x8],0x2

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

printf("%d/n",add(a,b));
00401034         8B45 F8               mov eax,dword ptr ss:[ebp-0x8]
00401037 50 push eax
00401038 8B4D FC mov ecx,dword ptr ss:[ebp-0x4]
0040103B 51 push ecx
0040103C E8 BFFFFFFF call StackFra.00401000

开始执行add()函数 & 生成栈帧

long add(long a,long b)
00401000         55                    push ebp
00401001 8BEC mov ebp,espa

设置 add()函数的局部变量(x,y)

long x=a, y=b;
00401003         83EC 08               sub esp,0x8
00401006 8B45 08 mov eax,dword ptr ss:[ebp+0x8] ;[ebp+8]=param a
00401009 8945 F8 mov dword ptr ss:[ebp-0x8],eax ;[ebp-8]=local x
0040100C 8B4D 0C mov ecx,dword ptr ss:[ebp+0xC] ;[ebp-8]=local x
0040100F 894D FC mov dword ptr ss:[ebp-0x4],ecx ;[ebp-4]=local y

注意:多个函数栈帧的生成:

主函数会参数栈帧,程序运行到其他函数的时候也会产生栈帧,栈帧生成之后 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
00401015 0345 FC add eax,dword ptr ss:[ebp-0x4] [ebp-4]=local y

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

return (x+y)
00401018         8BE5                  mov esp,ebp
0040101A 5D pop 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
00401045 68 84B34000 push StackFra.
0040104A E8 18000000 call StackFra.00401067
0040104F 83C4 08 add esp,0x8

printf() 函数有两个参数,大小都是八个字节,(32位寄存器+32位常量=64位=8字节),所以在40104F地址处使用ADD命令,将ESP加上8个字节,把函数的参数从栈中删除。

此时栈内地址如下:

EBP-8    0012FF38      00000002
EBP-4 0012FF3C 00000001
EBP ==> 0012FF40 /0012FF88
EBP+4 0012FF44 |00401250 返回到 StackFra.00401250 来自 StackFra.00401020
EBP+8 0012FF48 |00000001
EBP+C 0012FF4C |001E18A8 返回到 001E18A8
EBP+10 0012FF50 |001E18F8

设置返回值

return 0;
00401052         33C0                  xor eax,eax

汇编将数置零的操作:

  1. MOV EAX,0

  2. XOR EAX,EAX ;两个相同的值进行异或运算,,结果为零

异或运算的执行速度较快,常用于寄存器的初始化操作。

删除栈帧 & main() 函数终止

return 0;
}
00401054         8BE5                  mov esp,ebp
00401056 5D pop ebp
00401057 C3 retn

此时主函数执行完毕并且返回,程序执行流程跳转到地址(401250),该地址指向 Visual C++ 的启动函数区域,随后执行进程终止代码。

2020-10-17

⬆︎TOP