栏目
/ Category
联系我们
/ CONTACT US
  • 地 址:江苏省如皋市中山路东方豪都6楼
  • 电 话:0513-87289069/68760932
  • 传 真:0513-87289069
  • 邮 箱:webmaster@jsyyl.com
技术文章|C++函数的堆栈结构分析

一、概述:

高级语言的函数就是汇编语言的子程序。

在主程序和子程序中传递参数有三种常用方法:

1、通过寄存器传递;

2、通过数据区的变量来传递;

3、通过堆栈传递。

前两种方式一般在汇编语言里可以应用实现,在C/C++以及其他高级语言中,函数的参数都是通过堆栈来传递的。C/C++语言中的库函数以及windows API等也都使用堆栈方式来传递参数。本文描述的就是最常见的第三种传递方式。

二、函数调用栈的实现原理:

当编译器为函数调用产生代码时,它首先把所有的参数压栈,然后调用函数。在函数内部产生代码,向下移动栈指针(Esp)为局部变量提供存储单元。

在汇编语言中,有四种主要的方式可以改变栈指针Esp:

1、进栈指令push SRC

功能:栈指针Esp4SRC保存在Esp指向的堆栈单元中。

2、出栈指令pop DST

功能:从Esp指向的堆栈单元中取出数据送到DST中,堆栈指针加4.

3、调用子程序指令call SRC

描述:子程序的入口地址为SRC

功能:call指令将下一条指令的地址(即返回地址)压栈(Esp4),将IP设置为SRC,进入子程序中运行。

4、返回主程序指令ret [SRC]

描述:SRC为可选。带SRC时,SRC必须是一个立即数,返回主程序时,Esp的值要加上SRC

功能:ret指令将返回地址出栈(Esp4),将IP设置为返回地址,回到主程序中运行。

汇编语言中其他的一些指令pushfdpopfdenterleave不过是以上这些指令与mov指令的组合。这里不再赘述。

在这里提供一个在call以后栈框架的样子,此时在函数中已为局部变量分配了存储单元。

三、帧指针和栈指针:

要理解上面的函数堆栈结构,就需要讲解帧指针与栈指针,只有理解了这两个指针,才能彻底理解函数的调用栈框架。

简单地说,帧指针就是Ebp,栈指针就是Esp

几个基本概念:

1、函数调用堆栈的地址是从大到小的方向,即堆栈增长的方向是Esp在不断减少。

2、帧指针是相对不变的,在某个子程序作用区域,有一个固定不变的帧指针Ebp

3、栈指针是不断变化的,栈指针的变化只随着前面四个指令而变化。

4C/C++语言本质上就是一个函数嵌套语言,即系统函数调用main函数,main函数调用其他函数。体现在函数堆栈中就是帧指针的变化。

5、在某一时刻,只有一个函数在运行。当该函数A调用其他函数B时,所有函数必须遵守寄存器用法统一惯例。该惯例指出:

1)寄存器eaxedxecx的内容必须由调用者A自己负责保存。当函数B被函数A调用时,函数B可以在不用保存这些寄存器内容的情况下任意使用它们而不会毁坏函数A所需要的任何数据。

2)寄存器ebxesiedi的内容必须由被调用者B来保护。当被调用者需要使用这些寄存器中的任意一个时,必须首先在栈中保存其内容,并在退出时恢复这些寄存器的内容。因为调用者A(或者一些更高层的函数)并不负责保存这些寄存器内容,但可能在以后的操作中还需要用到原先的值。

3)寄存器ebpesp也必须遵守第二个惯例用法。

子程序的帧指针Ebp1其实就是主程序的栈顶指针Esp,此处保存了主程序的帧指针Ebp0。如果这里感觉很绕,一定是指针基础还不够扎实。来区分下一个概念,一个存储单元空间,有两个属性:1CPU访问这个存储单元需要依赖的地址值;2、这个存储单元所存储的数值。空间地址值和空间内存储的数值,区分这两个值是理解指针概念的基础。子程序的帧指针Ebp1的地址中存储的数值是主程序的帧指针Ebp0,也就是Ebp0=dword ptr[Ebp1],用C++语言表述就是*Ebp1=Ebp0。相应地,如果子程序还引用了子程序,则有*Ebp2=Ebp1C++函数就是通过帧指针和返回地址来设置嵌套函数引用。

 

四、返回地址和函数参数:

