一、堆栈基础——内存区域
1、内存区域相关概念
内存区域:一个进程可能被分配到不同的内存区域去执行:
代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
数据区:用于存储全局变量等。
堆区:进程可以再堆区动态请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。
一个进程对这些区域进行合理利用的过程:
每个可执行文件包含了二进制级别的机器代码,将被装载到内存中的代码区;处理器(CPU)将到内存的代码区一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;如果代码中请求开辟动态内容,则会在内存的堆区分配一块大小何止的区域返回给代码区的代码使用;
当函数调用发生时,函数的调用关系等信息回动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。
2.栈区
栈:是向低地址扩展的数据结构,是一块连续的内存区域。栈顶(esp栈顶指针)的地址和栈的最大容量是系统预先规定好的,在windows下,栈的默认大小是2M,如果申请的空间超过栈的剩余空间时,将提示overflow(栈溢出)。
3.堆区
堆:是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限与计算机的虚拟内存。
操作系统有一个纪录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表;
操作系统分配堆区大小的操作:
第一种情况:寻找第一个空间大于所申请的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。
第二种情况:由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动的将多余的部分重新放入空闲链表中。
4.堆区和栈区的区别
(1)申请方式
栈:由系统自动分配。例如声明一个局部变量 int a,系统自动在栈中为a开辟空间。
堆:需要程序员自己申请。并指明大小,在c中malloc函数,如p=chr(*)malloc(10)。
(2)申请效率
栈:由系统自动分配,速度较快,但程序员是无法控制的。
堆:是由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。
二、堆栈基础——函数调用
函数调用时候将借助系统栈来完成函数状态的保存和恢复。
以下是一个关于函数调用的代码:
根据上图我们可以知道,函数调用过程为:main函数->func_A()函数->func_B()函数。
1、介绍这些函数在代码区中精确的跳转实现
这些代码区中精确的跳转都是在与系统栈巧妙配合过程中完成的。
当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并将它压入栈中。
栈帧的定义:**每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量**。从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等信息。
当函数返回时,系统栈会弹出该函数所对应的栈帧。
2.详细介绍以上展示代码中函数的跳转过程
当我们双击编译生成的二进制可执行文件后,可执行文件会以进程的形式执行,然后将二进制机器码存放在代码区中,接着处理器到代码区中根据对应的机器码取出数据进行计算。遇到函数调用,此时系统会自动生成一个栈区,main函数的栈帧位于栈区的顶部,因为main函数中需要调用func_A()函数,所以在main函数栈帧的顶部,生成func_A()的栈帧,**该栈帧中包含func_A的变量,返回地址等。根据展示代码内容,我们可以知道func_A()函数还调用了func_B()函数,所以同样需要在func_A()栈帧上创建func_B()函数对应的栈帧。该栈帧同样包括局部变量、返回地址等。当func_B()函数执行完成后,就会返回到func_A中继续执行。因为栈帧对应的是未运行完的函数,所以这时func_B()函数对应的栈帧弹出栈区**。同理当func_A执行结束后,从栈区中弹出func_A()函数对应的栈帧。继续到main函数中执行。
3.函数调用的步骤
(1)参数入栈:将参数从右向左一次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:保存当前栈帧状态值,已备后面恢复本栈帧时使用。将当前栈帧切换到新栈帧。
三、堆栈基础——常见寄存器与栈帧
1.寄存器概念
寄存器(register):是中央处理器CPU的组成部分。寄存器是有限存储容量的高数存储部件,它们可用来暂存指令、数据和地址。我们常常看到32位CPU、64位CPU这样的名称,其实指的就是寄存器的大小。32位CPU寄存器大小就是4字节。
指令寄存器:指令寄存器(extend instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址,其作用是:EIP指向哪里,CPU就会去执行哪里的指令。
指令寄存器的实际使用:在栈帧切换的时候,将栈帧的返回地址传递给EIP寄存器,这时CPU就会跳转到返回地址处继续执行,从而完成栈帧切换。
2.栈帧与寄存器的联系
每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧:
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。(即esp指针指向正在运行函数栈帧的栈顶)
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。(即ebp指针指向正在运行函数栈帧的底部)
3、函数栈帧定义及其内容
函数栈帧:ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP表示当前栈帧的顶部。
示意图如下:
函数栈帧中,一般包含以下几类重要信息:
(1)局部变量:为函数局部变量开辟的内存空间
(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过栈帧平衡计算得到),用于在本栈帧弹出后,恢复出上一个栈帧。
(3)函数返回地址:保存当前函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。