从指令角度掌握函数调用堆栈详细过程

栈空间

        栈空间是从高地址向低地址扩充,堆地址是从低地址向高地址扩充。

        堆栈是一种具有一定规则的数据结构,我们可以按照一定的规则进行添加和删除数据。它使用的是后进先出的原则。在x86等汇编集合中堆栈与弹栈的操作指令分别为:

PUSH:将目标内存推入栈顶。

POP:从栈顶中移除目标。

         当执行一个函数的时候,相关的参数以及局部变量等等都会被记录在ESP、EBP中间的区域。一旦函数执行完毕,相关的栈帧就会从堆栈中弹出,然后从预先保存好的上下文中进行恢复,以便保持堆栈平衡。CPU必须要知道函数调用完了之后要去哪里执行(pc寄存器指向)

ESP和EBP

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

        根据上述的定义,在通常情况下ESP是可变的,随着栈的生产而逐渐变小(因为栈向低地址扩充,栈顶寄存器数值不断变小),而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。

在上述的定义中使用ESP来标记栈的底部,他随着栈的变化而变化

pop ebp;出栈 栈扩大4byte 因为ebp为32位

push ebp;入栈,栈减少4byte        

add esp, 0Ch;表示栈减小12byte

sub esp, 0Ch;表示栈扩大12byte

        ebp寄存器的出现则是为了另一个目标,通过固定的地址与偏移量来寻找在栈参数与变量。而这个固定值者存放在ebp寄存器中,。但是这个值会在函数的调用过程发生改变。而在函数执行结束之后需要还原,因此,在函数的出栈入栈过程中进行保存

示例

#include <iostream>

int sum(int a, int b)
{
  int temp = 0;
  temp = a + b;
  return temp;
}

int main()
{
  int a = 10;
  int b = 20;

  int ret = sum(a, b);

  return 0;
}

打断点,调试,查看反汇编:

    10: int main()
    11: {
00F81860 55                   push        ebp  
00F81861 8B EC                mov         ebp,esp  
00F81863 81 EC E4 00 00 00    sub         esp,0E4h  
00F81869 53                   push        ebx  
00F8186A 56                   push        esi  
00F8186B 57                   push        edi  
00F8186C 8D BD 1C FF FF FF    lea         edi,[ebp-0E4h]  
00F81872 B9 39 00 00 00       mov         ecx,39h  
00F81877 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00F8187C F3 AB                rep stos    dword ptr es:[edi]  
00F8187E B9 27 D0 F8 00       mov         ecx,offset _4A8A7142_c++test@cpp (0F8D027h)  
00F81883 E8 99 F9 FF FF       call        @__CheckForDebuggerJustMyCode@4 (0F81221h)  
    12:   int a = 10;
00F81888 C7 45 F8 0A 00 00 00 mov         dword ptr [a],0Ah  
    13:   int b = 20;
00F8188F C7 45 EC 14 00 00 00 mov         dword ptr [b],14h  
    14: 
    15:   int ret = sum(a, b);
00F81896 8B 45 EC             mov         eax,dword ptr [b]  
00F81899 50                   push        eax  
00F8189A 8B 4D F8             mov         ecx,dword ptr [a]  
00F8189D 51                   push        ecx  
00F8189E E8 E9 F7 FF FF       call        sum (0F8108Ch)  
00F818A3 83 C4 08             add         esp,8  
00F818A6 89 45 E0             mov         dword ptr [ret],eax  
    16: 
    17:   return 0;
00F818A9 33 C0                xor         eax,eax  
    18: }
00F818AB 5F                   pop         edi  
00F818AC 5E                   pop         esi  
00F818AD 5B                   pop         ebx  
00F818AE 81 C4 E4 00 00 00    add         esp,0E4h  
00F818B4 3B EC                cmp         ebp,esp  
00F818B6 E8 70 F9 FF FF       call        __RTC_CheckEsp (0F8122Bh)  
00F818BB 8B E5                mov         esp,ebp  
00F818BD 5D                   pop         ebp  
00F818BE C3 

