如何新增/自定义扩展(三):修改spike,支持运行自定义扩展指令

如何新增/自定义扩展(三):修改spike,支持运行自定义扩展指令

——最近研究了一下如何基于开源toolchain和spike,自定义扩展。特此做一个记录/分享。

⚠️ 注意
   旨在分享,为避免某些收费平台转用,特此申明,本文不允许转载,只可贴本文链接。

写在最前面

全文共分三个章部分,列表如下:

Spike是 RISC-V ISA 模拟器。 可以仿真单核和多核的 RISC-V 处理器的功能模型。源码集成/关联在 riscv-tools 中。

Spike运行指令流程


Spike运行指令流程示意图
  1. spike main解析参数后,将isa字符串通过sim_t传递给processor_t
  2. 自定义扩展对象的实现通过libcustomext.so链接进 spike。
    1. 自定义的扩展通过REGISTER_EXTENSION注册到扩展表。REGISTER_EXTENSION宏将定义对应的注册类,声明static变量时调用构造函数。
    2. 构造函数中调用register_extension函数。该函数中将扩展写入(或者说注册进)extensions map 中(注意:代码中的extensions存放的是自定义扩展)。

Spike注册自定义指令
  1. processor_t构造时
    a) 实例化isa_parser_t解析isa字符串。
    b) 注册isa参数所指定的所有扩展,以及其指令(base insn)。
    c) 实例化 MMU(build_opcode_map),指令 load 进 iCache。

  2. processor_t step函数,一次执行一条指令。

    1. 先判断是否 debug 模式。是则执行 debug 模式操作;否则进行下一步。
    2. 执行指令序列:
      1. pending 中断处理
      2. 从 mmu 读取 insn
      3. 执行
      4. PC+=1

综上,根据isa_parser_t解析指令时的两条路径,可以按照标准 extension 方式新增 extension,也可以按照自定义方式新增 extension。

新增extension方案:类似std extension方式

修改isa parser

  1. riscv-isa-sim/riscv/isa_parser.h文件的isa_extension_t枚举中,添加EXT_XIM,示例如下。

添加XiM扩展枚举
  1. riscv-isa-sim-mod/disasm/isa_parser.cc文件中,在‘X’ extension之前,添加新增 extension,示例如下。

解析isa时,支持XiM扩展
🗨️ 说明
   如上图所示,对于‘X’自定义扩展,此处将扩展名称存入std::set extensions中。

指令定义

RISC-V 标准指令通过模板/宏定义的方式生成。可以参考上一篇文章《如何新增/自定义扩展(二):修改Toolchain,支持编译自定义扩展》。

声明指令

riscv-isa-sim/riscv/encoding.h文件中添加新的操作码定义。

#define MATCH_MDMUL 0x800002b
#define MASK_MDMUL 0xf800007f
......
DECLARE_INSN(mdmul, MATCH_MDMUL, MASK_MDMUL)
🗨️ 说明
   其中的 DECLARE_INSN 定义在processor.cc文件中。
void processor_t::register_base_instructions()
{
  #define DECLARE_INSN(name, match, mask) \
    insn_bits_t name##_match = (match), name##_mask = (mask); \
    isa_extension_t name##_ext = NUM_ISA_EXTENSIONS; \
    bool name##_overlapping = false;
  #include "encoding.h"
  #undef DECLARE_INSN
  ......
}

定义指令

修改编译脚本,在riscv-tools/riscv-isa-sim/riscv/riscv.mk.in中添加如下示例行。


