今天的目標是徹底接管系統的記憶體控制權。我們實作並建立了完整的虛擬內存管理員(VMM)。這意味著內核現在可以動態地控制虛擬地址與物理地址之間的映射關係,為未來的多工處理(Multitasking)鋪平道路。
VMM 的任務是管理頁目錄(Page Directory)與頁表(Page Table)。在Day 12和 Day 13,我們使用的是啟動時的臨時映射,今天我們實作了內核正式的虛擬地址空間配置。
#include "vmm.h"
#include "pmm.h"
#include "mem.h"
#include "gdt.h"
#include "idt.h"
#include "display.h"
// 獲取地址的高 10 位 (PDE 索引)
#define PD_INDEX(vaddr) ((vaddr) >> 22)
// 獲取地址的中間 10 位 (PTE 索引)
// 移除最低的12為(offset), 使用掩碼獲得中間的10位
#define PT_INDEX(vaddr) (((vaddr) >> 12) & 0x3FF)
// comes from link.ld definition
extern u32 _kernel_end;
// 內核全局頁目錄 (放在高位)
page_directory_t* kernel_directory = NULL;
void vmm_map(page_directory_t* pd, u32 vaddr, u32 paddr, u32 flags) {
u32 pd_idx = PD_INDEX(vaddr);
u32 pt_idx = PT_INDEX(vaddr);
page_entry_t* pde = &pd->entries[pd_idx];
// 1. 檢查頁目錄項是否存在 (Present 位)
if (!(*pde & VMM_PAGE_PRESENT)) {
// 如果不存在,分配一個物理頁面作為新的頁表
u32 new_pt_phys = (u32)pmm_alloc_page();
// 將新的頁表掛載到頁目錄 (注意存入的是物理地址)
// 權限通常給予 Present | RW | User,具體權限由 PTE 控制
*pde = new_pt_phys | VMM_PAGE_PRESENT | VMM_PAGE_RW;
// 清空這個新頁表的內容 (需要先轉成虛擬地址才能訪問)
page_table_t* new_pt_virt = (page_table_t*)PHYS_TO_VIRT(new_pt_phys);
for (int i = 0; i < ENTRIES_PER_TABLE; i++) {
new_pt_virt->entries[i] = 0;
}
}
// 2. 找到頁表並設置 PTE
// pde 的高 20 位是頁表的物理地址
u32 pt_phys = *pde & ~0xFFF;
page_table_t* pt_virt = (page_table_t*)PHYS_TO_VIRT(pt_phys);
// 填入目標物理地址和標誌位
pt_virt->entries[pt_idx] = (paddr & ~0xFFF) | flags;
// 3. 通知 CPU 刷新 TLB (如果修改的是當前正在運行的頁表)
// 簡單的做法是重新加載 CR3,或者使用 invlpg
__asm__ __volatile__("invlpg (%0)" : : "r" (vaddr) : "memory");
}
// 切換 CR3 的簡單彙編封裝
void vmm_switch_directory(u32 pd_phys) {
__asm__ __volatile__("mov %0, %%cr3" : : "r" (pd_phys) : "memory");
}
void init_vmm() {
// 1. 進入關鍵區:關閉中斷,防止切換期間發生異常導致 Triple Fault
__asm__ __volatile__("cli");
// 2. 分配物理頁面作為頁目錄 (Page Directory)
u32 pd_phys = (u32)pmm_alloc_page();
// 獲取其虛擬地址以便內核訪問
kernel_directory = (page_directory_t*)PHYS_TO_VIRT(pd_phys);
// 3. 初始化頁目錄:將所有條目設為「不存在」
for (int i = 0; i < ENTRIES_PER_TABLE; i++) {
kernel_directory->entries[i] = 0;
}
// 4. Identity Map 低端內存 (0 - 4MB)
// 確保在切換 CR3 及其後的幾行代碼執行時,EIP 仍然指向有效地址
for (u32 addr = 0; addr < 0x400000; addr += PAGE_SIZE) {
vmm_map(kernel_directory, addr, addr, VMM_PAGE_PRESENT | VMM_PAGE_RW);
}
// 5. 映射 High Half 內核空間 (0xC0000000 - 0xC0400000)
// 這裡直接映射完整的 4MB 區域,確保覆蓋內核代碼、數據、BSS、GDT/IDT 和 PMM 位圖
for (u32 i = 0; i < 0x400000; i += PAGE_SIZE) {
vmm_map(kernel_directory, 0xC0000000 + i, i, VMM_PAGE_PRESENT | VMM_PAGE_RW);
}
// 6. 手動映射頁目錄自身
// 這是為了確保當前正在使用的 Page Directory 在分頁開啟後依然可以被訪問
vmm_map(kernel_directory, (u32)kernel_directory, pd_phys, VMM_PAGE_PRESENT | VMM_PAGE_RW);
// 7. 切換 CR3 暫存器,正式啟用新的分頁結構
vmm_switch_directory(pd_phys);
// 8. 刷新環境:在新的虛擬位址空間重新加載 GDT 和 IDT
// 這一點至關重要,因為舊的指針可能指向了未映射或不正確的物理地址
init_gdt();
load_idt();
// 9. 恢復中斷處理
__asm__ __volatile__("sti");
kprint("VMM enabled and environment reloaded successfully.\\n");
}
今天在切換到新的 VMM 系統時,遇到了三個導致系統不斷重啟的硬核問題:
問題描述:在創建新的頁表時,忘記將頁目錄(Page Directory)本身的地址映射進去。 技術分析:當內核需要動態修改頁表條目時,處理器必須能訪問到頁目錄。如果沒有進行自映射(Recursive Mapping),一旦開啟新的分頁結構,內核將無法修改自己的頁表,導致隨後的地址訪問發生 Page Fault。 解決方案:我增加了一個對page_directory物理地址和虛擬地址的mapping,另外一個可行的方案是在頁目錄的最後一個 Entry 指向自己,實現 Recursive Mapping 技巧。
問題描述:在切換 CR3 暫存器加載新頁目錄時,系統發生了 Triple Exception。
技術分析:切換頁表的瞬間是非常敏感的原子操作。如果此時觸發了時鐘中斷,CPU 會嘗試跳轉到 IDT 指向的中斷處理代碼,但此時新的地址空間可能尚未完全就緒或映射不一致,導致 CPU 崩潰。
解決方案:在切換分頁結構前執行 cli,完成環境刷新後再 sti。
問題描述:分頁切換成功後,系統隨即崩潰。
原因分析:GDT 和 IDT 的基地址是線性地址(Linear Address)。當我們更換了整個頁表結構後,舊的線性地址可能已經失效或指向了錯誤的內容,導致 CPU 找不到段描述符或中斷處理入口。
解決方案:在 VMM 初始化完成並開啟後,立即執行 init_gdt() 與 load_idt(),確保 CPU 的所有控制寄存器都同步到新的虛擬地址空間中。
目前的內核地址佈局: