Win32 环境下的堆栈(一)

发表于:2007-07-01来源:作者:点击数: 标签:
Win32 环境下的堆栈(一) 简介 在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解

  

Win32 环境下的堆栈(一)

 

简介

在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.

 

关键字

调试 堆栈 Stack Stack-Frame

 

目录

1.堆栈是什么?

2.堆栈里面放的都是什么信息?

3.堆栈是在什么时候被建立起来的?它的默认大小是多少?

4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?

5.什么叫Stack Frame?

6.在一次函数调用中,堆栈是如何工作的?

7.老大,结合一个例子讲讲吧?

 

1.堆栈是什么?

       从内存管理角度看,堆栈是就是一块连续的内存空间,对它的操作采用先入后出的规则,他的生长方向与内存的生长方向正好相反,也就是说它是从高地址向低地址生长.

       从Win32程序内部的角度看,每一个线程有自己的堆栈,它主要用来给线程提供一个暂时存放数据的区域,程序使用POP/PUSH指令来对堆栈进行操作.

 

2.堆栈里面放的都是什么信息?

    堆栈中存放的信息包括:当前正在执行的函数的局部变量,函数返回地址,该函数的上层函数传给该函数的参数,EBP的值,一些通用寄存器(EDI,ESI…)的值,注意这里提到的正在执行的函数,比如有下面的一段C代码:

   void B()

   {

       printf(“B\n”);

   }

 

   void A()

   {

        B();

   }

   那么当程序执行到B函数的printf函数的时候我们说正在执行的函数包括A和B而不仅仅是B函数,这一点需要注意.

 

3.堆栈是在什么时候被建立起来了?它的默认大小是多少?

   堆栈是在我们的main主函数被系统调用之前被建立起来的,对于非主线程它是在线程被建立之前创建的,它的默认大小是1M,如果需要修改堆栈的大小的话可以在VC6++中通过使用/STACK编译项实现:

   #pragma comment(linker,"/STACK:2048,1024")  // 预约(Reserve)2M,提交(Commit)1M

          关于预约(Reserve)和提交(Commit)的概念请参看”Programming Applications for Microsoft Windows“( Jeffrey Richter,Chapter 15 Using Virtual Memory in Your Own Applications)

 

4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?

系统通过使用异常捕获(Exception Handling)机制来捕获应用程序企图去访问超过该程序提交(Commit)的堆栈范围这种异常,假如你程序预约了2M并且提交了1M大小的堆栈,那么当你的程序企图访问超过1M的范围的时候会产生一个异常并且被系统捕获,系统会帮你继续从另外1M预约的内存中提交内存来满足你的需求,如果你要求提交的大小甚至超过了2M(你一开始预约的大小)在 NT系统下(98除外)系统也会尝试去分配(allocate)内存来满足你,但是系统并不保证分配会成功

 

5.什么叫Stack Frame?

         Stack Frame这个词你可以在各种各样的汇编书籍中看到,到底它表示什么意思呢?也许你看完文章的后半部分就会明白,在此我们先给它一个定义,你看完整篇文章在回过头来回味一下就会知道它的确切含义了,Stack Frame是堆栈中的一块区域,它保存着一个函数的返回地址,和该函数内部使用的局部数据(Local Data),它是由函数入口处的SUB ESP,48h之类的语句来建立的.

 

