Hansen's ink

Back

Spike机制#

Spike 仿真器模拟 RISC-V 处理器的指令执行,而宿主机是运行仿真器的物理计算机。tohostfromhost 寄存器的通信机制允许目标系统(仿真中的 RISC-V 处理器)与宿主机之间进行数据交换,主要作用包括:

  • 输入/输出(I/O)处理:例如,当仿真程序需要与外部设备(如控制台、磁盘等)交互时,这些通信机制允许目标系统通过宿主机的资源来执行这些操作。
  • 系统调用(Syscalls):当仿真程序执行系统调用(如文件操作、内存分配等)时,tohostfromhost 机制将系统调用的请求从目标系统传递到宿主机,由宿主机模拟相应的系统调用,并返回结果。
  • 异常与中断处理:仿真过程中,宿主机负责处理一些来自目标系统的异常情况或中断信号,通过 tohost 发送中断请求,宿主机可以模拟响应这些事件。

tohostfromhost 寄存器的工作机制:#

  • tohost 寄存器:目标系统将数据写入 tohost 寄存器,通常用于发送命令或请求给宿主机。例如,仿真程序可能请求宿主机进行某种I/O操作或触发系统调用。
  • fromhost 寄存器:宿主机通过 fromhost 寄存器将响应数据传递回目标系统,例如返回系统调用的结果或提供输入数据。

通信机制未启用时#

  • 如果 tohost_addr 为零,通信未启用。在这种情况下,仿真器会简单地执行目标系统的指令而不与宿主机进行任何交互。这意味着仿真器处于一个纯粹的“裸机模式”(bare-metal mode),运行的是不需要宿主机协助的程序。
  • 在这个模式下,仿真器通过不断调用 idle() 函数执行指令,直到接收到退出信号。在这种情况下,仿真程序无法与外部系统(宿主机)进行交互,所以适合用于一些不依赖 I/O 的测试程序或无操作系统的裸机程序。

Spike的运行#

Spike的程序启动逻辑位于spike.cc文件的主函数(main())中。包括以下关键部分:

  • 参数解析:解析输入参数并根据这些参数设置仿真器的配置。
  • 初始化内存和处理器:根据配置初始化仿真的内存和处理器核心。
  • 加载程序:将内核和初始RAM盘加载到仿真内存中。
  • 启动仿真:通过sim_t::run()进入主仿真循环,开始指令的执行。

通过sim_t::run()调用htif_t::run()的主运行循环(它是仿真器和宿主机之间的接口主循环。htif_t::run() 会不断调用 sim_t::idle() 函数来推进仿真的进程。)

 int htif_t::run()
 {
   start();
 
   auto enq_func = [](std::queue<reg_t>* q, uint64_t x) { q->push(x); };
   std::queue<reg_t> fromhost_queue;
   std::function<void(reg_t)> fromhost_callback =
     std::bind(enq_func, &fromhost_queue, std::placeholders::_1);
 
   if (tohost_addr == 0) { // 裸机模式运行
     while (!signal_exit)
       idle();  // 从这里进入,在 sim_t::idle() 函数中,仿真器会执行指令并让时间前进。
   }
   ......
 }
cpp

sim_t::idle() 负责推进仿真器的时间状态,通常通过让仿真中的处理器核心执行指令。它可以处理中断、定时器事件等,模拟系统的实际行为。

 void sim_t::idle()
 {
   if (done())
     return;
 
   if (debug || ctrlc_pressed)
     interactive();
   else
     step(INTERLEAVE); // 默认  static const size_t INTERLEAVE = 5000;
 
   if (remote_bitbang)
     remote_bitbang->tick();
 }
cpp

step(INTERLEAVE)idle() 函数调用 sim_t::step() 函数来推进仿真器的执行。INTERLEAVE 是仿真器每次执行的指令数量,它是一个预定义的值或根据仿真配置设定。

sim_t::step() 函数是仿真器的主执行循环,它遍历每个处理器核心,并调用每个核心的 processor_t::step() 函数来执行指令:

processor_t::step(size_t n) 是 Spike 中用于实现指令取指(fetch)、解码(decode)、执行(execute)循环的核心函数。它负责让每个处理器核心(hart)执行 n 条指令,同时处理调试模式、陷阱、触发器和中断等情况。

