内存分段、堆和 mmap

som*_*iam 0 linux memory x86 memory-segmentation

这个问题主要与x86、linux相关

在 x86 上,访问内存的每个操作都需要在执行程序时从某个“段”内完成,您的程序保存三个指向某些内存地址的段寄存器,例如

  • 你有CSwhich 指向一些内存,你可以在其中读取和执行
  • 你有DSwhich 指向一些内存,你可以在其中读写
  • 你有SSwhich 指向一些内存,在那里你也可以读写

在执行一些代码时,每次JMPCALL您对某个地址的指令将通过您的处理器检查是你内CS,如果你的范围内,每一个读/写操作系你就全局变量在您的代码将被CPU检查DS内存限制,并因此SS,鉴于此,我们提供了一些提供内存保护的内存模型。

现在我的问题是,当我们动态分配一些内存时会发生什么?还是内存映射?,操作系统给我分配一些内存是不够的,执行程序必须有一些寄存器指向进行 IO 操作的地方......我假设这是 FS / GS / ES 寄存器启动,但我不确定。如果这里有在该领域有更多经验的人可以向我解释这一点,我希望它...

我之前写了一些代码测试它并反汇编它,输出令人失望。

我的代码是这样的

int main()
{
 int * mem = (int *) malloc(4096)

 mem[0] = 5;

 return 0;
}
Run Code Online (Sandbox Code Playgroud)

我希望在反汇编代码中看到类似的东西

lea eax, mem_addr
mov fs:[eax], 5
Run Code Online (Sandbox Code Playgroud)

但这并没有发生..如果有人能向我澄清这一点,我会很高兴的

Mar*_*oom 7

我以两种方式解释了您的问题,第一种是询问如何在现代操作系统上完成内存管理;而第二个作为存储器管理如何进行的分段架构完成。

第一部分非常广泛,所以我将只关注支持结论的最少背景。

第二部分,只是我的好奇心,我抓住机会写点东西。


据我所知,Linux 从未真正支持分段内存模型1,而是依靠分页。

分段与分页

在分页模型中,每个程序都使用相同的一组段来存储代码、数据和堆栈。这些段与整个地址空间一样大;使从逻辑地址线性地址的转换成为身份映射,从而有效地禁用分段2
在上图中,红色和蓝色程序通过以下方式隔离: A) 使用不同的段,如左侧 B) 使用不同的页面,如右侧。

请注意,在分页模型中,从线性地址范围的角度来看,两个进程占用相同的范围,它们重叠。
分页步骤根据进程将相同的线性地址映射到不同的物理地址

Linux 使用分页模型,几乎没有使用分段,这就是为什么fs在内存访问之前您不会看到任何选择器寄存器(如)被加载的原因。
此外malloc,对于 32 位系统,返回 32 位值而不是 48 位值(16 位用于选择器,32 位用于偏移量)。

选择忽略分段机制的一个原因实际上是这种架构中的内存管理过于复杂和低效。

由于在 Linux 机器上,程序不受任何人为段限制的限制:

  • 不需要使用不同的段来访问分配的缓冲区,实际上这意味着指针总是接近3

  • 没有必要移动、增长或缩小一个段,而不是依赖远指针,操作系统可以乐观地增长/缩小一个段,如果可能的话,或者完全移动它。

  • 分配的内存不需要是连续的。

事实上,分配只是找到一个空闲页面(或多个)并将其映射到要求的过程中。

                     页分配

在上图中,红色进程只请求了一页内存,黄色的。

分页本身就是一个主题,但这是主要概念。
mmaping 与分配相同,但页面不受交换空间支持,而是由特定文件4 支持


在分段模型上分配内存

本节讨论对您的问题的第二种解释。
或者它只是提供信息。

当实际受限于在分段架构上工作时,程序和操作系统也被迫处理分段。

分割的主要问题是:

  1. 该段小于整个地址空间,因此尽管大部分计算机内存都是空闲的,但程序可能会耗尽空间。
  2. 大于段大小的数据处理起来有问题。
  3. 从硬件的角度来看,切换段是昂贵的,因为特定的负载必须与正常指令交错,从编译器的角度来看,因为必须实现正确的“段跟踪”。
  4. 可能存在别名,位于三个不同段中的三种不同数据可能恰好可以在一个段内访问。

抽象分段的最简单方法是始终使用远指针。
每次访问都使用远指针完成。
即使有明显的优化,这也是非常昂贵的,因为:A) 大多数程序不需要太多内存 B) 指针大小加倍 C) 必须在任何访问之前加载段寄存器。

所以程序员并没有完全从分段中抽象出来,他们必须声明他们打算如何使用内存。
这产生了各种内存模型:

  • 微小的。数据、代码和堆栈在同一段上。所有的指针都在附近。
  • 。代码独立,数据和堆栈在同一段上。所有的指针都在附近。
  • 紧凑。自己编码,数据和堆栈在多个段上。代码指针5近,数据和堆栈指针远。
  • 。多个段上的代码,单个段上的数据和堆栈。代码指针远,段和数据近。
  • 。代码、堆栈和数据位于多个段上。每个指针都很远。
  • 巨大的。与Compact类似,但单个数据元素(例如数组)可以跨越多个段。
  • 。如第一节所述。

根据声明的模式,编译器使用正确的指针类型。
您的示例在紧凑型大型模型中编译时应该会给出您期望的结果。

分配内存的方式取决于请求的分配类型,近或远。
对于远分配,操作系统必须找到一个空闲段,将其标记为忙并返回指向它的远指针。
如果需要分配多个段,操作系统可以使用各种机制:A) 分配连续的段 B) 返回一个句柄,并使用另一个函数来获取指向分配区域中窗口的指针 C) 拒绝它,强制执行程序进行多次调用并放弃将其隐藏给程序员的负担。

如果段具有固定大小,则分配本地(即附近)缓冲区在技术上是多余的,段已经全部分配。对 的调用malloc完全由 C 运行时解决,它保留了段中分配的内存的堆6

如果段可以增长和缩小,则分配本地缓冲区可能会更棘手,例如,可以将段保持为必要大小并根据请求增长。
最终它甚至可以被移动,因为所有近指针都是相对于它的开始的。

关于在分段架构上分配内存问题的两个有趣的历史窗口:


1它需要 386+。
2因为它实际上在 x86_64 中。
3除了 TLS,但选择器是隐含的和固定的,所以它们不是真正的远指针。
4除非匿名。
5包括jmp, call, ... 的代码目标
6是的,那个堆。