特权级机制与特权级切换

1.2k 词 7 分钟

Privilege level Switch

System call and Function call

Overview

  • Function call is running in user mode, using the user source, and there is no need for privilege switching and context restoring.
  • System call is the request from user mode to higher privilege mode, for getting into system to use kernel source such as I/O device, network interface.

System call

System call is the ABI(Application Binary Interface), which provide interface for U mode to use OS source. The follow is the process of system call:

  1. The user program prepares for system call by loading the system call number and the arguments that needed in predefined registers(a7: system call number, a0~a6: Hold arguments of system call, a0 also hold the return value from the system call)
  2. After that, user program issue the ecall instruction. After the ecall executed, a trap occurs, and the trap context switch performs. The CPU transfers control to the operating system’s trap handler. The trap handler use the sscause and system call number in a7 to execute requested system call. Once the system call is completed, the return value (if any) is placed in register a0.
  3. Finally, restore user context to return user mode.
Image description
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.section .data
message:
.asciz "Hello, World!\n"

.section .text
.globl _start
_start:
# System call to write
li a7, 64 # Load system call number for 'write' (64) into a7
li a0, 1 # File descriptor 1 (stdout) into a0
la a1, message # Load address of message into a1
li a2, 14 # Message length (14 bytes) into a2
ecall # Issue the system call

# System call to exit
li a7, 93 # Load system call number for 'exit' (93) into a7
li a0, 0 # Exit code 0 into a0
ecall # Issue the system call

ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS

You may wonder why a call to a system call, such as open() or read(), looks exactly like a typical procedure call in C; that is, if it looks just like a procedure call, how does the system know it’s a system call, and do all the right stuff? The simple reason: it is a procedure call, but hidden inside that procedure call is the famous trap instruction. More specifically, when you call open() (for example), you are executing a procedure call into the C library. Therein, whether for open() or any of the other system calls provided, the library uses an agreed-upon calling convention with the kernel to put the arguments to open in well-known locations (e.g., on the stack, or in specific registers), puts the system-call number into a well-known location as well (again, onto the stack or a register), and then executes the aforementioned trap instruction. The code in the library after the trap unpacks return values and returns control to the program that issued the system call. Thus, the parts of the C library that make system calls are hand-coded in assembly, as they need to carefully follow convention in order to process arguments and return values correctly, as well as execute the hardware-specific trap instruction. And now you know why you personally don’t have to write assembly code to trap into an OS; somebody has already written that assembly for you.

Trap Management

When user program issues the ecall instruction, it trigger the trap execution to switch the processor from user mode to supervisor mode.
Then the first step is to find the trap handler entry address to jump to.
To do this, the kernel set the trap table at boot time.

In rCore, at boot time, trap entry address is set by

1
2
3
4
5
6
7
8
9
/// initialize CSR `stvec` as the entry of `__alltraps`
pub fn init() {
extern "C" {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}

There is only a single trap entry _alltraps. All traps get into _alltraps to do trap context save and load and run different functionality base on what exception was(sscause).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read(); // get trap cause
let stval = stval::read(); // get extra value
match scause.cause() {
Trap::Exception(Exception::UserEnvCall) => {
cx.sepc += 4;
cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
println!("[kernel] PageFault in application, kernel killed it.");
run_next_app();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!("[kernel] IllegalInstruction in application, kernel killed it.");
run_next_app();
}
_ => {
panic!(
"Unsupported trap {:?}, stval = {:#x}!",
scause.cause(),
stval
);
}
}
cx

Trap context

User Stack and Kernel Stack

Feature User Stack Kernel Stack
Privilege Level Used in user mode (low-privilege). Used in kernel mode (high-privilege).
Access Control Only accessible to the process in user mode. Only accessible when in kernel mode.
Memory Location Part of user process memory (virtual memory). Part of the kernel memory (not accessible to user processes).
Switching Active when process runs in user mode. Active when process switches to kernel mode (via system calls, interrupts, exceptions).
Size Larger, typically grows dynamically. Smaller, fixed size, often just a few kilobytes.
Protection Protected by the OS to prevent corruption. Protected from user process access.

Structure

1
2
3
4
5
6
7
8
9
10
/// Trap Context
#[repr(C)]
pub struct TrapContext {
/// general regs[0..31]
pub x: [usize; 32],
/// CSR sstatus
pub sstatus: Sstatus,
/// CSR sepc
pub sepc: usize,
}
Image description
Why should save *sstatus* and *spec* register?
Image description
*sstatus* used to back to the mode before trap. *spec* save the last instruction before trap, which used to continue user program(PC is set sepc) after issued *sret* return user mode.

Save and Restore Trap context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# os/src/trap/trap.S

.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm

.align 2
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8
# save general-purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they were saved on kernel stack
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it on the kernel stack
csrr t2, sscratch
sd t2, 2*8(sp)
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp
call trap_handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__restore:
# case1: start running app by __restore
# case2: back to U after handling trap
mv sp, a0
# now sp->kernel stack(after allocated), sscratch->user stack
# restore sstatus/sepc
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
# restore general-purpuse registers except sp/tp
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack
csrrw sp, sscratch, sp
sret