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的入口地址

操作系统

成功跳转到异常入口地址之后, 我们就要在软件上开始真正的异常处理过程了.异常处理过程包括:

  1. 保存上下文(程序的状态)
  2. 事件分发(按照异常号分发事件和按照事件分发具体事件处理)
  3. 恢复上下文

异常处理的时候,首先会将程序异常状态的上下文保存起来。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();
  }

至此完结

posted @ 2024-11-13 09:19  上山砍大树  阅读(7)  评论(0编辑  收藏  举报