yield test
yield test
从yield test
调用yield()
开始, 到从yield()
返回的期间, 这一趟旅程具体经历了什么?
准备工作
在调用自陷操作前,CTE已经做好了初始化CTE环境,设置好CTE的异常处理程序__am_asm_trap
地址,同时注册特定的事件处理函数simple_trap
.
CTE(simple_trap)
static Context* (*user_handler)(Event, Context*) = NULL;
bool cte_init(Context*(*handler)(Event, Context*)) {
// initialize exception entry
asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap));
// register event handler
user_handler = handler;
return true;
}
应用程序调用yield()
hello_intr
调用函数yield()
void hello_intr() {
printf("Hello, AM World @ " __ISA__ "\n");
printf(" t = timer, d = device, y = yield\n");
io_read(AM_INPUT_CONFIG);
iset(1);
while (1) {
for (volatile int i = 0; i < 1000000; i++) ;
yield();
}
}
yield的实现
void yield() {
asm volatile("li a7, -1; ecall");
}
- 将立即数
0xFFFFFFFF
赋值给寄存器a7
- 调用自陷指令
ecall
使用a7
寄存器的原因:程序的许多事件都会使用自陷指令ecall
,所以CTE使用寄存器a7
的值来区分不同的事件。a7
的用途是由 CTE(Context Extension)和操作系统(AM)约定的,因此在执行 ecall
之前,用户程序会将系统调用号放入 a7
,供操作系统在处理时读取。
硬件
译码自陷指令 ecall
时,译码器的操作为:
- 将异常号和当前PC传递给异常响应硬件处理
- 跳转到异常入口地址
在 RISC-V 架构中,ecall
指令的异常号是由硬件定义的。而此异常号会被控制状态寄存器(CSR)mcause
寄存器保存。这里我们需要在riscv32的NEMU中加入CSR寄存器的相关支持(在nemu/src/isa/riscv32/include/isa-def.h
中定义)
// 用于控制和监控 CPU 状态的特殊寄存器
typedef struct control_and_status_registers {
word_t mtvec; // 异常入口地址
word_t mepc; // 触发异常的PC
word_t mstatus;// 处理器的状态
word_t mcause; // 触发异常的原因
}CSRs;
// 常用 CSR 地址
typedef enum {
// Machine Trap Setup
CSR_MSTATUS = 0x300, // mstatus
CSR_MTVEC = 0x305, // mtvec
// Machine Trap Handling
CSR_MEPC = 0x341, // mepc
CSR_MCAUSE = 0x342, // mcause
}csr_id;
typedef struct {
word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
vaddr_t pc;
CSRs csrs;
} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);
对于其CSR的读写功能,则放到寄存器功能模块中实现(在nemu/src/isa/riscv32/local-include/reg.h
中定义)
word_t get_csr_val_by_id(int csr_id);
void set_csr_val_by_id(int csr_id, word_t val);
#define read_csrs(idx) (get_csr_val_by_id(idx))
#define write_csrs(idx, val) (set_csr_val_by_id(idx, val))
将 RISC-V 机器中的手册中mcause
中保存的异常号定义为枚举类(在nemu/src/isa/riscv32/include/isa-def.h
中定义):
typedef enum {
//mcause 的最高位在发生中断时置 1,发生同步异常时置 0
INSTRUCTION_ADDRESS_MISALIGNED = 0, // 指令地址未对齐
INSTRUCTION_ACCESS_FAULT = 1, // 指令访问故障
ILLEGAL_INSTRUCTION = 2, // 非法指令
BREAKPOINT = 3, // 断点
LOAD_ADDRESS_MISALIGNED = 4, // 加载地址未对齐
LOAD_ACCESS_FAULT = 5, // 加载访问故障
STORE_ADDRESS_MISALIGNED = 6, // 存储地址未对齐
STORE_ACCESS_FAULT = 7, // 存储访问故障
ENVIRONMENT_CALL_FROM_U_MODE = 8, // 用户模式的环境调用
ENVIRONMENT_CALL_FROM_S_MODE = 9, // 管理模式的环境调用(若存在)
ENVIRONMENT_CALL_FROM_M_MODE = 11, // 机器模式的环境调用
INSTRUCTION_PAGE_FAULT = 12, // 指令页面故障
LOAD_PAGE_FAULT = 13, // 加载页面故障
STORE_PAGE_FAULT = 15, // 存储页面故障
// 中断的最高位为1,这里使用更大的数值表示
INTERRUPT_MACHINE_TIMER = 0x80000007, // 机器定时器中断
INTERRUPT_MACHINE_EXTERNAL = 0x8000000b // 机器外部中断
} RiscV_ExceptionCode;
因为PA不涉及特权级的切换, 这里不需要关心和特权级切换相关的内容,所以自陷指令ecall
的异常号设置为ENVIRONMENT_CALL_FROM_U_MODE
(8)即可。
随后译码器调用硬件的异常处理模块,并将自陷指令的异常码和当前PC作为参数,进行异常响应。这里模拟硬件的异常响应机制的函数为isa_raise_intr()
:
/** 模拟硬件异常响应机制*/
word_t isa_raise_intr(word_t NO, vaddr_t epc) {
// 将当前PC值保存到mepc寄存器
cpu.csrs.mepc = epc;
// 在mcause寄存器中设置异常号
cpu.csrs.mcause = NO;
// 从mtvec寄存器中取出异常入口地址
return cpu.csrs.mtvec;
}
这样译码ecall
的操作为
#define ECALL(dnpc) { dnpc = isa_raise_intr(ENVIRONMENT_CALL_FROM_U_MODE, s->pc);}
INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall , N, ECALL(s->dnpc));
译码结束后,此时PC指向异常处理程序__am_asm_trap
的入口地址
操作系统
成功跳转到异常入口地址之后, 我们就要在软件上开始真正的异常处理过程了.异常处理过程包括:
- 保存上下文(程序的状态)
- 事件分发(按照异常号分发事件和按照事件分发具体事件处理)
- 恢复上下文
异常处理的时候,首先会将程序异常状态的上下文保存起来。CTE定义通用的上下文包括:
- 通用寄存器
- 触发异常时的PC和处理器状态
- 异常号
- 地址空间
看代码的操作可以得知,CTE从栈顶sp
位置从近到远依次保存了:
- 通用寄存器:32个通用寄存器(不包含0号和2号寄存器)
- mcause
- mstatus
- mepc
地址空间去哪了?栈申请了36个空间保存上下文成员,此时栈底sp+35
还存在一个空闲位置,应该就是存放地址空间的位置了。
中途用到的CSR读写指令为
INSTPAT("??????? ????? ????? 001 ????? 11100 11", csrrw , I, R(rd) = read_csrs(imm); write_csrs(imm, src1));
INSTPAT("??????? ????? ????? 010 ????? 11100 11", csrrs , I, R(rd) = read_csrs(imm); word_t val = read_csrs(imm) | src1; write_csrs(imm,val));
分析异常事件处理函数__am_irq_handle()
,其入参为一个指向上下文结构体Context
的指针。而栈sp
就是保存着结构体Context
的指针,所以上下文保存完毕后,将sp
作为参数,调用函数__am_irq_handle()
。
在CTE中,按照栈保存上下文的顺序,可以得出结构体Context
的顺序:
struct Context {
uintptr_t gpr[NR_REGS], mcause, mstatus, mepc;
void *pdir;
};
保存上下文结束后,调用的异常处理函数__am_irq_handle()
进行异常号的分发。此操作会将执行流切换的原因打包成事件, 然后调用在cte_init()
中注册的事件处理回调函数(simple_trap()
), 将事件交给yield test
来处理.
这时候需要__am_irq_handle()
通过异常号识别出自陷异常,并打包成编号为EVENT_YIELD
的自陷事件。要想在AM上识别RISCV32硬件的异常号,还需要再从AM上定义一份mcause
的异常号(在abstract-machine/am/src/riscv/riscv.h
中定义)。实现后的__am_irq_handle()
:
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause)
{
case ENVIRONMENT_CALL_FROM_U_MODE: ev.event = EVENT_YIELD; break;
default: ev.event = EVENT_YIELD; break;
}
c = user_handler(ev, c); // simple_trap
assert(c != NULL);
}
return c;
}
其中调用注册的simple_trap()
函数进行事件的处理:
Context *simple_trap(Event ev, Context *ctx) {
switch(ev.event) {
case EVENT_IRQ_TIMER:
putch('t'); break;
case EVENT_IRQ_IODEV:
putch('d'); break;
case EVENT_YIELD:
putch('y'); break;
default:
panic("Unhandled event"); break;
}
return ctx;
}
根据事件EVENT_YIELD
输出了一个字符y
。
这样异常处理函数__am_irq_handle()
处理完应用程序的异常号后,返回到异常处理程序__am_asm_trap
。随后进行恢复上下文。此时由于CSR寄存器mepc
保存的是调用ecall
时候的地址,需要在恢复过程中,将epc
设置为执行ecall
的下一条指令。这样异常处理程序最后调用mret
指令取出下一条指令的地址就是正确的。
INSTPAT("0011000 00010 00000 000 00000 11100 11", mret , N, s->dnpc = read_csrs(CSR_MEPC));
异常处理程序执行完毕后,PC指向ecall
的下一条指令。此时调用yield()
触发自陷操作的函数hello_intr
,会进入一个循环,循环结束后,又会再来调用yield()
触发自陷操作。
while (1) {
for (volatile int i = 0; i < 1000000; i++) ;
yield();
}
至此完结