堆栈和堆

我们将应用程序的运行过程想象为这个旅行团正在开一个大Party,比如说十八大?大会中往往会有各种小会,比如说我们的18大就有各种各样的分组讨论。这些小会,我们可以称之为“函数”。函数的运行需要一些上下文,正如这些小会的讨论不能脱离大会的主题一样,我们开小会需要知道大会给我们提供了什么,茶水啊,饮料啊,点心啊之类的,这些东西给了我们,我们就需要个地方放,但只要我们的小会开完了,这些东西就不需要了,我们放这些东西的地方也就不需要了。为了满足这些小会的需要,我们就给这些小会安排了一个大房间,每个小会开的时候都是用这个房间,但这个房间不会完全归某个小会使用,这个小会需要多大的地方放东西就安排多大地方,开完会就把这块地方还给我们。为了方便我们打扫房间,我们规定,凡是放在这个房间的东西,必须按照先后顺序,先放的放在下面,后放的,放在上面,我们清理的时候,则从上往下清理。我们把这个大房间称为“堆栈”或者“”。应用程序中的函数,会有一些运行需要的上下文,比如说环境变量,参数等,这些就是放在堆栈中的,一旦函数运行完成,它所占用的堆栈空间就被清理回收了。

但是,(但是如此之多,总叫人无奈),我们开的小会中可能会产生一些文件,决议之类的东西是给大会用的,这些东西不能因为我们的小会开完了就销毁,这些文件或者决议也需要有个地方放。我们就又准备了一个更大的房间,用来存放这些东西。每个小会需要多大的地方需要自己申请,比如说,我们“山西代表团讨论会”需要一个1立方米大小的文件柜,就提出申请(malloc/new),大会组织方就给我们从这个大房间划出我们需要的那么大的文件柜给我们。我们把这样的大房间称为“”。我们在这个大房间申请的文件柜需要我们自己来管理,比如说,可能我们的大会开了一段时间后,这个文件柜里的东西已经不需要了,我们就可以把这个文件柜里的东西销毁,清空这个文件柜,如果我们忘了做这个事情,那这个文件柜就会被一直占用着,直到整个大会开完(程序运行结束),任何人都不能用这个文件柜。这就是所谓的“内存泄露”。

函数调用

(这一部分比较技术性,可能会有一定的理解困难)

当一个函数被调用的时候,应用程序做的第一件事情就是在栈上开一块空间给这个函数使用(放茶水点心之类的)。我们下面看一个最简单的函数的汇编表示:

函数原型(C)

void fun2()

{

}

int fun1(int x)

{

     fun2();

return x;

}

int main()

{

     fun1(1);

return 0;

}

汇编:

int fun1(int x)

{

00EF13D0  push        ebp                 //保存栈底指针

00EF13D1  mov         ebp,esp                   //将栈底指针移动到栈顶

00EF13D3  sub         esp,0C0h            //增加栈空间

00EF13D9  push        ebx 

00EF13DA  push        esi 

00EF13DB  push        edi 

00EF13DC  lea         edi,[ebp-0C0h]      //保存栈空间的大小

00EF13E2  mov         ecx,30h                   //初始化寄存器

00EF13E7  mov         eax,0CCCCCCCCh      //初始化寄存器

00EF13EC  rep stos    dword ptr es:[edi]        //初始化栈内存

     fun2();

00EF13EE  call        fun2 (0EF10EBh)     //调用函数fun2,后面的地址是fun2的内存地址

     return x;

00EF13F3  mov         eax,dword ptr [x]  //通过eax寄存器保存返回值

}

00EF13F6  pop         edi 

00EF13F7  pop         esi 

00EF13F8  pop         ebx 

00EF13F9  add         esp,0C0h 

00EF13FF  cmp         ebp,esp 

00EF1401  call        __RTC_CheckEsp (0EF1140h) 

00EF1406  mov         esp,ebp 

00EF1408  pop         ebp 

00EF1409  ret

下面是地址0EF10EBh的指令:

fun2:

00EF10EB jmp         fun2 (0EF1420h)

我们看到它跳转到了地址0EF1420h,我们再看地址0EF1420h是什么:

void fun2()

{

00EF1420 push        ebp 

00EF1421  mov         ebp,esp 

00EF1423  sub         esp,0C0h 

00EF1429  push        ebx 

00EF142A  push        esi 

00EF142B  push        edi 

00EF142C  lea         edi,[ebp-0C0h] 

00EF1432  mov         ecx,30h 

00EF1437  mov         eax,0CCCCCCCCh 

00EF143C  rep stos    dword ptr es:[edi] 

}

00EF143E  pop         edi 

00EF143F  pop         esi 

00EF1440  pop         ebx 

00EF1441  mov         esp,ebp 

00EF1443  pop         ebp 

00EF1444  ret 

我们可以看到函数调用实际上就是通过一个地址跳转指令实现的。

现在我们可以回答一个问题:

为什么递归深度太深的话,会造成栈溢出?

从上面的汇编我们可以看出每次进入一个函数,首先作的事情就是开辟栈空间,这个栈空间直到这个函数调用结束后,才会释放,因而,每次递归都回开辟新的栈空间,而在内层递归结束之前没有机会释放这块栈空间。栈空间的大小是有限的,所以递归太深就会用完这些栈空间,导致栈溢出。

为什么我听说尾递归不会造成问题?这里实际上是因为编译器优化的原因,编译器会将尾递归优化为普通的一条jmp指令,而不会为其开辟新的栈空间,如果没有编译器优化的话,一样会造成问题的。

下面是一个尾递归和其对应的未优化的汇编代码:

int fun1(int x)
{
    return fun1(x);
}

这是一个死递归,永远都不会执行完,但没关系,我们这里不关心这个问题,我们关心的是它是怎么使用的栈空间。我们看汇编:

int fun1(int x)

{

004013D0  push        ebp 

004013D1  mov         ebp,esp 

004013D3  sub         esp,0C0h 

004013D9  push        ebx 

004013DA  push        esi 

004013DB  push        edi 

004013DC  lea         edi,[ebp-0C0h] 

004013E2  mov         ecx,30h 

004013E7  mov         eax,0CCCCCCCCh 

004013EC  rep stos    dword ptr es:[edi] 

    return fun1(x);

004013EE  mov         eax,dword ptr [x] 

004013F1  push        eax 

004013F2  call        fun1 (04011EFh) 

004013F7  add         esp,4 

}

004013FA  pop         edi 

004013FB  pop         esi 

004013FC  pop         ebx 

004013FD  add         esp,0C0h 

00401403  cmp         ebp,esp 

00401405  call        __RTC_CheckEsp (0401140h) 

0040140A  mov         esp,ebp 

0040140C  pop         ebp 

0040140D  ret 

下面是开启编译器优化后的汇编代码:

    return fun1(x);

001D1F40  jmp         fun1 (01D1F40h) 

这就是函数调用,以及在函数调用过程中对堆栈的使用。

posted @ 2013-06-14 02:46  如斯夫  阅读(366)  评论(0编辑  收藏  举报