在main函数的入口和退出:{ 会进行入栈操作,}进行出栈操作

        按照字面上理解,上面两句话的意思是将ebp推入栈中,之后让esp等于ebp

        为什么这么做呢?因为ebp作为一个用于寻址的固定值是有时间周期的。只有在某个函数执行过程中才是固定的,在函数调用与函数执行完毕后会发生改变。

        在函数调用之前,将调用者的函数(caller)的ebp存入栈,以便于在执行完毕后恢复现场是还原ebp的值。下一步,必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。

 sub esp, 0E4h;减去的值根据程序而定

之后会根据情况看是否保存某些特定的寄存器(EBX,ESI和EDI)

之后ebp的值会保持固定。此后局部变量和临时存储都可以通过基准指针EBP加偏移量找到了

在函数执行完毕,控制流返回到调用者的函数(caller)之前会进行下述操作

         所谓有始有终,这是会还原上面保存的寄存器值,之后还原esp的值(上一个函数调用之前的esp被保存在固定的ebp中)与ebp值。这一过程被称为还原现场之后通过ret返回上一个函数

main函数内

    int a = 10; 执行一条mov 指令: mov         dword ptr [a],0Ah

 同理 int b = 20;mov         dword ptr [b],14h

 接下来是int ret = sum(a,b):

00F81896 8B 45 EC             mov         eax,dword ptr [b]  
00F81899 50                   push        eax     #压栈 b的值
00F8189A 8B 4D F8             mov         ecx,dword ptr [a]  
00F8189D 51                   push        ecx     #压栈 a的值
00F8189E E8 E9 F7 FF FF       call        sum (0F8108Ch)   #执行call
00F818A3 83 C4 08             add         esp,8  
00F818A6 89 45 E0             mov         dword ptr [ret],eax 

函数调用参数的压栈顺序:参数由右向左压入堆栈。

因此上面对应的是:

先将b的值压入堆栈,再将a的值压入堆栈。

执行call        sum (0F8108Ch)   #执行call :

call函数首先会将下一行执行的地址入栈:假设下一行指令的地址位0x08124458

 第二步进入函数调用:sum

函数调用第一步: 将调用函数(main)函数的栈底指针ebp压栈

第二步:将新的栈底ebp指向原来的栈顶esp

第三步:将esp指向新的栈顶(开辟了函数的栈帧):大小:0cch

 接着执行 int temp = 0;//mov         dword ptr [temp],0

 temp = a + b;由于a,b的值之前入栈,可以通过ebp+12字节找到b的值,ebp+8字节找到a的值,最后将运算结果赋值给temp

 接着运行return temp;: mov         eax,dword ptr [temp]

 

 接着是函数的右括号“}”

(1)mov esp,ebp  回退栈帧 将栈顶指针指向栈底

(2)pop ebp 栈顶出栈,并将出栈内容赋值给ebp,也是将main的栈底重新赋值给ebp

(3) ret  栈顶出栈,并将出栈的内容赋值给pc寄存器,也就是将之前压榨的call sun的下一条指令赋值到pc寄存器执行

接着调用函数完毕,回到主函数:
利用了PC寄存器,使得程序知道退出sum后运行哪一条指令:

 接下来执行:

 add         esp,8 ,将压栈的a b 形参空间回收

 mov         dword ptr [ret],eax  # 在sum中,最后将temp赋值到eax寄存器,这里将eax赋值给ret

 

 最后return 0,程序结束

vs栈空间的大小

VC++默认的栈空间是1M

 栈溢出

 出现栈内存溢出的常见原因有2个:
   1 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈。
   2 局部静态变量体积太大
   第一种情况不太常见,因为很多情况下我们都用其他方法来代替递归调用,所以只要不出现无限制的调用都应该是没有问题的,起码深度几十层我想是没问题的。检查是否是此原因的方法为,在引起溢出的那个函数处设一个断点,然后执行程序使其停在断点处, 然后按下快捷键Alt+7调出call stack窗口,在窗口中可以看到函数调用的层次关系。

   第二种情况比较常见 在函数里定义了一个局部变量,是一个类对象,该类中有一个大数组

即如果函数这样写:
    void test_stack_overflow()
    {
      char* chdata = new[2*1024*1024];
      delete []chdata;
    }
   是不会出现这个错误的,而这样写则不行:
    void test_stack_overflow()
    {
      char chdata[2*1024*1024];
    }
   大多数情况下都会出现内存溢出的错误,


    解决办法大致说来也有两种:
   1 增加栈内存的数目
   2 使用堆内存

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值