C/C++堆栈指引

来源:互联网 发布:郎咸平 离婚 知乎 编辑:程序博客网 时间:2024/04/30 13:46

转自:http://blog.sina.com.cn/s/blog_44c488680100rg11.html

Binhua Liu

Stack
Heap

局部变量存储在 stack中
debug 时,查看stack 可以知道函数的调用顺序
函数调用时传递函数,事实上是把参数压入堆栈

本文的编译环境是 VisualC/C++ 
本文讨论的stack,是指程序员为每个线程分配的默认 stack.而不是自己定义的stack
本文讨论的平台intelx86


1基本的知识和概念
    1)程序的堆栈是由处理器直接支持的。在intel x86中,堆栈在内存中从高->低扩展。因此,栈顶

地址是不断减小的。
    2)和堆栈操作相关的寄存器 esp、ebp
执行push时,esp-4,数据拷贝到esp指向的地址。
执行pop时,esp指向的数据拷贝到 内存地址/寄存器 中,esp+4。
   3)堆栈中到底存储了什么数据?
   包括了:函数的参数
          函数的局部变量
          寄存器的值(用以恢复寄存器)
          函数的返回地址
          用于结构化异常处理的数据。

   这些数据是按照一定的顺序组织在一起的,我们称之为一个堆栈帧 stackframe

   一个堆栈帧对应一次函数的调用。
  
   在函数开始时,对应的堆栈帧已将完整地建立了(所有的局部变量在函数帧建立时就
已将分配好空间了,而不是随着函数的执行而不断创建和销毁的);在函数退出时,整个函数帧将被销毁

   4)在文中,我们把函数的调用者称为caller(调用者),被调用的函数称为callee(被调用者)。之

所以引入这个概念,是因为一个函数帧的建立和清理,有些工作是由Caller完成的,有些则是由Callee完

成的。

2堆栈是如何工作的
   堆栈是用来支持函数的调用和执行的。

int foo1(intm,int n)
{intp=m*n;return p;}

int foo(int a,int b)
{int c=a+1;intd=b+1; int e=foo1(c,d);return e;}

intmain()
{intresult=foo(3,4);return 0;}

用代码跟踪堆栈的建立,使用,销毁。

3堆栈的建立
   我们从main函数执行的第一行代码,即intresult=foo(3,4);开始跟踪。这时main以及之前的函数对

应的堆栈帧已将存在在堆栈中了。
1)参数入栈
当foo函数被调用,首先,caller(main函数)把foo函数的两个参数:a=3,b=4压入堆栈。
参数入栈的顺序是由函数的调用约定(CallingConvention)决定。一般,参数都是从右往左入栈的,因此

,b=4先入栈,a=3后入栈。
2)返回入栈地址
我们知道,当函数结束时,代码要返回到上一层函数继续执行,那么,函数如何知道该返回到哪个函数的

什么位置执行呢?函数被调用时,会自动把下一条指令的地址压入堆栈,函数结束时,从堆栈读取这个地

址,就可以跳转到该指令执行了。
如果当前"callfoo"指令的地址是0x00171482,由于call指令占5个字节,那么下一个指令的地址为

0x00171487,0x00171487将被压入堆栈:
3)代码跳转到被调用函数执行
返回地址入栈后,代码跳转到被调用函数foo中执行。
到目前为止,堆栈帧的前一部分,是由caller构建的;而在此之后,堆栈的其他部分是由callee来构建。
4)ebp指针入栈
在foo函数中,首先将ebp寄存器的值压入堆栈。因为此时EBP寄存器的值还是用于main函数的,用来访问

main函数的参数和局部变量的,因此需要将它暂存在堆栈中,在foo函数退出时恢复。同时,给EBP赋于新

值。

   1)将EBP压入堆栈

   2)把ESP的值赋给EBP
   这样一来,我们很容易发现当前EBP寄存器指向的堆栈地址就是EBP先前值的地址,你还会发现发现,

EBP+4的地址就是函数返回值的地址,EBP+8就是函数的第一个参数的地址(第一个参数地址并不一定是

EBP+8,后文中将讲到)。因此,通过EBP很容易查找函数是被谁调用的或者访问函数的参数(或局部变量

)。
5)为局部变量分配地址
   接着,foo函数将为局部变量分配地址。程序并不是将局部变量一个个压入堆栈的,而是将ESP减去某

个值,直接为所有的局部变量分配空间,比如在foo函数中有ESP=ESP-0x00E4,(根据烛秋兄在其他编译

环境上的测试,也可能使用push命令分配地址,本质上并没有差别,特此说明)

奇怪的是,在debug模式下,编译器为局部变量分配的空间远远大于实际所需,而且局部变量之间的地址

不是连续的(据我观察,总是间隔8个字节)

