什么叫“虚拟地址空间”?
一句话:它是 CPU 看得见、App 以为自己独享,但实际上会被内核和硬件(MMU)动态翻译到真实 物理内存 的一整块“虚拟地图”。
1. 背景:为什么要“虚拟”?
需求 | 虚拟地址空间能做什么 |
---|---|
进程隔离 | 给每个进程各分一套 0 → N 的地址,互不重叠,进程 A 读不到进程 B 的数据。 |
内存保护 | 页级读/写/执行权限由页表标记;越界访问马上抛 EXC_BAD_ACCESS 。 |
弹性分配 | 物理内存碎片化无关紧要,只要虚拟地址连续即可(“看上去一整块”)。 |
高级特性 | Copy‑on‑Write、内存映射文件 (mmap )、共享库复用、ASLR、PAC … |
2. 在 iOS/arm64 上怎么实现?
2.1 MMU + 页表
- MMU(Memory Management Unit):CPU 访存时,把 64‑bit 虚拟地址 拆成多级索引,通过页表(
TTBR0
/TTBR1
指向)翻译成 48‑bit 物理地址。 - 页大小:iOS 全系 16 KiB;一页是虚拟空间管理的基本粒度。
- 属性位:每个页表条目有 RWX 标志、用户/内核态位、内存类型(缓存/设备)等。
2.2 用户态 vs 内核态
区域 | arm64 虚拟高位 | 典型范围 (48‑bit VA) | 说明 |
---|---|---|---|
用户空间 | [0, 0x0000_FFFF_FFFF] | 0 → 128 TiB | 每个进程独占;App 代码、堆、栈、JIT、dyld shared cache… |
内核空间 | [0xFFFF_0000_0000_0000, 2⁶⁴) | 顶部 128 TiB | 所有进程共享同一内核映像与数据;受 KTRR/PACDMA 保护 |
- 两块空间由 异常级别(EL0 / EL1) 与
TTBR
的切换隔离:App 只能使用下半部地址,高位一旦访问就触发权限异常。 - 高位还用到 Top‑Byte‑Ignore (TBI):高 8 bit 可存自定义 tag(例如 Swift 的指针压缩、MTE 内存标记等)。
3. 64‑bit 设备典型虚拟地址布局(示意)
0x0000_0000_0000_0000
│ 保留页 (NULL, guard)
├─ Mach-O 主可执行 (PIE, text+data)
├─ __DATA_CONST / 读取‑仅映射
├─ Heap ⇡ 动态增长
│
│ (空洞,可供 mmap / JIT / stack 使用)
│
├─ Thread #N Stack ⇣ 向下增长
│ └─ Guard Page (不可访问)
│
├─ dyld shared cache (私有或共享段)
└─ … (高地址)
0x0000_FFFF_FFFF_FFFF ← 用户空间顶
───────────────────────
0xFFFF_0000_0000_0000 ← 内核空间起
├─ Kernel Mach‑O + KEXT (KASLR)
└─ I/O 映射、vmalloc、kstack…
0xFFFF_FFFF_FFFF_FFFF ← 64‑bit 顶
每次 App 启动、设备冷启动 时,这些段会因为 ASLR 被整体“滑动”(加上 slide 值),但相对排列不变。
4. 与开发者相关的日常场景
你在做什么 | 虚拟地址空间发生了什么 |
---|---|
使用 malloc | 内核在“堆区”后方找一块尚未用过的虚拟页,映射物理内存并返回 虚拟指针。 |
调试崩溃日志 | 0x104a74000 这样的指针是“已经加了 slide 的虚拟地址”;需要减去 slide 才能对应符号表。 |
开启 JIT / Metal | 虚拟页被标记为可执行或设备内存,MMU 读到属性位后,用不同缓存策略访问。 |
访问空指针 / 越界 | 目标虚拟页没有映射或权限位不匹配 → MMU 触发 Page Fault,内核抛异常。 |
5. 小结
- 虚拟地址空间 = “给每个进程画的一张 私有地图”;
- MMU + 页表 做“坐标翻译”与“边界安检”;
- iOS 在这张地图上再叠加 ASLR、PAC、KTRR、代码签名 等多重防护。
借助虚拟地址空间,iOS 既能让每个 App 看到一个 连续、干净且安全 的内存世界,也能让内核在背后高效地管理、隔离、复用有限的物理内存资源。