1.用户态的syscall

级别 编码 名称
0 00 用户/应用模式 (U, User/Application)
1 01 监督模式 (S, Supervisor)
2 10 虚拟监督模式 (H, Hypervisor)
3 11 机器模式 (M, Machine)
  • riscv的特权级如上,一共四个特权级,特权级越高,对硬件的控制能力越强。
  • opensbi运行在M模式下,从而S模式下可以通过SBI接口控制硬件
  • U模式下可以通过ABI调用S模式的服务

image-20240612151148719

U模式通过ecall调用S模式,并且S模式通过SBI调用M模式硬件的流程如下:

image-20240612151523660

riscv 系统调用简介

syscall 的调用参数和返回值传递通过遵循如下约定实现:

  • 调用参数
    • a7 寄存器存放系统调用号,区分是哪个 Syscall
    • a0-a5 寄存器依次用来表示 Syscall 编程接口中定义的参数
  • 返回值
    • a0 寄存器存放 Syscall 的返回值

ecall 指令会根据当前所处模式触发不同的执行环境切换异常:

  • in U-mode: environment-call-from-U-mode exception
  • in S-mode: environment-call-from-S-mode exception
  • in M-mode: environment-call-from-M-mode exception

Syscall 场景下是在 U-mode(用户模式)下执行 ecall 指令,主要会触发如下变更:

  • 处理器特权级别由 User-mode(用户模式)提升为 Supervisor-mode(内核模式)
  • 当前指令地址保存到 sepc 特权寄存器
  • 设置 scause 特权寄存器
  • 跳转到 stvec 特权寄存器指向的指令地址

具体的测试过程见:实现U模式的trap机制 | TimerのBlog (yanglianoo.github.io)

2.trap机制

**trap机制**:

riscv 架构中,异常,系统调用和中断的过程都被统称为trap

参考:RISC-V 32架构实践专题九(从零开始写操作系统-trap机制)_trap riscv 指令-CSDN博客

什么是ecall

ecall 指令是 RISC-V 指令集架构(ISA)中的一个指令,用于触发环境调用(environment call)。这在不同的特权级别下有不同的用途,但最常见的是用于从用户模式(U-mode)触发系统调用(system call),以请求操作系统内核提供服务。

工作流程:

  1. **用户模式(U-mode)程序执行 ecall**:当用户模式的程序执行 ecall 指令时,CPU 会产生一个陷阱(trap),跳转到内核模式(S-mode)进行处理。
  2. 保存上下文:CPU 保存当前的执行上下文(如程序计数器、寄存器等),以便在处理完系统调用后能够恢复。
  3. 陷入内核:CPU 跳转到内核中预定义的陷阱处理程序(trap handler)。
  4. 处理系统调用:内核根据系统调用号和参数,执行相应的服务,如读写文件、分配内存等。
  5. 恢复上下文:系统调用处理完成后,恢复之前保存的上下文。
  6. 返回用户模式:CPU 返回用户模式,继续执行用户程序。

目标:

  • U模式下 通过ecall指令 调用S模式下 OS
  • OS 对 调用进行处理
  • 处理完毕 返回U模式,并恢复调用时候的上下文继续执行U模式下的程序

注意:

  • 切换前后需要保证程序的上下文不变
  • 通常包含通用寄存器和栈空间

与S模式相关的异常寄存器

Supervisor Status Register (sstatus)

image-20240612155021153

bit[8]:SPP:表示在进入s模式之前正在执行的特权级别,

  • 当接收到trap时,如果该trap来自用户模式,则SPP设置为0,否则设置为1
  • 当执行一条SRET指令从trap处理程序返回时,如果SPP位为0,则特权级别被设置为U模式,如果SPP位为1,则特权级别被设置为S模式

Supervisor Trap Vector Base Address Register (stvec)

image-20240612155330823

stvec寄存器用于设置发生trap时,异常处理程序的地址。

  • MODE 位于 [1:0],长度为 2 bits;
    • 当mode 字段为0时,stvec 被设置为 Direct 模式,此时进入 S 模式的 Trap 无论原因如何,处理 Trap 的入口地址都是 BASE<<2 ,CPU 会跳转到这个地方进行异常处理。
    • 当字段为1时,异常触发后会跳转到以BASE字段对应的异常向量表,每个向量占4个字节。
  • BASE 位于 [63:2],长度为 62 bits。

