XV6 Operator System: 02-The Starter


在上一节中,我们已经了解了xv6-riscv是如何从qemu中引导内核到指定入口点,并从该入口点进入转跳到特定start函数的。那么现在,我们就来解析start函数是如何工作的。

Start Function

在开始之前,我们需要知道的是,在start函数中,我们并未真正地进入内核。这是什么意思呢?实际上就是,start函数实际上还是属于引导程序的一部分,其最终的目的就是跳转到main函数中,从而进入内核态执行。同时,有一个最浅显的概念是:xv6-riscv的内核态是运行在S态的,而非M态,因此,我们从入口点转跳到start函数中时,其仍是M态。也就是说,start函数的另一个目的就是需要为进入main函数设置合适的环境配置

因此,这里就很自然的引出了一个问题:如何从M态降入S态呢?现在,我们来了解一些关于risc-v privilege的知识。

mstatus csr(Machine Status Register)

mstatus寄存器是一个MXLEN位比特的可读写的寄存器,其跟踪并控制了hart的当前运行状态。也就是说,我们可以通过修改mstatus的一些特定位,来使得hart在当前的运行状态发生变化。下面给出了rv32 mstatusrv64 mstatus的格式:

risc-v32 mstatus

risc-v64 mstatus

在这里,我并不打算完整的将mstatus的所有标志位介绍完,只会介绍本节中我们将涉及到的标志位

全局中断使能标志位:MIESIE,分别用于M-modeS-mode。这些位主要被用于确保当前特权级模式下的中断处理程序的原子性,也就是说:设置xIE以处理M态或S态中断。并且,允许被一个单独的csr指令所设置

当一个hart在$x$特权级下执行时,当$xIE = 1$时,全局启用中断;当$xIE = 0$时,全局关闭中断。对于低特权级的中断而言,如果$x \gt w$,那么不论低特权级设置了何种$wIE$,总是全局中断的;对于高特权级的中断而言,如果$y \ge x$,那么不论如何,总是全局开启的。如何理解呢:如果从一个高特权级的视角看待低特权级,那么低特权级的中断设置对于高特权级没有任何影响;如果从低特权级的视角看待高特权级,那么高特权级总是启用的

也就是说,实际上的riscv特权级有着以下规定:高特权级总是能够打断低特权级的。同时,这里也给出了一个优势,高特权级的代码能够使用单独的每个中断使能来禁用选定的高特权级中断,然后再将控制权转交给低特权级。而回忆一下我们的目标,我们需要从M态转变到S态,而内核态是常驻于S态的,因此,我们并不希望M态的中断被经常触发从而导致内核的任务被强行打断,使得效率变低,因此,我们需要对中断位进行设置(在后续介绍)。

为了支持嵌套trap,每一个能够响应中断的特权级模式$x$都有一个包含中断使能位和特权级模式的两级栈。$xPIE$保存了trap发生之前激活的中断使能位的值,而$xPP$保存了上一个特权级模式

Two-level Stack
两级栈指的是有两个层次的栈结构,用于存储中断使能位和特权模式。第一级存储当前的中断使能位和特权模式,第二级存储在处理中断时被保存的中断使能位和特权模式。

$xPP$字段最多只能包含$x$个特权级模式,因此,MPP有两个比特的位宽,而SPP只有一位。当trap从特权级模式$y$进入到特权级$x$时,$xPIE$被设置为$xIE$的值,而$xIE$将会被置零,与此同时,$xPP = y$。

对于低特权级而言,任何trap(同步或异步的)通常在进入时以中断禁用的状态进入更高特权级模式。高特权级的trap处理程序将处理该trap并使用堆栈信息返回,或者,如果不立即返回到中断的上下文,将在重新启动中断之前保存特权级堆栈,因此每个堆栈只需要一个入口

到现在我们就还剩一个问题,高特权级是如何返回到低特权级的呢?

