# Page Tables и отображение памяти: `paging.rs` ## Концептуальная модель x86-64 использует **4-уровневую иерархию таблиц страниц**: ``` Виртуальный адрес (48 бит): ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │ PML4 │ PDPT │ PD │ PT │ offset │ │ bits │ bits │ bits │ bits │ (12) │ │ 47:39 │ 38:30 │ 29:21 │ 20:12 │ 11:0 │ └────┬────┴────┬────┴────┬────┴────┬────┴─────────┘ │ │ │ │ ▼ ▼ ▼ ▼ PML4 ──► PDPT ──► PD ──► PT ──► Физический фрейм (512 ent) (512 ent) (512 ent) (512 ent) (4 KiB) ``` Каждая запись (PTE) = 8 байт: ``` Bit 63: NX (No Execute) Bits 62:52: Ignored/AVL Bits 51:12: Physical frame address (4KiB aligned) Bits 11:9: Ignored/AVL Bits 8: Global Bits 7: PS (Page Size = 1 GiB для PDPT, 2 MiB для PD) Bits 6: Dirty Bits 5: Accessed Bits 4: PCD (Cache Disable) Bits 3: PWT (Write Through) Bits 2: U/S (User/Supervisor) Bits 1: R/W (Read/Write) Bit 0: Present ``` ## PageTable — представление таблицы ```rust #[repr(C, align(4096))] pub struct PageTable { entries: [u64; 512], } ``` - `512` entries × 8 байт = 4096 байт (ровно одна страница). - `align(4096)` — гарантирует, что структура размещается на физической странице, как требует аппаратура. ## PageTableFlags — битовые флаги ```rust bitflags! { pub struct PageTableFlags: u64 { const PRESENT = 1 << 0; const WRITABLE = 1 << 1; const USER = 1 << 2; const WRITE_THROUGH = 1 << 3; const NO_CACHE = 1 << 4; const ACCESSED = 1 << 5; const DIRTY = 1 << 6; const HUGE_PAGE = 1 << 7; const GLOBAL = 1 << 8; const COW = 1 << 9; // Elyz-specific: Copy-on-Write marker const NO_EXECUTE = 1 << 63; } } ``` Флаг `COW` (бит 9) — Elyz-специфичный, используется в AddressSpace для маркировки страниц, которые нужно копировать при записи. ## Основные операции PageTable ### map_page(virt, phys, flags) — отображение одной страницы ```rust pub fn map_page(&mut self, virt: VirtAddr, phys: PhysAddr, flags: PageTableFlags) { let p1 = self.walk_to_p1_mut(virt, true).expect("..."); let p1_idx = ((virt.0 >> 12) & 0x1FF) as usize; p1.entries[p1_idx] = phys.0 | flags.bits(); asm!("invlpg [{}]", in(reg) virt.0); } ``` **Процесс:** 1. `walk_to_p1_mut(virt, create=true)` проходит 4 уровня (P4→P3→P2→P1), создавая недостающие таблицы через `pmm_alloc()`. 2. Записывает физический адрес с флагами в P1 entry. 3. `INVLPG` — сбрасывает TLB для этой страницы. ### map_region(virt, phys, size, flags) — отображение диапазона Вызывает `map_page()` для каждой страницы в диапазоне. `size.div_ceil(4096)` — количество страниц. ### unmap_page(virt) — удаление отображения ```rust pub fn unmap_page(&mut self, virt: VirtAddr) { // walk_to_p1_mut(virt, false) — не создаём, только ищем let Some(p1) = self.walk_to_p1_mut(virt, false) else { return; }; p1.entries[p1_idx] = 0; // Clear PTE asm!("invlpg [{}]", in(reg) virt.0); } ``` **Важно:** не освобождает физический фрейм — это ответственность Caller'а (VMM). Идемпотентна: если PTE нет, ничего не делает. ### translate(virt) — трансляция виртуального адреса Проходит 4 уровня, проверяя PRESENT на каждом уровне. Поддерживает huge pages (1 GiB и 2 MiB): - Если P3 entry имеет HUGE_PAGE — 1 GiB страница. - Если P2 entry имеет HUGE_PAGE — 2 MiB страница. ### get_flags(virt) / update_flags(virt) — чтение/запись флагов `get_flags` возвращает флаги PTE для виртуального адреса. `update_flags` меняет флаги, сохраняя физический адрес, и вызывает INVLPG. ### activate(phys_addr) — загрузка в CR3 ```rust pub unsafe fn activate(&self, phys_addr: PhysAddr) { asm!("mov cr3, {0}", in(reg) phys_addr.0); } ``` Полная замена таблиц страниц. Полный TLB flush (без PCID). В `vmm.rs::AddressSpace::activate()` используется улучшенная версия с PCID и битом NOFLUSH. ## walk_to_p1_mut — сердце навигации ```rust fn walk_to_p1_mut(&mut self, virt: VirtAddr, create: bool) -> Option<&mut Self> { let p4_idx = ((virt.0 >> 39) & 0x1FF) as usize; let p3_idx = ((virt.0 >> 30) & 0x1FF) as usize; let p2_idx = ((virt.0 >> 21) & 0x1FF) as usize; let p3 = self.get_or_create_next_table(p4_idx, create)?; let p2 = p3.get_or_create_next_table(p3_idx, create)?; p2.get_or_create_next_table(p2_idx, create) } ``` ### get_or_create_next_table(index, create) — следующий уровень ```rust fn get_or_create_next_table(&mut self, index: usize, create: bool) -> Option<&mut Self> { if entry & PRESENT == 0 { if !create { return None; } // Аллоцируем новый фрейм для таблицы let pt_phys = pmm_alloc().expect("OOM"); // Обнуляем (все entry = 0 = not present) core::ptr::write_bytes(pt_virt.as_mut_ptr::(), 0, 4096); // Устанавливаем entry: PRESENT | WRITABLE | USER self.entries[index] = pt_phys.0 | (PRESENT | WRITABLE | USER); } // Преобразуем физический адрес entry в ссылку на PageTable let next_phys = PhysAddr(self.entries[index] & PTE_ADDR_MASK); Some(unsafe { &mut *next_phys.to_virt().as_mut_ptr::() }) } ``` ### pte_addr_mask — маска адреса ```rust const PTE_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000; ``` Биты 51:12 — физический адрес с выравниванием 4KiB. (0x000F_FFFF_FFFF_F000 = 52 бита адреса, очищенные нижние 12 бит). ## pmm_alloc — шлюз к PMM ```rust pub fn pmm_alloc() -> Option { PMM.lock().as_mut()?.alloc_frame() } ``` Тонкая обёртка, чтобы избежать циклической зависимости между paging и pmm. ## Ключевые инварианты 1. Каждая PageTable занимает ровно одну физическую страницу (4 KiB). 2. Физический адрес таблицы всегда 4KiB-выровнен. 3. Все intermediate таблицы создаются с флагом USER (чтобы и ядро, и пользователь могли их использовать). Разделение на user/kernel контролируется на уровне P1 entry. 4. `INVLPG` вызывается после каждого изменения PTE для консистентности TLB.