Repository navigation
Page Fault Handler
cupid-os implements a unique interactive page fault handler that transforms what would normally be a fatal system crash into an educational debugging experience. Instead of halting the system, page faults drop users into a "fault shell" where they can inspect the fault details and choose how to proceed.
Page faults occur when a program attempts to access memory that is not currently mapped or accessible. In most operating systems, this results in immediate termination. However, cupid-os provides an interactive fault shell that allows users to:
- Inspect fault details: View the faulting address, error codes, and CPU state
- Understand the fault: Decode error flags to understand what went wrong
- Choose recovery options: Continue execution, reboot, or get more information
- Learn about memory management: Hands-on experience with virtual memory concepts
Page faults are triggered by:
- Accessing unmapped memory: Reading/writing to addresses without page table entries
- Permission violations: Writing to read-only pages or accessing kernel pages from user mode
- Reserved bit violations: Setting reserved bits in page table entries
- Instruction fetch faults: Executing code from non-executable pages
When a page fault occurs:
- Hardware saves state: CPU pushes error code, EIP, CS, EFLAGS to stack
- CR2 register: Stores the faulting virtual address
- Error code: Provides details about the fault type
- Interrupt 14: Transfers control to the page fault ISR
Instead of the typical kernel panic, cupid-os:
if (r->int_no == 14) { // Page fault vector
uint32_t cr2;
__asm__ volatile("mov %%cr2, %0" : "=r"(cr2));
// Temporarily enable interrupts for interactive prompt
__asm__ volatile("sti");
pf_action_t action = shell_pagefault_prompt(r, cr2);
__asm__ volatile("cli");
if (action == PF_ACTION_REBOOT) {
system_reboot(); // Never returns
}
// PF_ACTION_CONTINUE: Return and retry the faulting instruction
return;
}The fault shell is triggered automatically on page faults. To test it manually:
> testpf
Testing page fault handler...
Attempting to access invalid memory address 0xDEADBEEFThis executes:
volatile uint32_t* invalid_addr = (volatile uint32_t*)0xDEADBEEF;
uint32_t value = *invalid_addr; // This triggers the page faultWhen a page fault occurs, the system displays:
PAGE FAULT
CR2: 0xDEADBEEF
ERR: 0x00000000 (not-present read kernel )
EIP: 0x0010ABCD CS: 0x0008 EFLAGS: 0x00000202
Type: continue | reboot | info | help
pf>
- CR2: The virtual address that caused the fault
- ERR: Error code in hexadecimal and decoded format
- EIP: Instruction pointer (address of faulting instruction)
- CS: Code segment selector
- EFLAGS: CPU flags at time of fault
The error code is a bit field that provides detailed fault information:
| Bit | Name | Description |
|---|---|---|
| 0 | P | Present - 0 = page not present, 1 = protection violation |
| 1 | W | Write - 0 = read access, 1 = write access |
| 2 | U | User - 0 = kernel mode, 1 = user mode |
| 3 | R | Reserved - Reserved bit was set |
| 4 | I | Instruction - 0 = data access, 1 = instruction fetch |
-
0x00000000:(not-present read kernel)- Reading unmapped kernel memory -
0x00000002:(not-present write kernel)- Writing to unmapped kernel memory -
0x00000004:(not-present read user)- Reading unmapped user memory -
0x00000006:(not-present write user)- Writing to unmapped user memory -
0x00000001:(present read kernel)- Protection violation reading kernel memory
Attempts to continue execution by returning from the fault handler.
pf> continue
Continuing (iret)...-
What it does: Returns to the faulting instruction via
IRET - Risk: May fault again if the underlying problem isn't resolved
- Use case: Testing if fault was transient or recoverable
Performs a hard reboot of the system.
pf> reboot-
What it does: Calls
system_reboot()to restart the system - Use case: When fault cannot be recovered from
-
Note: This is equivalent to the
rebootshell command
Redisplay the fault information.
pf> info
PAGE FAULT
CR2: 0xDEADBEEF
ERR: 0x00000000 (not-present read kernel )
EIP: 0x0010ABCD CS: 0x0008 EFLAGS: 0x00000202
Type: continue | reboot | info | help
pf>- Use case: When fault details scroll off screen or need to be re-examined
Display available commands.
pf> help
continue (c) - return from the page fault handler
reboot (r) - hard reboot
info - reprint CR2/flags/registersThe fault shell is a minimal line editor that runs during interrupt context:
pf_action_t shell_pagefault_prompt(const struct registers* r, uint32_t cr2) {
char buf[80 + 1];
int pos = 0;
// Display fault information
pf_print_info(r, cr2);
print("\nType: continue | reboot | info | help\n");
print("pf> ");
// Minimal command processing loop
while (1) {
key_event_t event;
keyboard_read_event(&event);
// Process input...
}
}During page fault processing, interrupts are temporarily re-enabled:
// Enable interrupts for keyboard input in fault shell
__asm__ volatile("sti");
pf_action_t action = shell_pagefault_prompt(r, cr2);
// Disable interrupts before returning to avoid races
__asm__ volatile("cli");This allows the keyboard driver to function while in the fault shell, but requires careful state management.
The fault shell has access to the complete CPU state at the time of the fault:
struct registers {
// Segment registers
uint32_t ds, es, fs, gs;
// General purpose registers
uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax;
// Interrupt information
uint32_t int_no, err_code;
// Execution state
uint32_t eip, cs, eflags, useresp, ss;
};The page fault handler provides hands-on experience with:
- Virtual memory concepts: Understanding address translation
- Page table structure: How virtual addresses map to physical memory
- Memory protection: Kernel vs user space, read/write permissions
- Error handling: How operating systems deal with faults
Users can learn:
- Fault analysis: Reading error codes and CR2 values
- Memory inspection: Understanding what addresses are valid
- Recovery strategies: When and how to continue vs reboot
- System stability: The importance of proper error handling
int* ptr = NULL;
*ptr = 42; // Page fault at CR2: 0x00000000Error code: 0x00000002 (not-present write kernel)
Cause: Attempting to write to address 0, which is not mapped
void recursive() {
char buffer[1024];
recursive(); // Eventually exhausts stack space
}Error code: 0x00000002 (not-present write kernel)
Cause: Stack pointer goes beyond allocated stack pages
int* ptr = kmalloc(sizeof(int));
// ... use ptr ...
kfree(ptr);
*ptr = 42; // Page fault if page was unmappedError code: Varies depending on memory state Cause: Accessing memory after it was freed and unmapped
// Attempting to access kernel-only addresses from user mode
volatile uint32_t* kernel_addr = (volatile uint32_t*)0xC0000000;
uint32_t value = *kernel_addr;Error code: 0x00000005 (present read user + protection violation)
Cause: User mode accessing kernel memory
- Ring 0 execution: All code runs in kernel mode
- No process isolation: Faults can affect the entire system
- Debugging access: Full system visibility for educational purposes
- Interrupt overhead: Page faults disable interrupts during handling
- Interactive delays: User must respond to fault prompts
- Recovery limitations: Cannot always safely continue after faults
- State corruption: Registers and memory may be in inconsistent state
- Resource leaks: Continuing may leave resources in undefined states
- Nested faults: Faults during fault handling can be problematic
Future enhancements could include:
-
Memory inspection:
examine <address>to view memory contents -
Stack trace:
backtraceto show call stack -
Register modification:
set reg valueto modify CPU state -
Memory mapping:
mapto allocate pages and retry
- Fault logging: Record faults to filesystem for analysis
- Recovery scripts: Automated recovery for known fault patterns
- Context saving: Save/restore system state around faults
- Multi-fault handling: Handle faults during fault processing
Test various fault scenarios:
# Test different types of invalid accesses
> testpf # Basic invalid access
# Create custom test cases
void test_custom_fault() {
// Test specific fault conditions
volatile char* ptr = (volatile char*)0xDEADBEEF;
*ptr = 'x'; // Write fault
volatile int* read_ptr = (volatile int*)0xCAFEBABE;
int value = *read_ptr; // Read fault
}Create shell commands to test different fault types:
static void shell_test_read_fault(const char* args) {
print("Testing read fault...\n");
volatile uint32_t* ptr = (volatile uint32_t*)0xDEADBEEF;
uint32_t value = *ptr;
print("Unexpected: read succeeded with value ");
print_hex(value);
print("\n");
}
static void shell_test_write_fault(const char* args) {
print("Testing write fault...\n");
volatile uint32_t* ptr = (volatile uint32_t*)0xDEADBEEF;
*ptr = 0xCAFEBABE;
print("Unexpected: write succeeded\n");
}- Intel Architecture Manuals: Page fault specifications
- OSDev Wiki: Page fault handling guides
- Linux Kernel: Advanced fault handling techniques
For more information about memory management:
- Memory Management - Virtual memory and paging
- Interrupts and Exceptions - Exception handling system
- Shell Guide - Interactive shell features
Getting Started
Shell & Scripting
Compilers
- CupidC Compiler
- CupidC Language Reference
- CupidC 2D Graphics Library
- CupidASM Assembler
- Toolchain Bootstrap
- Floating Point
Programs & Docs
GUI
Kernel
Filesystems
Hardware