MRETSRET指令解决了这一个疑问,MRETSRET分别被用于从一个处于M态或S态的trap中返回。假设$xPP = y,当执行一条$xRET$指令时,那么$xIE = xPIE$,特权级模式被设置为$y$,$xPIE = 1$,$xPP$将被设置为最低特权级模式(U mode, 如果U未被实现,则设置为M)。并且,如果$y \ne M \rightarrow MPRV = 0$。

这里简单的解释一下上面这句话的含义:当xRET指令执行时,保存着上一特权级信息的xPIExPP就会起效,xPIE在赋值完成后,将被设置为1,这是因为高特权级的中断总是启用的。而MPRV表示内存访问的特权模式。如果返回的特权模式y不是M,则将MPRV置为0。这意味着内存访问将不再使用M mode,而是使用当前的特权模式。

比较难以理解的是,为什么xPP会被设置为最低特权级模式。设置xPP为最低特权级模式有助于识别在两级特权级栈管理中的软件错误

值得注意的是,xPP字段是WARL类型字段,其只能包含$x$特权级模式或任何实现的低于$x$的特权级模式如果特权级模式$x$未被实现那么$xPP$字段必须被设置为只读的$0$。

WARL(Write Any Values, Reads Legal Values)
某些读/写CSR字段仅在特定的比特编码子集中定义,但允许写入任何值,同时保证在读取时返回一个合法值。假设写入CSR没有其他副作用,可以通过尝试写入一个期望的设置,然后读取以查看该值是否被保留,从而确定支持的值范围。这些字段在寄存器描述中标记为WARL(Write-Any Read-Legal)
实现不会因为向WARL字段写入不支持的值而引发异常。当读取一个WARL字段时,如果上一次写入的是一个非法值,实现可以返回任何合法值,但返回的合法值应当与写入的非法值以及hart的架构状态有确定性的关系。

mepc csr(Machine Exception Program Counter)

在这里,我并不会详细介绍mepc的所有用法,只会提及一点:start函数调用mret, mepc的值应该被设置为main函数的地址

这里可以给出一个比较正式的用法:当trap进入M态时,mepc会被写入引发中断或遇到异常的指令的虚拟地址。因此,在mret调用时,会根据mepc的值进行跳转。

mepc

satp(Supervisor Address Translation and Protection Register)

satp寄存器是一个SXLEN位宽的读写寄存器,其控制了S模式下的地址转换和保护。satp有两种形式,分别为$SXLEN = 31$或$SXLEN = 64$:

SXLEN = 32

SXLEN = 64

satp寄存器包含一个根页表的物理页码(PPN, Physical Page Number),该物理页码是由S mode的物理地址除以$4 KiB$而来;一个地址空间标识符(ASID, Address Space Identify),该标识符有助于在每个地址空间执行地址转换屏障(fence);以及一个模式(MODE)字段,其决定了当前地址的转换方法。

其中,对于MODE字段我们需要详细介绍一下。当$MODE = Bare$时,S特权级的虚拟地址直接等价于物理地址,除了物理内存保护方案外没有其他额外的内存保护。为了选择$MODE = Bare$,软件必须satp剩余字段置零试图在$MODE = Bare$的情况下使用一个非零的satp将对未置零的字段的值会产生一个UNSPECIFIED(未指明的)影响,并对地址转换和保护行为产生一个UNSPECIFIED影响

对于$SXLEN = 32$时,MODE的唯一一个有效设置为Sv32,一种虚拟内存分页策略。当$SXLEN = 64$时,一共有三种虚拟内存分页策略可供选择:Sv39Sv48Sv57。其余的MODE设置保留以供将来使用,并且可以定义satp中其他字段的不同解释。

satp mode

satp寄存器在S modeU mode下才被认为是激活状态。地址转换算法只可能在satp被激活时使用一个给定的satp值开始执行。

medeleg & mideleg csr(Machine Trap Delegation Registers)

在默认情况下,任何特权级的所有trap都是在M mode被处理的,不过M mode的处理程序可以使用MRET指令将trap重定向到合适的特权级