Supervisor Scratch Register (sscratch)

image-20240612155754188

sscratch寄存器是一个可读/写的辅助寄存器,通常,在hart执行用户代码时,sscratch用于切换上下文的栈。

Supervisor Exception Program Counter (sepc)

image-20240612160219204

sepc记录了 Trap 发生之前执行的最后一条指令的地址

Supervisor Cause Register (scause):star:

image-20240612160323835

这个寄存器记录了S模式下异常发生的原因。

  • 最高位interrupt1的时候表示触发的类型为中断,为0的时候表示异常
  • 其余位表示具体的trap原因Exception Code
Interrupt Exception Code Description
1 0 Reserved
1 1 Supervisor software interrupt
1 2–4 Reserved
1 5 Supervisor timer interrupt
1 6–8 Reserved
1 9 Supervisor external interrupt
1 10–15 Reserved
1 ≥16 Designated for platform use
0 0 Instruction address misaligned
0 1 Instruction access fault
0 2 Illegal instruction
0 3 Breakpoint
0 4 Load address misaligned
0 5 Load access fault
0 6 Store/AMO address misaligned
0 7 Store/AMO access fault
0 8 Environment call from U-mode
0 9 Environment call from S-mode
0 10–11 Reserved
0 12 Instruction page fault
0 13 Load page fault
0 14 Reserved
0 15 Store/AMO page fault
0 16–23 Reserved
0 24–31 Designated for custom use
0 32–47 Reserved
0 48–63 Designated for custom use
0 ≥64 Reserved

特权级切换流程机制

当cpu准备从 U特权级 trap 到 S特权级的时候,会执行如下流程:

  1. sstatusSPP 字段会被修改为 CPU 当前的特权级(U/S)
  2. sepc 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。
  3. scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。
  4. CPU 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。这里会根据scause中保存的异常原因进行分发处理

当CPU完成trap 准备返回到 U特权级的时候,需要通过 一条S特权级的指令 sret来完成:

  1. CPU 会将当前的特权级按照 sstatusSPP 字段设置为 U 或者 S ;
  2. CPU 会跳转到 sepc 寄存器指向的那条指令,然后继续执行。 sepc保存了trap之前待执行的下一条指令

上下文的保存和恢复需要用到栈空间

  • 应用程序通过 ecall 进入到内核状态时,操作系统保存被打断的应用程序的 Trap 上下文;
  • 操作系统根据Trap相关的CSR寄存器内容,完成系统调用服务的分发与处理;
  • 操作系统完成系统调用服务后,需要恢复被打断的应用程序的Trap 上下文,并通 sret 让应用程序继续执行。

3.trap机制实现

OS/types.h

实现一个数据类型的定义,用typedef定义了一个reg_t的类型用于定义使用的寄存器

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __TYPES_H__
#define __TYPES_H__
// 定义无符号整型
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
/*
* RISCV64: 寄存器的大小是64位的
*/
typedef uint64_t reg_t;
#endif

OS/riscv.h

定义了一些获取寄存器值的函数

需要在os.h 中 加入 types.h的头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#ifndef __RISCV_H__
#define __RISCV_H__

#include "os.h"
/* 读取 sepc 寄存器的值 */
static inline reg_t r_sepc()
{
reg_t x;
asm volatile("csrr %0, sepc" : "=r" (x) );
return x;
}
/* scause 记录了异常原因 */
static inline reg_t r_scause()
{
reg_t x;
asm volatile("csrr %0, scause" : "=r" (x) );
return x;
}
// stval 记录了trap发生时的地址
static inline reg_t r_stval()
{
reg_t x;
asm volatile("csrr %0, stval" : "=r" (x) );
return x;
}
/* sstatus记录S模式下处理器内核的运行状态*/
static inline reg_t r_sstatus()
{
reg_t x;
asm volatile("csrr %0, sstatus" : "=r" (x) );
return x;
}
static inline void w_sstatus(reg_t x)
{
asm volatile("csrw sstatus, %0" : : "r" (x));
}

/* stvec寄存器 */
static inline void w_stvec(reg_t x)
{
asm volatile("csrw stvec, %0" : : "r" (x));
}
static inline reg_t r_stvec()
{
reg_t x;
asm volatile("csrr %0, stvec" : "=r" (x) );
return x;
}
#endif

