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)。

x86_64 虚拟地址空间布局(48位) 用户空间(User Space) 0x0000000000000000 ~ 0x00007FFFFFFFFFFF 大小:128 TB 每个进程独立,切换时CR3寄存器更新 第47位分界线(0x00007FFFFFFFFFFF / 0xFFFF800000000000) 内核空间(Kernel Space) 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF 大小:128 TB 所有进程共享,仅内核态可访问 注意:用户空间地址第47位为0,内核空间地址第47位为1,高16位符号扩展

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。
Linux x86_64 内核空间详细布局 内核空间(128 TB) 直接映射区(Direct Mapping)— 物理内存直接映射 vmalloc 区域 — 内核动态分配非连续内存 模块区域(Module Region)— 内核模块加载 固定映射区(Fixmap)— 特定物理地址映射 内核栈(Kernel Stack)— 每个进程独立,8~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增强安全性:随机化布局使缓冲区溢出等攻击更加困难