本节内容
函数调用时发生了什么?首先一个进程启动后,到底什么东西执行代码的?我们知道所有代码都是由线程来执行的,进程仅是资源单位,真正执行代码的是线程。那么每个进程至少有一个线程。当线程执行一个函数的时候,究竟发生什么事呢?
首先为这个线程分配一段内存,这个线程启动的时候都会分配内存。内存干嘛用?执行一个函数的时候,比如传递参数,返回值或者局部变量需要存储,我们把内存分为两块,第一块分配所有线程的内存,每个线程都会有一段内存,比如T1,T2。那么不同的操作系统默认给这个线程分配的内存大小会不一样,有1MB或者10MB,或者说有些程序比如Go语言会自主控制一个线程分配多少内存。除此之外还有些内存使用new或者malloc之类的命令分配的。这两块内存都是线程在运行期分配的,后者称之为堆,堆上面有很多内存块,我们可以按照需要的大小进行分配,堆上的内存申请完之后必须要释放,这个释放要么是主动释放要么是GC释放,我们可以看到堆上内存基本上是平面线性结构,比如我们分配的时候,先分配了一个区域,然后再分配另外一个区域,中间就有大大小小的碎块,所以堆上的内存分配有很多麻烦,比如说内存碎片化,下次分配很大的内存时我们要找一个没有分配的区域足够大的空间给它,所以很多GC程序在垃圾回收时候会把这些没有释放的内存搬到一起进行压缩,这样把回收过后正在用的内存搬到一块,后面全部是自由空间,这样便于分配内存,所以在堆上分配内存有很多的讲究。
为线程分配的内存称之为栈,就是说所有线程带的内存通常称之为执行栈。举一个例子说明,假如有一个线程,分配了一段内存,刚开始的时候,既然是栈,栈基本的结构是先进后出。
线程栈怎么样去维持一个函数调用呢?我们首先搞清楚函数调用是什么样的状况,假如main函数调用了a,a调用了b。b再返回到a,a再返回到main,调用的过程中形成了类似一种链状结构,这样的链状结构在内存上怎么去管理呢?栈的空间从高往低分配,高位在下面低位在上面。当main调用a的时候,首先,把main函数的空间分配出来,因为main函数调用a的时候还得返回,所以main函数本身的状态必须保留,那么这个main函数的内存块是保留的;然后当我们执行a的时候,在上面为a分配好内存,a调用b的时候,再为b分配好内存,这样的话在线程栈上为函数调用分配不同的内存,当b调用结束以后,b所在的内存就会被收回,当a结束时候,a所在的内存也会被收回。假如main接下来去调用c,c调用d。原来a的内存就会被分配c了,原来b的内存就会被分配d。现在看这样的结构很简单,相当于往垂直的空间里面放不同的书,一本一本的放,最先拿起的一本书肯定是最后放的一本,当你拿了很多书了以后空间实际上是可以重复使用的。
所以我们知道栈上的空间是重复使用的,也就意味着它的内存回收机制肯定和堆上是不一样的。堆上的内存空间是平面线性结构,栈的内存是先进后出的结构。
当main函数执行时候,怎么确定这个内存空间大小呢?这个地方会涉及到两个专用的寄存器,BP寄存器指向底部,SP寄存器执行顶部。当我们进数据时候,sp会一直往上增长,也就是说sp寄存器永远指向栈顶的位置,BP表示某个基准位置。这样大概对栈的结构有了初步了解。那么简单了解下一个函数调用是究竟什么状态。
main函数或者add函数所在的内存块我们称之为堆栈帧(stack frame),整个的调用过程的总和称之为调用堆栈(call stack),这个名字其实翻译成中文之后觉得很古怪,可能翻译成调用栈也许更合理一些,但不知道最早时候谁翻译的后来就成了约定俗成了。也就意味这当我们在add函数上加上断点时候,除了我们能看到add函数里面的内存数据以外,实际上main函数的内存数据也在。所以在调用堆栈上面,我们可以看到调用堆栈上一级甚至更上一级整个数据状态。
假如一个main函数调用add(x,y)函数。首先在栈上为main函数分配空间,BP表示main函数底部,SP表示main函数顶部。这两个寄存器里面存储的是内存地址。真正的空间大小是BP-SP就是main函数所使用的这段内存。接下来调用add函数的时候,先不管参数传递,必须为add函数分配内存,那么SP移到add函数顶部,BP指向add函数底部,即BP指向当前函数的底部,SP指向当前函数的顶部。这时候有个问题,add函数执行结束后怎么恢复main函数的内存空间呢?所以这时有个问题就是当我们call一个函数的时候,首先做的是现场保护,就是保护当前函数执行场景,上下文,通常像BP、SP等其他寄存器的值,保护好了之后再去执行add函数,执行add函数之后回收内存空间,然后还要做现场恢复,现场恢复后才可以回到当时调用main函数时的场景,那么我们除了需要把BP、SP保存起来,还要保存IP寄存器,因为如果main函数有这样一条指令,分配x,y变量,接下来call add,接下来print add的结果,正常情况下执行add的时候,接下来执行print指令,当执行print指令时IP指向print,也就是说当执行完print时,IP寄存器也需要恢复,要不然就不知道接下来执行哪一行了,当时从哪一行出去的回来时候需要从哪一行下面一行执行,最基本的BP、SP、IP三个寄存器的值需要保护,BP描述了main函数的底部,SP描述了main函数的顶部,IP保存了执行完add函数以后接下来要执行哪条指令。最起码有这样三个值需要做现场保护。
这个系列的每篇文章有大半篇幅内容属于付费阅读。提供微信支付或支付宝支付打赏50元备注留言手动提供付费文章访问密码。
来源: http://www.cnblogs.com/lyj/p/foundation_10_callstack_free.html