OS/batch.c

在这个文件中 定义内核栈和用户栈,内核栈可以用来保存 trap情况下的用户程序上下文

KernelStackUserStack的大小被定义为8kb。

1
2
3
4
5
6
7
8
#include <stddef.h>
#include "os.h"

#define USER_STACK_SIZE (4096 * 2)
#define KERNEL_STACK_SIZE (4096 * 2)

uint8_t KernelStack[KERNEL_STACK_SIZE];
uint8_t UserStack[USER_STACK_SIZE];

OS/context.h

用来保存上下文寄存器信息,Trap上下文执行流的数据就是寄存器中的数据,有x0~x31总共32个通用寄存器以及sstatussepc等控制寄存器需要保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef __CONTEXT_H__
#define __CONTEXT_H__
#include "os.h"
/*S模式的trap上下文*/
typedef struct pt_regs {
reg_t x0;
reg_t ra;
reg_t sp;
reg_t gp;
reg_t tp;
reg_t t0;
reg_t t1;
reg_t t2;
reg_t s0;
reg_t s1;
reg_t a0;
reg_t a1;
reg_t a2;
reg_t a3;
reg_t a4;
reg_t a5;
reg_t a6;
reg_t a7;
reg_t s2;
reg_t s3;
reg_t s4;
reg_t s5;
reg_t s6;
reg_t s7;
reg_t s8;
reg_t s9;
reg_t s10;
reg_t s11;
reg_t t3;
reg_t t4;
reg_t t5;
reg_t t6;
/* S模式下的寄存器 */
reg_t sstatus;
reg_t sepc;
}pt_regs;
#endif

OS/kerneltrap.S

汇编文件中定义了两个函数::__alltraps 、__restore

用于对trap的上下文进行保存和恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# __alltraps 函数:
.globl __alltraps
.align 4 #4*4 = 16字节对齐
__alltraps:
# 从sscratch获取S模式下的SP,把U模式下的SP保存到sscratch寄存器中
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
sd x4, 4*8(sp) #sd xN, N*8(sp):将通用寄存器 xN 的值存储到堆栈上 sp + N*8 的位置。
sd x5, 5*8(sp)
sd x6, 6*8(sp)
sd x7, 7*8(sp)
sd x8, 8*8(sp)
sd x9, 9*8(sp)
sd x10,10*8(sp)
sd x11, 11*8(sp)
sd x12, 12*8(sp)
sd x13, 13*8(sp)
sd x14, 14*8(sp)
sd x15, 15*8(sp)
sd x16, 16*8(sp)
sd x17, 17*8(sp)
sd x18, 18*8(sp)
sd x19, 19*8(sp)
sd x20, 20*8(sp)
sd x21, 21*8(sp)
sd x22, 22*8(sp)
sd x23, 23*8(sp)
sd x24, 24*8(sp)
sd x25, 25*8(sp)
sd x26, 26*8(sp)
sd x27, 27*8(sp)
sd x28, 28*8(sp)
sd x29, 29*8(sp)
sd x30, 30*8(sp)
sd x31, 31*8(sp)

# 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(TrapContext)
mv a0, sp
call trap_handler

__alltraps 函数就是发生异常时的处理函数,在此函数中:

  • csrrw sp, sscratch, sp中将sscratchsp的值进行了交换,
    • 在进入此函数之前sp指向的是用户栈,sscratch中的值保存的是内核栈的栈顶。
    • 进行交换后,由于此时进入了S态,所以需要切换栈,由此就切换到了内核栈。
  • 然后就是将寄存器的值保存进内核栈中,在context.h上下文的定义可以看见pt_regs中定义了34个寄存器,所以通过addi sp, sp, -34*8指令来压栈,然后依次保存寄存器的值
  • 最后两行将内核栈的sp保存进a0寄存器用于传参,所以将用户态寄存器保存进内核栈后,调用了trap_handler函数,在此函数中可通过a0传入的参数访问内核栈中储存的寄存器的值。