我还不知道编译器为什么这么设计,或许是为了在堆栈中插入调试数据
6)通用寄存器入栈
    最后,将函数中使用到的通用寄存器入栈,暂存起来,以便函数结束时恢复。在foo函数中用到的通

用寄存器是EBX,ESI,EDI,将它们压入堆栈。

至此,一个完整的堆栈帧建立起来了。

4堆栈特性的分析
上一节中,一个完整的堆栈帧已经建立起来,现在函数可以开始正式执行代码了。本节我们对堆栈的特性

进行分析,有助于了解函数与堆栈帧的依赖关系。

  1)一个完整的堆栈帧建立起来后,在函数执行的整个生命周期中,它的结构和大小都是保持不变的;

不论函数在什么时候被谁调用,它对应的堆栈帧的结构也是一定的。

  2)在A函数中调用B函数,对应的,是在A函数对应的堆栈帧“下方”建立B函数的堆栈帧。例如在foo

函数中调用foo1函数,foo1函数的堆栈帧将在foo函数的堆栈帧下方建立。

3)函数用EBP寄存器来访问参数和局部变量。我们知道,参数的地址总是比EBP的值高,而局部变量的地

址总是比EBP的值低。而在特定的堆栈帧中,每个参数或局部变量相对于EBP的地址偏移总是固定的。因此

函数对参数和局部变量的的访问是通过EBP加上某个偏移量来访问的。比如,在foo函数中,EBP+8为第一

个参数的地址,EBP-8为第一个局部变量的地址。
   
4)如果仔细思考,我们很容易发现EBP寄存器还有一个非常重要的特性,

 我们发现,EBP寄存器总是指向先前的EBP,而先前的EBP又指向先前的先前的EBP,这样就在堆栈中形成

了一个链表!这个特性有什么用呢,我们知道EBP+4地址存储了函数的返回地址,通过该地址我们可以知

道当前函数的上一级函数(通过在符号文件中查找距该函数返回地址最近的函数地址,该函数即当前函数

的上一级函数),以此类推,我们就可以知道当前线程整个的函数调用顺序。事实上,调试器正是这么做

的,这也就是为什么调试时我们查看函数调用顺序时总是说“查看堆栈”了。

5返回值是如何传递的
栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆

(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层

函数,这是怎么做的呢?

   首先,caller和callee在这个问题上要有一个“约定”,由于caller是不知道callee内部是如何执行

的,因此caller需要从callee的函数声明就可以知道应该从什么地方取得返回值。同样的,callee不能随

便把返回值放在某个寄存器或者内存中而指望Caller能够正确地获得的,它应该根据函数的声明,按照“

约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”: 
   1)首先,如果返回值等于4字节,函数将把返回值赋予EAX寄存器,通过EAX寄存器返回。例如返回值

是字节、字、双字、布尔型、指针等类型,都通过EAX寄存器返回。

   2)如果返回值等于8字节,函数将把返回值赋予EAX和EDX寄存器,通过EAX和EDX寄存器返回,EDX存

储高位4字节,EAX存储低位4字节。例如返回值类型为__int64或者8字节的结构体通过EAX和EDX返回。

    3) 如果返回值为double或float型,函数将把返回值赋予浮点寄存器,通过浮点寄存器返回。

   4)如果返回值是一个大于8字节的数据,将如何传递返回值呢?这是一个比较麻烦的问题,我们将详

细讲解:

6堆栈帧的销毁
  当函数将返回值赋予某些寄存器或者拷贝到堆栈的某个地方后,函数开始清理堆栈帧,准备退出。堆

栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)

  1)如果有对象存储在堆栈帧中,对象的析构函数会被函数调用。

   2)从堆栈中弹出先前的通用寄存器的值,恢复通用寄存器。

   3)ESP加上某个值,回收局部变量的地址空间(加上的值和堆栈帧建立时分配给局部变量的地址大小

相同)。

   4)从堆栈中弹出先前的EBP寄存器的值,恢复EBP寄存器。

   5)从堆栈中弹出函数的返回地址,准备跳转到函数的返回地址处继续执行。

   6)ESP加上某个值,回收所有的参数地址。

   前面1-5条都是由callee完成的。而第6条,参数地址的回收,是由caller或者callee完成是由函数使

用的调用约定(callingconvention )来决定的。下面的小节我们就来讲解函数的调用约定。
函数的调用约定(callingconvention)

  
7函数的调用约定(calling convention)

  函数的调用约定(callingconvention)指的是进入函数时,函数的参数是以什么顺序压入堆栈的,函数

退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有2个办法可以指定函数使用的调用约定


1)在函数定义时加上修饰符来指定,如

1void__thiscall mymethod();
2{
...
4}
   2)在VS工程设置中为工程中定义的所有的函数指定默认的调用约定:在工程的主菜单打开

Project|ProjectProperty|Configuration Properties|C/C++|Advanced|CallingConvention,选择调