6.在一次函数调用中,堆栈是如何工作的?

  假设我们的主角叫A函数…

  a.首先上级函数传给A函数的参数被压入堆栈中(至于是谁来做这个压栈操作取决于A函数的调用方式:是__stdcall, __cdecl还是其他);

  b.然后是返回地址(A函数执行完后接下来程序继续执行的地址)入栈;

  c.接下来是当前的EBP;

  d.如果A函数有局部变量,就在堆栈中开辟相应的空间以构造那些变量变量(A函数执行结束,这些局部变量的内容将被忽略/遗弃,但是不被清除,比如A函数中有一个变量int m存在于地址0x0012FFCC处,函数结束时9依然存在于0x0012FFCC处没有被清除,但是此时它已经没有任何意义了, 

  e.在函数返回的时候,弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。

 

7.老大,结合一个例子讲讲吧?

    下面就是我们要拿来做模特的代码,程序很简单,wWinMain调用AFunc,AFunc再调用BFunc,下面的讲解过程中我们要观摩这个程序的汇编代码形式,可以通过在VC6++该工程的Debug模式中按F5然后Ctrl+Tab做到,我想这对于Win32程序员应该不是难事.

        int BFunc(int i,int j)
       {
             int m = 1;
             int n = 2;
 

             m = i;
             n = j;

             return m;
       }

      

       int AFunc(int i,int j)
      {
             int m = 3;
             int n = 4;

             

             m = i;
             n = j;

             BFunc(m,n);

             return 8;
        }

       

        int APIENTRY wWinMain(HINSTANCE hInstance,
                                                   HINSTANCE hPrevInstance,
                                                   LPSTR     lpCmdLine,
                                                   int       nCmdShow)
        {
              AFunc(5,6);
             return 0;
         }

 

步骤1.我们从wWinMain调用AFunc函数开始

       wWinMain调用AFunc的时候,先把参数压栈(至于为什么压栈顺序是6,5而不是5,6请参看附录.注解1)参数压栈结束后此时ESP = 0x0012FEDC,EBP = 0x0012FF30,

     

 

这是进入AFunc函数之前的堆栈形势图:

 

                                                                    图 1

 

步骤2.记住进入AFcun函数之前的ESP,EBP的值,然后我们进入AFunc…

     为方便大家观摩,先把AFunc函数的全貌贴出来

29:   int AFunc(int i,int j)

30:   {

004010D0   push        ebp         ;  先把EBP入栈保存

004010D1   mov        ebp,esp   ;  再把此时的ESP赋给EBP,这样EBP就可以拿来访问本函数的局部变量

004010D3   sub          esp,48h  ;  为AFunc函数在堆栈重开辟一块空间,一般来说开辟的空间大小是40+

                                                  ;  函数内所有局部变量的大小;

004010D6   push        ebx         ;  通用寄存器入栈,算保存现场吧

004010D7   push        esi

004010D8   push        edi

004010D9   lea           edi,[ebp-48h]

004010DC   mov         ecx,12h

004010E1   mov          eax,0CCCCCCCCh

004010E6   rep stos    dword ptr [edi]

31:       int m = 3;

004010E8   mov         dword ptr [ebp-4],3  ; 为什么局部变量m位于ebp-3处?

32:       int n = 4;

004010EF   mov         dword ptr [ebp-8],4  ; 为什么局部变量n位于ebp-8处?

33:

34:       m = i;

004010F6   mov         eax,dword ptr [ebp+8]      ; ebp+8处存的是什么?

004010F9   mov         dword ptr [ebp-4],eax

35:       n = j;

004010FC   mov         ecx,dword ptr [ebp+0Ch] ; ebp+0ch处存的是什么?

004010FF   mov         dword ptr [ebp-8],ecx

36:

37:       BFunc(m,n);

00401102   mov         edx,dword ptr [ebp-8]  ; AFunc调用BFunc之前先把传给BFunc的参数入栈

00401105   push        edx

00401106   mov         eax,dword ptr [ebp-4]

00401109   push        eax

0040110A   call        @ILT+25(BFunc) (0040101e)

0040110F   add         esp,8                              ; 这个出栈操作为什么?

38:

39:       return 8;

00401112   mov         eax,8

40:   }

00401117   pop         edi                                 ; 恢复现场

00401118   pop         esi

00401119   pop         ebx

0040111A   add         esp,48h                         ; 收回函数一开始在栈中开辟的空间

                                                                        ; 对应于一开始的sub esp,48h

0040111D   cmp         ebp,esp

0040111F   call        __chkesp (00401220)

00401124   mov         esp,ebp

00401126   pop         ebp                                ; 恢复调用前的EBP

00401127   ret

 

(未完待续)


原文转自:http://www.ltesting.net