特色技术

专业从事预应力结构体系的设计、施工一体化解决方案

资讯分类

浅析函数的堆栈框架

  • 分类:程序技术
  • 作者:
  • 来源:
  • 发布时间:2017-10-29 15:44
  • 访问量:

【概要描述】  一、概述:   高级语言的函数就是汇编语言的子程序。   在主程序和子程序中传递参数有三种常用方法:   1、通过寄存器传递;   2、通过数据区的变量来传递;   3、通过堆栈传递。

浅析函数的堆栈框架

【概要描述】  一、概述:

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

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

  1、通过寄存器传递;

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

  3、通过堆栈传递。

  • 分类:程序技术
  • 作者:
  • 来源:
  • 发布时间:2017-10-29 15:44
  • 访问量:
详情

  一、概述:

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

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

  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

扫二维码用手机看

搜索