为了提高性能,具体实现可以在medelegmideleg中通过提供单独的读写位来表明一些特定的异常和中断应该被低特权级直接处理。medeleg(machine exception delegation)是一个$64$位宽的读写寄存器;而mideleg(machine interrupt delegation)是一个MXLEN位宽的读写寄存器。

medeleg

mideleg

S mode下的hart中,medelegmideleg必须存在,并且在S modeU mode下发生对应的trap时,在medelegmideleg中设置一位把该trap委托给S mode下的trap handler

当一个trap被委托给S mode时,会执行以下操作:

  • scause寄存器写入trap的原因
  • sepc寄存器写入引发trap的指令的虚拟地址
  • stval寄存器写入特定于异常的数据
  • mstatus寄存器中的SPP字段写入发生trap时激活的特权级模式
  • mstatus寄存器中的SPIE字段写入发生trap时的SIE字段的值
  • mstatus.SIE字段置零
  • mcausemepcmtval以及mstatus.MPPmstatus.MPIE字段不会被写入

值得注意的是:trap永远不会从更高特权级转换到更低特权级,这一情况不会发生。只可能在水平上进行发生,也就是:如果M mode委托了一个trapS mode,那么引发异常的trap能够在S mode下进行处理。

Supervisor Interrupt Registers(sip and sie)

在本节中,并不会对sip寄存器做出介绍,因为这里的重点是sie寄存器。

sie寄存器是一个$SXLEN$位宽的读写寄存器,其包含了中断使能位。中断原因号与sie的位号相对应。比特15:0只分配给标准中断原因,而比特16及以上被指定为平台使用。

sie

scause values

sie portion

  • sie.SSIE是用于S mode外部中断的中断使能位
  • sie.STIP是用于S mode时钟中断的中断使能位
  • sie.SEIE是用于S mode软件中断的中断使能位
  • 如果实现了Sscofpmf扩展,sie.LCOFIE是本地计数器溢出中断的中断使能位。如果未实现该扩展,sie.LCOFIE则是只读的0

Physical Memory Protection CSRs

在这里只介绍两个寄存器:pmpcfg0pmpaddr0

PMP(物理内存保护)条目由一个8位的配置寄存器和一个MXLEN位的地址寄存器描述。一些PMP设置还会使用与前一个PMP条目相关联的地址寄存器。最多支持64PMP条目。实现中可以实现0个、16个或64PMP条目;必须首先实现编号最低的PMP条目。所有PMP CSR字段都是WARL,并且可能是只读零。PMP CSR仅在M mode下可访问

PMP configuration寄存器被密集地打包到CSRs中,以最小化上下文切换时间。对于RV32,有十六个CSRs(pmpcfg0–pmpcfg15),用于保存64PMP条目的配置(pmp0cfg–pmp63cfg)。对于RV64,有八个偶数编号的CSRs(pmpcfg0、pmpcfg2、……、pmpcfg14),用于保存64PMP条目的配置。对于RV64奇数编号的配置寄存器(pmpcfg1、pmpcfg3、……、pmpcfg15)是非法的

rv32 pmp configuration

rv64 pmp configuration

PMP address寄存器是命名为pmpaddr0-pmpaddr63CSRs。每个PMP address寄存器在RV32中编码一个34位物理地址的第33到第2位。对于RV64,每个PMP address寄存器编码一个56位物理地址的第55到第2位。并非所有物理地址位都需要实现,因此pmpaddr寄存器是WARL的。

rv32 pmp address

rv64 pmp address

下图显示了PMP configuration寄存器的布局。RWX位分别表示PMP条目允许指令执行当其中某一位被清除时,相应的访问类型被拒绝RWX字段组成一个集体的WARL字段,其中$R = 0$和$W = 1$的组合是保留的。其余两个字段AL在后续章节中描述。

pmp configuration layout

Start Code

至此,关于理解start函数逻辑的前置知识就介绍完毕。现在就让我们来分析start函数中的具体内容:

