Rust OS 入门:物理地址 ≠ RAM 地址——地址空间、MMIO 与 Memory Map

物理地址(Physical Address)是一张地址地图,RAM 只是地图上的一种东西。

本文是《Writing an OS in Rust》(https://os.phil-opp.com/)入门系列的第三篇,接上《Rust OS 入门:从按下电源键到内核接管》里”内存映射:Kernel 需要知道机器长什么样”那一节,把物理地址空间里到底有什么、Bootloader 为什么要把 Memory Map 传给 Kernel 彻底讲透。

📚 基本概念速读

名称 定义 省流
物理内存(Physical Memory) 真正的 RAM 硬件 可读写的存储芯片
物理地址(Physical Address) CPU 访问物理资源时产生的地址 不一定是 RAM 的地址
物理地址空间(Physical Address Space) CPU 眼中全部地址的集合 一张地址地图,RAM 只是其中一种
MMIO(Memory-Mapped I/O) 把设备寄存器映射进地址空间 读写内存 = 操作硬件
Memory Map 记录地址区域用途的地图 Kernel 靠它分辨能碰/不能碰
Reserved 地址空间里存在但 OS 不能用的区域 千万别拿来当 RAM
虚拟地址(Virtual Address) 程序看到的地址,经 MMU 翻译 程序眼里的地址是假的

🧩 物理地址 ≠ RAM 地址

先想象 CPU 眼里的世界。假设电脑有 16GB RAM,如果地址空间里只有 RAM,那确实很简单:

1
2
3
4
5
0x00000000 ───────┐
│
│ 16 GB RAM
│
0x3FFFFFFFF ──────┘

但真实电脑不是这样。CPU 执行 mov rax, [0x12345678] 时产生的地址叫物理地址(Physical Address),而这个地址背后的东西不一定是 RAM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
物理地址空间

0x00000000 ───────────────
│
│ RAM
│
0x0009FFFF ───────────────
│
│ Reserved
│
0x000F0000 ───────────────
│
│ Firmware / ROM
│
0x000FFFFF ───────────────
│
│ RAM / ...
│
...

物理地址是 CPU 用来定位物理资源的地址;RAM 只是其中一种资源。

🗺️ 物理地址空间里有什么

对一台典型的 x86 PC,物理地址空间大致可以分成五类:

1
2
3
4
5
6
7
物理地址空间
│
├── RAM 可读写存储
├── Firmware / ROM 主板上的固件
├── MMIO 设备寄存器
├── Reserved 存在但不能用
└── 其他硬件相关区域

逐个来看:

RAM:最好理解,CPU 访问它就是普通读写。RAM 本身不规定这里必须放什么,启动后由操作系统动态填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RAM
│
├── Kernel
├── Kernel Stack
├── Page Tables
├── 进程 A
│ ├── Code
│ ├── Data
│ ├── Heap
│ └── Stack
├── 进程 B
├── Disk Cache
├── Network Buffers
└── ...

Firmware / ROM:例如物理地址 0x000F0000 附近对应的可能是主板上的 Flash ROM(BIOS)。CPU 访问它时路径是:

1
CPU → Memory Controller / Chipset → 主板上的 Firmware Flash

而不是 CPU → RAM。这就是 Memory-mapped ROM。

MMIO:见下一节。

Reserved:存在于地址空间里,但操作系统不能随便用。

🔌 MMIO:内存映射 I/O

MMIO(Memory-Mapped I/O,内存映射 I/O) 的思想很简单:

把硬件设备的寄存器映射到物理地址空间里。

硬件工程师的思路是:CPU 本来就有读地址 / 写地址的机制,那干脆把设备寄存器也放进地址空间,CPU 就不需要额外的指令来访问设备了。

1
2
3
4
5
6
7
8
CPU 看起来:

地址 0x1000 → RAM
地址 0x2000 → RAM
地址 0x3000 → RAM
地址 0xF000 → GPU
地址 0xF100 → 网卡
地址 0xF200 → NVMe

假设显卡把某个寄存器规定在物理地址 0xF0000000:

1
mov eax, [0xF0000000]   ; 不是读 RAM,而是在读 GPU 寄存器的值

经典的例子是传统 PC 的显存区域:CPU 往物理地址 0xA0000 附近写数据,就会影响屏幕显示内容。现代 PCIe 设备也大量使用 MMIO,只是地址和机制更复杂。

⚠️ Reserved:为什么不能碰

某些物理地址区域存在于地址空间里,但操作系统不能随便使用。原因可能是:

  • 被硬件使用
  • 被固件使用
  • 对应 MMIO
  • 有特殊用途
  • 芯片组保留区域

如果 OS 把它当普通 RAM 存数据,后果很严重:

1
2
3
4
5
6
7
写入数据
↓
实际上写到了某个硬件寄存器
↓
硬件收到奇怪命令
↓
💥

所以 OS 必须非常小心地区分:绿色 = 可以拿来用,红色 = 千万别碰,黄色 = 硬件。

📦 Memory Map:Bootloader 为什么要传给 Kernel

现在就能理解为什么 Bootloader 要把 Memory Map(内存映射) 传给 Kernel 了——Kernel 不能靠猜:

1
2
3
0x00000000 → RAM?
0x000A0000 → RAM?
0xF0000000 → RAM?

它需要一张地图:

1
2
3
4
5
6
7
8
9
Physical Memory Map

起始地址 结束地址 类型
0x00000000 0x0009FFFF RAM
0x000A0000 0x000FFFFF Reserved
0x00100000 0x7FFFFFFF RAM
...
0xF0000000 0xF0FFFFFF MMIO
...

整个启动链路串起来就是:

1
2
3
电脑启动 → BIOS → 检测 RAM / 硬件 / 物理地址布局
→ Bootloader → 保存并传递 Memory Map
→ Kernel → 区分可分配 / 不可分配 / 设备 MMIO

🧱 从 Memory Map 到 Frame Allocator

拿到 Memory Map 之后,Kernel 才能建立自己的内存管理系统。以 Rust OS 为例,下一步就是 Frame Allocator(物理页帧分配器):

假设 Memory Map 告诉 Kernel:0x100000 ~ 0x80000000 是可用 RAM,Kernel 就把它切成一个个 4 KiB page frame:

1
2
3
4
5
6
7
8
9
10
RAM

0x100000
│
├── Frame 0 4 KiB
├── Frame 1 4 KiB
├── Frame 2 4 KiB
├── Frame 3 4 KiB
├── ...
└── Frame N

之后所有内存需求都从这里申请:

1
2
3
需要创建页表? → 申请一个 Frame
需要创建进程? → 申请一些 Frame
需要 Kernel Heap? → 申请一些 Frame

Memory Map → Frame Allocator → Page Table → Virtual Memory 就是接下来最自然的学习路线。

🕳️ 虚拟地址:程序看到的地址是假的

到这里还有一个很容易混淆的点。你写代码时看到的地址:

1
int *p = malloc(100);   // p ≈ 0x7FF123456000

通常并不是 RAM 的物理地址,而是虚拟地址(Virtual Address)。现代操作系统的地址翻译链路是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
程序
│
│ 虚拟地址
▼
┌─────────────┐
│ MMU │
│ Page Tables │
└──────┬──────┘
│
│ 翻译
▼
物理地址
│
▼
RAM / MMIO / ...

举例:程序执行 *p = 123,p 是虚拟地址 0x7FFF0000,实际过程是:

1
2
3
4
CPU 写虚拟地址 0x7FFF0000
→ MMU 查 Page Table
→ 翻译成物理地址 0x12345000
→ 写入 RAM

所以 0x7FFF0000 不是 RAM 第 0x7FFF0000 个字节,而是当前这个进程看到的虚拟地址。由此可以建立三层概念:

层级 内容 示例
第一层:虚拟地址空间 程序看到的地址 0x7FFF0000
第二层:物理地址空间 MMU 翻译后的地址 0x12345000
第三层:真正的硬件 地址最终对应的东西 RAM / MMIO / Firmware / Reserved

🎨 一张图串起全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
          用户程序
│
│ 虚拟地址
▼
┌──────────────┐
│ MMU │
│ Page Table │
└──────┬───────┘
│
│ 物理地址
▼
┌───────────────────────────┐
│ Physical Address Space │
│ │
│ 0x00000000 │
│ │ │
│ ├── RAM │
│ ├── Reserved │
│ ├── Firmware │
│ ├── RAM │
│ ├── MMIO │
│ ├── RAM │
│ └── ... │
└───────────────────────────┘

再回到开头的疑问:16GB RAM 里面有什么?要分两种情况回答:

  • 问 RAM 这块硬件运行时存什么:Kernel、Kernel Stack/Heap、Page Tables、各进程的 Code/Data/Heap/Stack、文件缓存、网络缓冲区、设备缓冲区等。
  • 问 CPU 的整个物理地址空间里有什么:可用 RAM、Reserved 区域、Firmware/ROM、MMIO、ACPI 硬件信息、APIC 控制区域等。

这两处提到的内存内容不是一回事。

✅ 总结

物理地址是 CPU 用来定位物理资源的地址,RAM 只是地址空间里的一种资源。 Kernel 靠 Bootloader 传的 Memory Map 分辨能碰 / 不能碰 / 是硬件,才能安全地建立 Frame Allocator,进而进入虚拟内存的世界。

下一步建议沿着 Memory Map → Frame Allocator → Page Table → Virtual Address 继续:搞清一个 4 KiB 的物理页帧到底是什么、Page Table 怎么把虚拟地址翻译成物理地址,前面的所有概念就会真正串起来。

Happy Hacking! 🎉