欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言 栈帧

程序员文章站 2022-07-07 12:09:54
...

C语言 栈帧

一、栈帧

   简单来说,栈帧就是利用EBP(栈帧指针)寄存器访问栈内局部变量、参数、函数返回地址等的手段。比如在某函数时,该函数的在栈内分配空间存放临时数据,那么EBP存放的就是片空间的基址(首地址)。(ESP寄存器通常指向栈顶位置)。

C语言 栈帧

文字叙述栈帧过程:

每当开始调用一个函数,就会在*开辟一段属于该函数的栈空间,
也就是说,该函数的临时变量都是存放在这一段内存空间的。
函数执行完毕,这一段内存空间就会被系统回收
(就是系统可以将此片内存空间分给其他应用程序,或其他函数等使用)。
用一个寄存器保存这一段内存空间的固定一端的地址(称为基址),
另外一端可以适当拓展,称为栈顶,也将此地址保存在一个寄存器中。
访问函数内的局部变量,我们可以通过,基址或栈顶的偏移位置进行访问。
举例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语言 栈帧

二、分析(对应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动作栈顶指针往上一个单位)

C语言 栈帧

 

此时:

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));

C语言 栈帧

其实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

 

相关标签: 栈帧