RiscV 集成解码器

本教程的目标是:

  • 了解生成的 ISA 和二元解码器如何协同工作。
  • 编写必要的 C++ 代码,为 RiscV 创建全指令解码器 结合了 ISA 和二进制解码器的 RV32I。

了解指令解码器

在给定指令地址的情况下,指令解码器负责读取 从内存获取指令字词,并返回 表示该指令的 Instruction

顶级解码器实现如下所示的 generic::DecoderInterface

// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
 public:
  // Return a decoded instruction for the given address. If there are errors
  // in the instruciton decoding, the decoder should still produce an
  // instruction that can be executed, but its semantic action function should
  // set an error condition in the simulation when executed.
  virtual Instruction *DecodeInstruction(uint64_t address) = 0;
  virtual ~DecoderInterface() = default;
};

如您所见,只需实现一种方法:cpp virtual Instruction *DecodeInstruction(uint64_t address);

现在,我们来看看生成的代码提供了哪些功能以及需要哪些功能。

首先,考虑文件中的顶级类 RiscV32IInstructionSet riscv32i_decoder.h:在 ISA 解码器。要重新查看内容,请导航到 重新生成全部教程

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

现在将您的目录更改回代码库的根目录, 生成的源代码为此,请将目录更改为 bazel-out/k8-fastbuild/bin/riscv_isa_decoder(假设您使用的是 x86) host - 对于其他主机,k8-Fastbuild 将是另一个字符串)。

$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

您会看到列出了生成的 C++ 代码的四个源文件:

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

打开第一个文件 riscv32i_decoder.h。这里有三个类, 请参阅以下内容:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

请注意类的命名。所有类都是根据 “isa”中指定的名称的 Pascal 大小写形式声明: isa RiscV32I { ... }

我们先从 RiscVIInstructionSet 类开始。如下所示:

class RiscV32IInstructionSet {
 public:
  RiscV32IInstructionSet(ArchState *arch_state,
                         RiscV32IInstructionSetFactory *factory);
  Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);

 private:
  std::unique_ptr<Riscv32Slot> riscv32_decoder_;
  ArchState *arch_state_;
};

此类中没有虚拟方法,因此这是一个独立的类, 请注意两点首先,构造函数会获取一个指向 RiscV32IInstructionSetFactory 类。这是一个由 解码器将使用它来创建 RiscV32Slot 类的实例, 解码为 slot RiscV32 定义的所有指令,如 riscv32i.isa 文件。其次,Decode 方法接受一个额外的参数, 是指向 RiscV32IEncodingBase 的指针,这是一个提供 第一个教程中生成的 ISA 解码器与 解码器。

RiscV32IInstructionSetFactory 类是一个抽象类,我们从 必须为完整解码器派生我们自己的实现。在大多数情况下 非常简单:只需提供一个方法来调用每个 我们在 .isa 文件中定义的 slot 类。在这个例子中,这非常简单,就像 只有一个这样的类:Riscv32Slot(名称为 riscv32 的 Pascal 大小写形式) 与 Slot 串联在一起)。系统不会为您生成该方法, 一些高级用例,在这些用例中,派生子类可能会很有用 并调用其构造函数。

在本单元稍后的部分中,我们将介绍最后一个类 RiscV32IEncodingBase。 因为这是另一个练习的主题。


定义顶级指令解码器

定义工厂类

如果您针对第一个教程重新构建了项目,请务必改回 riscv_full_decoder 目录中。

打开文件 riscv32_decoder.h。所有必要的包含文件都具有 并且命名空间已设置完毕。

在标记为 //Exercise 1 - step 1 的注释后,定义类 RiscV32IsaFactoryRiscV32IInstructionSetFactory 继承。

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

接下来,定义 CreateRiscv32Slot 的替换项。由于我们并未使用 派生了 Riscv32Slot 的类,因此只需使用以下代码即可分配新实例: std::make_unique

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处

定义解码器类

构造函数、析构函数和方法声明

接下来,定义解码器类。在上述同一文件中,前往 RiscV32Decoder 的声明。将声明扩展为类定义 其中,RiscV32Decoder 继承自 generic::DecoderInterface

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