__restore函数:将内核栈中的存储的寄存器的值恢复,然后通过sret指令返回从S态到用户态继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
.globl __restore
.align 4
__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)
ld x4, 4*8(sp)
ld x5, 5*8(sp)
ld x6, 6*8(sp)
ld x7, 7*8(sp)
ld x8, 8*8(sp)
ld x9, 9*8(sp)
ld x10,10*8(sp)
ld x11, 11*8(sp)
ld x12, 12*8(sp)
ld x13, 13*8(sp)
ld x14, 14*8(sp)
ld x15, 15*8(sp)
ld x16, 16*8(sp)
ld x17, 17*8(sp)
ld x18, 18*8(sp)
ld x19, 19*8(sp)
ld x20, 20*8(sp)
ld x21, 21*8(sp)
ld x22, 22*8(sp)
ld x23, 23*8(sp)
ld x24, 24*8(sp)
ld x25, 25*8(sp)
ld x26, 26*8(sp)
ld x27, 27*8(sp)
ld x28, 28*8(sp)
ld x29, 29*8(sp)
ld x30, 30*8(sp)
ld x31, 31*8(sp)

# release TrapContext on kernel stack
addi sp, sp, 34*8
# now sp->kernel stack, sscratch->user stack 交换内核态和用户态的指针
csrrw sp, sscratch, sp
# now sp->user stack, sscratch->kernel stack 返回
sret
  • __restore函数的定义为__restore(pt_regs *next),所以在第一行传入内核栈地址,然后将内核栈中存放的寄存器的值恢复,然后切换sp,最后通过sret返回用户态继续执行
  • 在最后两行会将sp指向用户栈,sscratch指向内核栈

4.测试

OS/trap.c

在汇编文件中的__alltraps中调用了中断处理函数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
28
29
30
31
#include "os.h"
#include "context.h"
#include "riscv.h"

extern void __alltraps(void);

pt_regs* trap_handler(pt_regs* cx)
{
reg_t scause = r_scause() ;
printf("cause:%x\n",scause);
printf("a0:%x\n",cx->a0);
printf("a1:%x\n",cx->a1);
printf("a2:%x\n",cx->a2);
printf("a7:%x\n",cx->a7);
printf("sepc:%x\n",cx->sepc);
printf("sstatus:%x\n",cx->sstatus);
printf("sp:%x\n",cx->sp);
while (1)
{
}
return cx;
}


void trap_init()
{
/*
* 设置 trap 时调用函数的基地址
*/
w_stvec((reg_t)__alltraps);
}
  • pt_regs* cx 是一个指向 pt_regs 结构体的指针,包含了所有保存的寄存器值(上下文)
  • r_scause() 函数读取 scause 寄存器,获取 Trap 的原因。
  • trap_init 函数用于初始化 Trap 机制,设置 Trap 向量基地址为 __alltraps

OS/batch.c

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stddef.h>
#include "os.h"
#include "context.h"
#define USER_STACK_SIZE (4096 * 2)
#define KERNEL_STACK_SIZE (4096 * 2)
#define APP_BASE_ADDRESS 0x80600000


size_t syscall(size_t id, reg_t arg1, reg_t arg2, reg_t arg3) {
long ret;
asm volatile (
"mv a7, %1\n\t" // Move syscall id to a0 register
"mv a0, %2\n\t" // Move args[0] to a1 register
"mv a1, %3\n\t" // Move args[1] to a2 register
"mv a2, %4\n\t" // Move args[2] to a3 register
"ecall\n\t" // Perform syscall
"mv %0, a0" // Move return value to 'ret' variable
: "=r" (ret)
: "r" (id), "r" (arg1), "r" (arg2), "r" (arg3)
: "a7", "a0", "a1", "a2", "memory"
);
return ret;
}

void testsys() {

// //int len = strlen(message);
//reg_t sstatus = r_sstatus();
//printf("sstatus:%x\n", sstatus);
int ret = syscall(2,3,4,5);
// while (1)
// {
// /* code */
// }

//printf("ret:%d\n",ret);
}


uint8_t KernelStack[KERNEL_STACK_SIZE];
uint8_t UserStack[USER_STACK_SIZE]={0};

extern void __restore(pt_regs *next);

