3. 64位系统下的地址空间:x86_64架构的地址空间特点
从32位时代进入64位时代,虚拟地址空间发生了质的变化。x86_64架构(也称AMD64或Intel 64)将虚拟地址从32位扩展到64位,但实际硬件实现并未使用全部64条地址线,而是采用了48位虚拟地址。这一设计选择在地址空间大小、硬件成本和实现复杂度之间取得了平衡。
核心要点: x86_64架构使用48位虚拟地址,支持高达256 TB的虚拟地址空间(128 TB用户空间 + 128 TB内核空间),远大于32位系统的4 GB限制。
3.1 48位虚拟地址的由来
为什么是48位而不是64位?这主要基于以下考虑:
- 硬件成本:完整的64位地址需要更多的页表层级和TLB条目,增加芯片面积和功耗
- 实际需求:256 TB的地址空间已远超当前任何应用的需求
- 扩展性:保留未来扩展能力,后续架构可逐步增加地址位数(如57位)
48位虚拟地址通过4级页表(PML4、PDPT、PD、PT)实现地址翻译,每级页表负责9位地址索引,加上12位页内偏移,共48位(9+9+9+9+12=48)。
| 页表层级 | 名称 | 索引位数 | 作用 |
|---|---|---|---|
| L4 | PML4(Page Map Level 4) | 9 | 顶级页表,指向下一级页表 |
| L3 | PDPT(Page Directory Pointer Table) | 9 | 二级页表,指向页目录 |
| L2 | PD(Page Directory) | 9 | 三级页表,指向页表 |
| L1 | PT(Page Table) | 9 | 四级页表,指向物理页帧 |
| — | 页内偏移 | 12 | 4 KB页内的字节偏移 |
提示: 48位地址的高16位(位48~63)必须与第47位相同,这一规则称为符号扩展(Sign Extension)。第47位为0表示用户空间地址,为1表示内核空间地址。
3.2 用户态与内核态的巨大空间
x86_64架构将虚拟地址空间划分为两个主要区域:用户空间和内核空间。以第47位为分界线,用户空间占据低半部分(0x0000000000000000 ~ 0x00007FFFFFFFFFFF),内核空间占据高半部分(0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF)。
3.3 用户空间布局详解
在Linux系统中,用户空间的128 TB并非全部可用,而是按照固定布局划分为多个区域:
| 区域名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 代码段(Text) | 0x0000000000400000 | 通常几MB | 程序可执行代码 |
| 数据段(Data/BSS) | 代码段之后 | 动态 | 全局变量、静态变量 |
| 堆(Heap) | 数据段之后 | 动态增长 | malloc/brk分配 |
| 内存映射区(mmap) | 堆与栈之间 | 动态 | 文件映射、共享内存、动态库 |
| 栈(Stack) | 0x00007FFFFFFDE000 | 通常8 MB | 函数调用、局部变量 |
| vsyscall/vvar | 0xFFFFFFFFFF600000 | 几KB | 内核辅助调用(用户态可见) |
注意: 用户空间地址从0x0000000000000000开始,但最低的几KB(通常0~0x10000)被保留为空指针陷阱区域,用于捕获NULL指针解引用。代码段通常从0x400000开始加载。
3.4 内核空间布局详解
内核空间的128 TB被进一步细分为多个逻辑区域,每个区域承担不同的内核功能:
- 直接映射区(Direct Mapping / Linear Mapping):将全部物理内存直接映射到内核虚拟地址空间,起始地址为0xFFFF880000000000。内核通过简单的偏移即可访问任意物理页。
- vmalloc区域:用于内核动态分配非连续物理内存,起始地址在直接映射区之后。适用于需要大块虚拟地址但物理内存不连续的场景。
- 模块区域(Module Region):用于加载内核模块(.ko文件),通常位于内核空间的高端。
- 固定映射区(Fixmap):用于映射特定物理地址(如ACPI、APIC寄存器),地址固定不变。
- 内核栈(Kernel Stack):每个进程在内核态运行时拥有独立的内核栈,通常为8 KB或16 KB。
3.5 用户态与内核态的切换代价
虽然用户空间和内核空间在虚拟地址上被清晰分割,但两者之间的切换(系统调用、中断、异常)需要付出性能代价:
| 切换类型 | 主要操作 | 延迟(典型值) |
|---|---|---|
| 系统调用(syscall) | 切换栈、保存寄存器、更新CR3(可选) | ~100 ns |
| 中断处理 | 保存上下文、查找中断向量表 | ~200 ns |
| 异常处理 | 保存上下文、错误码处理 | ~300 ns |
优化技巧: x86_64引入了syscall/sysret指令替代传统的int 0x80,大幅降低了系统调用延迟。此外,内核通过PCID(Process Context IDentifiers)技术避免每次切换都刷新TLB,进一步减少切换开销。
3.6 地址空间布局随机化(ASLR)
现代Linux系统在x86_64上默认启用ASLR,随机化用户空间各区域的基地址,增加攻击者预测内存布局的难度:
- 栈基址随机化:每次进程启动时,栈的起始地址在0x7ffffffff000附近随机偏移
- mmap基址随机化:内存映射区域的起始地址随机化,影响动态库加载位置
- 堆基址随机化:堆的起始地址在数据段之后随机偏移
- 代码段随机化:PIE(Position Independent Executable)程序加载地址随机化
# 查看进程的地址空间布局(示例)
$ cat /proc/self/maps
00400000-00401000 r-xp 00000000 08:01 1234567 /usr/bin/cat
00600000-00601000 r--p 00000000 08:01 1234567 /usr/bin/cat
00601000-00602000 rw-p 00001000 08:01 1234567 /usr/bin/cat
7f8a1c000000-7f8a1c021000 r-xp 00000000 08:01 1234568 /usr/lib/libc.so.6
7f8a1c021000-7f8a1c220000 ---p 00021000 08:01 1234568 /usr/lib/libc.so.6
7f8a1c220000-7f8a1c224000 r--p 00020000 08:01 1234568 /usr/lib/libc.so.6
7f8a1c224000-7f8a1c226000 rw-p 00024000 08:01 1234568 /usr/lib/libc.so.6
7ffd8e200000-7ffd8e421000 rw-p 00000000 00:00 0 [stack]
7ffd8e5f0000-7ffd8e5f3000 r--p 00000000 00:00 0 [vvar]
7ffd8e5f3000-7ffd8e5f5000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
关键观察: 从上述输出可以看到,用户空间地址从0x400000开始,栈位于0x7ffd区域,内核vsyscall位于0xffffffffff600000。每个区域的地址都因ASLR而随机化,每次运行结果不同。
3.7 本章小结
x86_64架构的48位虚拟地址空间为操作系统提供了前所未有的灵活性:
- 128 TB用户空间:每个进程拥有独立的巨大地址空间,无需像32位系统那样频繁进行地址空间映射
- 128 TB内核空间:内核可以轻松映射全部物理内存,并保留大量区域用于动态分配和模块加载
- 清晰的用户/内核分割:第47位符号扩展机制天然实现了用户态与内核态的隔离
- ASLR增强安全性:随机化布局使缓冲区溢出等攻击更加困难