接下来,在编写构造函数之前,我们快速看一下代码 关于二进制解码器的第二版教程中生成的示例。除了 Extract 函数,则还有函数 DecodeRiscVInst32

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

该函数接受需要解码的指令字词,并返回 与该指令匹配的操作码。另一方面, DecodeInterfaceRiscV32Decoder仅在 地址。因此,RiscV32Decoder 类必须能够访问内存 读取要传递给 DecodeRiscVInst32() 的指令字词。在此项目中 访问内存的方法是通过 .../mpact/sim/util/memory 恰如其分地命名为 util::MemoryInterface,如下所示:

  // Load data from address into the DataBuffer, then schedule the Instruction
  // inst (if not nullptr) to be executed (using the function delay line) with
  // context. The size of the data access is based on size of the data buffer.
  virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
                    ReferenceCount *context) = 0;

此外,我们还需要能够将 state 类实例传递给 其他解码器类的构造函数。相应的状态类 riscv::RiscVState 类,派生自 generic::ArchState,添加了 RiscV 的功能。也就是说,我们必须声明构造函数, 可以获取指向 statememory 的指针:

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

删除默认构造函数并替换析构函数:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

接下来声明 DecodeInstruction 方法,我们需要替换 generic::DecoderInterface

generic::Instruction *DecodeInstruction(uint64_t address) override;

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处


数据成员定义

RiscV32Decoder 类需要私有数据成员来存储 构造函数形参和指向工厂类的指针。

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

它还需要一个指向从上派生的编码类的指针 RiscV32IEncodingBase,我们将其称为 RiscV32IEncoding( (在练习 2 中)。此外,它还需要一个指向 RiscV32IInstructionSet,因此请添加:

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

最后,我们需要定义与内存接口搭配使用的数据成员:

  generic::DataBuffer *inst_db_;

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处

定义解码器类方法

接下来,您需要实现构造函数、析构函数和 DecodeInstruction 方法结合使用。打开文件 riscv32_decoder.cc。空 方法以及命名空间声明中, 共 using 项声明。

构造函数的定义

构造函数只需要初始化数据成员。首先,初始化 state_memory_

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

接下来,分配每个解码器相关类的实例,传入 适当的参数

  // Allocate the isa factory class, the top level isa decoder instance, and
  // the encoding parser.
  riscv_isa_factory_ = new RiscV32IsaFactory();
  riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
  riscv_encoding_ = new RiscV32IEncoding(state);

最后,分配 DataBuffer 实例。它是使用工厂分配的 可通过 state_ 成员访问。我们分配一个数据缓冲区 单个 uint32_t,因为这是指令字词的大小。

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

析构函数定义

析构函数很简单,只需释放我们在构造函数中分配的对象即可, 但要注意一个方面数据缓冲区实例是对引用计数的,因此 针对该指针调用 delete 后,我们对该对象执行 DecRef() 操作:

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

方法定义

在本例中,此方法的实现非常简单。我们假设 地址已正确对齐,且无需进行额外的错误检查 必填字段。

首先,必须使用内存从内存中提取指令字词 接口和 DataBuffer 实例。

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

接下来,我们调用 RiscVIEncoding 实例来解析指令字词, 必须在调用 ISA 解码器本身之前完成。回想一下,ISA 解码器直接调用 RiscVIEncoding 实例来获取操作码 和运算数指定。我们还没有实施 类,不过让我们使用 void ParseInstruction(uint32_t) 作为该方法。

  riscv_encoding_->ParseInstruction(iword);

最后,我们调用 ISA 解码器,传入地址和 Encoding 类。

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处


编码类

编码类会实现解码器类使用的接口 来获取指令运算码及其源运算数和目标运算数,以及 资源运算数。这些对象都依赖于二进制文件中的信息 格式解码器(例如操作码)、 它与解码器类分开,以保持其 不限定编码,并且支持多种不同的编码方案 。

RiscV32IEncodingBase 是一个抽象类。我们需要使用一组方法, 在派生类中实现的功能如下所示。

class RiscV32IEncodingBase {
 public:
  virtual ~RiscV32IEncodingBase() = default;

  virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;

  virtual ResourceOperandInterface *
              GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                       SimpleResourceVector &resource_vec, int end) = 0;

  virtual ResourceOperandInterface *
              GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                        ComplexResourceEnum resource_op,
                                        int begin, int end) = 0;

  virtual PredicateOperandInterface *
              GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                           PredOpEnum pred_op) = 0;

  virtual SourceOperandInterface *
              GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                        SourceOpEnum source_op, int source_no) = 0;

  virtual DestinationOperandInterface *
              GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                             DestOpEnum dest_op, int dest_no, int latency) = 0;

  virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no) = 0;
};

