Graphorall

Back

从非法指令到解码路径:RISC-V 编码空间结构化的实践

  • public:: true

  • 调试 CPU 或内核时,最令人恼火的错误之一,是反汇编器只丢给你一句 illegal instruction
    没有上下文,没有线索——你不知道这条非法编码离哪条合法指令最近,不知道它落入了哪个编码子空间,更不知道它的各个字段(opcode / funct3 / funct6 / imm 等)如果按某种格式解读会是什么样子。

    这让我开始思考:我们能不能让解码器在遇到非法指令时,不只说“非法”,而是说“在哪个空间里非法”?


  • 现状:扁平的表,分层的空间#

    RISC-V 的编码空间本身是有层次的:

    • 30-bit 基础空间(bit 31–2)

    • 25-bit 主要操作码(Major Opcode)空间

    • 22-bit 次要操作码(funct3)空间

    • 进一步由 funct7 / funct6 / imm 等字段细分

  • 这个结构天然对应一棵解码决策树。然而,当前流行的机器可读数据源,并没有直接描述这棵树。

    • riscv-opcodes:提供的是 (mask, match) 对,本质是一张扁平表。解码器只能回答“这个编码匹配哪条指令”,无法回答“这个编码在哪个子空间”。

    • RISC-V Unified Database (UDB):它引入 type-subtype 继承机制,初衷是复用指令格式定义(比如 R-type 的字段位置)。但它无法表达“部分匹配”的节点——例如“opcode=0b0010011, funct3=0b001, funct7 是 don’t-care”这样的中间子空间。继承是 deep-merge + 覆盖,子级必须填所有字段,不能只填部分、留其余为 don’t-care。

      • 我仔细分析了 UDB 的继承实现(yaml_resolver.rb 中的 deep-merge 逻辑),结论是:type-subtype 解决的是“指令的形状”,而编码空间层次是“解码树的形状”,两者正交,不能互相推导。

  • 转折:gem5 的 decoder.isa 是一棵现成的树#

  • 正当我苦恼于没有结构化数据时,注意到了 gem5 模拟器中的 src/arch/riscv/isa/decoder.isa
    它用 gem5 的领域特定语言(DSL)手写了完整的解码决策树:

    decode QUADRANT {
      0x0: decode COMPRESSED { ... }
      0x3: decode OPCODE5 { ... }
    }
    plaintext

    这棵树不仅包含了所有指令的精确匹配,更重要的是它显式地编码了逐级细分的逻辑——每一步检查哪个字段、根据什么值分支、子空间里有哪些合法值。
    这正是我们需要的“解码路径”信息。

    于是,我们决定从 gem5 的 parser 中提取这棵树,转换成 JSON,然后编写一个独立的解码脚本,让它能“走”这棵树并报告每一步

  • 实现:从 gem5 到独立 skill#

  • 1. 修改 gem5 的 ISA parser#

  • gem5 的 isa_parser.py 在解析 decoder.isa 时,已经构建了 AST,但只用于生成 C++ 代码。
    我们给 parser 增加了:

    • DecodeTreeNode 类(支持递归序列化为 dict)

    • p_decode_blockp_decode_stmt_decodep_inst_0/1 等 production 中挂接,生成 blockcaseinstruction 三种节点

    • 添加 subset 标签收集(从指令参数中识别 OPIVVOPFVVOPIVI 等向量子集)

    • 最终输出 decode_tree.json

  • 这个 patch 只有 200 多行,不影响 gem5 原有的 C++ 生成,仅额外导出 JSON。

  • 2. 编写独立解码脚本 decode_inst.py#

    脚本核心逻辑:

  • 加载 decode_tree.json

  • 对每条 32-bit 指令,从根节点开始遍历:

    • 若当前节点是 block,提取指定字段的值,在 case 列表中查找匹配分支

    • 若匹配成功,记录字段值和位范围,进入下一节点

    • 若匹配失败,记录失败字段,并尝试从兄弟指令推导期望的格式(用于非法指令的 operand 展示)

  • 最后输出:

    • 每一步的字段名、值、位范围、子集标签

    • 完整 32-bit 二进制按字段划分的紧凑表示(含 operand 字段)

  • 3. 格式与 operand 的映射#

    最初我们硬编码了 VS2/VS1/VD 作为 operand 字段,但这是错误的——不同指令格式的 operand 不同。
    因此我们建立了 FMT_OPERANDS 字典,覆盖了 gem5 中约 70 种格式,为每种格式指定其 operand 字段(例如 IOprd, rs1, imm12VectorIntFormatvd, vs2, vs1)。
    这样,最终二进制展开就能正确显示与指令格式匹配的 operand。

  • 效果展示#

  • 以两个非法指令为例:

  • 例1:vmv<nr>rSIMM3 字段非法#

    指令 0x9e053057 在 NEMU 中曾引发异常。我们的工具输出:

    0x9e053057
    -> QUADRANT=0x3 [1:0]
    -> OPCODE5=0x15 [6:2] [OPFVF/OPFVV/OPIVI/OPIVV/OPIVX/OPMVV/OPMVX/VConfOp]
    -> FUNCT3=0x3 [14:12] [OPIVI]
    -> VFUNCT6=0x27 [31:26] [OPIVI]
    -> VM=0x1 [25:25] [OPIVI]
    -> SIMM3=0x2 ✗ not in ['0x0', '0x1', '0x3', '0x7']
    = 100111<VFUNCT6> 1<VM> 00000<vs2> 010<SIMM3 ✗> 011<FUNCT3> 00000<vd> 10101<OPCODE5> 11<QUADRANT>
    ILLEGAL INSTRUCTION
    SIMM3=0x2 not in ['0x0', '0x1', '0x3', '0x7']
    hint: SIMM3 encodes NREG-1; legal: 0,1,3,7 (NREG=1,2,4,8)
    plaintext

    它明确告诉我们:指令匹配到了 OPIVI 子空间,进入了 vmv<nr>r 的候选范围,但 SIMM3 字段的值 0x2 不在允许集合中。

  • 例2:vlmwidth 的非法组合#

    0x02b56487 是另一个 NEMU bug 案例:LUMOP=0xb(vlm)却搭配了 width=110(VLE32),而 vlm 要求 width=000。我们的工具输出:

    0x02b56487
    -> QUADRANT=0x3 [1:0]
    -> OPCODE5=0x1 [6:2] [Load/VlIndexOp/VlSegOp/VlStrideOp/VlWholeOp/VleOp/VlmOp]
    -> FUNCT3=0x6 [14:12] [VlIndexOp/VlSegOp/VlStrideOp/VlWholeOp/VleOp]
    -> MOP=0x0 [27:26] [VlSegOp/VlWholeOp/VleOp]
    -> LUMOP=0xb ✗ not in ['0x0', '0x8', '0x10']
    = 00<MOP> 01011<LUMOP ✗> 01010<rs1> 110<FUNCT3> 01001<vd> 00001<OPCODE5> 11<QUADRANT>
    ILLEGAL INSTRUCTION
    LUMOP=0xb not in ['0x0', '0x8', '0x10']
    plaintext

    这里我们看到,解码树在 FUNCT3=0x6 分支下,LUMOP 的合法值只有 0x0(常规单元步长)、0x8(整寄存器)、0x10(fault-only-first),而 0xb 被拒。
    同时,二进制展开中 rs1vd 字段也被正确识别(尽管它们不是解码路径的一部分),帮助用户直观理解该编码的构成。

  • 讨论:为什么没有流行的 RISC-V 解码 skill?#

    在开发过程中,我搜索了现有的开源工具和 skill,发现:

  • 大多数解码器(objdumpriscv-isatinyrv 等)都是精确查表器,对未知编码只报“invalid”或输出原始字节。

    • 没有工具能“汇报决策树路径”或“猜测最相似格式”。

    • 社区中也没有现成的 skill 或插件能一键完成这种分析。

  • 这是为什么?我觉得有几个可能的原因:

      1. 需求不常见:大多数开发者只关心合法指令的汇编,非法指令通常被视为“不该出现”的异常,一旦出现就归咎于工具链或编译器,不需要精细分析。
      2. 手工数 bit 的惯性:在调试底层问题时,很多开发者仍然习惯于手动拆解二进制(“数 bit”),虽然慢但直接,没有意识到可以自动化。
      3. 工具链门槛:要构建这样的工具,需要深入 gem5 的 ISA parser 或 UDB 的继承机制,这对普通开发者来说并不友好,导致没有形成通用的解决方案。
      4. RISC-V 的碎片化:指令集扩展繁多,不同配置下的编码空间差异大,一个静态的“最相似”算法难以覆盖所有组合,进一步阻碍了通用工具的出现。
  • 但我认为,随着 RISC-V 在 CPU 设计、内核开发、硬件加速等领域越来越深入,这种“能告诉你为什么非法”的工具会变得更有价值。它不仅帮助调试,也能用于教学(展示编码空间的层次结构),以及自定义扩展开发(帮助寻找可用的编码空间)。

  • 总结#

    本文从一次非法指令调试的痛点出发,梳理了 RISC-V 编码空间的结构化问题,指出当前 flat 表数据源的不足,并借助 gem5 的 decoder.isa 实现了解码路径追踪非法指令的诊断信息输出
    我们将其封装为一个独立的 skill(riscv-inst-decoder),并提供 gem5 parser 的 patch,以便社区复现和扩展。

    如果你也曾在面对 illegal instruction 时感到茫然,希望这个工具能帮你少数几个 bit,多几分从容。

  • 相关资源

从非法指令到解码路径:RISC-V 编码空间结构化的实践
https://blog.graphorall.top/blog/%E4%BB%8E%E9%9D%9E%E6%B3%95%E6%8C%87%E4%BB%A4%E5%88%B0%E8%A7%A3%E7%A0%81%E8%B7%AF%E5%BE%84%EF%BC%9ARISC-V%20%E7%BC%96%E7%A0%81%E7%A9%BA%E9%97%B4%E7%BB%93%E6%9E%84%E5%8C%96%E7%9A%84%E5%AE%9E%E8%B7%B5
Author rubbishzyc
Published at June 24, 2026