启用PAE分页时32位内核在KVM及真实硬件触发GPF
我正在开发支持PAE分页的32位OS内核,执行mov %eax, %cr0设置PG位启用分页时,QEMU的TCG模式下运行正常,但KVM加速或真实硬件上会直接触发General Protection Fault(GPF)。
✅ 正常工作场景
- QEMU无
-enable-kvm参数的TCG模式下,分页可正常启动 - 分页结构已完成分配与填充
- 启用分页前VGA文本输出正常
- 已测试4GiB范围的恒等映射
❌ 仅在KVM/真实硬件出现的故障
- 启用分页后立即触发GPF
- 崩溃时EIP指向有效代码(已通过inline call/pop技巧确认)
- 故障发生在设置PG位后,尚未实际使用虚拟内存
核心问题
为何启用PAE恒等映射分页仅在KVM及真实硬件下触发GPF?是否存在KVM下EIP或CR3未映射的已知问题?还有哪些原因会导致启用分页时立即触发GPF?
配置代码
// Allocate memory for PDPT, PDs, and PTs alignas(PAGE_SIZE) pdpt_t* pdpt = (pdpt_t*)pmm::alloc_frame(1); alignas(PAGE_SIZE) pd_t* pds = (pd_t*)pmm::alloc_frame(4); alignas(PAGE_SIZE) pt_t* pts = (pt_t*)pmm::alloc_frame(32); // Checking if any allocation failed if (!pdpt || !pds || !pts) { kernel_panic("Paging structures allocation failed!\n"); return; } uint64_t frame_addr = 0; // Physical address starts at 0x00000000 // Set up PDPT for (int i = 0; i < 4; i++) { pdpt->entries[i] = ((uint64_t)&pds[i] & ~0xFFF) | PRESENT | WRITABLE; } // Set up PDs and PTs for (int i = 0; i < 4; i++) { for (int j = 0; j < 512; j++) { int pt_index = (i * 512) + j; pds[i].entries[j] = ((uint64_t)&pts[pt_index] & ~0xFFF) | PRESENT | WRITABLE; for (int k = 0; k < 512; k++) { pts[pt_index].entries[k] = (frame_addr & ~0xFFF) | PRESENT | WRITABLE; frame_addr += PAGE_SIZE; // Move to next 4KiB frame } } } // Load PDPT, setting up higher-half kernel, enable PAE, enable paging and flush the TLB enable_pae(); active_pdpt = pdpt; // Setting up higher half kernel // Mapping 0x100000-0x400000 to 3GiB in virtual memory uint64_t kernel_base = uint64_t(&__kernel_phys_base); for(uint64_t addr = kernel_base, v_addr = KERNEL_BASE; addr < kernel_base + 0x400000; addr += PAGE_SIZE, v_addr += PAGE_SIZE) vmm::map_page(v_addr, addr, PRESENT | WRITABLE); // Identity mapping up to 4GiB (or to max RAM installed if 4GiB is not available) uint64_t target = (0x100000000 > pmm::total_installed_ram) ? pmm::total_installed_ram : 0x100000000; for (uint64_t addr = 0; addr < target; addr += PAGE_SIZE) vmm::map_page(addr, addr, PRESENT | WRITABLE); set_pdpt(uint32_t(pdpt)); flush_tlb(); enable_paging(); flush_tlb(); enabled_paging = true; vga::printf("Paging initialized with identity map up to 4GiB\n");
已确认项
- CR3指向正确物理地址
- 启用分页前打印的EIP处于映射范围内
- PDPT在启用分页前已完成映射
- GDT有效,0x08为内核代码选择子
- 所有分页结构均按页大小对齐
问题分析与解决方案
1. PAE模式下CR3的严格对齐要求
PAE模式中,CR3存储的是PDPT物理地址的高27位,地址必须对齐到32字节(低5位必须为0)。TCG模式可能对对齐错误宽松,但KVM/真实硬件会严格校验。检查set_pdpt实现,确保写入CR3前将地址与~0x1F按位与:
set_pdpt: mov eax, [esp+4] and eax, ~0x1F ; 清零低5位,符合PAE对齐要求 mov cr3, eax ret
2. 分页结构地址的物理/虚拟混淆
如果pmm::alloc_frame返回的是虚拟地址而非物理地址,分页条目会被错误填充为虚拟地址——TCG模式可能允许虚拟地址直接作为物理地址使用,但KVM/真实硬件严格区分两者。确认pmm::alloc_frame返回物理地址,或在填充条目时将虚拟地址转换为对应物理地址。
3. PAE启用与CR3加载的顺序错误
当前代码先启用PAE再加载CR3,部分CPU/KVM要求PAE启用前CR3必须指向有效PDPT。正确顺序应为:
- 准备所有分页结构
- 加载CR3(指向PDPT物理地址)
- 启用PAE(设置CR4.PAE)
- 启用分页(设置CR0.PG)
调整后CPU启用PAE时可立即识别有效页表,避免临时无效状态触发GPF。
4. 页表权限与代码段权限不匹配
内核代码段描述符如果是只读属性,对应页表条目却设置了WRITABLE位,部分CPU会触发权限冲突GPF。同时检查PRESENT/WRITABLE宏定义,确保未意外设置PAE页表条目的XD(执行禁用)位(位63)。
5. TLB刷新实现问题
PAE模式下加载CR3会自动刷新TLB,额外的flush_tlb()若实现错误(如使用非PAE兼容指令),可能导致TLB残留无效条目。确认flush_tlb()的实现为标准的mov cr3, cr3指令。
内容的提问来源于stack exchange,提问作者Ioane Baidoshvili