1
2
3
4
5
6
7
8
9
#define MSTATUS_MPP_MASK (3L << 11)
#define MSTATUS_MPP_S (1L << 11)
r_mstatus => asm volatile("csrr %0, mstatus" : "=r"(x));
r_mstatus => asm volatile("csrw mstatus, %0" : : "r"(x));

unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);

这是从入口点_entrystart函数的第一个操作,我们在之前已经知道:start函数最终目的是要转跳到main函数中,并在此之前进行一系列配置,然后进入内核态。那么,xv6-riscv的内核态是运行在S mode的,而从_entry进入到start的此时,是处于M mode的。因此,我们需要设置进入main函数后的特权级。

r_mstatus便是读取当前hart的运行状态,而x &= ~MSTATUS_MPP_MASKmstatus.MPP进行置零操作,然后通过x |= MSTATUS_MPP_S设置mstatus.MPP = S。这样,我们就能够在mret指令后,将特权级设置为S

但是,我们发现:我们现在只完成了设置特权级,但是并未设置到底要转跳到哪一个位置。因此:

1
w_mepc((uint64)main);

在上面我们已经了解了,mret的返回地址是根据mepc寄存器而决定的,因此我们将mepc的值设置为main函数的地址,就能够在mret执行的时候转跳到对应的main函数从而进入内核态。

这样,我们就完成了一个简易的内核态的入口,但是,对于一个xcv6-riscv来说,还需要进行一些具体的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// disable paging for now.
w_satp(0);

// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);

// ask for clock interrupts.
timerinit();

// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);

让我们来逐行分析这些代码的含义:

我们知道satp寄存器用于管理页表基地址、地址空间标识符以及启动或禁用分页机制。简单思考一下,操作系统初始化早期,我们并不需要启动太多的事项,而且由于有一些需要直接访问物理内存进行配置,如果启动分页机制,就需要进行页表转换,那么我们在初始化阶段就需要浪费一定的资源来额外的进行初始化操作。我们在这里设置$satp = 0$是为了简化内存管理,并且不需要过早的进行分页管理,等到进入内核态后,再启用分页机制支持虚拟内存。

medelegmideleg是为了提高效率,因为频繁的通过M mode来处理trap会极大的影响内核的运行。因此,在此处,我们直接将所有的异常和中断都委托给了S mode来处理,也就是说,内核态对trap具有全部的处理能力,这也符合一般认知,内核会处理用户的异常和中断,而不是交由M mode

现在我们已经将trap的处理权交付给了S mode,但是我们需要显示开启S特权级下的中断使能,因此w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE)便是允许内核态对外部中断、时钟中断和软件中断的响应。这也对应了上面M modetrap的处理全权委托给了内核态。

当我们有了处理权限后,我们就可以考虑内存访问了,在上一节中,我们了解到xv6-riscv的内存布局,而其物理地址是$56$位宽的,因此w_pmpaddr0(0x3fffffffffffffull)PMP的第一个条目的地址寄存器设置为0x3fffffffffffff,刚好覆盖了整个物理地址空间,因此该条目可以匹配任何物理地址。而w_pmpcfg0(0xf)则是对该条目进行具体配置,允许了S mode对整个物理地址具有读、写和执行访问权限

timerinit()则是设置了时钟中断源,使得xv6-riscv能够获得时间片。

而最后一点,便是获取当前hartID,写入tp寄存器。tp寄存器用于存储线程指针,即当前执行流ID

Timer

CLINT

risc-v中,CLINT的定义是由平台具体实现的,而qemu virt参考了SIFIVECLINT设计。因此,在这里我参考了SiFive FE310-G000型号的开发板进行分析。

xv6-riscvCLINT是根据qemu virt中的设置而来,因此可以看见CLINT的基址位于0x2000'0000处。

1
2
3
static const MemMapEntry virt_memmap[] = {
[VIRT_CLINT] = { 0x2000000, 0x10000 },
};

了解到CLINT在实际物理地址中的基址后,我们就需要学习关于CLINT的一些基本概念:

