Spike 模拟器基础
Spike 机制
Spike 仿真器模拟 RISC-V 处理器的指令执行,宿主机则是运行仿真器的物理计算机。tohost 和 fromhost 寄存器构成的通信机制允许目标系统(仿真中的 RISC-V 处理器)与宿主机交换数据,主要作用包括:
- 输入/输出(I/O)处理:仿真程序需要与外部设备(如控制台、磁盘等)交互时,通信机制允许目标系统借助宿主机的资源完成这些操作。
- 系统调用(Syscalls):仿真程序执行系统调用(如文件操作、内存分配等)时,
tohost和fromhost机制把请求从目标系统传递到宿主机,由宿主机模拟系统调用并返回结果。 - 异常与中断处理:仿真过程中,宿主机负责处理来自目标系统的异常和中断信号;目标系统通过
tohost发送中断请求,宿主机模拟响应这些事件。
tohost 和 fromhost 寄存器的工作机制
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() 进入主运行循环。该循环是仿真器与宿主机之间的接口主循环,会不断调用 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() 函数中,仿真器会执行指令并让时间前进。
}
......
}cppsim_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();
}cppstep(INTERLEAVE):idle() 调用 sim_t::step() 推进仿真器的执行。INTERLEAVE 是每次执行的指令数量,可以是预定义值,也可以根据仿真配置调整。
sim_t::step() 是仿真器的主执行循环,它遍历每个处理器核心,调用各核心的 processor_t::step() 执行指令:
void sim_t::step(size_t n)
{
for (size_t i = 0, steps = 0; i < n; i += steps) // 这个循环会持续执行,直到仿真器核心已经执行了 n 条指令。
{
steps = std::min(n - i, INTERLEAVE - current_step); // steps 变量决定每次当前处理器核心将执行的指令数
procs[current_proc]->step(steps); // procs[current_proc]->step(steps):这一行代码调用当前处理器核心的 step() 函数,执行steps条指令。
current_step += steps;
if (current_step == INTERLEAVE) // 当 current_step 达到 INTERLEAVE 时,仿真器将 current_step 重置为 0,并切换到下一个处理器核心
{
current_step = 0;
procs[current_proc]->get_mmu()->yield_load_reservation();
if (++current_proc == procs.size()) { // 如果所有核心都轮流执行了一次(即 current_proc == procs.size()),仿真器会调用设备的 tick() 函数更新其状态。rtc_ticks 是计算出来的虚拟时钟周期,表示在执行的指令数之间设备所经历的虚拟时间
current_proc = 0;
reg_t rtc_ticks = INTERLEAVE / INSNS_PER_RTC_TICK;
for (auto &dev : devices) dev->tick(rtc_ticks);
}
}
}
}cppprocessor_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;
......
}cppinstret记录已执行的指令数量。pc是程序计数器,指向当前要执行的指令地址。_mmu指向内存管理单元(MMU),用于加载和存储指令。state.prv_changed和state.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();cpptake_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(周期计数器)按执行的指令数递增。