Page Fault
OS in Rust
Your Task
A Note
- With Wednesday off next week, doing a split homework over two weeks with this lab.
- That is the paging implementation homework, which is slated for after lecture next week.
- It will be posted when its ready!
Background
Implementation
One thing that we did not mention yet: Our kernel already runs on paging. The bootloader has already set up a 4-level paging hierarchy that maps every page of our kernel to a physical frame. The bootloader does this because paging is mandatory in 64-bit mode on x86_64.
This means that every memory address that we used in our kernel was a virtual address. Accessing the VGA buffer at address 0xb8000 only worked because the bootloader identity mapped that memory page, which means that it mapped the virtual page 0xb8000 to the physical frame 0xb8000.
Paging makes our kernel already relatively safe, since every memory access that is out of bounds causes a page fault exception instead of writing to random physical memory. The bootloader even sets the correct access permissions for each page, which means that only the pages containing code are executable and only data pages are writable.
Page Faults
Let’s try to cause a page fault by accessing some memory outside of our kernel. First, we create a page fault handler and register it in our IDT, so that we see a page fault exception instead of a generic double fault:
src/interrupts.rs
extern "x86-interrupt" fn page_fault_handler(
stack_frame: x86_64::structures::idt::InterruptStackFrame,
error_code: x86_64::structures::idt::PageFaultErrorCode,
) {
crate::println!("EXCEPTION: PAGE FAULT");
crate::println!("Accessed Address: {:?}", x86_64::registers::control::Cr2::read());
crate::println!("Error Code: {:?}", error_code);
crate::println!("{:#?}", stack_frame);
crate::halt();
}The CR2 control register is automatically set by the CPU on a page fault and contains the accessed virtual address that caused the page fault. We use the Cr2::read function of the x86_64 crate to read and print it. The PageFaultErrorCode type provides more information about the type of memory access that caused the page fault, for example, whether it was caused by a read or write operation. For this reason, we print it too. We can’t continue execution without resolving the page fault, so we halt.
Now we can try to access some memory outside our kernel:
src/main.rs
For me this worked.
The CR2 register indeed contains 0xdeadbeaf, the address that we tried to access. The error code tells us through the CAUSED_BY_WRITE that the fault occurred while trying to perform a write operation.
We see the current instruction pointer, so we know that this address points to a code page. Code pages are mapped read-only by the bootloader, so reading from this address works but writing causes a page fault. You can try this by changing the 0xdeadbeaf pointer to the address your handler reports, such as 0x205b06 or 0x2031b2.
Try these separately.
- Read:
src/main.rs
- Write:
Expect to see that the “read worked” message is printed, which indicates that the read operation did not cause any errors. However, instead of the “write worked” message, a page fault occurs. This time the PROTECTION_VIOLATION flag is set in addition to the CAUSED_BY_WRITE flag, which indicates that the page was present, but the operation was not allowed on it. In this case, writes to the page are not allowed since code pages are mapped as read-only.
Accessing the Page Tables
Let’s try to take a look at the page tables that define how our kernel is mapped:
src/main.rs
The Cr3::read function of the x86_64 returns the currently active level 4 page table from the CR3 register. It returns a tuple of a PhysFrame and a Cr3Flags type. We are only interested in the frame, so we ignore the second element of the tuple. We recall that many control registers, including e.g. RFLAGS, contain both bit fields and bit flags.
I see the following (within QEMU):
Level 4 page table at: PhysAddr(0x1000)
So the currently active level 4 page table is stored at address 0x1000 in physical memory, as indicated by the PhysAddr wrapper type. The question now is: how can we access this table from our kernel?
Accessing physical memory directly is not possible when paging is active, since programs could easily circumvent memory protection and access the memory of other programs otherwise. So the only way to access the table is through some virtual page that is mapped to the physical frame at address 0x1000. This problem of creating mappings for page table frames is a general problem since the kernel needs to access the page tables regularly, for example, when allocating a stack for a new thread.
Solutions to this problem are set aside for next week!