【原创C&C++】RISCVemu:60/60通过riscv-tests的RV32IMA指令集模拟器,含完整交互式调试器【长期更新项目】
0xfe. 已完成FDQ扩展 见回帖和v0.3tag
(希望uint8够用)(笑)
0x00. 为什么写这个模拟器?
RISC-V 作为开源指令集架构,近年来在学术界和工业界都备受关注。对于学习计算机体系结构的人来说,亲手实现一个能跑的模拟器,比读一百遍手册都管用。
RISCVemu 是我用 C23 写的一个 RV32IMA(U) 指令集模拟器,目标是:
- 干净:代码结构清晰,模块化设计,易于阅读和修改
- 可测试:完整通过
riscv-tests 全部 60 个用例
- 可用:自带交互式调试器,支持单步、断点、反汇编、寄存器/内存读写
- 长期更新: 还在肝F D Q V扩展中
- 最后: 跑一下OpenSBI和Linux
项目地址:https://github.com/Phi-Lia093/RISCVemu
0x01. 整体架构
┌─────────────────────────────────────────────────────────────┐
│ RISCVemu │
├─────────────┬─────────────┬─────────────┬─────────────────┤
│ emu.c │ exec.c │ mem.c │ debugger.c │
│ (主循环) │ (指令执行) │ (内存管理) │ (调试器) │
├─────────────┴─────────────┴─────────────┴─────────────────┤
│ disasm.c (反汇编器) │
├─────────────────────────────────────────────────────────────┤
│ extension/ (M/A/Zicsr/Zifencei) │
└─────────────────────────────────────────────────────────────┘
核心数据结构 machine_state:
struct machine_state
{
uint32_t gpr[32]; // 32个通用寄存器
uint32_t pc; // 程序计数器
uint8_t *main_memory; // 4GB 内存空间(按需分配)
int terminated; // 终止标志
#ifdef CONFIG_ENABLE_A_EXTENSION
struct hashmap mmu_flags; // A扩展用到的内存标志
#endif
#ifdef CONFIG_ENABLE_DEBUGGER
int single_step; // 单步模式
uint32_t breakpoint; // 断点地址
uint32_t breakpoint_enabled;
#endif
uint32_t privilege; // 特权级(目前固定为U模式)
};
C23 特性应用:
__maybe_unused / __always_inline:更清晰的编译器提示
constexpr 风格的宏配置
static_assert 用于编译时检查(虽然这里没用但项目里有用到)
0x02. 指令解码与执行引擎
核心设计思路:按 opcode 分发,再按 funct3/funct7 细分。
void exec(uint32_t ins)
{
uint32_t opcode = get_opcode(ins);
uint32_t funct3 = get_funct3(ins);
uint32_t funct7 = get_funct7(ins);
uint32_t rs1 = get_rs1(ins);
uint32_t rs2 = get_rs2(ins);
uint32_t rd = get_rd(ins);
switch (opcode)
{
// R格式:算术/逻辑运算
case 0b0110011:
{
// M扩展指令(乘除)
if (unlikely(funct7 == 0b0000001))
{
m_ins_optable[funct3][funct7](rs2, rs1, rd);
}
else
{
switch (funct3)
{
case 0: // ADD / SUB
if (likely(funct7 == 0b0000000))
insi_r_add(rs2, rs1, rd);
else if (funct7 == 0b0100000)
insi_r_sub(rs2, rs1, rd);
// ...
// ...
}
}
break;
}
// I格式:立即数运算 / Load
case 0b0010011: // 算术立即数
{
uint32_t imm = sign_extend_12((ins >> 20) & 0xFFF);
// ...
break;
}
// ... 更多 opcode
}
}
M扩展指令表:用二维函数指针数组实现,简洁高效。
static void (*m_ins_optable[8][128])(uint32_t, uint32_t, uint32_t) = {
[0] = { [0b0000001] = &insm_r_mul },
[1] = { [0b0000001] = &insm_r_mulh },
[2] = { [0b0000001] = &insm_r_mulsu },
// ...
};
A扩展(原子操作):支持 LR/SC 和全部 AMO 指令。
case 0b0101111: // AMO
{
if (likely(funct3 == 0x2))
{
uint32_t funct5 = (ins >> 27) & 0x1F;
switch (funct5)
{
case 0x00: insa_r_amoadd_w(rs1, rs2, rd); break;
case 0x01: insa_r_amoswap_w(rs1, rs2, rd); break;
case 0x02: insa_r_lr_w(rs1, rd); break;
case 0x03: insa_r_sc_w(rs1, rs2, rd); break;
// ... 全部 AMO 操作
}
}
break;
}
性能优化:exec()函数大小内联7.1KB 保证可以置入L1-I缓存 perf实测miss率<1.7% ; 使用严谨的热度排序 保证分支预测miss率最小
0x03. 内存管理:按需分配,支持原子操作标志
内存管理采用按需分配策略,并非一开始就分配完整的 4GB 空间:
void init_mem(void)
{
g_state.main_memory = (uint8_t *)calloc(MEM_SIZE, sizeof(uint8_t));
// MEM_SIZE = 0x100000000 (4GB) 但 calloc 是虚拟的,物理页按需分配
#ifdef CONFIG_ENABLE_A_EXTENSION
hashmap_init(&g_state.mmu_flags);
#endif
}
UART 模拟:写入 0x10000000 的数据会作为 ASCII 字符直接输出到 stdout。这个特性让 printf 可以直接工作:
// 在 mem_write32 中:
if (addr == UART_BASE) {
putchar((char)value & 0xFF);
return;
}
0x04. 交互式调试器
调试器是让这个模拟器可用的关键。支持的命令包括单步、断点、寄存器修改、反汇编等。
命令分发核心逻辑:
void tick_debugger(void)
{
// 显示当前指令
uint32_t ins = mem_read32_unsigned(g_state.pc);
if (show_disasm) {
char *disasm_str = disasm(ins);
printf("0x%08x: %08x %s\n", g_state.pc, ins, disasm_str);
}
while (1) {
printf("DEBUG> ");
fgets(line, sizeof(line), stdin);
// 解析命令...
switch (cmd) {
case 's': case 'n': cmd_single_step(); return;
case 'c': cmd_continue(); return;
case 'r': cmd_registers(); break;
case 'R': cmd_set_register(args); break;
case 'u': cmd_disasm(args); break;
case 'b': cmd_breakpoint_set(args); break;
// ...
}
}
}
寄存器设置支持数字或名称,比如 R a0 0x1234 或 R 10 0x1234 都可以。
断点实现:简单地在每次执行前检查 g_state.pc == breakpoint。
0x05. 测试与验证
测试结果:运行 make run_tests,全部 60 个测试通过:
==========================================
TEST COMPLETED!
TOTAL: 60
PASSED: 60
FAILED: 0
SKIPPED: 0
==========================================
测试覆盖:
- **rv32ui-p-***:基础整数指令(add、sub、lui、auipc、分支、跳转、加载/存储等)
- **rv32um-p-***:M扩展(mul、div、rem 等)
- **rv32ua-p-***:A扩展(LR/SC、全部 AMO 指令)
其中 rv32ui-p-ma_data 涉及特权级/中断处理,但模拟器依然处理得当。
0x06. 运行效果
正常运行示例程序:
15:58:43 [INFO] RISC-V Emulator starting...
15:58:43 [INFO] Program: run.bin
15:58:43 [INFO] Base address: 0x1000
15:58:43 [INFO] loaded 8383 bytes to 0x1000
15:58:43 [INFO] starting execution at PC=0x1000
Hello, World!
15:58:43 [INFO] ECALL instruction executed, Terminating...
15:58:43 [INFO] Execution terminated. Return value: 0x325
调试模式:
0x00001000: ff010113 addi sp, sp, -16
DEBUG> s
0x00001004: 00112623 sw ra, 12(sp)
DEBUG> r
PC=0x00001004
REGISTERS:
zero=0x00000000 ra=0x00000000 sp=0xfffffff0 gp=0x00000000
tp=0x00000000 t0=0x00000000 t1=0x00000000 t2=0x00000000
s0=0x00000000 s1=0x00000000 a0=0x00000000 a1=0x00000000
...
DEBUG> b 0x100c
Breakpoint set at 0x0000100c
DEBUG> c
15:59:34 [INFO] breakpoint hit at PC=0x100c
0x0000100c: 01010413 addi s0, sp, 16
0x07. 构建与使用
依赖:
- C23 兼容编译器(GCC 13+ / Clang 16+)
- CMake 3.10+
- (测试用)RISC-V GNU Toolchain
构建:
git clone --recursive https://github.com/你的用户名/RISCVemu
cd RISCVemu
mkdir build && cd build
cmake .. && make
运行:
./RISCVemu run.bin 0x1000 # 正常运行
./RISCVemu run.bin 0x1000 -d # 调试模式
./RISCVemu run.bin 0x1000 -d -b 0x1200 # 带断点启动
CMake 配置选项(可在 ccmake .. 中调整):
| 选项 |
说明 |
默认 |
CONFIG_ENABLE_DEBUGGER |
调试器 |
ON |
CONFIG_ENABLE_M_EXTENSION |
M扩展 |
ON |
CONFIG_ENABLE_A_EXTENSION |
A扩展 |
ON |
CONFIG_ENABLE_ZICSR_EXTENSION |
CSR指令 |
ON |
CONFIG_ENABLE_UART_DEVICE |
UART模拟 |
ON |
还有很多扩展欢迎尝试
0xff. 总结
RISCVemu 是一个干净、可测试、功能完整的 RV32IMA 模拟器,适合以下场景:
- 学习 RISC-V 指令集
- 学习模拟器开发
- 作为嵌入式开发的测试平台
如果觉得这个项目对你有帮助,欢迎到 GitHub 上给个 Star ⭐
项目地址:https://github.com/Phi-Lia093/RISCVemu
参考资料:
本模拟器仅供学习和研究使用,欢迎 Issue 和 PR!