内存布局和程序栈

参考文献

可寻址模型和内存布局

内存是由操作系统统一管理的, 内存里面一个字节就等于8个二进制位, 然后操作系统就为内存空间进行编号, 这就是我们所说的寻址模型. 我们经常说的32位指的是什么呢? 对于32位操作系统, 操作系统给内存编号到2的32次方, 而每个编号逻辑上喜欢使用十六进制来表示, 而且用于表示内存的具体位置. 形式通俗说就是4GB的内存大小. 这就是为什么32位x86的操作系统无法使用大于4GB的内存条的额外空间? 其原因就在这里

32位、64位其实这里指的是字长. 对于64位操作系统, 能给内存编号到2的64次方.

C/C++程序(进程)的内存寻址模型

像其他程序一样, BIOS和OS都需要内存**(废话)**, 此处位了表示计算机内存模型的完整性. 我们都把所有内存相关的内存区域都一一列出了, 对于程序员感兴趣的主要内存区域是代码段, 数据段和字面量池和bss, 堆栈和堆.

从程序的组织的方式来查看程序的内存布局.

  • 代码段: 程序的所有指令都会存放再这个区域, 这是已经编译好的机器码.
  • 字面量池: 是程序初始化时的一些字符串字面量, 再程序中用于显示文字.
  • 全局数据段: 程序初始化的常量和全局/静态的变量. C/C++用global/static生命的变量都存放再这个区域, 对说有函数公开可见.
  • 堆: 这里保存的数据只是为了临时存储一些值而创建的, 而我们可能在程序运行过程中可能会回收此内存. 因为我们在程序执行期间不需要很长时间, 所以使用C中的new或malloc这类内存分配程序来为我们所需的特定数据类型提供新的空间, 并且随着我们要求越来越多的动态数据空间而该区域不断扩大, 并且在内存中逐渐增长到更高的地址.
  • 栈: 当我们执行这个过程调用时, 堆的基本特性是LIFO, 存储着该程序”上下文”, 它将内存的高层地址开始, 然后向另一个方向向下扩展. 上下文其实就是程序中各函数逐渐调用的先后顺序

这种典型的内存布局有一个比较有趣的地方, 实际上栈向低层地址不断增长, 动态数据会向高层地址增长, 只要你的程序足够糟糕, 例如用无止境的递归和不断抢占堆可用的空间, 这两个货始终会碰面, 这将是一件非常糟糕的事情. 这是一种严重的错误, 这种情况操作系统说它内存不足时, 例如windows臭名昭著的蓝屏提示

IA32平台的程序栈

IA-32体系结构的调用栈堆. 我们将栈堆的底部放在内存的顶部, 并将堆栈的顶部放在内存的底部. 这就是我们使用的约定.

也有人将栈顶定于为上方且栈底定义在下方, 但如果没有显式标注高地址和低地址, 那就是”误人子弟”, 毫无意义

栈是朝着内存低地址方向增长, ia32栈中有一个特殊的寄存器, 称为esp. 该寄存器始终指向堆栈的顶部元素, 即放置在栈堆上的最后一个元素.

注意,下列指令跟x86-64有些不同

push操作

1
pushl 寄存器名称 或push 某个类型的指针

表示一个32位的值, 并为其指定了要入栈的源寄存器或内存位置, 基本上它是他会从该源获取值, 无论它是寄存器还是内存位置都会推入到栈顶. 它还会将栈指针递减4, 为什么要减4, pushl刚好是4个字节, 并且是超低地址方向增长的, 因此栈地址递减.

如图所示, 现在栈指针指向内存中已将该值添加或复制到内存中的新位置.

pop操作

1
popl 寄存器名称 或 popl 某个类型的指针

popl指令将数据从栈堆中移出. 在这种情况下, 我们还为它提供了dst参数, 以获取从栈中弹出的值, 然后将该值放入某个内存地址指向的位置或CPU中的寄存器.

我们从栈帧顶删除某个值, 并再次为该32位字的esp向上调整堆栈指针.

pop操作的时候是真的”删除”原先的值吗?

这个值并未删除, 它仍然存在于内存中, 只是我们不在引用它. 因为我们已经调整了堆栈指针, 使其指向栈中的下一个值. 但是原先这些位的数据仍然驻留在原先的内存位置, 只是程序不在解释这些位中的二进制
码. 已经在某种意义上有效地删除了它们, 因为我们可以回收该控件并将新数据压入栈并覆盖这些位. 因此需要保留被弹出的数据, 只需将它们拷贝到指定的位置即可.

程序的过程调用概述

注意, 这一块只是大致讲述调用的过程, 已省去了许多细节

调用者将设置一些参数, 并在执行call指令后, 该指令将控制流跳转到被调用者的函数, 之前在被调用者初始化的参数也一同传递给被调用者.

此时控制权在被调用者的函数中, 被调用者会创建一些局部变量, 在执行一些运算的操作, 并且运算的结果设为一个返回值, 该返回值是被调用函数给调用者函数的.

在被调用者函数执行return之前, 要清理创建的局部变量, 并回收空间, 最后执行return指令以告诉CPU要把控制权交还给调用函数.

并转到调用者函数原先执行点之后的下一条指令, 由于调用者的下一条指令后没有其他指令了就开始清理空间, 该控件最初用于设置参数所占用的控件都会被回收. 此处, 我们应该要清楚原先被调用者函数所占用的空间已被回收, 并且调用者函数再执行后也会销毁自己, 这就是调用过程设置.

由于调用和被调用方在同一个CPU上运行, 因此它们当然使用该CPU中的同一寄存器, 因此要有一种机制确保不会同时争夺CPU的资源. 这种机制就是:

  • 如果调用者要使用某个寄存器, 而刚好被调用者也需要使用该寄存器, 调用者在交出寄存器的控制权之前,它先回保持该寄存器(通常是一个地址), 当这一步完成后, 就将寄存器让给调用者.
  • 同理, 被调用者也可能会保持当前使用的寄存器的地址后, 才让出寄存器的控制权.