一、内存空间分配情况
我们按照编程语法(如,C/C++)编写的函数,会被编译器编译为机器指令,这些编译的函数指令存在代码区。全局未初始化、静态局部未初始化的数据(变量)存在bss段。全局初始化、静态局部初始化的数据(变量)存在data段。bss和data都是属于数据区中。代码区是只读的,不可更改的,字符常量、用const修饰的全局变量这两个存在常量区。
当运行可执行程序的时候,除了代码区、数据区、常量区之外,还会增加栈区与堆区。(栈区内存使用情况是与函数调用、局部变量相关的。堆区内存使用情况是与程序员编写的代码有关,如malloc、new等开辟的内存,开辟后需要在不使用的时候进行free、delete等释放内存的操作。)
二、参数入栈分析
首先简单介绍一下可执行文件运行的时候栈空间是如何变化的,首先会先给栈分配空间,栈是从高地址向下增长,越往下地址越小。开始的时候 栈底(栈基)地址 与 栈顶(栈指针)地址 都是相同的,然后 指令指针寄存器 读取指令交给CPU进行执行,CPU根据指令进行执行,这里的指令基本就是指的 代码区 的指令,如,下图中“入栈 3”的这个指令。根据“入栈 3”这个指令,就会把3入栈,然后 栈指针寄存器 下移动一个单位(这里用单位没有用字节的原因是不同机器存入栈的大小是不好确定的,同一采用单位来代称)。 指令指针寄存器会读取下一条指令,具体过程如下2幅图所示。
图中寄存器下面表示的都是寄存器,用来存栈基、栈指针、指令指针等。此图表示是IP要读出指令,指令内容是“入栈 3”。
这个幅图是指令指针寄存器读取了“入栈3”的指令,把它交给CPU执行后,栈指针向下移动,指令指针读取下一条指令。
需要注意2点:
1)栈基寄存器、栈指针寄存器存的都是 栈的内存地址 ,作用是进行存储,划分各个函数的调用内存区间(函数栈帧)。
2)指令的执行 是由 指令指针寄存器IP 进行读取,然后由CPU执行。
三、函数调用栈
函数调用栈就是函数在计算机中调用的过程,函数调用经常是嵌套的,在同一时刻,栈区中会有多个函数的信息,每个函数占用一个连续的空间区域。一个函数占用的区域被称作 帧 ,也就是 函数栈帧 。函数调用的方式主要为在一个函数中调用另一个函数,这样一个过程中,编译器会生成一个call指令,程序执行到这条call指令的时候,就会跳转到被调用的函数的入口。每个函数在执行结束的后面都有一个ret指令,在函数执行后跳转到开始调用这个函数的地方。(跳转到 返回地址 ,读取地址对应的 指令 )
下面来分析call与ret,具体如下。
call指令会做两个事情:
1)将下一条指令的地址入栈,这个就是返回地址,被调用函数结束后回跳会这里。
2)跳转到被调用函数入口处执行。
在执行ret指令之前,编译器还会增加2个指令,一个是恢复调用者的栈基。一个是释放自己的栈帧空间。
ret指令会做两个事情:
1)弹出执行call指令时,压入栈的 返回地址。 2)跳转到这个返回地址。
例如:一个函数A在 地址a1 处调用 地址b1 处的 函数B。栈基地址是 s1,栈指针地址是 s3,指令地址寄存器读取代码段 a1地址 对应的 指令,读取后CPU执行call指令。
call指令执行是把 地址a2 入栈,a2是函数执行后的返回地址。a2入栈 栈指针 往下移动到s4。指令指针跳转转到地址b1处,读取b1对应的指令,也就是开始执行函数B。
执行函数B,b1对应指令是把sp向下移动到s7处,开辟足够大的栈帧。然后执行b2地址对应的指令,这条指令是把调用者栈基s1(这个栈基是函数A的)存到s5对应的栈内存中。b3对应的指令是把 函数B的栈基(s5)存入 栈基寄存器(bp)中。
(详解:b2对应指令是把函数A的栈基s1存入栈中。 b3对应指令是 根据b2指令可以得知道s1存入栈后的地址是s5,在把s5存入栈基寄存器(bp)中,采用s5作为函数B的栈基。)
上面这些指令是要保持调用函数的栈基信息和执行完函数后要返回的地址信息,这些做完后剩下的就是执行函数B剩余的指令了。(执行这些的时候,指令指针寄存器一直在读取指令交给CPU去执行)
下面是当执行完函数B的时候,栈的变化。首先要恢复调用者栈基的地址,就是回复函数A的栈基寄存器地址,从s5 改为之前的 s1。然后释放自己的栈帧空间(释放函数B的),将栈指针寄存器从 s7 改为之前的 s4。指令指针寄存器读取bn地址对应的ret指令。
执行ret指令, 1)首先弹出call指令的压栈地址,弹出地址后 栈指针 后移动一个单元,从s4变为s3。 2)跳转到返回地址a2处开始执行,指令指针寄存器IP开始读取a2地址对应的指令,然后交给CPU执行。
返回地址a2是分情况的,当然这个也要考虑编译器的情况,如,是否优化等等。比如函数B是有返回值,然后函数A中有变量回接这个返回值,那返回地址a2会是这个赋值语句的指令。如果函数A中没有变量接函数B的返回值,那返回地址a2的指令会是函数A中执行完函数B的下一条语句的指令。
补充一点:函数调用,参数一般是从右至左的入栈(函数调用参数入栈的顺序是有 调用惯例 的)
这个视频说的还是不错的,对文章有疑惑的或者是喜欢看视频的朋友们可以看一下这个。