struct pt_regs tasks;
void app_init_context()
{

reg_t user_sp = &UserStack + USER_STACK_SIZE;
printf("user_sp:%p\n", user_sp);

reg_t stvec = r_stvec();
printf("stvec:%x\n", stvec);


trap_init();

reg_t sstatus = r_sstatus();
// 设置 sstatus 寄存器第8位即SPP位为0 表示为U模式
sstatus &= (0U << 8);
w_sstatus(sstatus);
printf("sstatus:%x\n", sstatus);


tasks.sepc = (reg_t)testsys;
printf("tasks sepc:%x\n", tasks.sepc);

tasks.sstatus = sstatus;

tasks.sp = user_sp;



pt_regs* cx_ptr = &KernelStack[0] + KERNEL_STACK_SIZE - sizeof(pt_regs);
printf("pt_regs: %d\n",sizeof(pt_regs));
cx_ptr->sepc = tasks.sepc;
printf("cx_ptr sepc :%x\n", cx_ptr->sepc);
printf("cx_ptr sepc adress:%x\n", &(cx_ptr->sepc));
cx_ptr->sstatus = tasks.sstatus;
cx_ptr->sp = tasks.sp;
// *cx_ptr = tasks[0];
printf("cx_ptr adress:%x\n", cx_ptr);

__restore(cx_ptr);

}

解释:

1
2
3
4
5
6
7
8
#include <stddef.h>
#include "os.h"
#include "context.h"

#define USER_STACK_SIZE (4096 * 2)
#define KERNEL_STACK_SIZE (4096 * 2)
#define APP_BASE_ADDRESS 0x80600000

  • 包含了必要的头文件
  • 定义了用户堆栈和内核堆栈的大小 4k * 2 = 8k
  • 定义了应用程序的起始地址 0x80600000

系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
size_t syscall(size_t id, reg_t arg1, reg_t arg2, reg_t arg3) {
long ret;
asm volatile (
"mv a7, %1\n\t" // 将系统调用号移动到 a7 寄存器
"mv a0, %2\n\t" // 将第一个参数移动到 a0 寄存器
"mv a1, %3\n\t" // 将第二个参数移动到 a1 寄存器
"mv a2, %4\n\t" // 将第三个参数移动到 a2 寄存器
"ecall\n\t" // 执行系统调用
"mv %0, a0" // 将返回值移动到 'ret' 变量
: "=r" (ret)
: "r" (id), "r" (arg1), "r" (arg2), "r" (arg3)
: "a7", "a0", "a1", "a2", "memory"
);
return ret;
}

  • 该函数执行一个系统调用,传递系统调用号和三个参数。
  • 使用内联汇编将参数传递给适当的寄存器,并执行 ecall 指令。 — 执行__alltrap保存上下文 ,然后调用handler 函数处理ecall ,最后恢复
1
2
3
4
void testsys() {
int ret = syscall(2, 3, 4, 5);
// printf("ret:%d\n", ret);
}
  • 定义了测试函数
1
2
3
4
5
6
7
uint8_t KernelStack[KERNEL_STACK_SIZE];
uint8_t UserStack[USER_STACK_SIZE] = {0};

extern void __restore(pt_regs *next);

struct pt_regs tasks;

  • 定义了用户和内核堆栈
  • 声明了外部的汇编函数restore,用于恢复上下文
  • 定义一个 pt_regs 结构体实例 tasks,用于保存任务上下文。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void app_init_context() {
reg_t user_sp = (reg_t)(&UserStack) + USER_STACK_SIZE;
printf("user_sp:%p\n", user_sp);

reg_t stvec = r_stvec();
printf("stvec:%x\n", stvec);

trap_init();

reg_t sstatus = r_sstatus();
// 设置 sstatus 寄存器第 8 位(SPP 位)为 0,表示 U 模式
sstatus &= ~(1U << 8);
w_sstatus(sstatus);
printf("sstatus:%x\n", sstatus);

tasks.sepc = (reg_t)testsys;
printf("tasks sepc:%x\n", tasks.sepc);

tasks.sstatus = sstatus;
tasks.sp = user_sp;

pt_regs* cx_ptr = (pt_regs*)((uint8_t*)&KernelStack[0] + KERNEL_STACK_SIZE - sizeof(pt_regs));
printf("pt_regs: %d\n", sizeof(pt_regs));
cx_ptr->sepc = tasks.sepc;
printf("cx_ptr sepc: %x\n", cx_ptr->sepc);
printf("cx_ptr sepc address: %p\n", &(cx_ptr->sepc));
cx_ptr->sstatus = tasks.sstatus;
cx_ptr->sp = tasks.sp;
printf("cx_ptr address: %p\n", cx_ptr);

__restore(cx_ptr);
}

