7.7 KiB
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 — представление таблицы
#[repr(C, align(4096))]
pub struct PageTable {
entries: [u64; 512],
}
512entries × 8 байт = 4096 байт (ровно одна страница).align(4096)— гарантирует, что структура размещается на физической странице, как требует аппаратура.
PageTableFlags — битовые флаги
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) — отображение одной страницы
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);
}
Процесс:
walk_to_p1_mut(virt, create=true)проходит 4 уровня (P4→P3→P2→P1), создавая недостающие таблицы черезpmm_alloc().- Записывает физический адрес с флагами в P1 entry.
INVLPG— сбрасывает TLB для этой страницы.
map_region(virt, phys, size, flags) — отображение диапазона
Вызывает map_page() для каждой страницы в диапазоне.
size.div_ceil(4096) — количество страниц.
unmap_page(virt) — удаление отображения
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
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 — сердце навигации
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) — следующий уровень
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::<u8>(), 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::<Self>() })
}
pte_addr_mask — маска адреса
const PTE_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
Биты 51:12 — физический адрес с выравниванием 4KiB. (0x000F_FFFF_FFFF_F000 = 52 бита адреса, очищенные нижние 12 бит).
pmm_alloc — шлюз к PMM
pub fn pmm_alloc() -> Option<PhysAddr> {
PMM.lock().as_mut()?.alloc_frame()
}
Тонкая обёртка, чтобы избежать циклической зависимости между paging и pmm.
Ключевые инварианты
- Каждая PageTable занимает ровно одну физическую страницу (4 KiB).
- Физический адрес таблицы всегда 4KiB-выровнен.
- Все intermediate таблицы создаются с флагом USER (чтобы и ядро, и пользователь могли их использовать). Разделение на user/kernel контролируется на уровне P1 entry.
INVLPGвызывается после каждого изменения PTE для консистентности TLB.