解析isa时,支持XiM扩展
🗨️ 说明
1. insn_list.h文件,其中定义指令,如DEFINE_INSN(mdmul)DEFINE_INSN定义在processor.cc文件中register_base_instructions函数中。
点击查看代码
void processor_t::register_base_instructions()
{
  ......
  #define DEFINE_INSN(name) \
    extern reg_t fast_rv32i_##name(processor_t*, insn_t, reg_t); \
    extern reg_t fast_rv64i_##name(processor_t*, insn_t, reg_t); \
    extern reg_t fast_rv32e_##name(processor_t*, insn_t, reg_t); \
    extern reg_t fast_rv64e_##name(processor_t*, insn_t, reg_t); \
    extern reg_t logged_rv32i_##name(processor_t*, insn_t, reg_t); \
    extern reg_t logged_rv64i_##name(processor_t*, insn_t, reg_t); \
    extern reg_t logged_rv32e_##name(processor_t*, insn_t, reg_t); \
    extern reg_t logged_rv64e_##name(processor_t*, insn_t, reg_t);
  #include "insn_list.h"
  #undef DEFINE_INSN
  ......
  // add overlapping instructions first, in order
  #define DECLARE_OVERLAP_INSN(name, ext) \
  ......
  // add all other instructions.  since they are non-overlapping, the order
  // does not affect correctness, but more frequent instructions should
  // appear earlier to improve search time on opcode_cache misses.
  #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_INSN
    从上面代码可以看到,DEFINE_INSN使用了两次。
  • 第一次#include "insn_list.h"为一条指令声明了 8 个外部函数,然后定义 overlap 的指令。
  • 第二次#include "insn_list.h"定义非 overlap 的指令。
  • 定义指令调用了函数register_base_insn
2. 代码文件build/insn/mdmul.cc。在该文件中,定义了上述第一次#include "insn_list.h"时声明的 8 个外部函数,示例如下。
点击查看代码
reg_t logged_rv64i_mdmul(processor_t* p, insn_t insn, reg_t pc)
{
  #define xlen 64
  PROLOGUE;
  #include "insns/mdmul.h"
  EPILOGUE;
  #undef xlen
}
可见,在其中使用语句#include "insns/mdmul.h"引用该指令的具体实现。

实现指令

riscv-isa-sim/riscv/insns目录下,新增mdmul.h并实现该指令的逻辑功能。

点击查看代码
require_extension(EXT_XIM);
sreg_t rd = sext_xlen(RD);
sreg_t rs = sext_xlen(READ_REG(insn.xim_rs1()));
sreg_t rt = sext_xlen(READ_REG(insn.xim_rs2()));
uint32_t i = sext_xlen(insn.xim_imm());
for(uint32_t m=0; m<i; m++)
  for(uint32_t n=0; n<i; n++)
  {
    uint32_t rs1 = MMU.load<uint32_t>(rs);
    uint32_t rs2 = MMU.load<uint32_t>(rt);
    MMU.store<uint32_t>(rd, rs1 * rs2);
    rs+=4;
    rt+=4;
    rd+=4;
  }

decode(if necessary)

如果增加的 extension/instruction 沿用现有 RISC-V 指令类型,不需要修改 decode。
本文示例中,自定义寄存器 bit 位,因此需要实现对应的 decode 功能。
在文件riscv-isa-sim/riscv/decode.h,类class insn_t中,添加如下示例代码。

  //xim extension
  uint64_t xim_rs1() { return x(12, 5); }
  uint64_t xim_rs2() { return x(17, 5); }
  uint64_t xim_imm() { return x(22, 5); }
🗨️ 说明
   rd沿用 RISC-V 指令格式 bit 位,因此不用再次定义。

指令asm解析

将指令解析位 asm 语句。否则运行时无法显示指令参数。


Spike运行时解析汇编指令失败示例

在文件riscv-isa-sim/disasm/disasm.cc中,增加如下示例代码。

点击查看代码
//xim extension
struct : public arg_t {
  std::string to_string(insn_t insn) const {
    return xpr_name[insn.xim_rs1()];
  }
} xim_rs1;

struct : public arg_t {
  std::string to_string(insn_t insn) const {
    return xpr_name[insn.xim_rs2()];
  }
} xim_rs2;

struct : public arg_t {
  std::string to_string(insn_t insn) const {
    return std::to_string((int)insn.xim_imm());
  }
} xim_imm;

然后在函数void disassembler_t::add_instructions(const isa_parser_t* isa, bool strict)中增加如下示例代码。

点击查看代码
void disassembler_t::add_instructions(const isa_parser_t* isa, bool strict)
{
  ......
  // XIM extension
  if(ext_enabled(EXT_XIM))
  {
    add_insn(new disasm_insn_t("mdmul", match_mdmul, mask_mdmul, {&xrd, &xim_rs1, &xim_rs2, &xim_imm}));
  }
  ......
}
⚠️ 注意
1. 宏定义ext_enabled展开后调用的时isa_parser_t中的extension_enabled函数。
从下面定义可见,常用的单字符扩展(如'I'、'A'、'M'等)可以传递字符类型。
本示例是'XIM',因此此处采用isa_extension_t枚举类型。
点击查看代码
  bool extension_enabled(unsigned char ext) const {
    return extension_enabled(isa_extension_t(ext));
  }
  bool extension_enabled(isa_extension_t ext) const {
    return extension_table[ext];
  }
2. 文件riscv-isa-sim/disasm/disasm.cc中重新定义了宏DECLARE_INSN,其中定义变量match_mdmul, mask_mdmul
点击查看代码
#define DECLARE_INSN(code, match, mask) \
  const uint32_t match_##code = match; \
  const uint32_t mask_##code = mask;
  #include "encoding.h"
#undef DECLARE_INSN

解析后的指令格式如下所示。


Spike运行时解析汇编指令成功示例

新增extension方案:customext方式

和章节《3.1 修改 isa parser》不同,自定义扩展方式,不需要在‘X’ extension之前,添加新增 extension。因此,就进入‘X’自定义扩展,将扩展名称存入std::set<std::string> extensions中。

定义扩展

riscv-isa-sim/customext文件夹中,创建xim.cc,并在其中实现扩展。

点击查看代码
#include "insn_macros.h"
#include "extension.h"
#include "decode_macros.h"
#include "mmu.h"
#include <cstring>

#define MATCH_MDMUL 0x800002b
#define MASK_MDMUL  0xf800007f

#define ILLEGAL_INSN_FUNC &::illegal_instruction

//定义指令各个bit字段
struct xim_insn_t
{
  unsigned opcode   : 7;
  unsigned xim_rd   : 5;
  unsigned xim_rs1  : 5;
  unsigned xim_rs2  : 5;
  unsigned xim_imm  : 5;
  unsigned funct    : 5;
};

//自定义指令和标准指令结构之间的转换
#define CHANGE_INSN_TO_XIM insn_bits_t insn_bits = insn.bits(); \
                           xim_insn_t xim; \
                           memcpy(&xim, &insn_bits, sizeof(xim));

//定义指令解析汇编字符串
#define DISASM_REG(name) struct: public arg_t \
{ \
  std::string to_string(insn_t insn) const \
  { \
    CHANGE_INSN_TO_XIM \
    return xpr_name[xim.xim_##name]; \
  } \
}xim_##name;

DISASM_REG(rd)
DISASM_REG(rs1)
DISASM_REG(rs2)
DISASM_REG(imm)

//实现指令功能
// implement for insn mdmul
static reg_t custom_xim_mdmul(processor_t* p, insn_t insn, reg_t pc)
{
  CHANGE_INSN_TO_XIM;

  state_t* state = p->get_state();
  sreg_t rd = state->XPR[xim.xim_rd];
  sreg_t rs = state->XPR[xim.xim_rs1];
  sreg_t rt = state->XPR[xim.xim_rs2];
  uint32_t i = xim.xim_imm;

  for(uint32_t m=0; m<i; m++)
    for(uint32_t n=0; n<i; n++)
    {
      uint32_t rs1 = p->get_mmu()->load<uint32_t>(rs);
      uint32_t rs2 = p->get_mmu()->load<uint32_t>(rt);
      p->get_mmu()->store<uint32_t>(rd, rs1 * rs2);
      rs+=4;
      rt+=4;
      rd+=4;
    }
  return pc + 4; \
}

//自定义扩展类
class xim_t : public extension_t
{
 public:
  const char* name() { return "xim"; }

  xim_t() {}

  //扩展包含的指令
  std::vector<insn_desc_t> get_instructions()
  {
    std::vector<insn_desc_t> insns;
    insns.push_back((insn_desc_t)
    {
      MATCH_MDMUL, MASK_MDMUL, 
      custom_xim_mdmul,   //fast_rv32i
      custom_xim_mdmul,   //fast_rv64i
      custom_xim_mdmul,   //fast_rv32e 
      custom_xim_mdmul,   //fast_rv64e
      custom_xim_mdmul,   //logged_rv32i
      custom_xim_mdmul,   //logged_rv64i
      custom_xim_mdmul,   //logged_rv32e
      ILLEGAL_INSN_FUNC   //logged_rv64e, assume not surpport
    });
    return insns;
  }

  //扩展中的指令汇编
  std::vector<disasm_insn_t*> get_disasms() {
    std::vector<disasm_insn_t*> insns;
    insns.push_back(new disasm_insn_t("mdmul", MATCH_MDMUL, MASK_MDMUL, {&xim_rd, &xim_rs1, &xim_rs2, &xim_imm}));
    return insns;
  }
};

//注册自定义扩展
REGISTER_EXTENSION(xim, []() { return new xim_t; })

修改 makefile,集成自定义扩展

riscv-isa-sim/customext/customext.mk.in中添加xim.cc行,示例如下。


添加自定义扩展源码文件

方案比较

一句话概括,相比类似std extension方式,customext方式是将所有实现集中在一个文件中

方案 优点 缺点
类似std extension方式 需要多处修改
customext方式 只需要修改两处 需要自定义指令字段转换

编译运行测试

riscv-tools/riscv-isa-sim目录,重新编译spike,然后运行测试程序。

# 前面编译过,存在build目录。若不存在,请先 mkdir build && cd build && ../configure
$cd riscv-tools/riscv-isa-sim/build
$make -j8

# 运行
$spike pk mdmul.elf
z[0]=4, z[1]=8, z[0]=12
z[3]=16, z[4]=20, z[5]=24
z[6]=28, z[7]=32, z[8]=36
posted @ 2025-02-27 18:41  Sky_Yuan  阅读(302)  评论(0)    收藏  举报