目的:通过app_init...初始化用户态程序的上下文,并最后通过调用 __restore(cx_ptr) 将控制权移交给用户态程序, 最后在用户态程序中才是真的ecall

  • reg_t user_sp初始化用户堆栈指针,并打印出来
    • 因为栈是从高地址往低地址向下增长的,所以用户栈的地址为&UserStack + USER_STACK_SIZE
  • 然后调用trap_init函数来设置stvec寄存器的值为__alltraps,这里告诉cpu发生trap时去哪里执行
    • stvec寄存器用于设置发生trap时,异常处理程序的地址。
  • 然后设置sstatus寄存器的SPP位为0。这是为啥呢?在上面对寄存器的介绍中提到“当执行一条SRET指令从trap处理程序返回时,如果SPP位为0,则特权级别被设置为U模式,如果SPP位为1,则特权级别被设置为S模式;”所以我们为了从S模式返回用户模式去执行testsys()中的代码,我们需要将SPP位设置为0。
  • 然后就是构造一段内核栈,设置sstatussepcsp的值,这里由于下一阶段为用户模式,所以sepc会设置成用户态程序的地址(testsys),sp设置为用户栈的地址。
  • 最后恢复上下文,返回用户态执行

OS/os.h

还需要添加头文件以及函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef __OS_H__
#define __OS_H__

#include <stddef.h>
#include <stdarg.h>
#include "types.h"
#include "context.h"
#include "riscv.h"

/* printk */
extern int printk(const char* s, ...);
extern void panic(char *s);
extern void sbi_console_putchar(int ch);

/* batch.c */
extern void app_init_context();

/* trap.c */
extern void trap_init();

#endif /* __OS_H__ */

OS/main.c

添加打印测试的U模式下函数

1
2
3
4
5
6
7
8
9
10
11
//extern sbi_console_putchar(int ch);
#include "os.h"

void os_main()
{
printk("hello!!!\n");
app_init_context();
while(1) {

}
}

OS/Makefle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

CROSS_COMPILE = riscv64-unknown-elf-
CFLAGS = -nostdlib -fno-builtin -I/opt/riscv/riscv64-unknown-elf/include -mcmodel=medany

# riscv64-unknown-elf-gcc 工具链可以同时编译汇编和 C 代码
CC = ${CROSS_COMPILE}gcc
OBJCOPY = ${CROSS_COMPILE}objcopy
OBJDUMP = ${CROSS_COMPILE}objdump

SRCS_ASM = \
entry.S \
kerneltrap \

SRCS_C = \
sbi.c \
main.c \
printk.c \
batch.c \
trap.c \

# 将源文件替换为 .o 文件
OBJS = $(SRCS_ASM:.S=.o)
OBJS += $(SRCS_C:.c=.o)


os.elf: ${OBJS}
${CC} ${CFLAGS} -T os.ld -Wl,-Map=os.map -o os.elf $^
${OBJCOPY} -O binary os.elf os.bin

%.o : %.c
${CC} ${CFLAGS} -c -o $@ $<

%.o : %.S
${CC} ${CFLAGS} -c -o $@ $<


.PHONY : clean
clean:
rm -rf *.o *.bin *.elf

测试结果

image-20240613211217244

可以看到 cause的值为8,查看scause 8表示 U模式的下的ecall

trap 处理的总体流程

  1. 首先通过 __alltraps 将 Trap 上下文保存在内核栈上,
  2. 然后跳转到编写的 trap_handler 函数完成 Trap 处理。
  3. trap_handler 返回之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。
  4. 最后通过一条 sret 指令回到应用程序执行。

本节添加函数逻辑:star:

  • S模式:首先在app_init_context()这个函数中,初始化用户态栈指针,初始化trap处理程序,初始化内核栈,最后通过__restore(cx_ptr);恢复到用户态(testsys)执行

    • trap_init中 初始化了 __alltrap汇编函数 这个函数用于保存上下文信息 以及调用 trap_handler对trap进行处理
  • U模式:用户态程序 testsys执行,函数调用了syscall函数

  • U模式:syscall函数,对寄存器进行操作,并且通过内联汇编的方式调用ecall,使得操作系统陷入了trap

  • S模式:trap中,首先调用了__alltrap对上下文进行了保存,并调用trap_handler对trap进行处理(也是打印相关的寄存器信息)

  • S模式:最后处理完 会调用__restore函数