CLINT(Core Local Interruptor)是一个处理器内部模块,负责处理和管理核本地的中断和定时器功能CLINT的主要功能包括:

  • 本地中断管理(Local Interrupt Management)CLINT处理核本地的中断请求,这些中断请求通常不需要通过全局中断控制器(如PLIC,Platform-Level Interrupt Controller)进行处理。CLINT管理的中断通常是核内的特殊事件,例如软件中断和定时器中断。
  • 定时器功能(Timer Functionality)CLINT提供核本地的定时器功能,用于生成周期性中断。每个处理器核都有一个独立的定时器,通过编程可以设置定时器的触发时间。当定时器到达设定时间时,会触发一个中断,处理器核可以用这个中断来执行周期性任务或进行时间管理

risc-v中,操作CLINT是有着专属寄存器的:

  • msip(Machine-mode Software Interrupt Pending Register):用于管理软件中断。每个核都有各自的msip寄存器。写入这个寄存器会触发相应核的机器模式软件中断(Machine Software Interrupt)
  • `mtime(Machine Timer Register):这是一个64位的计时器寄存器,用于跟踪时间它通常由一个全局的、统一递增的计时器硬件单元提供时间戳
  • mtimecmp(Machine Timer Compare Register):每个处理器核都有一个独立的$64$位mtimecmp寄存器。处理器核会不断地比较mtimemtimecmp的值,当mtime达到或超过mtimecmp的值时,会触发机器模式定时器中断(Machine Timer Interrupt)

对于CLINT的专属寄存器,risc-v手册中并未给出详细定义地址,因此,我们参考SiFive FE310-G000能够得到其在物理地址中的映射地址:

CLINT register remap

1
2
3
4
// core local interruptor (CLINT), which contains the timer.
#define CLINT 0x2000000L
#define CLINT_MTIMECMP(hartid) (CLINT + 0x4000 + 8 * (hartid))
#define CLINT_MTIME (CLINT + 0xBFF8) // cycles since boot.
  • CLINTCLINT的基址,即CLINT寄存器的起始地址。
  • CLINT_MTIMECMP(hartid):用于计算给定hartidMTIMECMP寄存器的地址。在CLINT中,每个hart都有一个MTIMECMP寄存器,用于设置定时器中断触发的时间比较值。
  • CLINT_MTIME:用于访问MTIME寄存器的地址。MTIME寄存器用于跟踪自启动以来的时钟周期数,它通常用于实现定时器。

在正式介绍Timer的代码前,我们需要对一些寄存器做出了解。

mscratch csr(Machine Scratch Register)

mscratch寄存器是一个$MXLEN$位宽的读写寄存器,其只能被M mode所使用通常,它用于保存指向M modehart-local上下文空间的指针,并在进入M mode trap handler时与用户寄存器交换

当处理器进入机器模式处理中断或异常时,通常会使用mscratch寄存器保存上下文信息,例如保存当前的寄存器状态、程序计数器等。这样可以在处理完中断或异常后恢复处理器状态。

mtvec csr(Machine Trap-Vector Base-Address Register)

mtvec寄存器是一个$MXLEN$位宽的读写寄存器,其保存了由一个向量基址(Vector Base Address)和向量模式(Vector Mode)组成的trap vector configuration

mtvec

mtvec总是被实现的,至少会包含一个可读的值;如果mtvec以可写的方式实现,那么mtvec所保存的值的集合根据实现的不同而不同。mtvec.BASE的值必须始终是四字节对齐的,而mtvec.MODE设置的值可能会对mtvec.BASE施加额外的对齐操作。

Encoding of mtvec MODE field

mtvec.MODE的编码如上所示。当$mtvec.MODE = Direct$时,所有进入到M态的trap都会导致pc被设置为mtvec.BASE字段中的值;当mtvec.MODE = Vectored时,所有进入到M态的同步异常都会导致pc被设置为mtvec.BASE字段中的值,而所有进入到M态的异步中断都会导致pc被设置为$mtvec.BASE + cause \times 4$。

Supervisor Interrupt Registers(sip and sie)

我们在介绍start函数时,曾介绍过sie寄存器。现在,我们对sip寄存器做出介绍。

sip寄存器是一个$SXLEN$位宽的读写寄存器,其包含了挂起的中断的信息

sip

scause values

sip portion

  • sip.SEIP用于S态的外部中断的中断挂起,如果实现,SEIPsip中是只读的,并且由执行环境设置和清除,通常通过特定于平台的中断控件
  • sip.STIP用于S态的定时器中断的中断挂起,如果实现,STIPsip中是只读的,并且由执行环境设置和清除
  • sip.SSIP用于S态的软件中断的中断挂起,如果实现,SSIPsip中是可写的,并且可能被平台特定的中断控制器置1

Timer Code

Timer Init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];

void timerinit() {
// each CPU has a separate source of timer interrupts.
int id = r_mhartid();

// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64 *)CLINT_MTIMECMP(id) = *(uint64 *)CLINT_MTIME + interval;

// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);

// set the machine-mode trap handler.
w_mtvec((uint64)timervec);

// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);

// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}

让我们来逐行分析上述代码:首先,获取当前的hartid,以便于后续计算CLINT的相关信息;

然后*(uint64 *)CLINT_MTIMECMP(id) = *(uint64 *)CLINT_MTIME + interval: 在CLINT中设置一个定时器中断。这行代码将CLINTMTIMECMP寄存器(用于设置定时器中断触发的时间比较值)的值设置为当前的MTIME寄存器值加上一个指定的间隔。在这里,间隔为$100’0000$个CPU周期,大约相当于qemu中的$1/10$秒。

uint64 *scratch = &timer_scratch[id][0]: 创建一个指向timer_scratch数组的指针,并将其设置为当前hart对应的上下文的地址。这个数组用于存储一些与定时器中断相关的信息。

scratch[3] = CLINT_MTIMECMP(id): 将CLINTMTIMECMP寄存器的地址存储在scratch数组的第3个条目中。

scratch[4] = interval: 将定时器中断触发的时间间隔(以CPU周期数表示)存储在scratch数组的第4个条目中

w_mscratch((uint64)scratch): 将scratch数组的地址存储在MSRATCH寄存器中,以便后续的处理,这里实际上就是对定时器中断的信息进行了初始化,保存在了当前hart的上下文中。

也就是说,在timer init中,我们创建了一个针对于每一个hart单独的定时器的上下文配置,具体如下所示:

1
2
3
4
5
6
7
timer scratch[5] = {
0, reserve for parameters
8, reserve for parameters
16, reserve for parameters
24, address of CLINT MTIMECMP register
32, desired interval (in cycles) between timer interrupts
}

w_mtvec((uint64)timervec): 将M mode trap向量基址寄存器(MTVEC)设置为timervec函数的地址。这意味着当发生M modetrap时,处理器将跳转到timervec函数中执行相应的处理,而对于定时器中断,我们将其设置为M mode trap handler,因此只要发生定时器中断,那么就可以跳转到timervec中进行处理。

w_mstatus(r_mstatus() | MSTATUS_MIE): 使能M mode中断(MIE)。这行代码将MSTATUS寄存器的MIE位设置为1,允许M mode中断。

w_mie(r_mie() | MIE_MTIE): 使能机器模式的定时器中断(MTIE)。这行代码将MIE寄存器的MTIE位设置为1,允许机器模式的定时器中断,这里就与上述代码对应了。

Time Interrupt Handler

实际上,我认为在这里将Timer Trap Handler不是太合适,因此此处仅仅列出代码,并不做任何解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.globl timervec
.align 4
timervec:
csrrw a0, mscratch, a0
sd a1, 0(a0)
sd a2, 8(a0)
sd a3, 16(a0)

# schedule the next timer interrupt
# by adding interval to mtimecmp.
ld a1, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)

# arrange for a supervisor software interrupt
# after this handler returns.
li a1, 2
csrw sip, a1

ld a3, 16(a0)
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0

mret

最后,所有的准备工作完成后,我们就能够正式进入内核态进行各种初始化配置和运行了。