子程序的返回地址、函数参数从逻辑上来说仍然处于主程序的堆栈区域,但是可以方便地应用子程序的Ebp1来得到这些地址。即返回地址就是子程序的Ebp1+4,函数参数为Ebp1+8,Ebp1+12,余此类推。

从之前的汇编指令可以看出,调用子程序call指令通过CPU把程序代码中的函数调用指令的地址(严格来说是返回地址,即call指令后面的IP值)压栈,ret指令利用这个返回地址返回到调用点。特别需要指出的是,这个地址是非常重要的,因为没有它,程序将迷失方向。所以为了慎重起见,将该地址设置在不可以更改的堆栈框架中也就很自然了。

五、局部变量:

1、局部变量主要是为了定义一些仅仅在某个子程序内部使用的变量。相对于全局变量,使用局部变量能提高程序的模块化程度。局部变量也可以称为自动变量。局部变量在函数的栈中分配内存。

2、局部变量的实现原理:

(1)在进入子程序的时候,通过修改栈指针Esp(指令sub Esp,x)来预留出需要的空间。

(2)在返回主程序之前,通过恢复Esp释放这些空间,在堆栈中不再为局部变量保留空间。

3、汇编语言中的具体实现:

如图1所示,假定Ebp1为子程序的帧指针,子程序的第一个局部变量的地址为Ebp1-4,第二个局部变量的地址为Ebp1-8,余此类推。

六、举例说明:

1、假设有一段C++程序:

#include "stdafx.h"

int addproc(int a,int b)

{

       return a+b;

}

 

int main(int argc, char* argv[])

{

       int r,s;

       r=addproc(10,20);

       s=addproc(r,-1);

       return 0;

}

从中可以看出main函数中有局部变量rs,它调用了子程序addproc,有两个参数。

2VC++6.0编译的汇编代码:

11:   int main(int argc, char* argv[])

12:   {

0040D6F0   push        ebp

0040D6F1   mov         ebp,esp

0040D6F3   sub         esp,48h

0040D6F6   push        ebx

0040D6F7   push        esi

0040D6F8   push        edi

0040D6F9   lea         edi,[ebp-48h]

0040D6FC   mov         ecx,12h

0040D701   mov         eax,0CCCCCCCCh

0040D706   rep stos    dword ptr [edi]

13:       int r,s;

14:       r=addproc(10,20);

0040D708   push        14h

0040D70A   push        0Ah

0040D70C   call        @ILT+5(addproc) (0040100a)

0040D711   add         esp,8

0040D714   mov         dword ptr [ebp-4],eax

15:       s=addproc(r,-1);

0040D717   push        0FFh

0040D719   mov         eax,dword ptr [ebp-4]

0040D71C   push        eax

0040D71D   call        @ILT+5(addproc) (0040100a)

0040D722   add         esp,8

0040D725   mov         dword ptr [ebp-8],eax

16:       return 0;

0040D728   xor         eax,eax

17:   }

0040D72A   pop         edi

0040D72B   pop         esi

0040D72C   pop         ebx

0040D72D   add         esp,48h

0040D730   cmp         ebp,esp

0040D732   call        __chkesp (004010e0)

0040D737   mov         esp,ebp

0040D739   pop         ebp

0040D73A   ret

 

5:    int addproc(int a,int b)

6:    {

00401010   push        ebp

00401011   mov         ebp,esp

00401013   sub         esp,40h

00401016   push        ebx

00401017   push        esi

00401018   push        edi

00401019   lea         edi,[ebp-40h]

0040101C   mov         ecx,10h

00401021   mov         eax,0CCCCCCCCh

00401026   rep stos    dword ptr [edi]

7:        return a+b;

00401028   mov         eax,dword ptr [ebp+8]

0040102B   add         eax,dword ptr [ebp+0Ch]

8:    }

0040102E   pop         edi

0040102F   pop         esi

00401030   pop         ebx

00401031   mov         esp,ebp

00401033   pop         ebp

00401034   ret

 

3、在我的机器上的寄存器初始数值:

EAX = 007217A8 EBX = 7EFDE000 ECX = 00000001

 EDX = 00721830 ESI = 00000000 EDI = 00000000

 EIP = 0040D6F0 ESP = 0018FF4C EBP = 0018FF88

4、分析过程:

上一篇:程序技术 |一个超级实用、好用的C++分割函数
下一篇:工程案例|东台万颐广场预应力工程总结报告
返回顶部