Location>code7788 >text

[rCore Study Notes 018] Implementing Privilege Level Switching

Popularity:377 ℃/2024-07-28 13:26:17

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 correspondingSEEassume (office)Supervisor Execution Enviroment As the name suggests, it is in theMachineMachine Layer BuiltPrivileged Application Runtime Environment .

We do this byRust-SBIEstablished 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 callsys_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 theMPrivilege-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 aboutUPrivileged andSPrivilege 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 architectureTrapThe 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 onCSRof the content, theCSR Control and Status Register As the name implies.Control and Status Registers , stores the currentTrapThe state information of theTrapPart 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 exceptionsTrapThe situation.

  1. 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 Register sstatus et al. (and other authors)
  2. 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 tostvec 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.stveccan help us switch to post-privilege level switch processingTrapAddress.

  1. Some anomaly in the execution of the higher-level software or thespecial case , need to use the functions provided in the implementation environment
    1. 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
    2. The reasons why user-state applications trigger exceptions directly from user-state to kernel-state can be categorized into two general categories
      1. One is the execution of special instructions by the userland software to obtain the service functions of the kernel operating system.
        1. The instructions themselves belong to a highly privileged class, such as thesret Command (indicates return from S-mode to U-mode)
        2. 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)
      2. 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.
  2. Suspend the functionality of the higher-level software and run the code of the execution environment instead (along with thePrivilege level switching )
  3. 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 thesstatus (used form a nominal expression)SPP The field is set to U or S;
  • CPU will jump tosepc 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 aboutstveccap (a poem)sretand the role of the United Nations.sstatuscan be givenTrapwhich privilege level the CPU was at before it occurred.stvalcan be givenTrapAdditional information can be found in theTrapSave the contents when it happens.

But we can't just wrap our heads around switching priorities, as we can when using theasmexploit (a resource)mcuWe 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 thesppointer, 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 theUPrivileged level data can only be accessed by the application program, and theSLevels 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 theUThe application data on the level is stored in theuser terminal , while running onSApplications on the level (kernel APIs) are stored in thekernel stack . So how do you switch the running state?

The answer is to simply modify thespregister 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 saveTrapThe register context after this occurs and is saved to the kernel stack, this can be done in three steps.

  1. Know what register contents need to be saved
  2. What kind of data structure is used to store
  3. 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 ofCSRregisters, and general-purpose registers.x0~x31Storage 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 asx0 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 thescause/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 size32array storagex0~x31, and using aSstatusThe structure that holds thesstatusregisters, using theuszieA variable that holds thesepcRegisters.

