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(); }
至此完结
本文作者:上山砍大树
本文链接:https://www.cnblogs.com/shangshankandashu/p/18543073
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步