实现异常响应机制

熟悉的读手册环节, 根据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-testhello等没有任何区别, 都是一个直接运行在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就是a7rv32e中为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;
  }
}