Combine multiple applications together with OS to form a binary image
├── os
│ ├── build.rs (new: generate link_app.S to link the application to the kernel as a data segment)
│ ├── Cargo.toml
│ ├── Makefile (modification: build the application before building the kernel)
│ └── src
│ ├── link_app.S (build product, output by os/build.rs)
Improve OS
Load and execute programs, privileges/context switching
├── os
│ └── src
│ ├── batch.rs (new: implement a simple batch processing system)
│ ├── main.rs (modification: the main function needs to initialize Trap processing and load/execute the application)
│ └── trap (new: Trap related submodule "trap")
│ ├── context.rs (contains Trap context "TrapContext")
│ ├── mod.rs (includes "trap_handler")
│ └── trap.S (contains the assembly code for saving and restoring the Trap context)
System Calls
├── os
│ └── src
│ ├── syscall (new: system call submodule "syscall")
│ │ ├── fs.rs (contains file I/O related syscalls)
│ │ ├── mod.rs (provides "syscall" method to distribute different syscall IDs)
│ │ └── process.rs (contains task execution related syscalls)
Add Application
Batch OS will load and execute them sequentially according to the order of numbers in the file names
└── user (new: test cases are saved in the "user" directory)
└── src
├── bin (Applications developed based on "user_lib", each application is placed in a separate source file)
│ ├── 00hello_world.rs # Application to display strings
│ ├── 01store_fault.rs # Application of illegal write operation
│ ├── 02power.rs # Application where computing/IO frequently switch
│ ├── 03priv_inst.rs # Application to execute privileged instructions
│ └── 04priv_csr.rs # Application to execute CSR operation instructions
Application library and application compilation support
Application library and application compilation support
└── user (new: application test cases are saved in the user directory)
└── src
├── console.rs # Functions and macros to support "println!"
├── lang_items.rs # Support "panic_handler" function
├── lib.rs(user library user_lib) # The underlying support library for applications
├── linker.ld # Application link script
└── syscall.rs (contains assembly instructions generated for system calls,
each specific syscall is implemented through "syscall")
Outline
Lab objectives
Lab steps
Software Architecture
4. Related hardware
Application Design
Kernel programming
RISC-V Trap instructions
ecall: Trigger different traps depending on the CPU's current privilege
ebreak: Trigger a breakpoint trap
RISC-V Privileged Instructions
sret: Trigger different traps depending on the CPU's current privilege
RISC-V exception vector
Interrupt
Exception Code
Description
0
0
Instruction address misaligned
0
1
Instruction access fault
0
2
Illegal instruction
0
3
Breakpoint
0
4
Load address misaligned
0
5
Load access fault
RISC-V exception vector
Interrupt
Exception Code
Description
0
6
Store/AMO address misaligned
0
7
Store/AMO access fault
0
8
Environment call from U-mode
0
9
Environment call from S-mode
0
11
Environment call from M-mode
RISC-V exception vector
Interrupt
Exception Code
Description
0
12
Instruction page fault
0
13
Load page fault
0
15
Store/AMO page fault
AMO: atomic memory operation
Outline
Lab objectives
Lab steps
...
5. Application Design
project structure
memory layout
system calls
Kernel programming
Separation of Application and underlying support library
└── user (application and underlying support library)
└── src
├── bin (this directory places applications developed based on "user_lib")
├── lib.rs(user library user_lib) # The underlying library functions
├── ...... # Support library related files
└── linker.ld # Application link script
import external library
#[macro_use]externcrate user_lib;
Design support library
In lib.rs we define the entry point _start for the user library:
Set the starting physical address of the program to 0x80400000, and the program will be loaded to this address to run;
Put the .text.entry (where _start is located at) to the beginning of the entire program. As long as the batch processing system jumps to 0x80400000 after loading, it has entered the entry point of the user library and will jump to the application. The system will jump to application main logic after initialization;
Provides the start and end addresses of the .bss section of the final executable file, which is used by clear_bss function.
The rest is the same as before.
Outline
Lab objectives
...
Application Design
project structure
memory layout
System Calls
Kernel programming
System call execution flow of the application
System call execution flow of the application
In the submodule "syscall", the application calls the interface provided by the batch system through "ecall"
The ecall instruction triggers an exception, named Environment call from U-mode
Trap falls into the S-Mode to execute exception handler code.
a0~a6 save the parameters of the system call, a0 saves the return value, and a7 is used to pass the syscall ID
Support library for System Call
/// Function: Dump the data in memory buffer into the file.
/// Parameter: `fd` indicates the file descriptor of the target file to be written into;
/// `buf` indicates the starting address of the memory buffer;
/// `len` indicates the length of the memory buffer.
/// Return value: Returns the length that has been written successfully.
/// syscall ID: 64
fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
/// Function: Exit the application and provide the batch system with return value.
/// Parameter: `xstate` represents the return value of the application.
/// Return value: This system call should not return.
/// syscall ID: 93
fn sys_exit(xstate: usize) -> !;
System call parameter passing
fnsyscall(id: usize, args: [usize; 3]) -> isize {
letmut ret: isize;
unsafe {
asm!(
"ecall",
inlateout("x10") args[0] => ret, //first parameter & return valuein("x11") args[1], //the second parameterin("x12") args[2], //the third parameterin("x17") id //syscall number
);
}
ret //return value
}
unsafefnload_app(&self, app_id: usize) {
// clear icache
asm!("fence.i");
// clear app area
...
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *constu8,
self.app_start[app_id + 1] - self.app_start[app_id] );
let app_dst = core::slice::from_raw_parts_mut(
APP_BASE_ADDRESS as *mutu8,
app_src.len());
app_dst.copy_from_slice(app_src);
}
Load the application binary
fence.i : used to clear i-cache
Note: fence.i is an i-cache barrier instruction, a non-privileged instruction, which belongs to the "Zifencei" extended specification
Why?
Load the application binary
fence.i : used to clear i-cache
Caches for physical memory include d-cache and i-cache
OS may modify the memory area that will be fetched by the CPU, which will make the i-cache contain inconsistent content with the memory
Here OS must use fence.i to manually clear the i-cache to invalidate all the contents, to prevent CPU from reading inconsistent data from cache.
Outline
...
Application Design
Kernel programming
Application management and loading
Privilege Switching
Trap context
Trap processing flow
Execution of applications
CSR for privilege switching
| CSR name | Trap-related function of this CSR |
| ------- | ----------------------------------------- ------------------- |
| sstatus | SPP will give the privilege (S/U) before the Trap occurs |
| sepc | Records the address of last instruction executed before the Trap occurrs |
|scause | Describes the reason for the Trap |
| stval | Gives additional information about the Trap |
| stvec | Records the entry address of the Trap handler |
Hardware logic after privilege switching
SPP of sstatus will record the current CPU privilege(U/S);
Set sepc to the return address after the Trap processing is completed(next instruction that will be executed);
Scause/stval will record the source of this Trap and relevant additional information;
CPU sets the current privilege to S, and jumps to the Trap handler entry address set by stvec.
When the application enters the kernel mode through "ecall", OS saves the Trap context of the interrupted application;
Trap context processing during system calls
OS distributes and finishes system call services according to the Trap-related CSR register;
Trap context processing during system calls
After OS completes system call services, it needs to restore the Trap context of the interrupted application, and let the application continue execution through the sret instruction.
Switch from user stack to kernel stack
sscratch CSR: An important transit register
When the privilege is switched, we need to save the Trap context on the kernel stack, so we need a register to temporarily store the kernel stack address, and use it as the base address pointer to save the contents of the Trap context.
But all general-purpose registers cannot be used as base address pointers, because they all need to be saved. Overwritting them will affect the execution of subsequent application control flow.
Switch from user stack to kernel stack
sscratch CSR: An important transit register
Temporarily save the address of the kernel stack
As a transit station, the value of sp (pointing to the address of the user stack) can be temporarily saved in sscratch
Only need csrrw sp, sscratch, sp // swap the two register contents of sp and sscratch
Complete the switching of user stack --> kernel stack
Save general-purpose registers in the Trap context
Macros to save general-purpose registers
# os/src/trap/trap.S
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
Outline
...
Application Design
Kernel programming
Application management and loading
Privilege switching
Trap context
Trap processing flow
Execution of applications
Trap processing flow
The overall workflow of Trap processing is as follows:
First save the Trap context on the kernel stack through __alltraps;
Then jump to the trap_handler function to complete trap distribution and processing.
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
# allocate a TrapContext on kernel stack
addi sp, sp, -34*8
Save Trap context
save general purpose registers
# 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
Save Trap context
save sstatus and sepc
# 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)
Save Trap context
Save user 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
Let the register a0 point to the stack pointer of kernel stack, which is the address of the Trap context we just saved. This is because we will call trap_handler for Trap processing next, and its first parameter cx is obtained from a0 by the calling specification.
Restore Trap context
Most of them are reverse operations of saving registers;
The last step is sret instruction //return from kernel mode to user mode
Note: We will talk about more details of "Restore Trap Context" later.