included among theseSstatusThe structure isriscv::registerThis bag is dedicated to storingsstatusA package of registers, thesstatusSome 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)]

  1. 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.
  2. 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.
  3. 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/srcdirectory to create thetrapfolder, create thecap (a poem), the structure we described above is located in theDescribed 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_contextmethod is a direct acquisition of a top-of-stack pointer (It's actually the bottom of the array), and then through theTrapContextThe 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 theecall 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 thesret 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 ofstvecregister, which controls the entry address of the Trap processing code. So in order to implement theDistribution and processing of system call services . Configuration requiredstvecto 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 theexternway of introducing__alltrapsthis onelabel, and then use thestvec::writedo sth (for sb)stvecwrite__alltrapsAs an entry point, the model isDirectIt 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.tipThat's it.csrThe beginning means that it is the CSR register for theatomic operation command So that it's easier to read later, for example.rIt's just readingrwJust 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 onenis a macro parameter that is passed in, expanding theSAVE_GP 5because ofsd x5, 5*8(sp), in which case each register number has a dedicated location on the stack.

And then there's.align 2will__alltrapsThe 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 combineCSRThe current value is read into the general purpose registerrdand then set the general-purpose registers torsWrite the value of theCSR . Thus what is at play here is the exchange ofsscratchcap (a poem)spThe effect of the Before this linesppoints to the user stack.sscratchpoints to the kernel stack (for reasons that will be explained later), nowsppoints to the kernel stack.sscratchPoints 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 thesscratchcap (a poem)sp
exchange, which we can understand as entering theSWhen the mode operates on the kernel stack, the user stack pointer is stored temporarily in thesscratch, back toUThe kernel stack pointer is temporarily stored when the mode operates on the user stack.ssratchIn.

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. ConsiderTrapContextby 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.x0cap (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 theTrapThe address interval of the context is $[sp+8n,sp+8(n+1))$, as per theTrapContextMemory layout of structures, based on kernel stack locations (spaddress) to place them in order from the low address to the high address.x0~x31These general-purpose registers, and finally thesstatuscap (a poem)sepc. Thus the general-purpose registerxnshould be saved in the address interval $[sp+8n,sp+8(n+1))$. To simplify the code, thex5~x31These 27 general-purpose registers are used by us through a loop-like.reptEach useSAVE_GPThe macros to save are essentially the same. Note that we need to add theAdd at the beginning.altmacroto be able to use it properly.reptOrder.

In lines 25 to 28, we change CSRsstatuscap (a poem)sepcvalues are read into registerst0cap (a poem)t1The 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 thet0cap (a poem)t1were overwritten because they had just been saved.

Row 30-31 specializationspThe question of. Firstly, thesscratchvalue is read into the registert2and save it to the kernel stack, note that.sscratchThe value of is the value that goes into theTrappriorspvalue, pointing to the user stack. The currentspthen it points to the kernel stack.

Order No. 33a0←sp, let the registera0The 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 thea0The 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 IDand 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.a0descriptions It's very important, it allows us to break.a0is used as the default parameter for calling the function.

Here's how we can diagram this paragraph:.

We can see here that it callstrap_handlerfunction 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_handerafter the function has finished executing, combined with our previous work on thesscratchThe 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 restorespPointer position and recoveryx0~x31registers, and is passed through thesepcto clarify the value of theTrapThe 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)sdThe instructions are reversed.ldcommand is to set thespThe data in the stack pointed to is reloaded back into thexnRegisters.

On line 10, you can see that it's a way to put thea0The value of this value has been recapturedsp, corresponds to__alltrapsincludespGet 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__alltrapsWalked through it. Here's a note.spThe pointer is shifted back to the top of the stack again, and is then combined with thesscratchswitch (telecom) .

this oneofficial documentI didn't make it clear until I was in this positionsscratchI'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_handlerWhat'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 thatx10actuallya0, both as the first argument of the function and as the output, the samex11homologousa1, as the second argument to the function.x12homologousa3The third register of the function. The aliases of the three registers can be seen asa0-2It is very reasonable, becauseahomologousargParameters.

In addition, it is important to understand that this is a call to the not-yet-exactly-implementedrun_next_appAnd it's easy to miss the point that if you're faced with thesecaseOther 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.

  1. is that we call the userlandsyscall
  2. Then useecall, stored the parameters in thex10,x11,x12,x17
  3. Next, the program falls into Trap.
  4. We call the__alltraps
  5. subsequent usetrap_handlerto handle Trap
  6. existtrap_handlerThe kernel state of the kernel is invoked in thesyscall, which handles thex10,x11,x12,x17
  7. Subsequently the kernel state of thesyscallaccording tox17The 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,x17As a bridge, according tox17The 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 themstatuscap (a poem)sstatusin the respectiveMPrivileged andSThe privilege level provides the bits that determine the current privilege level, and for theUThe 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 inidDetermines 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_handlerThere are also two types ofExpectationcalls 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 upsepc The content of the CSR is the application entry point0x80400000
  • switch modes or data streamsscratch cap (a poem)sp register, set thesp Points to the application user stack;
  • fulfillmentsret 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_stackSo that's the entry function.load_allstack used. Thenload_allcall (programming)run_next_appThe latter callsapp_init_contextHere's to theTrapContextoperations modify the bottom of the kernel stack, putting theUSER_TOPPass it on.TrapContextin the sp register and returned theTrapContextThe pointer to the

Then go to restore, first sentencemv sp, a0willTrapContextis 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 oneTrapContextstate of the program, and also here the program's current stack is removed from theboot_stackIt goes to the kernel stack. Then the various instructions are processing that one in the kernel stackTrapContextThe After that.csrw sscratch, t2Pass 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.