前几章的进程共享同一张页表——所有进程看到的是同一片内存。这意味着进程 A 知道进程 B 的地址,就能直接读写它的数据。一个 bug 就能破坏整个系统。

这一章解决这个问题:给每个进程一张自己的页表,互相看不见彼此的内存


切换页表就是切换世界

x86-64 的虚拟地址翻译规则写在页表里,页表的根地址放在 CR3 寄存器里。

这意味着:写 CR3 就是切换地址空间。进程 A 跑的时候 CR3 指向 A 的页表,进程 B 跑的时候 CR3 指向 B 的页表。同一个虚拟地址 0x400000,在 A 里翻译到物理页 X,在 B 里翻译到物理页 Y——两边完全隔离,互不干扰。

切换进程时,只需要一行:

void vmm_switch(uint64_t *pml4) {
    __asm__ volatile ("mov %0, %%cr3" :: "r"((uint64_t)pml4) : "memory");
}

硬件帮我们做了全部翻译工作。


创建新页表

每个进程需要自己的 PML4。但不能从空白开始——内核代码的映射必须保留,否则切过去之后 CPU 取不到内核指令,立刻 Page Fault。

做法:把内核的 kernel_pml4 整张复制一份作为起点,然后再往里加用户空间的映射。

uint64_t *vmm_create_page_table() {
    uint64_t *new_pml4 = (uint64_t *)pmm_alloc();
    for (int i = 0; i < 512; i++)
        new_pml4[i] = kernel_pml4[i];  // 继承内核映射
    return new_pml4;
}

往指定页表里建映射

之前的 map_page 只能操作当前 CR3 指向的页表。现在需要给进程建映射,但不想先切换过去,所以新接口接受一个显式的 pml4 参数:

void vmm_map_page(uint64_t *pml4, uint64_t vaddr, uint64_t paddr, uint64_t flags) {
    // 逐级找或创建 PDPT → PD → PT
    uint64_t *pt = ensure_table(...);
    pt[PT_IDX(vaddr)] = paddr | flags;
    flush_tlb(vaddr);
}

这样内核可以在不切换 CR3 的情况下,给任意进程建立内存映射。


验证:同一虚拟地址,两个进程看到不同内容

uint64_t *pml4_a = vmm_create_page_table();
uint64_t *pml4_b = vmm_create_page_table();

uint64_t vaddr = 0x8000000000ULL;
vmm_map_page(pml4_a, vaddr, phys_a, PAGE_USER_RW);
vmm_map_page(pml4_b, vaddr, phys_b, PAGE_USER_RW);

// 切到 A,写 0xAAAAAAAA,立刻切回
// 切到 B,写 0xBBBBBBBB,立刻切回
// 通过物理地址读取验证

输出:

[test] proc_a wrote 0xAAAAAAAA, read back = 0xAAAAAAAA  OK
[test] proc_b wrote 0xBBBBBBBB, read back = 0xBBBBBBBB  OK
[test] isolation (val_a != val_b): OK

同一虚拟地址,两边读到不同值,隔离成功。


踩坑

坑1:切换 CR3 的瞬间不能有函数调用

切换到新页表后,CPU 立刻用新页表取下一条指令。如果新页表里没有当前代码的映射,立刻 triple fault——没有任何错误信息,QEMU 直接重启。

解法:切换 CR3 和写内存用内联汇编写在一起,切换后立刻切回,中间不发生任何函数调用。

坑2:两张页表可能共享 PT

vmm_create_page_table 复制了 kernel_pml4 的全部 512 项,低地址区域的 PDPT/PD/PT 指针也被复制了——两张新页表指向同一棵子树。如果在这个区域做 vmm_map_page,两张表会共用同一个 PT,隔离失效。

解法:测试用完全空白的地址区域(比如 512GB 处 0x8000000000ULL),两张表在这里各自从头建树,天然不共享。

坑3:内联汇编立即数超范围

"movq $0xAAAAAAAA, (%1)\n"  // 报错:operand type mismatch

movq 的立即数最大 32 位符号扩展,0xAAAAAAAA 超了。改用寄存器中转。


小结

地址空间隔离的本质就是每个进程有自己的 CR3。切换进程 = 写 CR3,硬件完成剩下的一切。内核在不切换 CR3 的前提下,可以通过 vmm_map_page(pml4, ...) 给任意进程建立内存映射。

下一章在这个基础上实现 fork:子进程复制父进程的页表,用 Copy-on-Write 延迟真正的物理页复制。