1. 搭建RISC-V指令集模拟器框架
任务目标
实现一个可运行的RISC-V RV32I指令集模拟器核心框架,支持指令的取指、解码、执行以及寄存器堆状态更新。本案例聚焦于算术运算指令(ADD/SUB/ADDI)与分支指令(BEQ)的最小闭环,为后续扩展其他指令类别和微架构设计提供可验证的参考模型。
环境依赖
仅需标准C++17编译器(g++或clang++),无第三方库依赖。建议Linux或macOS环境。
核心实现
模拟器采用经典的冯·诺依曼结构:内存中顺序存放指令,PC指向当前指令地址。解码阶段根据opcode和funct3/funct7字段识别指令类型,执行阶段完成计算并更新寄存器堆。分支指令通过比较两个源寄存器决定是否跳转。
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
class RV32ISim {
public:
static constexpr uint32_t REG_COUNT = 32;
static constexpr uint32_t MEM_SIZE = 4096;
static constexpr uint32_t BASE_ADDR = 0x1000;
RV32ISim() : regs(REG_COUNT, 0), mem(MEM_SIZE, 0), pc(BASE_ADDR) {
regs[0] = 0; // x0 恒为0
}
// 加载程序到内存(从BASE_ADDR开始)
void loadProgram(const std::vector<uint32_t>& instructions) {
for (size_t i = 0; i < instructions.size(); ++i) {
uint32_t addr = BASE_ADDR + i * 4;
if (addr + 4 > BASE_ADDR + MEM_SIZE) break;
mem[addr - BASE_ADDR] = instructions[i];
}
}
// 执行一条指令,返回是否继续执行
bool step() {
if (pc < BASE_ADDR || pc + 4 > BASE_ADDR + MEM_SIZE) return false;
uint32_t instr = mem[pc - BASE_ADDR];
uint32_t opcode = instr & 0x7f;
uint32_t rd = (instr >> 7) & 0x1f;
uint32_t funct3 = (instr >> 12) & 0x7;
uint32_t rs1 = (instr >> 15) & 0x1f;
uint32_t rs2 = (instr >> 20) & 0x1f;
uint32_t funct7 = (instr >> 25) & 0x7f;
uint32_t next_pc = pc + 4;
switch (opcode) {
case 0x33: { // R-type
if (funct3 == 0 && funct7 == 0) {
// ADD
regs[rd] = regs[rs1] + regs[rs2];
} else if (funct3 == 0 && funct7 == 0x20) {
// SUB
regs[rd] = regs[rs1] - regs[rs2];
} else {
printf("不支持的R-type指令: funct3=%x funct7=%x\n", funct3, funct7);
return false;
}
break;
}
case 0x13: { // I-type (ADDI)
if (funct3 == 0) {
int32_t imm = static_cast<int32_t>(instr) >> 20; // 符号扩展
regs[rd] = regs[rs1] + imm;
} else {
printf("不支持的I-type指令: funct3=%x\n", funct3);
return false;
}
break;
}
case 0x63: { // B-type (BEQ)
if (funct3 == 0) {
int32_t imm = ((instr >> 31) & 1) << 12 |
((instr >> 7) & 1) << 11 |
((instr >> 25) & 0x3f) << 5 |
((instr >> 8) & 0xf) << 1;
if (imm & 0x1000) imm |= 0xffffe000; // 符号扩展
if (regs[rs1] == regs[rs2]) {
next_pc = pc + imm;
}
} else {
printf("不支持的B-type指令: funct3=%x\n", funct3);
return false;
}
break;
}
default:
printf("不支持的opcode: %x\n", opcode);
return false;
}
regs[0] = 0; // 确保x0始终为0
pc = next_pc;
return true;
}
void dumpRegs() {
for (int i = 0; i < 8; ++i) {
printf("x%d=%d x%d=%d x%d=%d x%d=%d\n",
i, regs[i], i+8, regs[i+8], i+16, regs[i+16], i+24, regs[i+24]);
}
}
uint32_t getPC() const { return pc; }
private:
std::vector<uint32_t> regs;
std::vector<uint32_t> mem;
uint32_t pc;
};
int main() {
// 测试程序:x1=5, x2=3, x3=x1+x2, x4=x1-x2, x5=x3+10, 若x3==8则跳转
std::vector<uint32_t> program = {
0x00500093, // addi x1, x0, 5 (x1 = 5)
0x00300113, // addi x2, x0, 3 (x2 = 3)
0x002081b3, // add x3, x1, x2 (x3 = 8)
0x40208233, // sub x4, x1, x2 (x4 = 2)
0x00a18293, // addi x5, x3, 10 (x5 = 18)
0x00318463, // beq x3, x3, 8 (跳转+8,跳过下一条)
0x00100313, // addi x6, x0, 1 (不应执行)
0x00000393, // addi x7, x0, 0 (x7 = 0)
};
RV32ISim sim;
sim.loadProgram(program);
int steps = 0;
while (steps < 20 && sim.step()) {
steps++;
}
printf("\n执行 %d 条指令后寄存器状态:\n", steps);
sim.dumpRegs();
printf("PC = 0x%x\n", sim.getPC());
// 验证关键结果
bool pass = true;
// 由于无法直接读取寄存器,通过dumpRegs人工验证
printf("\n预期: x3=8, x4=2, x5=18, x6=0(未执行), x7=0\n");
printf("若输出与预期一致,则测试通过。\n");
return 0;
}
编译与运行
g++ -std=c++17 -o rv32i_sim rv32i_sim.cpp
./rv32i_sim
验证方法
程序执行7条指令后停止(第8条BEQ跳转跳过第7条,第8条执行后PC指向内存末尾)。观察输出寄存器值:
- x3 应为8(ADD结果)
- x4 应为2(SUB结果)
- x5 应为18(ADDI结果)
- x6 应为0(BEQ跳转使其未执行)
- x7 应为0(跳转目标指令)
若输出与上述一致,说明指令解码、执行、寄存器更新和分支跳转逻辑均正确,模拟器框架验证通过。
常见问题
- 立即数符号扩展:ADDI和BEQ的立即数均为有符号数,需正确进行符号扩展,否则负数运算和反向跳转会出错。
- x0寄存器:硬件上x0恒为0,每次指令执行后需强制清零,防止被写入。
- PC更新时机:分支指令需先计算目标地址再更新PC,顺序执行时PC+4,本实现通过next_pc变量统一处理。