乍一看,这有点复杂,特别是 但对于像 RiscV 这样的简单架构,我们实际上忽略了大部分 这些参数,因为其值隐含其中。

下面我们将依次介绍每种方法。

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

GetOpcode 方法会返回当前的 OpcodeEnum 成员 指令,用于标识指令操作码。OpcodeEnum 类 (在生成的 ISA 解码器文件 riscv32i_enums.h 中定义)。该方法采用 这两个参数,对于我们的目的而言,可以忽略这两个参数。第 1 个 是槽类型(也在 riscv32i_enums.h 中定义的枚举类), 由于 RiscV 只有一个槽,因此它只有一个可能的值: SlotEnum::kRiscv32。第二个是槽的实例编号(如果 该槽有多个实例,这可能在某些 VLIW 中出现 架构)。

ResourceOperandInterface *
    GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                     SimpleResourceVector &resource_vec, int end)

ResourceOperandInterface *
    GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                      ComplexResourceEnum resource_op,
                                      int begin, int end);

接下来的两种方法用于对处理器中的硬件资源进行建模 以提高周期准确性。在教程练习中,我们不会使用 因此,在实现过程中,系统会对它们执行桩,返回 nullptr

PredicateOperandInterface *
            GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                         PredOpEnum pred_op);

SourceOperandInterface *
            GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                      SourceOpEnum source_op, int source_no);

DestinationOperandInterface *
            GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                           DestOpEnum dest_op, int dest_no, int latency);

这三个方法会返回 指令语义函数来访问任意指令的值 谓词运算数,每个指令源运算数,并写入新的 值转换为指令目标运算数。由于 RiscV 不使用 指令谓词,该方法只需返回 nullptr

这些函数的参数模式是类似的。首先,就像 GetOpcode 表示传入槽位和条目。然后是 为其创建操作数的指令。只有当 不同的操作码需要针对同一操作数返回不同的操作数对象 但此 RiscV 模拟器并非如此。

接下来是谓词、来源、目标、操作数枚举条目, 标识需要创建的操作数。这些信息来自 riscv32i_enums.h 中的 OpEnums,如下所示:

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

如果您回顾一下 riscv32.isa 您会注意到,它们对应于 在每个指令的声明中使用的操作数名称。通过使用不同的 表示不同位字段和操作数的操作数的操作数名称 它使得编写编码类变得更加轻松,因为枚举成员是唯一的 确定要返回的确切操作数类型,且无需 请考虑 slot、entry 或 opcode 参数的值。

最后,对于源操作数和目标操作数, 操作数会被传入(同样,我们可以忽略此操作),而对于目的地, 操作数,指令与命令提示符之间经过的延迟时间(以周期为单位) 且目标结果可用于后续指令。 在我们的模拟器中,延迟时间为 0,这意味着指令写入 系统会将结果立即输出到寄存器中。

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

最后一个函数用于获取特定目的地的延迟时间 操作数,前提是已在 .isa 文件中将其指定为 *。这种情况并不常见 和 不用于此 RiscV 模拟器,因此我们对此函数的实现 则会返回 0。


定义编码类

头文件 (.h)

方法

打开文件 riscv32i_encoding.h。所有必要的包含文件都具有 并且命名空间已设置完毕。所有代码添加操作均为 已关注评论“// Exercise 2.

首先,我们来定义一个类 RiscV32IEncoding,该类继承自 生成的接口

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

