|
| 1 | +--- |
| 2 | +slug: x86-base-malloc |
| 3 | +title: IA-32e 内存分配浅析 |
| 4 | +authors: [wjc133] |
| 5 | +tags: [x86] |
| 6 | +--- |
| 7 | + |
| 8 | +本文主要用于理顺 IA-32e 架构下的内存分配流程。 |
| 9 | + |
| 10 | +<!-- truncate --> |
| 11 | + |
| 12 | +## 分页基础回顾 |
| 13 | +### 名词解释 |
| 14 | +为了后续表述更加统一和清晰,先做名词解释。 |
| 15 | + |
| 16 | +- Page:页(物理内存页) |
| 17 | +- PT(Page Table):页表 |
| 18 | +- PD(Page Directory):页目录 |
| 19 | +- PDPT(Page Directory Pointer Table):页目录指针表 |
| 20 | +- PML4 (Page Map Level 4):4 级头表 |
| 21 | +- PML5 (Page Map Level 5):5 级头表 |
| 22 | +- PTE(Page Table Entry):页表项 |
| 23 | +- PDE(Page Directory Entry):页目录项 |
| 24 | +- PDPTE(Page Directory Pointer Table Entry):页目录指针表项 |
| 25 | +- PML4E (Page Map Level 4 Entry):4 级头表项 |
| 26 | +- PML5E (Page Map Level 5 Entry):5 级头表项 |
| 27 | + |
| 28 | +### 线性地址与物理地址 |
| 29 | + |
| 30 | + |
| 31 | +在 64 位模式下,通常不会使用全部 64 位作为有效地址。在支持 4 级分表的 64 位处理器上,只有低 48 位是有效地址,高 16 位需要符合扩高形式。 |
| 32 | + |
| 33 | +所以,一旦线性地址确定,那么它所对应的 PML4E、PDPTE、PDE、PTE 也就都确定下来了。这个过程是处理器硬件完成转换的,操作系统无法干预。 |
| 34 | + |
| 35 | +### PML4/PDPT/PD/PT 的本质 |
| 36 | +PML4/PDPT/PD/PT 本身也要占据内存空间,它们本质上也是物理页。 |
| 37 | + |
| 38 | +在典型的 4KB 分页中,每个 PML4/PDPT/PD/PT 内部都会存储 512 个表项,每一项 64 位,即一个 PML4/PDPT/PD/PT 即为 4KB。 |
| 39 | + |
| 40 | +## 内存分配整体流程 |
| 41 | +大致流程可以概括为:`确定要分配的大小-->分配线性地址-->分配物理内存页-->将页注册到多级分表中` |
| 42 | + |
| 43 | +详细流程如下图所示: |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +### 分配线性地址 |
| 48 | +我们要分配线性地址,即虚拟地址,肯定需要知道线性地址空间中哪些区域是空闲的,哪些是已使用的。 |
| 49 | + |
| 50 | +一种最基础的方案就是用一个变量来记录**当前线性地址空间使用到哪里了**。为了后面解释方便,我们把这个变量叫做 _core_next_linear。 |
| 51 | + |
| 52 | +在真实操作系统中,肯定需要考虑对内存的回收和重复利用,但我们先不考虑重复利用的流程,先把基础流程捋顺。 |
| 53 | + |
| 54 | +我们要求分配的线性内存是 8 字节对齐的,这有助于提高内存访问效率。 |
| 55 | + |
| 56 | +:::info |
| 57 | + |
| 58 | +现代 64 位系统架构的数据总线通常是 64 位(8 字节)宽的。这意味着 CPU 与 RAM 或高速缓存之间的一次数据传输最少就是 8 个字节。 |
| 59 | + |
| 60 | +即使你只需要读取一个 int(4 字节),CPU 实际上会读取包含该 int 的整个 8 字节对齐的内存块(或者更大的块)。 |
| 61 | + |
| 62 | +如果一个 int 的起始地址没有 8 字节对齐(例如,位于地址 0x1003),那么它就跨越了两个 8 字节的对齐块(0x1000 和 0x1008)。CPU 就需要发起**两次独立的访问内存操作**并组合所需的部分,才能得到这一个 int。这显然比一次读取慢得多。 |
| 63 | + |
| 64 | +::: |
| 65 | + |
| 66 | +### 线性地址决定了分页位置 |
| 67 | +48 位有效地址中,位 47~38 决定了 PML4E 在 PML4 中的位置。我们要做的是找到这个位置,并查看该位置上是否已经注册了 PML4E,如果有证明之前已经分配过,如果没有则需要分配 PML4E 并注册。 |
| 68 | + |
| 69 | +找到这个位置,其实就是访问内存中的 PML4,但 IA-32e 架构下,强制使用线性地址访问内存空间,即便你知道 PML4 的物理地址也没有用。必须构造出指向 PML4E 的线性地址才能够访问该内存空间。 |
| 70 | + |
| 71 | + |
| 72 | + |
| 73 | +一个 64 位的线性地址,其内容不确定,可以都使用「?」表示。其中低 48 位是有效地址。 |
| 74 | + |
| 75 | +位 47~38 就是 PML4E 的索引号,实际上每个 Entry 占用 8 个字节,所以索引号乘以 8 就是 Entry 在 PML4 中的偏移量。通过`and 0x0000_ff80_0000_0000`的操作,将这 9 位的值取出,其余位置 0,然后右移 36 次,可以得到 |
| 76 | + |
| 77 | +00000000 00000000 00000000 00000000 00000000 00000000 0000<font style={{color:"#DF2A3F"}}>???? ?????</font>000 |
| 78 | + |
| 79 | +它就是 PML4E 的表内偏移量了。 |
| 80 | + |
| 81 | +下面就是如何得到表的起始地址了。我们采用一种很巧妙的「递归」方式解决该问题。 |
| 82 | + |
| 83 | +即在初始化 PML4 时,就将最后一个 Entry 指向自身的物理地址。 |
| 84 | + |
| 85 | + |
| 86 | + |
| 87 | +这样,我们只需要最终构造出 |
| 88 | + |
| 89 | +11111111 11111111 11111111 11111111 11111111 11111111 1111<font style={{color:"#DF2A3F"}}>???? ?????</font>000 |
| 90 | + |
| 91 | +就可以访问线性地址的 PML4E 所在的位置了。 |
| 92 | + |
| 93 | +我们简单思考验证下: |
| 94 | + |
| 95 | +- 位 47~39 的 111111111 对应的是 PML4 的最后一个 Entry,其指回自身。 |
| 96 | +- 位 38~30 的 111111111 对应的是 PDPT 的最后一个 Entry,但这里的 PDPT 其实就是 PML4 自身,所以其又指回自身。 |
| 97 | +- 位 29~21 的 111111111 对应的是 PD 的最后一个 Entry,但这里的 PD 其实就是 PML4 自身,所以其又指回自身。 |
| 98 | +- 位 20~12 的 111111111 对应的是 PT 的最后一个 Entry,但这里的 PT 还是 PML4 自身,所以其又指回自身。 |
| 99 | +- 位 11~0 的 ?????????000 对应的是物理页中的偏移地址,这里的物理页是 PML4 自身,所以就能顺利访问 PML4E 的内存空间了。 |
| 100 | + |
| 101 | + |
| 102 | +同理,我们可以通过线性地址的位 38~30、位 29~21、位 20~12 分别构造出该线性地址对应的 PDPTE、PDE、PTE 的线性地址,访问并校验其是否已经生成并注册过了。 |
| 103 | + |
| 104 | +### 物理内存页的分配 |
| 105 | +和分配线性地址空间是一样的道理,我们必须知道当前物理页哪些被分配了,哪些没有被分配。由于物理页是已分配就是一页,且分配位置是随机分配的。因此,我们可以用一个 bitmap 来存储每个页是否有被占用。 |
| 106 | + |
| 107 | +bitmap 的 offset 用来表示是第几个物理内存页,对应位置的比特用来表示当前页是否已分配。 |
| 108 | + |
| 109 | +使用 bts 指令测试并设置 bitmap 中的指定比特。这条指令将选定的比特传送到标志寄存器的 CF 位。 |
| 110 | + |
| 111 | +通过判断标志寄存器的 CF 位是 0 还是 1,就知道被测试的比特是 0 还是 1。 |
| 112 | + |
| 113 | +- 如果是 0,证明未分配,将当前比特的 offset 左移 12 位(相当于乘以 4096),即可得到当前页的物理地址 |
| 114 | +- 如果是 1,证明已分配,将 offset+1,并继续测试下一个比特是否为 0 |
| 115 | + |
| 116 | +### Entry 的分配与注册 |
| 117 | +通过上述步骤,检查 Entry 是否已经注册过,主要是依靠 Entry 的 P 标识位。 |
| 118 | + |
| 119 | + |
| 120 | + |
| 121 | +:::warning |
| 122 | + |
| 123 | +上表并不是非常标准的 Entry 格式,因为各级 Entry 的格式有细微差别,但是大体结构是相同的。这里是做了一个通用表示。 |
| 124 | + |
| 125 | +::: |
| 126 | + |
| 127 | +我们只需要通过 Entry 的线性地址拿到内存空间中的数据,再检查一下 P 位是否为 1,即可得知当前位置是否已经有 Entry 注册。 |
| 128 | + |
| 129 | +如果没有注册,那我们需要做两个事情: |
| 130 | + |
| 131 | +1. 为 Entry 所对应的下级映射分配内存空间 |
| 132 | +2. 将该内存空间的物理地址注册到当前级别的映射表中 |
| 133 | + |
| 134 | +#### 为 Entry 对应的下级映射分配内存空间 |
| 135 | +- 假设当前的 Entry 是 PML4E,那么它所对应的下级映射就是 PDPT |
| 136 | +- 假设当前的 Entry 是 PDPTE,那么它所对应的下级映射就是 PD |
| 137 | +- 假设当前的 Entry 是 PDE,那么它所对应的下级映射就是 PT |
| 138 | + |
| 139 | +无论是哪种情况,我们都需要为下级映射分配内存空间。待分配的内存空间是 4KB,刚好一个 Page。 |
| 140 | + |
| 141 | +以 PML4E 为例,分配到 Page 后,将它作为对应 PDPT 的内存空间,我们需要初始化这部分空间,将其全部置 0,包括 P 位都要为 0。 |
| 142 | + |
| 143 | +#### 将 Entry 注册到映射表中 |
| 144 | +我们已经有了 Entry 的「线性地址」,也有下级映射表的起始物理地址,下一步就是构造一个 Entry,并写入到「线性地址」中。这部分不难理解,不做赘述。 |
0 commit comments