用约定(注意:这种做法对类成员函数无效)。
   常用的调用约定有以下3种:

   1)__cdecl。这是VC编译器默认的调用约定。其规则是:参数从右向左压入堆栈,函数退出时由

caller清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如printf方法。由于callee不

知道caller到底将多少参数压入堆栈,因此callee就没有办法自己清理堆栈,所以只有函数退出之后,由

caller清理堆栈,因为caller总是知道自己传入了多少参数。

   2)__stdcall。所有的WindowsAPI都使用__stdcall。其规则是:参数从右向左压入堆栈,函数退出

时由callee自己清理堆栈中的参数。由于参数是由callee自己清理的,所以__stdcall不支持可变数量的

参数。

    3)__thiscall。类成员函数默认使用的调用约定。其规则是:参数从右向左压入堆栈,x86构架下

this指针通过ECX寄存器传递,函数退出时由callee清理堆栈中的参数,x86构架下this指针通过ECX寄存

器传递。同样不支持可变数量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么

,将采用__cdecl或者__stdcall的规则来压栈和出栈,而this指针将作为函数的第一个参数最后压入堆栈

,而不是使用ECX寄存器来传递了。

8反编译代码的跟踪(不熟悉汇编可跳过)

   以下代码为和foo函数对应的堆栈帧建立相关的代码的反编译代码,我将逐行给出注释,可对照前文

中对堆栈的描述:

   main函数中 int result=foo(3,4); 的反汇编:


ain函数中 intresult=foo(3,4); 的反汇编:

008A147E push       4                  //b=4 压入堆栈 
008A1480 push       3                  //a=3压入堆栈,到达图2的状态
008A1482 call       foo (8A10F5h)       //函数返回值入栈,转入foo中执行,到达图3的状态
008A1487 add        esp,8              //foo返回,由于采用__cdecl,由Caller清理参数
008A148A mov        dword ptr [result],eax//返回值保存在EAX中,把EAX赋予result变量
   
下面是foo函数代码正式执行前和执行后的反汇编代码

008A13F0 push       ebp               //把ebp压入堆栈
008A13F1 mov        ebp,esp           //ebp指向先前的ebp,到达图4的状态
008A13F3 sub        esp,0E4h          //为局部变量分配0E4字节的空间,到达图5的状态
008A13F9 push       ebx               //压入EBX
008A13FA push       esi               //压入ESI
008A13FB push       edi               //压入EDI,到达图7的状态
008A13FC lea        edi,[ebp-0E4h]     //以下4行把局部变量区初始化为每个字节都等于cch
008A1402 mov        ecx,39h
008A1407 mov        eax,0CCCCCCCCh
008A140C rep stos   dword ptr es:[edi]
......                               //省略代码执行N行
......
008A1436 pop        edi               //恢复EDI 
008A1437 pop        esi                //恢复ESI
008A1438 pop        ebx                //恢复EBX
008A1439 add        esp,0E4h           //回收局部变量地址空间
008A143F cmp        ebp,esp            //以下3行为Runtime Checking,检查ESP和EBP是否一致
008A1441  call       @ILT+330(__RTC_CheckEsp)(8A114Fh)
008A1446 mov        esp,ebp
008A1448 pop        ebp                //恢复EBP
008A1449 ret                          //弹出函数返回地址,跳转到函数返回地址执行     

                               //(__cdecl调用约定,Callee未清理参数)

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 手机收藏不好使怎么办 window10连不上网怎么办 电脑没有家庭组怎么办 usb共享网络出错怎么办 电脑共享有密码怎么办 win10连不上网怎么办 w10网络重置了怎么办 点击网络重置后怎么办? 电脑启动找不到硬盘怎么办 电脑开机找不到硬盘怎么办 电脑找不到宽带连接怎么办 电脑文件找不到了怎么办 电脑找不到手机热点怎么办 win7电脑没有网络适配器怎么办 手机计算机桌面找不到怎么办 手机忘记开锁密码怎么办 电脑没网感叹号怎么办 本地连接2没有了怎么办 电脑上找不到本地连接怎么办 网络无访问权限怎么办 电脑无网络访问怎么办 xp连不上网怎么办 xp系统本地连接不见了怎么办 电脑xp系统本地连接怎么办 xp系统本地连接失败怎么办 xp系统找不到本地连接怎么办 台式电脑连不上网络怎么办 win7局域网要密码怎么办 xp网络不能上网怎么办 台式电脑连宽带怎么办 win7宽带813错误怎么办 电脑不显示本地连接怎么办 w7宽带连接不了怎么办 网络接收器坏了怎么办 电脑网页打开慢怎么办 win10无法添加打印机怎么办 2018杭州禁摩怎么办 中山个人怎么办社保卡 昆山房产证丢了怎么办 于一机交宽带费怎么办 租房子的怎么办宽带