C语言 栈帧
C语言 栈帧
一、栈帧
简单来说,栈帧就是利用EBP(栈帧指针)寄存器访问栈内局部变量、参数、函数返回地址等的手段。比如在某函数时,该函数的在栈内分配空间存放临时数据,那么EBP存放的就是片空间的基址(首地址)。(ESP寄存器通常指向栈顶位置)。
文字叙述栈帧过程:
每当开始调用一个函数,就会在*开辟一段属于该函数的栈空间,
也就是说,该函数的临时变量都是存放在这一段内存空间的。
函数执行完毕,这一段内存空间就会被系统回收
(就是系统可以将此片内存空间分给其他应用程序,或其他函数等使用)。
用一个寄存器保存这一段内存空间的固定一端的地址(称为基址),
另外一端可以适当拓展,称为栈顶,也将此地址保存在一个寄存器中。
访问函数内的局部变量,我们可以通过,基址或栈顶的偏移位置进行访问。
举例a函数调用b函数,当执行到b函数函数时,
需要将a函数的基址保存到*,执行完b函数还需要回到a函数的。
然后将当前栈顶作b函数的基址,
栈顶继续向上拓展,b函数的申请的临时变量就是在新的栈空间中。
当b函数执行完毕,栈基址,栈顶位置都要恢复到先前a函数的状态,
栈顶回到b函数时的基址,
此时栈顶存放的就是之前a函数的基址,
出栈,并将此值作为栈基址。
到此恢复为a函数时的栈状态,
继续执行a函数
二、 简单的a+b程序。
C代码
#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;
}
对应汇编部分代码
二、分析(对应C代码与汇编代码,观察寄存器值与栈内存)
假设main()函数调用前
ESP值:0012FF44 栈顶(当前指针位置)
EBP值:0012FF88 基址
注:ESP,EBP存放的是地址
1、从main函数开始分析
int main(int argc,char* argv[])
{
从main()函数运行开始就生成与其对应的函数栈帧
00401020 PUSH EBP
含义:将EBP值压入栈。此时EBP寄存器的值是启动函数(main()函数之前)的栈帧,将其备份到栈中,等待main()函数执行完,EBP值会恢复。(PUSH动作栈顶指针往上一个单位)
此时:
ESP值:0012FF40
EBP值:0012FF88
地址 | 值
0012FF40 | 0012FF88
00401021 MOV EBP ESP
含义:将ESP值传送到EBP寄存器。将当前栈顶位置作为要执行函数的--main()函数的基址。至此main()函数的栈帧就生成了(设置好main()的基址--EBP值)。
此时:
ESP值:0012FF40
EBP值:0012FF40
地址 | 值
0012FF40 | 0012FF88
2、设置局部变量
long a = 1,b = 2;
在栈中为局部变量分配空间
00401023 SUB ESP,8
含义:将ESP值减去8个字节。(a,b各占4个字节)
此时:
ESP值:0012FF38
EBP值:0012FF40
地址 | 值
0012FF40 | 0012FF88
00401026 MOV DWORD PTR SS:[EBP-4],1
0040102D MOV DWORD PTR SS:[EBP-8],2
含义:将1赋值到 以EBP-4为首地址的4个字节;
将2赋值到 以EBP-8为首地址的4个字节。
MOV DOWRD PTR SS:[EBP-4]可以看做C语言的*(DWORD*)(EBP-4)。其中DWORD占4个字节,SS为栈段(stack segment),EBP存放的是一个地址,EBP-4当然也是一个地址。
此时:
ESP值:0012FF38
EBP值:0012FF40
地址 | 值
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
3、add()函数参数传递与调用
printf("%d\n",add(a+b));
其实EAX,ECX的值就是add()函数的形参值,执行CALL命令之前,CPU会把函数的返回地址压入栈,就是函数执行完毕后返回到那个地址继续执行原来。此处将CALL命令后一个命令的地址(00401041)压入栈。
此时:
ESP值:0012FF2C
EBP值:0012FF40
地址 | 值
0012FF2C | 00401041
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
4、开始执行add()函数生成栈帧
long add(long a,long b)
{
00401000 PUSH EBP ;将main()函数的栈帧(基址)压入栈
00401001 MOV EBP,ESP ;将栈顶作为add()函数的栈帧(基址)
此时:
ESP值:0012FF28
EBP值:0012FF28
地址 | 值
0012FF28 | 0012FF40
0012FF2C | 00401041
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
5、设置add()函数的局部变量
long x = a,y = b;
00401003 SUB ESP,8 ;在栈中为x,y分配8字节的空间
00401006 MOV EAX,DWORD PTR SS:[EBP+8] ;形参a
00401009 MOV DWORD PTR SS:[EBP-8],EAX ; 局部变量x
0040100C MOV ECX,DWORD PTR SS:[EBP+C] ;形参b
0040100F MOV DWORD PTR SS:[EBP-4],ECX ;局部变量y
此时:
ESP值:0012FF20
EBP值:0012FF28
地址 | 值
0012FF20 | 00000001
0012FF24 | 00000002
0012FF28 | 0012FF40
0012FF2C | 00401041
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
6、ADD运算
return (x+y);
00401012 MOV EAX,DWORD PTR SS:[EBP-8] ;将x赋值到EAX寄存器
00401015 ADD EAX,DWORD PTR SS:[EBP-4] ;将y与EAX原值相加,结果存到EAX寄存器中。
7、删除函数add()栈帧&函数返回
return (x+y);
00401018 MOV ESP,EBP ;恢复main()函数时的ESP
此时:
ESP值:0012FF28
EBP值:0012FF28
地址 | 值
0012FF28 | 0012FF40
0012FF2C | 00401041
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
0040101A POP EBP ;恢复main()函数时的EBP。(将栈顶值POP到EBP)
此时:
ESP值:0012FF2C
EBP值:0012FF40
地址 | 值
0012FF2C | 00401041
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
0040101B RETN ;返回,继续执行下一指令
此时:
ESP值:0012FF30
EBP值:0012FF40
地址 | 值
0012FF30 | 00000001
0012FF34 | 00000002
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
8、从栈中删除函数add()d的参数
00401041 ADD ESP,8 ;下移当前指针 (相当与将参数a,b出栈)
此时:
ESP值:0012FF38
EBP值:0012FF40
地址 | 值
0012FF38 | 00000002
0012FF3C | 00000001
0012FF40 | 0012FF88
至此,完全栈恢复到执行add()函数之前,继续执行
9、调用printf()函数
printf("%d\n",add(a,b));
00401044 PUSH EAX ;将add()函数的返回值压入栈,参数2
00401045 PUSH 0040B384 ;将"%d\n"压入栈,参数1
0040104A CALL 00401067 ;调用printf()函数
0040104F ADD ESP,8 ;调用printf()函数结束后,将其两个参数出栈(栈顶指针往回移,相当于出栈)
10、设置返回值(main()函数的)
return 0;
00401052 XOR EAX,EAX ;XOR异或运算,两个相同值异或结果为0,。(XOR命令比MOV EAX,0命令执行的速度快)
11、删除栈帧&main()函数结束
}
00401054 MOV ESP,EBP ;恢复main()函数前的栈顶指针
00401054 POP EBP ;恢复main()函数前的栈帧
此时:
ESP值:0012FF44 栈顶(当前指针位置)
EBP值:0012FF88 基址
00401057 RETN ;main()函数结束返回,继续执行下一指令
三、总结
一般情况下,CALL命令会将函数返回地址压入栈,RETN命令会将出栈(弹出的就是函数的返回地址)回到调用该函数者的下一指令地址。函数的参数从右往左压入栈。
函数执行前会保存当前的栈帧
PUSH EBP
MOV EBP,ESP
函数执行结束会恢复上一栈帧
MOV ESP,EBP
POP EBP
上一篇: c函数调用过程(栈帧)详解
下一篇: 划分树 详解及模板