调试模式检查#

 if (!state.debug_mode) {
   if (halt_request == HR_REGULAR) {
     enter_debug_mode(DCSR_CAUSE_DEBUGINT);
   } else if (halt_request == HR_GROUP) {
     enter_debug_mode(DCSR_CAUSE_GROUP);
   } else if (state.dcsr->halt) {
     enter_debug_mode(DCSR_CAUSE_HALT);
   }
 }
cpp

在执行指令之前,Spike 会检查是否存在调试模式请求。如果有调试中断或调试模式标志被设置,处理器会进入调试模式。

主执行循环#

基本状态初始化#

 while (n > 0) {
     size_t instret = 0;
     reg_t pc = state.pc;
     mmu_t* _mmu = mmu;
     state.prv_changed = false;
     state.v_changed = false;
     ......
 }
cpp
  • instret 记录已经执行的指令数量。
  • pc 是当前的程序计数器,指向当前要执行的指令的地址。
  • _mmu 指向内存管理单元(MMU),用于加载和存储指令。
  • state.prv_changedstate.v_changed 用于跟踪特权级(privilege level)和矢量化(vectorization)的变化。

取指、解码和执行#

慢速路径(slow path)#

 if (unlikely(slow_path())) {
   while (instret < n) {
     // 执行单条指令
     insn_fetch_t fetch = mmu->load_insn(pc);
     pc = execute_insn_logged(this, pc, fetch);
     advance_pc();
   }
 }
cpp
  • 慢速路径:如果进入了“慢速路径”(即需要特殊处理的情况,如单步调试、触发器检测等),每条指令都会通过 mmu->load_insn(pc) 从内存中加载,并通过 execute_insn_logged() 执行。
  • 单步执行:在慢速路径下,指令会逐条取出、解码并执行,同时处理调试触发器和中断。

快速路径(fast path)#

 else while (instret < n) {
   for (auto ic_entry = _mmu->access_icache(pc); ; ) {
     auto fetch = ic_entry->data;
     pc = execute_insn_fast(this, pc, fetch);
     ic_entry = ic_entry->next;
     if (unlikely(ic_entry->tag != pc))
       break;
     if (unlikely(instret + 1 == n))
       break;
     instret++;
     state.pc = pc;
   }
   advance_pc();
 }
cpp
  • 快速路径:在不需要特殊处理的情况下,Spike 使用指令缓存(icache)加速执行流程。处理器从缓存中读取指令,并通过 execute_insn_fast() 执行指令。如果缓存匹配,则直接执行下一个指令块,减少频繁的内存访问。
  • 跳转指令:如果遇到分支跳转或者缓存不匹配的情况,指令缓存中的执行流会中断,并重新从新地址获取指令。

中断和陷阱处理#

在指令执行过程中,可能会发生各种异常、中断或者调试陷阱:

中断处理

 take_pending_interrupt();
cpp
  • take_pending_interrupt():检查并处理待处理的中断。如果检测到中断,将触发中断处理流程。

陷阱处理

catch (trap_t& t) {
  take_trap(t, pc);
  n = instret;
}
cpp
  • 如果在指令执行过程中发生陷阱(如非法指令、内存访问异常等),Spike 会捕获陷阱,并调用 take_trap() 处理陷阱。

触发器匹配

catch (triggers::matched_t& t) {
  take_trigger_action(t.action, t.address, pc, t.gva);
}
cpp
  • 如果检测到触发器匹配事件,Spike 会执行相应的触发器操作(如中断、陷阱等)。

等待中断

catch (wait_for_interrupt_t& t) {
  n = ++instret;
  in_wfi = true;
}
cpp
  • 当执行 WFI(等待中断)指令时,处理器会进入等待状态,并等待中断发生。此时会退出循环,并标记为 in_wfi 状态。

指令计数和周期计数#

state.minstret->bump(instret);
state.mcycle->bump(instret);
cpp
  • Spike 模拟了处理器的性能计数器。在每次指令执行后,minstret(已完成指令计数器)和 mcycle(周期计数器)会根据执行的指令数量进行递增。
Spike 模拟器基础
https://astro-pure.js.org/blog/simulator/spike-base
Author Hansen W.
Published at September 15, 2024
Comment seems to stuck. Try to refresh?✨