spike基础机制及运行

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() 函数来推进仿真的进程。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() 函数中,仿真器会执行指令并让时间前进。
}
......
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}
}
}

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

调试模式检查

1
2
3
4
5
6
7
8
9
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);
}
}

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

主执行循环

基本状态初始化

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

取指、解码和执行

慢速路径(slow path)
1
2
3
4
5
6
7
8
if (unlikely(slow_path())) {
while (instret < n) {
// 执行单条指令
insn_fetch_t fetch = mmu->load_insn(pc);
pc = execute_insn_logged(this, pc, fetch);
advance_pc();
}
}
  • 慢速路径:如果进入了“慢速路径”(即需要特殊处理的情况,如单步调试、触发器检测等),每条指令都会通过 mmu->load_insn(pc) 从内存中加载,并通过 execute_insn_logged() 执行。
  • 单步执行:在慢速路径下,指令会逐条取出、解码并执行,同时处理调试触发器和中断。
快速路径(fast path)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
  • 快速路径:在不需要特殊处理的情况下,Spike 使用指令缓存(icache)加速执行流程。处理器从缓存中读取指令,并通过 execute_insn_fast() 执行指令。如果缓存匹配,则直接执行下一个指令块,减少频繁的内存访问。
  • 跳转指令:如果遇到分支跳转或者缓存不匹配的情况,指令缓存中的执行流会中断,并重新从新地址获取指令。

中断和陷阱处理

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

中断处理

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

陷阱处理

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

触发器匹配

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

等待中断

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

指令计数和周期计数

1
2
state.minstret->bump(instret);
state.mcycle->bump(instret);
  • Spike 模拟了处理器的性能计数器。在每次指令执行后,minstret(已完成指令计数器)和 mcycle(周期计数器)会根据执行的指令数量进行递增。

spike基础机制及运行
http://willimt.com/2024/08/02/模拟器/spike基础机制及运行/
作者
Willimt
发布于
2024年8月2日
许可协议