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

一、概述:

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


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


1、通过寄存器传递;

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

3、通过堆栈传递。


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


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


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


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


1、进栈指令push src

功能:栈指针Esp减4,src保存在Esp指向的堆栈单元中。


2、出栈指令pop dst

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


3、调用子程序指令call src

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

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


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

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

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


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


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


图1

三、帧指针和栈指针:


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


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


几个基本概念:


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


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


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


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


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


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


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


(3)寄存器ebp和esp也必须遵守第二个惯例用法。


子程序的帧指针Ebp1其实就是主程序的栈顶指针Esp,此处保存了主程序的帧指针Ebp0。


如果这里感觉很绕,一定是指针基础还不够扎实。来区分下一个概念,一个存储单元空间,有两个属性:

1、CPU访问这个存储单元需要依赖的地址值;

2、这个存储单元所存储的数值。


空间地址值和空间内存储的数值,区分这两个值是理解指针概念的基础。


彻底理解:子程序的帧指针Ebp1的地址中存储的数值是主程序的帧指针Ebp0,也就是Ebp0=dword ptr[Ebp1],用C++语言表述就是*Ebp1=Ebp0。相应地,如果子程序还引用了子程序,则有*Ebp2=Ebp1。C++函数就是通过帧指针和返回地址来设置嵌套函数引用。

 

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


如图1所示,子程序的返回地址、函数参数从逻辑上来说仍然处于主程序的堆栈区域,但是可以方便地应用子程序的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[])

{

       intr,s;

       r=addproc(10,20);

       s=addproc(r,-1);

       return 0;

}

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


2、VC++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、分析过程经作者制成了流程图如图2所示:



图2


上一篇:公司新闻|与深圳招商房地产有限公司签署技术开发合同
下一篇:技术新闻|Safe前处理程序新的小版本0.8
返回顶部