分段机制
大致描述
分段机制的逻辑是把内存按照使用途径划分成不同的段:
- 代码段
- 数据段
- 栈段
对应经典的段寄存器:
- CS CodeSegment
- DS DataSegment
- SS StackSegment
- ES FS GS …
CPU 不直接把程序给出的地址视为物理地址,而是将其表示为:逻辑地址 = 段选择子:段内偏移
假设一条指令需要读取数据:mov eax, [0x1234] 并假设当前 DS = 0x23,那么 CPU 使用的逻辑地址可以表示为 DS:0x1234
而逻辑地址会根据下面这条链路解析为真正的物理地址:通过分段机制解析为线性地址 -> 通过分页机制解析为物理地址
段寄存器
段寄存器中保存的通常不是段基址,而是一个十六位段选择子:

CPU 根据 Index 从描述符表中找到对应的段描述符
GDT 与 LDT
Wiki: 全局描述符表 (GDT) 是一个从 Intel x86-系列处理器 80286 开始用于界定不同内存区域的特征的数据结构。 全局描述表位于内存中。全局描述表的条目描述及规定了不同内存分区的各种特征,包括基地址、大小和访问等特权如可执行和可写等。 在 Intel 的术语中,这些内存区域被称为段 。
CPU 通过 GDTR 寄存器记录 GDT 的地址和大小。而 LDT 现在基本很少使用。
GDT 的是提供内存保护。在 80286 之前的处理器中只有实模式,所有程序都可访问任意内存。GDT 是保护模式下限制非法内存访问的一种方式。
GDT 结构示意:

- Base:段基址
- Limit:段界限
- Type:代码段、数据段等
- DPL:描述符特权级
- P:段是否存在
- G:界限粒度
- D/B:默认操作数或栈宽度
- L:是否为 64 位代码段
特权级机制
- CPL(Current)
- DPL(Descriptor)
- RPL(Request)
有效特权级必须满足段描述符规定的访问条件。数值越小,权限越高,因此不能把 Ring 3 的代码简单伪装成 Ring 0。
不过,现代 Windows 的内核隔离主要还会依赖页表中的 User/Supervisor、读写和 NX 等权限。
x64 下的分段
在 64 位模式下,传统分段被大幅弱化。对 CS、DS、ES、SS 而言:
- 大部分段基址被当作 0
- 大部分段界限检查被忽略
- 线性地址主要由 64 位偏移决定
- CS 仍影响执行模式和特权级
逻辑上可以近似理解为:
线性地址 ≈ 64 位虚拟地址,但是 FS 和 GS 是重要例外。它们仍然可以使用非零基址:线性地址 = FS/GS 基址 + 偏移,总体来说传统分段机制似乎没有那么有用了,大致了解一下应该就差不多了。
分页机制
概述
分页机制把内存划分为固定大小的块:
- 虚拟页(Page):虚拟地址空间中的块
- 物理页框(Page Frame):物理内存中的块
Windows 中常见的页面大小是 4 Kib = 0x1000 字节
分页可以帮助我们实现这些功能:
- 进程隔离
- 内存保护
- 非连续映射
- 按需分配
- 内存共享
在 x64 系统中,页表通常采用多级结构。
一个使用 4 KiB 页面的虚拟地址可以拆成:
63 48 47 39 38 30
+-----------------------+-----------+-----------+
| 符号扩展区域 | PML4 索引 | PDPT 索引 |
+-----------------------+-----------+-----------+
29 21 20 12 11 0
+-----------+-----------+----------------------+
| PD 索引 | PT 索引 | 页内偏移 |
+-----------+-----------+----------------------+
每一级有 2^9 = 512 个表项
Windows 页面类型
- Private Page 进程私有的页面,常见于:
- 堆
- 栈
- VirtualAlloc
- Mapped Page 来自内存映射文件或者共享 Section:
- Image Page 由 EXE 或 DLL 映射形成,通常根据 PE 节设置权限:
- .text 可执行,通常只读
- .data 可读写
- .rdata 只读
Windows 页面状态
虚拟地址通常存在三种状态 Free / Reserved / Committed。
同时还可以通过 Resident / non-resident 描述当前虚拟地址是否驻留在物理内存中。
Free: MEM_FREE 表示这段虚拟地址范围尚未被进程使用。用来描述进程地址空间中未被使用的虚拟内存编号。
- 虚拟地址范围:存在
- 地址归属:未分配
- 提交存储:没有
- 物理页面:没有
- 程序能否访问:不能
void *p = VirtualAlloc(
NULL,
64 * 1024,
MEM_RESERVE,
PAGE_READWRITE
);成功后,这段地址就能够从 Free → Reserved 状态。
Reserved: MEM_RESERVE 表示 Windows 已经把这段虚拟地址范围留给当前分配,其他普通分配不能再占用它,但尚未对其中的页面作出提交承诺。
- 虚拟地址范围:已占用
- 提交存储:没有
- 物理页面:通常没有
- 程序能否正常读写:不能
Reserved 可以帮助操作系统在需要大块内存的时候先记录内存所有的状态,暂时只使用其中的少量页面。
Committed: MEM_COMMIT 表示 Windows 已经承诺,在程序需要页面内容时,可以为它提供相应的存储支持。
char *p = VirtualAlloc(
NULL,
100 * 1024 * 1024,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);调用成功后获得 100 MiB 已提交虚拟空间,但如果程序尚未访问这些页面,实际驻留的物理内存通常远小于 100 MiB。
借助 VMmap 查看单个进程的进程状态和页面统计
首先,我们可以借助 Windows api 来帮助我们分配内存,其中比较重要的函数就是 VirtualAlloc。
VirtualAlloc(
nullptr,
64 * 1024 * 1024,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);对应关系:
- 64 * 1024 * 1024 65,536 K
- MEM_RESERVE 占用连续虚拟地址范围,即我们分配的内存是连续的。
- MEM_COMMIT Committed = 65,536 K
- PAGE_READWRITE Protection = Read/Write
- nullptr Windows 自动选择地址
这个内存地址是虚拟内存地址,而不是物理内存地址。同时我们还能看到通过 ASLR 机制载入的 DLL 动态链接库、堆地址、栈地址、未使用地址等等。
