write sth. upfront
This essay was written by a very rookie rookie. Please feel free to ask any questions.
You can contact: 1160712160@
GitHhub:/WindDevil (Nothing so far.
Contents of this section
Because risc-v exists hardware privilege level mechanism, we have to implement a can make the application work in the user level, make the operating system work in the privilege level. The reason is to ensure that the user application can not use the kernel instructions, to use the kernel instructions must be executed through the operating system, so that the operating system control and check, the program will not be due to the application problem caused by the entire operating system are running wrong.
RISC-V Privilege Level Switching
Why Privilege Level Switching
Going back to the chart we mentioned earlier.
It can be seen that the correspondingSEE
assume (office)Supervisor Execution Enviroment As the name suggests, it is in theMachine
Machine Layer BuiltPrivileged Application Runtime Environment .
We do this byRust-SBI
Established aSBI
Supervisor Binary Interface As the name suggests.Privileged Level Binary Interface , putmachine layer Some of the commands of theabstraction We're realizingprivileged application This makes it easy for privileged applications to call this interface to realizemigrate to RISC-V architectures that implement different extended instruction sets of theprocessing unit Up.
In the last big chapter we realized aOS
Operating System , i.e.operating system , which is actually a call toSBI
(used form a nominal expression)privileged application .
So, now we need to implement aAEE
Application Execution Environment , as the name suggestsApplication Runtime Environment , and to realizeABI
Application Binary Interface application binary interface, which enables user-tier applications to access the application binary by calling theABI
Thus, it can be ported to anyoperating system Up.
In the previous section, we implemented an application loader, and we revisited our previous work on thePrivileged level mechanism Description.
First, the operating system needs to provide the appropriate functional code that can be used in the execution of thesret
before preparing and restoring the context in which the userland executes the application. Secondly, after the application invokes theecall
After the instruction, it is possible to check the system call parameters of the application to ensure that the parameters do not corrupt the operating system.
Then the operating system implemented in the previous chapter, the batch function implemented in the previous section, and the application program implemented in the previous section are assembled in exactly the same way.Application context saving, privilege level switching, monitoring of application execution Functions.
The specific implementation is divided into the following parts, directly from theofficial documentPlease fetch it.
- When starting an application, it is necessary to initialize the application's user-state context and be able to switch to the user-state to execute the application;
- After an application initiates a system call (i.e., issues a Trap), it needs to go to the batch operating system for processing;
- When an application execution error occurs, you need to go to the batch operating system to kill the application and load and run the next application;
- When the execution of the application finishes, it is necessary to go to the batch operating system to load and run the next application (which is actually also done by the system call
sys_exit
(to realize it).
All of this processing involves privilege level switching and therefore requires the application, operating system and hardware to work together to accomplish the privilege level switching mechanism.
Privilege level switching related control status registers
When discussing the Trap mechanism of the RISC-V architecture in a general sense, there are two things that usually need to be kept in mind:
- The privilege level at which the CPU was running before the Trap was triggered;
- The privilege level to which the CPU needs to switch to process the Trap and return to the original privilege level when processing is complete.
But in fact, as we've mentioned before, there's a lot to be said about (Hypervisor.H
) The privilege specification for the model is not yet fully developed, and theM
Privilege-level details of the mechanism are then included as optional content in theAppendix C: Deeper into the Machine Model: RustSBI because we've already referenced theRust-SBI
, so just be concerned aboutU
Privileged andS
Privilege level switching.
When the CPU runs an application program at the user-state privileged level (U mode of RISC-V), executes to the Trap, switches to the kernel-state privileged level (S mode of RISC-V), the corresponding code of the batch operating system responds to the Trap and executes the system call service, and after the processing is complete, it returns from the kernel-state to the user-state application program to continue executing the subsequent instructions.
official documentfor the RISC-V architectureTrap
The characteristics are described in detail.low priority application will not trigger thehigh priority (used form a nominal expression)Trap
:
In the RISC-V architecture, there is an important rule about Trap: the privilege level before Trap is not higher than the privilege level after Trap. Therefore, if you switch to S privilege level after a Trap is triggered (hereinafter referred to as Trap to S), it means that the CPU can only run at S/U privilege level before the Trap occurs. In any case, as long as it is a Trap to S privilege level, the operating system will use the Trap-relatedControl Status Register (CSR, Control and Status Register) to assist Trap processing. When writing code related to Trap processing in a batch operating system running at the S privilege level, we need to use the S mode CSR register as shown below.
This paragraph also refers to the report onCSR
of the content, theCSR
Control and Status Register As the name implies.Control and Status Registers , stores the currentTrap
The state information of theTrap
Part of the code's functionality. These registers are used in thehere areThere is a detailed description.
take note ofsstatus
It is the most important CSR of the S privileged level and can control the CPU behavior and execution state of the S privileged level in many ways.
Privilege level switching
Recall what you learned earlier about triggering exceptionsTrap
The situation.
- One of them is the execution of special instructions by the userland software in order to obtain the service functions of the kernelland operating system
1. The instruction itself belongs to a highly privileged class, such assret
Command (indicates return from S-mode to U-mode)
2. The command accessedRegisters accessible only at the S-mode privilege level or memory, such as the one that indicates the state of the S-mode systemControl Status Registersstatus
et al. (and other authors) - The second is when an error is generated during the execution of an instruction (e.g., execution of an instruction that is not allowed to be executed by the userland or other errors) and is detected by the CPU.
insofar asprocessor register level, theofficial documentIn more detail, when the CPU finishes executing an instruction (e.g.ecall
and prepare to fall from the user's privilege level into theTrap
) to the S privilege level, the hardware automatically does these things.
-
sstatus
(used form a nominal expression)SPP
field is modified to the CPU's current privilege level (U/S). -
sepc
will be changed to the address of the next instruction that will be executed by default after Trap processing is complete. -
scause/stval
will be changed to the reason for the Trap and additional information about the Trap, respectively. - CPU will jump to
stvec
The Trap Processing entry address is set and the current privilege level is set to S, then execution starts at the Trap Processing entry address.
In RV64.stvec
is a 64-bit CSR that holds the entry address for interrupt processing in case of interrupt enable. It has two fields:
- MODE is located at [1:0] and is 2 bits long;
- The BASE is located at [63:2] and is 62 bits long.
When the MODE field is 0.stvec
is set to Direct mode, and the entry address for processing a Trap that enters S mode, regardless of the reason, isBASE<<2
The CPU will jump to this place for exception handling. In this book, we will only include thestvec
set to Direct mode. Andstvec
It can also be set to Vectored mode, for those who are interested, please refer to the RISC-V instruction set privilege level specification.
Thinking back to what we learned about exception control flow.stvec
can help us switch to post-privilege level switch processingTrap
Address.
- Some anomaly in the execution of the higher-level software or thespecial case , need to use the functions provided in the implementation environment
- Here you can see that although both are calledexceptions However, there are actually some special cases that require the use of functions in the execution environment, so you can't just put theexceptions see it ascorruptible
- The reasons why user-state applications trigger exceptions directly from user-state to kernel-state can be categorized into two general categories
- One is the execution of special instructions by the userland software to obtain the service functions of the kernel operating system.
- The instructions themselves belong to a highly privileged class, such as the
sret
Command (indicates return from S-mode to U-mode) - The command accesses theRegisters accessible only at the S-mode privilege level or memory, such as the one that indicates the state of the S-mode systemControl Status Register
sstatus
et al. (and other authors)
- The instructions themselves belong to a highly privileged class, such as the
- The second is when an error is generated during the execution of an instruction (e.g., execution of an instruction that is not allowed to be executed by the userland or other errors) and is detected by the CPU.
- One is the execution of special instructions by the userland software to obtain the service functions of the kernel operating system.
- Suspend the functionality of the higher-level software and run the code of the execution environment instead (along with thePrivilege level switching )
- Return to where the upper level software was paused to continue execution
When the CPU finishes processing the Trap and is ready to return, it needs to pass a privileged instruction at privilege level Ssret
To accomplish this, this one instruction specifically accomplishes the following functions:
- The CPU will set the current privilege level according to the
sstatus
(used form a nominal expression)SPP
The field is set to U or S; - CPU will jump to
sepc
register points to that instruction, and then continue execution.
As mentioned above.sret
It'll help us get it done.Context storage cap (a poem)resumption .
User stack and kernel stack
We've just mentioned that there's a lot of talk aboutstvec
cap (a poem)sret
and the role of the United Nations.sstatus
can be givenTrap
which privilege level the CPU was at before it occurred.stval
can be givenTrap
Additional information can be found in theTrap
Save the contents when it happens.
But we can't just wrap our heads around switching priorities, as we can when using theasm
exploit (a resource)mcu
We need to press the registers onto the stack and retrieve them when we need them, just as we need to save the user's context. We also need to save the user's context.
More abstractly, we think of switching priorities similarly to the way C calls "kernel library" (fictional) functions, resulting in nested function calls.
At this point we can naturally think of the function call stack that we learned about in the previous chapter, and similarly, we implement thememory allocation cap (a poem)push on It is also possible to implement such a stack.
But considering that we implemented this call stack itself using thesp
pointer, whereas if we want to actively save the user context we need to implement our ownpush on and push off and, more importantly, we need to be able toAssign a piece of address .
The moment the Trap is triggered, the CPU switches to privilege level S and jumps to thestvec
The position is indicated by the However, before we can move on to the S privilege level of Trap processing, as mentioned above, we must preserve the original control flow'sregister state This is generally accomplished throughkernel stack to save. Note that we need to use the kernel stack specifically for the operating system, not the user stack used when the application is running.
Use twodifferent stacks Mainly for the purpose ofsafetyIf two control flows (i.e., the application's control flow and the kernel's control flow) use the same stack, the application will be able to read the history of the Trap control flow, such as the addresses of some kernel functions, after returning, which poses a security risk. So, what we want to do is to add a piece of assembly code in the batch operating system to realize the switch from the user stack to the kernel stack, and in thekernel stack Save the application control flow on theregister state。
In other words, the use of two stacks also serves to separate the priorities, allowing the application to access theU
Privileged level data can only be accessed by the application program, and theS
Levels of data can only be accessed by the kernel. Therefore the stack storing the data for the two levels has to be implemented as well.
Now run theU
The application data on the level is stored in theuser terminal , while running onS
Applications on the level (kernel APIs) are stored in thekernel stack . So how do you switch the running state?
The answer is to simply modify thesp
register can be changed to the current program location, so what we have to do is actuallyswitching stacks , completing a stack switch during a privilege level switch.
So how do you implement this stack specifically?official documentThe reference is given, and the short answer is yes.arrays and implement a function for it that can be returned as astack top address (computing) method, and then instantiate such a structure as an array ofsingleton example Usage.
// os/src/
const USER_STACK_SIZE: usize = 4096 * 2;
const KERNEL_STACK_SIZE: usize = 4096 * 2;
#[repr(align(4096))]
struct KernelStack {
data: [u8; KERNEL_STACK_SIZE],
}
#[repr(align(4096))]
struct UserStack {
data: [u8; USER_STACK_SIZE],
}
static KERNEL_STACK: KernelStack = KernelStack { data: [0; KERNEL_STACK_SIZE] };
static USER_STACK: UserStack = UserStack { data: [0; USER_STACK_SIZE] };
impl UserStack {
fn get_sp(&self) -> usize {
.as_ptr() as usize + USER_STACK_SIZE
}
}
impl KernelStack {
fn get_sp(&self) -> usize {
.as_ptr() as usize + KERNEL_STACK_SIZE
}
}
Pay attention here.#[repr(align(N))]
indicates that the alignment of the type in memory should be at leastN
Bytes.
The next step is to saveTrap
The register context after this occurs and is saved to the kernel stack, this can be done in three steps.
- Know what register contents need to be saved
- What kind of data structure is used to store
- How to press into the kernel stack
Let's see.official document, you can see that in addition to our own look at the function we know to store a bunch ofCSR
registers, and general-purpose registers.x0~x31
Storage required.
- For general-purpose registers, the two control streams (application control stream and kernel control stream) run at different privilege levels, and the software they belong to may be written in different programming languages. Although only Trap processing-related code is executed in the Trap control stream, there are still a lot of modules that may be called either directly or indirectly, so it is difficult, if not impossible to find out which registers do not need to be saved. So it is difficult, if not impossible, to find out which registers do not need to be saved, so we have to save them all. There are some exceptions, such as
x0
is hardcoded to 0, which is naturally unchanged; and thetp(x4)
registers, and are generally not used unless we manually use them for some special purpose. Although they don't need to be saved, we still have theTrapContext
The main reason for reserving space for them in is for the convenience of subsequent implementations. - For CSRs, we know that when entering a Trap, the hardware immediately overwrites the
scause/stval/sstatus/sepc
All or part of it.scause/stval
case is that it is always used or saved elsewhere at the first sign of Trap processing, so there is no risk of it being modified and causing adverse effects. In the case ofsstatus/sepc
They will be meaningful throughout Trap processing (at the end of the Trap control flow).sret
), and it does happen that Trap nesting can cause their values to be overwritten. So we need to save them along with thesret
Restore the original before.
It is then necessary to design a structure to store the values of these registers: the
// os/src/trap/
#[repr(C)]
pub struct TrapContext {
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
}
As you can see here, a file of size32
array storagex0~x31
, and using aSstatus
The structure that holds thesstatus
registers, using theuszie
A variable that holds thesepc
Registers.
included among theseSstatus
The structure isriscv::register
This bag is dedicated to storingsstatus
A package of registers, thesstatus
Some of the main fields of the register include:
- SD (Supervisor Debug Mode): This bit indicates whether the processor is in supervised debug mode.
- SIE (Supervisor Interrupt Enable): This bit controls whether interrupts are allowed in supervisory mode.
- SPIE (Supervisor Previous Interrupt Enable): When returning from an interrupt, this bit is used to restore the previous interrupt enable state.
- SPP (Supervisor Previous Privilege Level): This bit indicates the previous privilege level before entering supervisory mode.
- SPRV (Supervisor Previous RVISZ): This bit indicates the last RVISZ value before entering supervisory mode.
- XS (User-mode Exception Specification): This bit is used to control the specification of user mode exception handling.
- FS (Floating-point Status): This bit is used to control the state of the floating point unit.
- XS (User-mode Exception Specification): This bit is used to control the specification of user mode exception handling.
So it makes sense to dedicate a structure to storing its information, and there's no reason not to use it when we have a well-designed wheel.
This one here.#[repr(C)]
It doesn't make sense. It looks like something related to C. Let's ask.lit. thousand questions on synonymy:#[repr(C)]
is a property in the Rust language that controls the layout and representation of a structure or union. Specifically, the#[repr(C)]
Instructs the Rust compiler to lay out a structure or union according to the rules of the C language.
Why use#[repr(C)]
?
- interoperability: When you need to interoperate with other languages, such as C or C++, it's important to make sure that Rust's structures have the same layout.
-
Binary compatibility: If you want to ensure binary compatibility of the structure's layout on different platforms, the
#[repr(C)]
can help you do that. -
external interface: If your structure will be exposed to other systems as part of an external interface, it is important to maintain a consistent layout.
#[repr(C)]
Behavioral characteristics of.
- Field order: The order of the fields will be preserved and will not be reordered by the Rust optimizer.
- line up in correct order: Each field will be aligned according to the default alignment of the C language.
- padding: Rust may add extra padding (pad) bytes between fields to satisfy alignment requirements.
- adults and children: The size of the structure will be based on the actual size and alignment requirements of the fields.
Specifically, inos/src
directory to create thetrap
folder, create thecap (a poem)
, the structure we described above is located in the
Described in.
cd os/src
mkdir trap
cd trap
touch
touch
At this point it is also necessary to implement a function that presses the data of this structure into the kernel stack, to implement a method for the kernel stack.
impl KernelStack {
fn get_sp(&self) -> usize {
.as_ptr() as usize + KERNEL_STACK_SIZE
}
pub fn push_context(&self, cx: TrapContext) -> &'static mut TrapContext {
let cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
unsafe {
*cx_ptr = cx;
}
unsafe { cx_ptr.as_mut().unwrap() }
}
}
can be seenpush_context
method is a direct acquisition of a top-of-stack pointer (It's actually the bottom of the array), and then through theTrapContext
The size of the stack is calculated from the bottom pointer, and the data is then placed directly into the location pointed to by the pointer.
Subsequently returns the bottom-of-stack position (the pointer position of the saved data) of thevariable reference .
Here's how to use theunwrap()
method to handle possibleNone
Situation.
Trap Management
At the heart of privilege level switching is the management of Traps. This involves some of the following:
- The application passes the
ecall
Upon entering the kernel state, the operating system saves the Trap context of the interrupted application; - The operating system accomplishes the distribution and processing of system call services based on the contents of the Trap-related CSR registers;
- After the operating system completes the system call service, it needs to restore the Trap context of the interrupted application and pass the
sret
Let the application continue to execute.
Trap Context Save and Restore
Note the second point mentioned above, the operating system needs to trap the contents of the relevant CSR registers to complete the distribution and processing of system call services.
think ofstvec
register, which controls the entry address of the Trap processing code. So in order to implement theDistribution and processing of system call services . Configuration requiredstvec
to set it as the entry point for Trap processing.
compileros/src/trap/
:
// os/src/trap/
global_asm!(include_str!(""));
pub fn init() {
extern "C" { fn __alltraps(); }
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
Here you can see that this code utilizes theglobal_asm!
macro-introduction, and then through the
extern
way of introducing__alltraps
this onelabel
, and then use thestvec::write
do sth (for sb)stvec
write__alltraps
As an entry point, the model isDirect
It corresponds to what was said above.
In RV64.stvec
is a 64-bit CSR that holds the entry address for interrupt processing in case of interrupt enable. It has two fields:
- MODE is located at [1:0] and is 2 bits long;
- The BASE is located at [63:2] and is 62 bits long.
next analyzeContent.
# os/src/trap/
.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
Here's one.tip
That's it.csr
The beginning means that it is the CSR register for theatomic operation command So that it's easier to read later, for example.r
It's just readingrw
Just read and write, hee hee.
The first is to define a macroSAVE_GP
:
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
this onen
is a macro parameter that is passed in, expanding theSAVE_GP 5
because ofsd x5, 5*8(sp)
, in which case each register number has a dedicated location on the stack.
And then there's.align 2
will__alltraps
The address is 4-byte aligned. This is a requirement of the RISC-V privilege level specification.
Line 9csrrw
The prototype iscsrrw rd, csr, rs
It is possible to combineCSR
The current value is read into the general purpose registerrd
and then set the general-purpose registers tors
Write the value of theCSR
. Thus what is at play here is the exchange ofsscratch
cap (a poem)sp
The effect of the Before this linesp
points to the user stack.sscratch
points to the kernel stack (for reasons that will be explained later), nowsp
points to the kernel stack.sscratch
Points to the user stack.
Pay attention here.sscratch
The registers are located in the privileged mode of the RISC-V architecture and are part of the supervisor mode, which is typically used to save and restore the values of critical registers, especially during exception and interrupt handling. So we can guess that there is another operation that temporarily stores the kernel stack location when the trap is triggered. Considering that thesscratch
cap (a poem)sp
exchange, which we can understand as entering theS
When the mode operates on the kernel stack, the user stack pointer is stored temporarily in thesscratch
, back toU
The kernel stack pointer is temporarily stored when the mode operates on the user stack.ssratch
In.
On line 12, we are going to store the Trap context on the kernel stack, so we preallocate a 34×8 byte stack frame, and here we change the sp to indicate that it is indeed on the kernel stack. ConsiderTrapContext
by definition, then it is of size34
:
pub struct TrapContext {
pub x: [usize; 32],
pub sstatus: Sstatus,
pub sepc: usize,
}
Lines 13 to 24, save the general registers of Trap context.x0~x31
, Skip.x0
cap (a poem)tp(x4)
For reasons previously explained. We don't save them here either.sp(x2)
Because we have to use it to find the right place where each register should be saved. In fact, after the stack frame has been allocated, we can use it to save theTrap
The address interval of the context is $[sp+8n,sp+8(n+1))$, as per theTrapContext
Memory layout of structures, based on kernel stack locations (sp
address) to place them in order from the low address to the high address.x0~x31
These general-purpose registers, and finally thesstatus
cap (a poem)sepc
. Thus the general-purpose registerxn
should be saved in the address interval $[sp+8n,sp+8(n+1))$. To simplify the code, thex5~x31
These 27 general-purpose registers are used by us through a loop-like.rept
Each useSAVE_GP
The macros to save are essentially the same. Note that we need to add theAdd at the beginning
.altmacro
to be able to use it properly.rept
Order.
In lines 25 to 28, we change CSRsstatus
cap (a poem)sepc
values are read into registerst0
cap (a poem)t1
The instruction reads the value of the CSR into a register and saves it to the corresponding location on the kernel stack. The function of the instruction is to read the value of the CSR into a register. We don't have to worry about thet0
cap (a poem)t1
were overwritten because they had just been saved.
Row 30-31 specializationsp
The question of. Firstly, thesscratch
value is read into the registert2
and save it to the kernel stack, note that.sscratch
The value of is the value that goes into theTrap
priorsp
value, pointing to the user stack. The currentsp
then it points to the kernel stack.
Order No. 33a0←sp
, let the registera0
The stack pointer to the kernel stack is also the address of the Trap context that we just saved, because we're going to call thetrap_handler
for Trap processing, its first parameter iscx
By calling the specification to start from thea0
The And the Trap handlertrap_handler
The Trap context is needed because it needs to know the values of some of the registers, such as those passed by the application during a system call.syscall ID
and the corresponding parameters. We can't use the current values of these registers directly, because they may have been modified, so we have to go to the kernel stack to find the values that have been saved.
This one here.a0
descriptions It's very important, it allows us to break.a0
is used as the default parameter for calling the function.
Here's how we can diagram this paragraph:.
We can see here that it callstrap_handler
function to do the Trap processing, but here theLet's not worry about the implementation Keep looking. The content of.
So then what comes next istrap_hander
after the function has finished executing, combined with our previous work on thesscratch
The description and.
After the operating system completes the system call service, it needs to restore the Trap context of the interrupted application and pass thesret
Let the application continue to execute.
We can tell that the next step should be to restoresp
Pointer position and recoveryx0~x31
registers, and is passed through thesepc
to clarify the value of theTrap
The address of the last instruction executed before this occurred.
So let's move on.Next Steps.
# os/src/trap/
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
__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
First we still see that it defines a macroLOAD_GP
:
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
cap (a poem)sd
The instructions are reversed.ld
command is to set thesp
The data in the stack pointed to is reloaded back into thexn
Registers.
On line 10, you can see that it's a way to put thea0
The value of this value has been recapturedsp
, corresponds to__alltraps
includesp
Get the pointer back to the kernel stack, and ignore the official documentation that says, "We'll ignore the weirdness of line 10 and assume that it never happened, so sp still points to the top of the kernel stack." I think the author is out of his mind. I'll bring up the issue later.
Lines 13 to 26 are responsible for recovering the general purpose registers and CSR from the Trap context at the top of the kernel stack. Note that we need to recover the CSR before recovering the general purpose registers so that the three temporary registers we are using can be recovered correctly.
Until line 28, sp points to the top of the kernel stack after the Trap context is saved, and sscratch points to the top of the user stack. On line 28, we reclaim the memory on the kernel stack occupied by the Trap context and return to the top of the kernel stack before Trap. On line 30, sscratch and sp are swapped again, with sp now pointing back to the top of the user stack, and sscratch still preserving its pre-Trap state and pointing to the top of the kernel stack.
After the application control flow state is restored, at line 31 we use thesret
The command returns to the U privilege level to continue running the application control flow.
The process basically takes the__alltraps
Walked through it. Here's a note.sp
The pointer is shifted back to the top of the stack again, and is then combined with thesscratch
switch (telecom) .
this oneofficial documentI didn't make it clear until I was in this positionsscratch
I'm not sure how useful he is, but I'm too gifted for this document.
Uses of sscratch CSR
During a privilege level switch, we need to save the Trap context on the kernel stack, so we need a register to store the kernel stack address temporarily and use it as a base address pointer to save the contents of the Trap context sequentially. However, all general-purpose registers cannot be used as base address pointers because they all need to be saved, and overwriting them will affect the execution of the subsequent application control flow.
The fact is that we are missing an important transit register, and thesscratch
The CSR was created for this purpose. As you can see from the assembly code above, it does two things when saving the Trap context: first, it saves the address of the kernel stack, and second, it serves as a staging area for thesp
(The value of (the address of the user stack currently pointed to) can be temporarily stored in thesscratch
. This requires only onecsrrw sp, sscratch, sp
Command (Swap Pair)sp
cap (a poem)sscratch
(two register contents) completes the switch from the user stack to the kernel stack, which is an extremely subtle implementation.
Trap distribution and processing
So remember when we didn't implementtrap_handler
What's that? Well, that's where it's going to be done.
existos/src/trap/
realizationtrap_handler
function, in which the distribution and processing of P.
// os/src/trap/
#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read();
let stval = stval::read();
match () {
Trap::Exception(Exception::UserEnvCall) => {
+= 4;
[10] = syscall([17], [[10], [11], [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}!", (), stval);
}
}
cx
}
The main thing to care about here is this piece of code.
Trap::Exception(Exception::UserEnvCall) => {
+= 4;
[10] = syscall([17], [[10], [11], [12]]) as usize;
}
In lines 8-11, we find that the Trap is triggered by a privileged Environment Call, which is a system call. Here we first modify sepc in the Trap context saved on the kernel stack to increase it by 4. This is because we know that this is a system call made by theecall
instruction-triggered system call, the hardware sets sepc to this line when entering Trapecall
instruction (since it was the last instruction executed before entering Trap). And after the Trap returns, we want the application control flow to start from theecall
to the next instruction in the Trap context. So all we need to do is modify sepc inside the Trap context so that it adds theecall
code length of the instruction, which is 4 bytes. This is the same as the code length of the__restore
When the sepc is restored, it points to theecall
The next instruction in thesret
After that it's implemented from there.
Thinking back to our previous implementation ofsyscall
:
// user/src/
use core::arch::asm;
fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id
);
}
ret
}
The main point to mention here is thatx10
actuallya0
, both as the first argument of the function and as the output, the samex11
homologousa1
, as the second argument to the function.x12
homologousa3
The third register of the function. The aliases of the three registers can be seen asa0-2
It is very reasonable, becausea
homologousarg
Parameters.
In addition, it is important to understand that this is a call to the not-yet-exactly-implementedrun_next_app
And it's easy to miss the point that if you're faced with thesecase
Other than the question, thenoperating system should also stop running, instead ofRun the next app , so it's a call topanic!
Macros to handle.
Implementation of the system call function
For system calls, thesyscall
function does not actually handle the system call, but simply distributes it to a specific handler function based on the syscall ID:
// os/src/syscall/
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
Here our minds mustPause. Think about what triggers the trap and what happens after the trap is triggered so that you can understand why the userland has asyscall
, the kernel state also has a followingsyscall
.
Think about it.
- is that we call the userland
syscall
- Then use
ecall
, stored the parameters in thex10
,x11
,x12
,x17
- Next, the program falls into Trap.
- We call the
__alltraps
- subsequent use
trap_handler
to handle Trap - exist
trap_handler
The kernel state of the kernel is invoked in thesyscall
, which handles thex10
,x11
,x12
,x17
- Subsequently the kernel state of the
syscall
according tox17
The case of calling the kernel's function
So there's really no magic involved. Going from the user state to the kernel state is equivalent to using thex10
,x11
,x12
,x17
As a bridge, according tox17
The agreed upon function number is executed, but the current privilege level has been changed to allow the execution of some instructions.
So at the hardware level, at themstatus
cap (a poem)sstatus
in the respectiveM
Privileged andS
The privilege level provides the bits that determine the current privilege level, and for theU
The privileged level can only try to execute instructions that are not allowed to be executed to see if an exception occurs.
Going back to the function, it takes our passed inid
Determines which function is currently being executed. Here both functions are in theos/src/syscall/
There are implementations in.
// os/src/syscall/
const FD_STDOUT: usize = 1;
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
match fd {
FD_STDOUT => {
let slice = unsafe { core::slice::from_raw_parts(buf, len) };
let str = core::str::from_utf8(slice).unwrap();
print!("{}", str);
len as isize
},
_ => {
panic!("Unsupported fd in sys_write!");
}
}
}
// os/src/syscall/
pub fn sys_exit(xstate: i32) -> ! {
println!("[kernel] Application exited with code {}", xstate);
run_next_app()
}
Execute the application
When the initialization of the batch operating system is complete, or when an application finishes running or makes an error, we have to call therun_next_app
function to switch to the next application.
aforementionedtrap_handler
There are also two types ofExpectation
calls therun_next_app
.
The problem to consider here is that the CPU is running at S privilege level and it wants to be able to switch to U privilege level.
In the RISC-V architecture, the only way to bring down the CPU privilege level is to execute the privileged instructions returned by the Trap, such assret
,mret
etc. In fact, before returning from the operating system kernel to run the application, the following is accomplished.
- Constructs the Trap context needed for the application to begin execution;
- pass (a bill or inspection etc)
__restore
function to recover some of the registers executed by the application from the Trap context just constructed; - set up
sepc
The content of the CSR is the application entry point0x80400000
; - switch modes or data streams
scratch
cap (a poem)sp
register, set thesp
Points to the application user stack; - fulfillment
sret
Switch from the S privilege level to the U privilege level.
Here's where the official architectural design gets really clever.
They can be reused by reusing the__restore
code to make it easier to do this. All we need to do is press in a specially constructed Trap context on the kernel stack for launching the application, and then pass it through the__restore
function, it is possible to get these registers to the context state needed to start the application.
// os/src/trap/
impl TrapContext {
pub fn set_sp(&mut self, sp: usize) { [2] = sp; }
pub fn app_init_context(entry: usize, sp: usize) -> Self {
let mut sstatus = sstatus::read();
sstatus.set_spp(SPP::User);
let mut cx = Self {
x: [0; 32],
sstatus,
sepc: entry,
};
cx.set_sp(sp);
cx
}
}
because ofTrapContext
realizationapp_init_context
method, modify the sepc register in it to the application entry pointentry
, the sp register is a stack pointer that we set, and the sstatus register of theSPP
The field is set to User.
Then it can be realized like thisrun_next_app
:
// os/src/
pub fn run_next_app() -> ! {
let mut app_manager = APP_MANAGER.exclusive_access();
let current_app = app_manager.get_current_app();
unsafe {
app_manager.load_app(current_app);
}
app_manager.move_to_next_app();
drop(app_manager);
// before this we have to drop local variables related to resources manually
// and release the resources
extern "C" { fn __restore(cx_addr: usize); }
unsafe {
__restore(KERNEL_STACK.push_context(
TrapContext::app_init_context(APP_BASE_ADDRESS, USER_STACK.get_sp())
) as *const _ as usize);
}
panic!("Unreachable in batch::run_current_app!");
}
What is being done at the highlighted line is to press in a Trap context on the kernel stack with thesepc
is the application entry address0x80400000
itssp
register points to the user stack with itssstatus
(used form a nominal expression)SPP
field is set to User.push_context
is the top of the kernel stack after the kernel stack is pressed into the Trap context, and it will be used as the return value of the__restore
parameter (look back at the__restore code At this point we can understand why__restore
(the beginning of the function completes sp←a0 ), which makes the__restore
in a functionsp
can still point to the top of the kernel stack. After this, it's the same as executing a normal__restore
Function calls are the same now.
When was sscratch set to the top of the kernel stack?
At program startup, as in Experiment 1, sp points to theboot_stack
So that's the entry function.load_all
stack used. Thenload_all
call (programming)run_next_app
The latter callsapp_init_context
Here's to theTrapContext
operations modify the bottom of the kernel stack, putting theUSER_TOP
Pass it on.TrapContext
in the sp register and returned theTrapContext
The pointer to the
Then go to restore, first sentencemv sp, a0
willTrapContext
is passed to sp. This step is equivalent to resetting the kernel stack, which resets the kernel stack so that the bottom of the stack has only oneTrapContext
state of the program, and also here the program's current stack is removed from theboot_stack
It goes to the kernel stack. Then the various instructions are processing that one in the kernel stackTrapContext
The After that.csrw sscratch, t2
Pass the bottom of the user stack that was just passed into x[2] to the sscratch register, last sentencecsrrw sp, sscratch, sp
, sscratch is set to the bottom of the kernel stack, and sp also points to the bottom of the user stack.