如何在i386分页目录中实现非连续物理页到连续虚拟地址空间的映射?
Hey Devin, great question—mapping non-contiguous physical pages to a contiguous virtual address space is exactly how virtual memory gives processes the illusion of a big, linear memory pool, even if the underlying physical RAM is scattered. Let’s walk through this step by step, using 32-bit x86 as our example (since that’s the most common starting point for simple OSes, and it uses the page directory + page table setup you mentioned).
First, let’s make sure we’re on the same page (pun intended) about how 32-bit x86 pagination works:
- A virtual address splits into three parts:
- Bits 31-22: Page Directory Index (10 bits → 1024 possible entries)
- Bits 21-12: Page Table Index (10 bits → 1024 entries per page table)
- Bits 11-0: Page Offset (12 bits → 4KB per page, which matches the size of physical pages)
- Each entry in the page directory (PDE) points to a physical page table.
- Each entry in a page table (PTE) points to a physical memory page, plus flags for permissions, presence, etc.
Every process needs its own page directory to isolate its virtual address space. Here’s how to set it up:
- Allocate a 4KB physical page for the page directory (use your OS’s physical memory allocator—make sure it’s 4KB-aligned!).
- Zero out the entire page directory to start with clean entries.
- For each range of virtual addresses you want to map (like your 0x0-starting contiguous space), allocate a page table if one doesn’t exist yet, and link it to the corresponding PDE in the page directory.
This is the core part you’re asking about. For each physical page you want to map to a virtual address, follow these steps:
- Take the target virtual address and split it into the page directory index, page table index, and offset (we only care about the first two for mapping).
- Find the page table pointed to by the PDE (if you haven’t allocated this page table yet, do that now and update the PDE).
- Set the corresponding PTE in that page table to point to the physical page’s address, plus the correct permission flags.
Let’s use your specific examples to make this concrete:
- Map physical 0x700000 → virtual 0x0:
- Virtual 0x0 → directory index 0, table index 0.
- Update page table entry 0 to point to physical 0x700000 (mask out the lower 12 bits, since they’re for flags).
- Map physical 0x715000 → virtual 0x10000:
- Virtual 0x10000 = 65536. Divide by 4096 (page size) → 16. So table index is 16, directory index is still 0.
- Update page table entry 16 to point to physical 0x715000.
- Map physical 0x8000 → virtual 0x10000:
- Virtual 0x10000’s table index is 16 (same as above), so update that same PTE if you want to replace the code segment mapping, or pick another virtual address (like 0x20000, which gives table index 32) for the stack page.
Here’s some pseudocode/C to show how this would look in practice:
First, define helpers for setting PDE/PTE flags:
#include <stdint.h> typedef uint32_t pde_t; typedef uint32_t pte_t; // Helper to set a Page Directory Entry: physical page table address + flags #define SET_PDE(pde_entry, phys_table_addr, is_writable, is_user) \ pde_entry = (phys_table_addr & 0xFFFFF000) | \ (is_writable ? 0x2 : 0) | \ (is_user ? 0x4 : 0) | \ 0x1; // Presence bit (must be set for valid entries) // Helper to set a Page Table Entry: physical page address + flags #define SET_PTE(pte_entry, phys_page_addr, is_writable, is_user) \ pte_entry = (phys_page_addr & 0xFFFFF000) | \ (is_writable ? 0x2 : 0) | \ (is_user ? 0x4 : 0) | \ 0x1;
Then initialize the page directory and map your pages:
// Assume these functions exist in your OS: // - alloc_phys_page(): returns a 4KB-aligned physical address of a free page // - memset(): clears a block of memory // Create the process's page directory pde_t* process_page_dir = (pde_t*)alloc_phys_page(); memset(process_page_dir, 0, 4096); // Zero out the directory // Allocate a page table for the virtual address range 0x0 to 0x3FFFFF (4MB) pte_t* process_page_table = (pte_t*)alloc_phys_page(); memset(process_page_table, 0, 4096); // Link the page table to the first entry in the page directory (user-accessible, writable) SET_PDE(process_page_dir[0], (uint32_t)process_page_table, 1, 1); // Map data segment: phys 0x700000 → virt 0x0 (writable, user-accessible) SET_PTE(process_page_table[0], 0x700000, 1, 1); // Map code segment: phys 0x715000 → virt 0x10000 (read-only, user-accessible) SET_PTE(process_page_table[16], 0x715000, 0, 1); // Map stack page: phys 0x8000 → virt 0x20000 (writable, user-accessible) SET_PTE(process_page_table[32], 0x8000, 1, 1); // Add more stack pages as needed (e.g., phys 0x9000 → virt 0x1F000 for downward-growing stack) SET_PTE(process_page_table[31], 0x9000, 1, 1);
Finally, when switching to this process, load its page directory into the CR3 register (this tells the CPU to use this process’s address space):
; In x86 assembly, load the physical address of the page directory into CR3 mov eax, [process_page_dir_phys_addr] mov cr3, eax
- 4KB Alignment: All physical pages (page directories, page tables, and the memory you’re mapping) must be 4KB-aligned. Your physical memory allocator should enforce this.
- Permission Flags: Set flags correctly: mark code segments as read-only, stack/data as writable, and user processes as user-accessible (U/S flag = 1) so they can’t access kernel memory.
- Page Faults: If your stack grows into un-mapped virtual pages, you’ll need a page fault handler to allocate new physical pages and map them on the fly (this is called demand paging). For now, you can pre-map all the stack pages you need.
- Context Switching: Always switch the CR3 register when switching processes—this is what isolates each process’s virtual address space.
内容的提问来源于stack exchange,提问作者Devin