接下来,构造函数应接受指向状态实例的指针,在本例中为 指向 riscv::RiscVState 的指针。应使用默认析构函数。

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

在添加所有接口方法之前,我们先添加由 RiscV32Decoder 用于解析指令:

void ParseInstruction(uint32_t inst_word);

接下来,让我们添加在丢弃 不使用的参数的名称:

// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
                                                   SimpleResourceVector &,
                                                   int) override {
  return nullptr;
}

ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
                                                    ComplexResourceEnum ,
                                                    int, int) override {
  return nullptr;
}

PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
                                        PredOpEnum) override {
  return nullptr;
}

int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }

最后,添加公共接口的其余方法替换项,但使用 遵循 .cc 文件的实现。


OpcodeEnum GetOpcode(SlotEnum, int) override;

SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
                                  SourceOpEnum source_op, int) override;

DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
                                            DestOpEnum dest_op, int,
                                            int latency) override;

为了简化每个操作数 getter 方法的实现 我们将创建两个 Callables(函数对象)数组,这些对象按 SourceOpEnumDestOpEnum 成员的数值。 这样,方法的正文可简化为调用 用于传入并返回其返回值的枚举值的函数对象 值。

为了组织这两个数组的初始化,我们定义了两个私有数组, 将通过构造函数调用的方法,如下所示:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

数据成员

所需的数据成员如下所示:

  • state_ 用于保存 riscv::RiscVState * 值。
  • uint32_t 类型的 inst_word_,用于存储当前值 说明文字。
  • opcode_,用于存储当前指令的运算码, ParseInstruction 方法。类型为 OpcodeEnum
  • source_op_getters_ 是一个数组,用于存储用于获取来源的 Callables 操作数对象。数组元素的类型是 absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_ 是一个数组,用于存储获取 目标操作数对象。数组元素的类型是 absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias:RscV 整数寄存器 ABI 名称的数组,例如“zero”和 “ra”而非“x0”和“x1”。

  riscv::RiscVState *state_;
  uint32_t inst_word_;
  OpcodeEnum opcode_;

  absl::AnyInvocable<SourceOperandInterface *()>
      source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
  absl::AnyInvocable<DestinationOperandInterface *(int)>
      dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];

  const std::string xreg_alias_[32] = {
      "zero", "ra", "sp", "gp", "tp",  "t0",  "t1", "t2", "s0", "s1", "a0",
      "a1",   "a2", "a3", "a4", "a5",  "a6",  "a7", "s2", "s3", "s4", "s5",
      "s6",   "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处

源文件 (.cc)。

打开文件 riscv32i_encoding.cc。所有必要的包含文件都具有 并且命名空间已设置完毕。所有代码添加操作均为 已关注评论“// Exercise 2.

辅助函数

首先,我们将编写几个辅助函数, 源寄存器和目标寄存器运算数。这些将在 该寄存器类型,并且将调用 RiscVState 对象以获取 注册对象,然后在寄存器对象中调用操作数工厂方法。

我们先从目标运算数帮助程序开始:

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency);
}

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency,
    const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency, op_name);
}

如您所见,有两个辅助函数。第二个需要额外 参数 op_name,允许操作数具有不同的名称或字符串 表示法,而不是底层寄存器。

同样,对于源运算数帮助程序:

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand();
  return op;
}

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

构造函数和接口函数

构造函数和接口函数非常简单。构造函数 只会调用两个初始化方法来初始化以下对象的 Callables 数组: 操作数 getter 的方法。

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction 先存储指令字,然后存储指令字和 从调用二进制解码器生成的代码中获取的。

// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
  inst_word_ = inst_word;
  opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}

最后,操作数 getter 从它调用的 getter 函数返回值 基于使用目标/源操作数枚举值的数组查找。


DestinationOperandInterface *RiscV32IEncoding::GetDestination(
    SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
  return dest_op_getters_[static_cast<int>(dest_op)](latency);
}

SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
                                                    SourceOpEnum source_op, int) {
  return source_op_getters_[static_cast<int>(source_op)]();
}

数组初始化方法

