Spike 运行机制详解
指令获取
Spike 通过 MMU(内存管理单元)加载指令,指令获取由 mmu->load_insn(pc) 函数完成,它根据程序计数器的地址从内存中读取一条指令。
struct insn_fetch_t
{
insn_func_t func;
insn_t insn;
};
struct icache_entry_t {
reg_t tag;
struct icache_entry_t* next;
insn_fetch_t data;
};
insn_fetch_t fetch = mmu->load_insn(pc);
-> inline insn_fetch_t load_insn(reg_t addr)
{
icache_entry_t entry;
return refill_icache(addr, &entry)->data;
}cppmmu->load_insn(pc):根据当前程序计数器(pc)的值,MMU 从内存中读取指令,返回一个 insn_fetch_t 结构,其中包含要执行的指令及其相关元数据。
Spike 提供指令缓存(ICache)来加速指令获取,避免反复从内存读取相同指令,从而提高仿真性能。
在快速路径中,Spike 直接从指令缓存中获取指令:
auto ic_entry = _mmu->access_icache(pc);
-> inline icache_entry_t* access_icache(reg_t addr)
{
icache_entry_t* entry = &icache[icache_index(addr)];
if (likely(entry->tag == addr))
return entry;
return refill_icache(addr, entry);
}
auto fetch = ic_entry->data;cpp缓存未命中或处于慢速路径时,Spike 直接通过 mmu->load_insn() 从内存加载指令,读取原始的指令二进制数据。
读取指令
refill_icache 用于填充指令缓存(ICache)。它从内存读取指令,并将指令及其解码结果缓存起来,后续访问即可直接从缓存获取,无需再次访问内存。
inline icache_entry_t* refill_icache(reg_t addr, icache_entry_t* entry)
{
if (matched_trigger)
throw *matched_trigger;
auto tlb_entry = translate_insn_addr(addr); // 将虚拟地址(addr)转换为物理地址
// 从内存中读取指令的前两个字节(16位),并将其存入 insn 变量。
insn_bits_t insn = from_le(*(uint16_t*)(tlb_entry.host_offset + addr));
int length = insn_length(insn); // 确定该指令的长度
// 根据 length 的值,Spike 从内存中获取剩余的指令字节,并组装成完整的指令
if (likely(length == 4)) {
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) << 16;
} else if (length == 2) {
// entire instruction already fetched
} else if (length == 6) {
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) << 16;
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 4)) << 32;
} else {
static_assert(sizeof(insn_bits_t) == 8, "insn_bits_t must be uint64_t");
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 2)) << 16;
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 4)) << 32;
insn |= (insn_bits_t)from_le(*(const uint16_t*)translate_insn_addr_to_host(addr + 6)) << 48;
}
insn_fetch_t fetch = {proc->decode_insn(insn), insn}; // 指令解码
entry->tag = addr; // 将缓存条目中的 tag 更新为当前指令的地址(addr),以便后续可以根据地址快速找到这条缓存条目。
entry->next = &icache[icache_index(addr + length)]; // 将 next 指针设置为下一个缓存条目地址
entry->data = fetch; // 将 fetch(包含解码后的指令和原始指令)存入 entry->data 中。
// 跟踪与异常处理
reg_t paddr = tlb_entry.target_offset + addr;;
if (tracer.interested_in_range(paddr, paddr + 1, FETCH)) {
entry->tag = -1;
tracer.trace(paddr, length, FETCH);
}
return entry;
}cpprefill_icache 的主要功能是:
- 地址转换(TLB)。
- 读取不同长度的指令。
- 解码指令。
- 将指令存入缓存。
- 监控触发器或调试器的状态。
指令解码
processor_t::decode_insn(insn_t insn) 负责把二进制指令解码为对应的操作函数。它先通过哈希表快速查找指令对应的处理函数(insn_func_t);若未命中,再通过线性搜索找到指令描述符,并缓存起来供后续查找。
decode_insn 的主要功能是:
- 从二进制指令中提取操作码。
- 查找指令对应的操作函数。
- 通过哈希表缓存机制加速指令解码。
- 处理自定义指令集和扩展。
通过哈希表查找指令
size_t idx = insn.bits() % OPCODE_CACHE_SIZE;
auto [hit, desc] = opcode_cache[idx].lookup(insn.bits());cppinsn.bits():提取指令的二进制位表示。idx = insn.bits() % OPCODE_CACHE_SIZE:对OPCODE_CACHE_SIZE取模,得到哈希表索引idx,指向缓存中的某个槽位。opcode_cache[idx].lookup(insn.bits()):在哈希表(opcode_cache)中查找该指令。若命中(hit为true),直接返回指令描述符desc。
处理自定义指令和标准指令
哈希表未命中(hit 为 false)时,Spike 改用线性搜索,在自定义指令和标准指令列表中寻找匹配的指令。
if (unlikely(!hit)) {
// fall back to linear search
auto matching = [insn_bits = insn.bits()](const insn_desc_t &d) {
return (insn_bits & d.mask) == d.match;
};
auto p = std::find_if(custom_instructions.begin(),
custom_instructions.end(), matching);
if (p == custom_instructions.end()) {
p = std::find_if(instructions.begin(), instructions.end(), matching);
assert(p != instructions.end());
}
desc = &*p;
opcode_cache[idx].replace(insn.bits(), desc);
}cpp- 线性搜索:缓存未命中时,Spike 在
custom_instructions(自定义指令集)中线性搜索。matching是一个匿名函数,通过指令的掩码(mask)和匹配值(match)来匹配指令。 std::find_if:在列表中查找第一个满足条件的指令描述符。若自定义指令集中未找到,Spike 继续搜索标准的instructions集合。- 缓存更新:找到匹配的指令编码后,Spike 将描述符插入哈希表缓存,加速后续解码。
指令什么时候被添加到队列中?
在 processor_t::processor_t 构造函数中,processor_t::register_base_instructions() 负责将指令添加到 instructions 容器,这也是标准 RISC-V 指令集注册的关键函数。
#define DEFINE_INSN(name) \
if (!name##_overlapping) \
register_base_insn((insn_desc_t) { \
name##_match, \
name##_mask, \
fast_rv32i_##name, \
fast_rv64i_##name, \
fast_rv32e_##name, \
fast_rv64e_##name, \
logged_rv32i_##name, \
logged_rv64i_##name, \
logged_rv32e_##name, \
logged_rv64e_##name});
#include "insn_list.h"
#undef DEFINE_INSNcppDEFINE_INSN宏:为每条指令声明相关的处理函数。每条指令有多个变种(如 RV32I、RV64I、RV32E、RV64E),每个变种都有对应的快速路径(fast)和日志记录(logged)版本。
DEFINE_INSN(add)
DEFINE_INSN(addi)
DEFINE_INSN(addiw)
DEFINE_INSN(addw)
DEFINE_INSN(and)
DEFINE_INSN(andi)
DEFINE_INSN(auipc)
DEFINE_INSN(beq)
DEFINE_INSN(bge)
DEFINE_INSN(bgeu)
DEFINE_INSN(blt)cppinsn_list.h:包含所有标准 RISC-V 指令的列表。通过包含该文件,Spike 为每条指令声明了相关的处理函数。
static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm) __attribute__ ((unused));
static uint32_t addi(unsigned int dest, unsigned int src, uint16_t imm)
{
return (bits(imm, 11, 0) << 20) |
(src << 15) |
(dest << 7) |
MATCH_ADDI;
}cpp随后调用 processor_t::register_insn 函数,将指令加入指令队列。
void processor_t::register_insn(insn_desc_t desc, bool is_custom) {
assert(desc.fast_rv32i && desc.fast_rv64i && desc.fast_rv32e && desc.fast_rv64e &&
desc.logged_rv32i && desc.logged_rv64i && desc.logged_rv32e && desc.logged_rv64e);
if (is_custom)
custom_instructions.push_back(desc);
else
instructions.push_back(desc);
}cpp获得指令执行函数
desc->func(xlen, rve, log_commits_enabled);cppdesc->func:指令描述符中包含对应的指令处理函数(insn_func_t 类型),Spike 据此返回该指令的处理函数。
insn_func_t func(int xlen, bool rve, bool logged) const
{
if (logged)
if (rve)
return xlen == 64 ? logged_rv64e : logged_rv32e;
else
return xlen == 64 ? logged_rv64i : logged_rv32i;
else
if (rve)
return xlen == 64 ? fast_rv64e : fast_rv32e;
else
return xlen == 64 ? fast_rv64i : fast_rv32i;
}cpp此时得到的指令执行函数形如 fast_rv32i_add,但实际并不存在这个函数,指令执行逻辑是通过宏定义实现的。
指令执行
获得指令的执行函数后,Spike 直接调用它。
static inline reg_t execute_insn_fast(processor_t* p, reg_t pc, insn_fetch_t fetch) {
return fetch.func(p, fetch.insn, pc);
}cppSpike 的指令逻辑定义在 ISA(指令集架构)模块中,具体位于 riscv/insns/ 目录下。
- 每条指令对应一个 C++ 文件,例如
add指令的编码格式定义在riscv/insns/add.h中。Spike 使用宏和位运算来解析和生成指令的二进制编码。 - 指令的解析和执行由仿真器内部的解码器完成。指令的二进制编码由 RISC-V ISA 标准规定,在 Spike 中由解码器解析并映射到相应的指令处理函数。
Spike 通过宏定义实现指令执行逻辑,让多条指令共享相似的结构,简化重复代码的编写。例如,add.h 中的宏实现可被 RV32I、RV64I 等多个变种复用,无需为每个变种单独编写函数。
构建时,Spike 的编译器会将宏展开为实际的指令处理逻辑。具体执行流程如下:
- 仿真器解码到
add指令时,通过指令表中的描述符(如fast_rv32i_add)找到add.h中的处理逻辑。 add.h中的宏展开为相应的操作,仿真器据此执行加法指令。
// add.h
WRITE_RD(RS1 & RS2);
|
||-> // riscv/decode_macros.h
#define RS1 READ_REG(insn.rs1())
#define RS2 READ_REG(insn.rs2())
||-> // riscv/decode_macros.h
#define WRITE_RD(value) WRITE_REG(insn.rd(), value)
#define WRITE_REG(reg, value) ({ \
reg_t wdata = (value); /* value may have side effects */ \
if (DECODE_MACRO_USAGE_LOGGED) STATE.log_reg_write[(reg) << 4] = {wdata, 0}; \
CHECK_REG(reg); \
STATE.XPR.write(reg, wdata); \
})cpp以上是 Spike 运行时指令获取与指令执行的大体流程。