Skip to content

Commit fdcf7c1

Browse files
committed
blog: IA-32e 内存分配浅析
1 parent b97a940 commit fdcf7c1

6 files changed

Lines changed: 145 additions & 0 deletions

File tree

84.7 KB
Loading

blog/2025-08-19-base-malloc/img/02.svg

Lines changed: 1 addition & 0 deletions
Loading
34.7 KB
Loading
54 KB
Loading
115 KB
Loading
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
![画板](img/01.jpeg)
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+
![](img/02.svg)
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+
![画板](img/03.jpeg)
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+
![画板](img/04.jpeg)
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+
![](img/05.png)
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

Comments
 (0)