您可能已经猜到,大部分工作是初始化 getter 数组,但不必担心,这是使用简单的重复模式完成的。让我们 请先从 InitializeDestinationOpGetters() 开始,因为只有 几个目标操作数。

调用 riscv32i_enums.h 中生成的 DestOpEnum 类:

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

对于 dest_op_getters_,我们需要初始化 4 个条目,kNone 各一个, kCsrkNextPckRd。为方便起见,每个条目都使用 lambda。签名 的 lambda 为 void(int latency)

到目前为止,我们还没有说过 在 MPACT-Sim 中定义的操作数。在本练习中,我们将仅使用 类型:generic::RegisterDestinationOperand(在 register.h, 和 generic::DevNullOperand 中定义的 devnull_operand.h。 目前,这些运算数的细节并不是很重要, 前者用于写入寄存器,后者会忽略所有写入操作。

kNone 的第一个条目很简单 - 只需返回一个 nullptr 作为可选的 记录错误。

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

下一条是kCsr。在这里,我们将进行一些作弊。“hello world”节目 不依赖于任何实际的 CSR 更新,但有一些样板代码 执行 CSR 指令。解决方法是使用 名为“CSR”的常规寄存器并向其传送所有此类写入

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

接下来是 kNextPc,它指的是“pc”注册。它用作 。名称在 RiscVState 中的定义为 kPcName

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

最后是 kRd 目标操作数。在 riscv32i.isa 中,操作数 rd 仅用于引用在“rd”中编码的整数寄存器字段 因此不会造成任何歧义。那里 只是一项复杂功能。寄存器 x0(abi 名称 zero)通过硬连接为 0, 因此对于该寄存器,我们使用 DevNullOperand

因此,在此 getter 中,我们首先使用rd 从 .bin_fmt 文件生成的 Extract 方法。如果值为 0,我们 返回“DevNull”运算数,否则我们将返回正确的寄存器运算数, 注意将适当的寄存器别名用作操作数名称。

  dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
    // First extract register number from rd field.
    int num = inst32_format::ExtractRd(inst_word_);
    // For register x0, return the DevNull operand.
    if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
    // Return the proper register operand.
    return GetRegisterDestinationOp<RV32Register>(
      state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
      xreg_alias_[num]);
    )
  }
}

现在进入 InitializeSourceOperandGetters() 方法,其中模式为 大致相同,但细节略有不同。

首先,我们看一下从 Cloud Functions 函数生成的 SourceOpEnum riscv32i.isa

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

通过检查,除了kNone,他们分为两个组。一个 是立即运算数:kBimm12kImm12kJimm20kSimm12kUimm20kUimm5。另一个是寄存器运算数:kCsrkRs1kRs2

kNone 运算数的处理方式与目标运算数的处理方式相同 - 返回 nullptr。

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

接下来,我们来处理寄存器运算数。我们将处理与kCsr类似的 对应目标运算数的处理方式 - 只需调用 使用“CSR”的辅助函数作为寄存器名称。

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

运算数 kRs1kRs2 的处理方式与 kRd 等效,不同之处在于 虽然我们不想更新 x0(或 zero),但我们需要确保, 始终从该操作数读取 0。为此,我们将使用 generic::IntLiteralOperand<> 类在 literal_operand.h。 此运算数用于存储字面值(与模拟的 立即值)。否则,模式是相同的:首先提取 指令字词中的 rs1/rs2 值,如果为零,则返回字面量 操作数为 0,否则返回常规寄存器 使用辅助函数的源操作数,使用 abi 别名作为操作数 名称。

  source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs1(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs2(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };

最后,我们处理不同的立即操作数。即时值包括 存储在以下类中定义的 generic::ImmediateOperand<> 类的实例中: immediate_operand.h。 直接运算数的不同 getter 之间的唯一区别 使用哪个提取器函数,以及存储类型是带符号还是 无符号。

  // Immediates.
  source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractBImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractImm12(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm5(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractJImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractSImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm32(inst_word_));
  };
}

如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处

本教程到此结束。希望这些内容对您有所帮助。