When a user-space program triggers a system call, it needs to switch from user mode to kernel mode. This process involves three main stages:
- When the application enters the kernel via an
ecallinstruction, the operating system saves the trap context of the interrupted application. - The operating system dispatches and processes the system call based on the contents of trap-related CSR registers.
- After completing the system call, the operating system restores the application's trap context and uses the
sretinstruction to resume its execution.
The trap handling flow is illustrated below:
!Trap Handling Flow
Let's examine these three stages in detail.
When an application executes any of the following system call functions, it triggers a kernel trap, and the kernel saves the application's trap context.
/// Writes data from a buffer in memory to a file.
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize;
/// Exits the application and reports a return code to the batch system.
pub fn sys_exit(xstate: i32) -> !;
/// Actively yields CPU ownership to switch to another application.
pub fn sys_yield() -> isize;
/// Gets the current time.
pub fn sys_get_time() -> isize;
/// Creates a new child process.
pub fn sys_fork() -> isize;
/// Clears the current process's address space and loads a specific executable.
pub fn sys_exec(path: &str) -> isize;
/// Waits for a child process to become a zombie, reclaims its resources, and collects its exit code.
pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;
/// Gets the process ID (PID).
pub fn sys_getpid() -> isize;
/// Reads content from a file into a buffer.
pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize;
/// Opens a regular file and returns a file descriptor.
pub fn sys_open(path: &str, flags: u32) -> isize;
/// Closes a file descriptor.
pub fn sys_close(fd: usize) -> isize;The TrapContext struct stores all the necessary state to resume an application after a trap.
pub struct TrapContext {
/// General-purpose registers x0-x31
pub x: [usize; 32],
/// CSR sstatus
pub sstatus: Sstatus,
/// CSR sepc: The instruction to return to after the trap
pub sepc: usize,
/// Token for the kernel address space (physical address of the kernel page table root)
pub kernel_satp: usize,
/// Virtual address of the current application's kernel stack pointer
pub kernel_sp: usize,
/// Virtual address of the trap handler entry point in the kernel
pub trap_handler: usize,
}The __alltraps function is executed to save the application's trap context, preserving its environment before the system call.
__alltraps:
# Swap sp with sscratch. `sp` now points to the kernel stack,
# and `sscratch` holds the user stack pointer.
csrrw sp, sscratch, sp
# Save general-purpose registers to the kernel stack
sd x1, 1*8(sp)
# Skip x2 (sp), which will be saved later, and x4 (tp)
sd x3, 3*8(sp)
# Use .rept to save registers x5 to x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# Read sstatus and sepc CSRs into temporary registers
csrr t0, sstatus
csrr t1, sepc
# Save them to the kernel stack
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# Read the original user stack pointer from sscratch and save it
csrr t2, sscratch
sd t2, 2*8(sp)
# Load kernel space token (SATP) and trap handler address from the TrapContext
ld t0, 34*8(sp) # kernel_satp
ld t1, 36*8(sp) # trap_handler
# Set sp to the top of the application's kernel stack
ld sp, 35*8(sp) # kernel_sp
# Switch to the kernel's address space
csrw satp, t0
# Flush the TLB
sfence.vma
# Jump to the Rust trap handler
jr t1The trap_handler() function in Rust processes the trap (e.g., an interrupt or system call). It reads the system call number and arguments from the saved TrapContext and dispatches to the appropriate handler function.
pub fn trap_handler() -> !;After the system call is processed, the __restore function is called to load the saved TrapContext and return to user mode.
__restore:
# a0: Virtual address of the TrapContext in the application's address space
# a1: Token (SATP value) for the application's address space
# Switch to the user address space
csrw satp, a1
# Flush the TLB
sfence.vma
# Store the address of the TrapContext in sscratch for the next trap
csrw sscratch, a0
# Set sp to point to the TrapContext to begin restoring registers
mv sp, a0
# Restore sstatus and sepc CSRs
ld t0, 32*8(sp)
ld t1, 33*8(sp)
csrw sstatus, t0
csrw sepc, t1
# Restore general-purpose registers (except x0, x2, x4)
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n,n+1
.endr
# Restore the user stack pointer into sp
ld sp, 2*8(sp)
# Return to User mode to continue application execution
sret