实现异常响应机制
熟悉的读手册环节, 根据NEMU的要求, 我们需要添加三个CSR寄存器:
// src/isa/riscv32/include/isa-def.h
typedef struct {
vaddr_t mepc;
word_t mcause;
word_t mstatus;
word_t mtvec;
} CSR;
typedef struct {
word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
vaddr_t pc;
CSR csr; // csr
uint8_t mode; // riscv特权级状态
} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);
根据官方手册, 每当程序调用ecall
时, cpu
会将当前pc
保存在mepc
中, 异常原因写入mcause
, 并修改mstatus
, 最后跳转到mtvec
保存的地址.
添加如下代码, 并添加指令ecall
, 在识别到指令ecall
时执行:
// src/isa/riscv32/inst.c
#define ECALL switch (MODE) { \
case MACHINE_MODE_U: s->dnpc = isa_raise_intr(0x08, s->pc); break; \
case MACHINE_MODE_S: s->dnpc = isa_raise_intr(0x09, s->pc); break; \
case MACHINE_MODE_M: s->dnpc = isa_raise_intr(0x0B, s->pc); break; \
}
#define CSR(i) *get_csr_register(i)
static vaddr_t *get_csr_register(word_t imm) {
switch (imm)
{
case CSR_MEPC: return &(cpu.csr.mepc);
case CSR_MCAUSE: return &(cpu.csr.mcause);
case CSR_MSTATUS: return &(cpu.csr.mstatus);
case CSR_MTVEC: return &(cpu.csr.mtvec);
default: panic("Error CSR!");
}
}
INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall , I, ECALL);
INSTPAT("??????? ????? ????? 001 ????? 11100 11", csrrw , I, R(rd) = CSR(imm); CSR(imm) = src1);
INSTPAT("??????? ????? ????? 010 ????? 11100 11", csrrs , I, R(rd) = CSR(imm); CSR(imm) |= src1);
INSTPAT("0011000 00010 00000 000 00000 11100 11", mret , N, s->dnpc = CSR(CSR_MEPC); MRET);
// src/isa/riscv32/system/intr.c
word_t isa_raise_intr(word_t NO, vaddr_t epc) {
cpu.csr.mepc = epc;
cpu.csr.mcause = NO;
cpu.csr.mstatus = (cpu.csr.mstatus & ~(1 << 7)) | ((cpu.csr.mstatus & (1 << 3)) << 4); // MIE -> MPIE
cpu.csr.mstatus &= ~(1 << 3); // 0 -> MIE
cpu.csr.mstatus &= ~((1 << 11) + (1 << 12)); // 00 -> MPP
cpu.csr.mstatus |= cpu.mode << 11; // mode -> MPP
cpu.mode = 0x03;
return cpu.csr.mtvec;
}
让DiffTest支持异常响应机制
重新组织Context结构体
照着任务说的做就行, trap.S
中压栈顺序与Context
中的顺序对应.
PS: 汇编里的宏看不懂可以用编译器展开后看.
struct Context {
uintptr_t gpr[NR_REGS], mcause, mstatus, mepc;
void *pdir;
};
/*
对应tarp.S中这几行
MAP(REGS, PUSH) // gpr[NR_REGS]
STORE t0, OFFSET_CAUSE(sp) // mcause
STORE t1, OFFSET_STATUS(sp) // mstatus
STORE t2, OFFSET_EPC(sp) // mepc
*/
理解上下文结构体的前世今生
根据tarp.S
的内容看, 可以发现在进入__am_irq_handle
之前, 会把栈指针保存到a0
寄存器:
# tarp.S
...
mv a0, sp
call __am_irq_handle
...
a0
寄存器在函数调用中是作为函数的第一个参数来传递的, 也就是说__am_irq_handle
中的Context *c
指向的位置就是栈地址.
我们在程序中将其视为Context
结构体指针, 那么我们在读取c
指向的内容时, 实际上就是在使用我们trap.S
中压栈的寄存器数据.
识别自陷事件
要识别并正确分发自陷, 首先得知道yield test
都干了什么.
程序在完成配置后会定时执行yield();
, 对应找到原函数就能看到:
void yield() {
#ifdef __riscv_e
asm volatile("li a5, -1; ecall");
#else
asm volatile("li a7, -1; ecall");
#endif
}
将-1
存入a7
寄存器后(rv32e
最高只有a5
), 触发ecall
.
也就是说a7 == -1
是我们识别的条件, 我们只需要在CPU进入自陷处理函数后加入判断即可.
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause) {
case ET_ECALL_FROM_MMODE: // 0x0B
if (xxx) { // 判断为xxx事件
ev.event = EVENT_xxx;
} else if (xxx) {
ev.event = xxx;
}
default: ev.event = EVENT_ERROR; break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
return c;
}
恢复上下文
这个算是卡住我的一个地方, 当时脑子没有转过来, 想的是我没有修改任何寄存器, 为什么还要恢复?
后续才反应到, 所有寄存器的值确实都在栈中, 没有修改.
但是执行完自陷处理函数后寄存器内的值与栈中的肯定是不同的.
所以需要从栈中读取数据后一个个存入寄存器, 也就是恢复上下文.
很简单的能想到, 恢复的过程肯定是在tarp.S
中.
从__am_irq_handle
返回后, 我们很难保证所有的寄存器都保留之前的数据, 甚至是我们存储的栈指针.
但是a0
比较特殊, 在手册中规定其用于传递返回值, 对应__am_irq_handle
中即可知道, 返回时a0
原封不动的将传入的栈指针传出.
那么我们只需要从中得到地址, 并逐步读取内容存入寄存器就能完成任务了.
**PS: 就算完全不知道怎么做, 仔细观察tarp.S
也会发现代码基本上是上下对称的, 找出那个没有对称语句的地方, 补上代码就能完成任务. **
为Nanos-lite实现正确的事件分发
Nanos-lite
本质上与之前的cpu-test
、hello
等没有任何区别, 都是一个直接运行在NEMU上的程序.
我们基本上能直接参考yield test
的行为来理解Nanos-lite
.
在识别自陷时我们已经将ev.event
赋值为对应的类型值了, 那么找到Nanos-lite
处理事件的函数即可:
static Context* do_event(Event e, Context* c) {
switch (e.event) {
case EVENT_xxx: Log("EVENT_YIELD"); break;
case EVENT_xxx: xxx; break;
default: panic("Unhandled event ID = %d", e.event);
}
return c;
}
由于识别yield
已经在__am_irq_handle
完成, 那么只需要从e.event
得到类型并作出对应操作即可.
实现loader
文档中讲解的很清楚, 在NEMU的ftrace
中也接触了elf
的解析.
这里我直接给出我的实现.
提示
fs_
前缀的函数是复用后面章节的功能(简易文件系统), 但不影响理解, 使用ramdisk_read
替代即可.
static uintptr_t loader(PCB *pcb, const char *filename) {
Elf_Ehdr ehdr; // 定义一个Elf文件头结构体变量
int fd = fs_open(filename, 0, 0); // 打开文件, 返回文件描述符
fs_lseek(fd, 0, SEEK_SET); // 将文件指针移动到文件开头
fs_read(fd, &ehdr, sizeof(Elf_Ehdr)); // 读取文件头信息到ehdr结构体
assert(!strncmp((char*)ehdr.e_ident, ELFMAG, SELFMAG)); // 检查文件是否为ELF格式
Elf_Phdr phdr[ehdr.e_phnum]; // 定义一个Elf程序头结构体数组, 大小为e_phnum
fs_lseek(fd, ehdr.e_phoff, SEEK_SET); // 将文件指针移动到程序头表的偏移位置
fs_read(fd, phdr, sizeof(Elf_Phdr)*ehdr.e_phnum); // 读取程序头表信息到phdr数组
for (int i = 0; i < ehdr.e_phnum; i++) { // 遍历程序头表
if (phdr[i].p_type == PT_LOAD) { // 检查程序头类型是否为可加载段
fs_lseek(fd, phdr[i].p_offset, SEEK_SET); // 将文件指针移动到段的偏移位置
fs_read(fd, (void*)phdr[i].p_vaddr, phdr[i].p_filesz); // 读取段内容到虚拟地址
memset((void*)(phdr[i].p_vaddr + phdr[i].p_filesz), 0, phdr[i].p_memsz - phdr[i].p_filesz); // 将段剩余部分清零
}
}
return ehdr.e_entry; // 返回程序入口地址
}
识别系统调用
添加分发, 与上文一模一样.
至于分发后干什么目前不知道.
不过我们已知, dummy
做了一件事, 即_syscall_(SYS_yield, 0, 0, 0)
, 查看函数内部发现, 只是将参数一个一个塞进寄存器后调用ecall
.
intptr_t _syscall_(intptr_t type, intptr_t a0, intptr_t a1, intptr_t a2) {
register intptr_t _gpr1 asm (GPR1) = type;
register intptr_t _gpr2 asm (GPR2) = a0;
register intptr_t _gpr3 asm (GPR3) = a1;
register intptr_t _gpr4 asm (GPR4) = a2;
register intptr_t ret asm (GPRx);
asm volatile (SYSCALL : "=r" (ret) : "r"(_gpr1), "r"(_gpr2), "r"(_gpr3), "r"(_gpr4));
return ret;
}
那么, 我们需要做的不就是在nanos
的处理函数中, 识别出传入_syscall_
的type
吗.
根据navy-apps/libs/libos/src/syscall.c
中的宏可以知道, GPR1
就是a7
(rv32e
中为a5
).
所以读取上下文中的a7
即可完成判断调用类型的任务.
在nanos-lite/src/syscall.c
已经写好了框架, 在do_event
中调用即可.
EVENT_YIELD与SYS_yield
~~这应该算是埋下的一个坑. ~~
EVENT_YIELD
指的自陷类型, 与EVENT_SYSCALL
一样, 代表着不同的自陷, 对应着不同的处理方式.
SYS_yield
是一种系统调用, 算是属于EVENT_SYSCALL
中的一部分, 当__am_irq_handle
确定本次自陷用于系统调用时, 就会将其分配到EVENT_SYSCALL
, 再由操作系统来完成名为yield
的系统调用.
实现SYS_yield / SYS_exit系统调用
简单, 不解释, 代码如下:
void do_syscall(Context *c) {
uintptr_t a[4];
a[0] = c->GPR1;
a[1] = c->GPR2;
a[2] = c->GPR3;
a[3] = c->GPR4;
switch (a[0]) {
case SYS_exit: c->GPRx = 0; halt(a[1]); break;
case SYS_yield: c->GPRx = 0; Log("SYS_yield"); break;
case SYS_xxx: c->GPRx = xxx; xxx; break;
default: panic("Unhandled syscall ID = %d", a[0]); break;
}
}
不知道GPR
是什么的话记得回看文档.
提示
完成后也记得修改navy-apps/libs/libos/src/syscall.c
中的参数与行为, 不然什么调用都是死循环.
在Nanos-lite上运行Hello world
这里只需要知道write
调用的输入与返回值分别是什么意思即可完成.
难点可能是不知道如何让系统调用拥有返回值, 不知道的记得看RISC-V手册, 上文也有提示.
实现堆区管理
文档写的比我清楚, 我就不在这丢人了, 贴个代码跑了:
// navy-apps/libs/libos/src/syscall.c
extern char _end;
void *_sbrk(intptr_t increment) {
static char* heap_end = NULL;
char* previous_heap_end;
if (heap_end == NULL) {
heap_end = (char*) &_end;
}
int ans = (_syscall_(SYS_brk, increment, 0, 0));
if (ans == 0) {
previous_heap_end = heap_end;
heap_end += increment;
} else {
previous_heap_end = (void *)-1;
}
return (void*) previous_heap_end;
}
// nanos-lite/src/syscall.c
intptr_t mybrk(uintptr_t increment) {
return 0;
}
void do_syscall(Context *c) {
uintptr_t a[4];
a[0] = c->GPR1;
a[1] = c->GPR2;
a[2] = c->GPR3;
a[3] = c->GPR4;
switch (a[0]) {
case SYS_exit: c->GPRx = 0; halt(a[1]); break;
case SYS_yield: c->GPRx = 0; Log("SYS_yield"); break;
case SYS_brk: c->GPRx = mybrk(a[1]); break;
case SYS_xxx: c->GPRx = xxx; xxx; break;
default: panic("Unhandled syscall ID = %d", a[0]); break;
}
}