バイナリ命令デコーダのチュートリアル

このチュートリアルの目的は次のとおりです。

  • バイナリ形式の説明ファイルの構造と構文について学習します。
  • バイナリ形式の説明が ISA の説明とどのように一致するかを確認します。
  • RiscV RV32I サブセット命令のバイナリ記述を記述します。

概要

RiscV バイナリ命令エンコード

バイナリ命令エンコードは、マイクロプロセッサで実行する命令をエンコードする標準的な方法です。通常は、実行可能ファイル(通常は ELF 形式)に保存されます。命令は、固定幅または可変幅のいずれかです。

通常、命令は少数のエンコード形式を使用し、各形式はエンコードされる命令のタイプに合わせてカスタマイズされます。たとえば、レジスタ間命令では、使用可能なオペコードの数を最大化する形式を使用できますが、レジスタ即値命令では、使用可能なオペコードの数をトレードオフして、エンコードできる即値のサイズを大きくする形式を使用します。分岐命令とジャンプ命令では、ほとんどの場合、大きなオフセットを持つ分岐をサポートするために、即値のサイズを最大化する形式が使用されます。

RiscV シミュレータでデコードする命令で使用される命令形式は次のとおりです。

R-Type 形式: レジスタ間命令に使用されます。

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd オペコード

I 型形式。レジスタ即時命令、ロード命令、jalr 命令(12 ビット即値)に使用されます。

31~20 19..15 14..12 11..7 6 ~ 0
12 5 3 5 7
imm12 rs1 func3 rd オペコード

特殊 I 型形式。即時命令によるシフトに使用、5 ビット即値:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd オペコード

U 型形式: 長い即値命令(luiauipc)、20 ビットの即値に使用されます。

31..12 11 ~ 7 6..0
20 5 7
uimm20 rd オペコード

B 型形式: 条件付き分岐、12 ビットの即値に使用されます。

31 30..25 24..20 19..15 14..12 11..8 7 6..0
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm オペコード

J 型形式。jal 命令に使用されます。20 ビットの即値。

31 30 ~ 21 20 19..12 11 ~ 7 6..0
1 10 1 8 5 7
imm imm imm imm rd オペコード

S 型形式。ストア命令に使用されます。12 ビット即時値。

31 ~ 25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm rs2 rs1 func3 imm オペコード

これらの形式からわかるように、これらの命令はすべて 32 ビット長で、各形式の下位 7 ビットはオペコード フィールドです。また、いくつかの形式では即値のサイズが同じですが、ビットは命令の異なる部分から取得されます。後で説明するように、バイナリ デコーダの仕様形式ではこれを表現できます。

バイナリ エンコードの説明

命令のバイナリ エンコードは、バイナリ形式(.bin_fmt)の説明ファイルに表現されます。バイナリ形式の命令デコーダを生成できるように、ISA 内の命令のバイナリ エンコードを記述します。生成されたデコーダは、オペコードを決定し、オペランド フィールドと即値フィールドの値を抽出して、前回のチュートリアルで説明した ISA エンコードに依存しないデコーダに必要な情報を提供します。

このチュートリアルでは、小さな「Hello World」プログラムで使用される命令をシミュレートするために必要な RiscV32I 命令のサブセットのバイナリ エンコード記述ファイルを作成します。RiscV ISA の詳細については、Risc-V 仕様{.external} をご覧ください。

まず、riscv_bin_decoder/riscv32i.bin_fmt ファイルを開きます。

ファイルの内容は複数のセクションに分かれています。

まず、decoder の定義です。

decoder RiscV32I {
  // The namespace in which code will be generated.
  namespace mpact::sim::codelab;
  // The name (including any namespace qualifiers) of the opcode enum type.
  opcode_enum = "OpcodeEnum";
  // Include files specific to this decoder.
  includes {
    #include "riscv_isa_decoder/solution/riscv32i_decoder.h"
  }
  // Instruction groups for which to generate decode functions.
  RiscVInst32;
};

デコーダ定義では、デコーダ RiscV32I の名前と、4 つの追加情報を指定します。1 つ目は namespace で、生成されたコードが配置される名前空間を定義します。2 つ目は opcode_enum です。これは、ISA デコーダによって生成されたオペコード列挙型を生成されたコード内で参照する方法の名前です。3 つ目に、includes {} は、このデコーダ用に生成されるコードに必要なインクルード ファイルを指定します。 この例では、これは前回のチュートリアルの ISA デコーダによって生成されたファイルです。追加のインクルード ファイルは、グローバル スコープの includes {} 定義で指定できます。これは、複数のデコーダが定義され、それらすべてに同じファイルの一部を含める必要がある場合に便利です。4 つ目は、デコーダが生成される命令を構成する命令グループの名前のリストです。この例では、RiscVInst32 のみです。

次に、3 つの形式の定義があります。これらは、ファイル内ですでに定義されている命令で使用される 32 ビット命令ワードのさまざまな命令形式を表します。

// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
  fields:
    unsigned bits[25];
    unsigned opcode[7];
};

// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
  fields:
    signed imm12[12];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
  fields:
    unsigned fm[4];
    unsigned pred[4];
    unsigned succ[4];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

1 つ目は、Inst32Format という名前の 32 ビット幅命令形式を定義します。この形式には、bits(25 ビット幅)と opcode(7 ビット幅)の 2 つのフィールドがあります。各フィールドは unsigned です。つまり、値が抽出されて C++ 整数型に配置されるときにゼロ拡張されます。ビットフィールドの幅の合計は、形式の幅と等しくする必要があります。不一致がある場合は、ツールによってエラーが生成されます。この形式は他の形式から派生していないため、最上位形式と見なされます。

2 つ目は、Inst32Format から派生した 32 ビット幅の命令形式 IType を定義し、これらの 2 つの形式を関連させます。この形式には、imm12rs1func3rdopcode の 5 つのフィールドがあります。imm12 フィールドは signed です。これは、値が抽出されて C++ 整数型に配置されるときに、値が符号拡張されることを意味します。IType.opcode は、同じ符号付き / 符号なし属性を持ち、Inst32Format.opcode と同じ命令ワード ビットを参照します。

3 つ目の形式は、fence 命令でのみ使用されるカスタム形式です。これはすでに指定されている命令であり、このチュートリアルでは考慮する必要はありません。

重要なポイント: 同じビットを表し、同じ符号付き / 符号なし属性を持つ限り、異なる関連形式のフィールド名を再利用します。

riscv32i.bin_fmt の形式定義の後に、命令グループ定義が続きます。命令グループ内のすべての命令は、同じビット長で、同じトップレベル命令形式から(間接的に)派生した形式を使用する必要があります。ISA で異なる長さの命令を使用できる場合は、長さごとに異なる命令グループが使用されます。また、ターゲット ISA デコードが Arm 命令と Thumb 命令などの実行モードに依存する場合は、モードごとに個別の命令グループが必要です。bin_fmt パーサーは、命令グループごとにバイナリ デコーダを生成します。

instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
  fence   : Fence  : func3 == 0b000, opcode == 0b000'1111;
  csrs    : IType  : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
  csrw_nr : IType  : func3 == 0b001, rd == 0,  opcode == 0b111'0011;
  csrs_nw : IType  : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};

命令グループは、名前 RiscV32I、幅 [32]"OpcodeEnum" を使用するオペコード列挙型の名前、基本命令形式を定義します。オペコード列挙型は、ISA デコーダのチュートリアルで説明されている形式に依存しない命令デコーダによって生成されるものと同じである必要があります。

各命令エンコードの説明は、次の 3 つの部分で構成されています。

  • オペコード名。2 つが連携するには、命令デコーダの説明で使用したものと同じにする必要があります。
  • オペコードに使用する命令形式。これは、最後の部分でビットフィールドへの参照を満たすために使用される形式です。
  • オペコードが命令語と正常に一致するために、すべて true である必要があるビットフィールド制約(==!=<<=>>=)のカンマ区切りのリスト。

.bin_fmt パーサーは、これらの情報をすべて使用して、次のようなデコーダを構築します。

  • すべての形式のビットフィールドごとに、必要に応じて抽出関数(符号付き/符号なし)を提供します。抽出関数は、形式名の下付き文字バージョンで命名された名前空間に配置されます。たとえば、形式 IType の抽出関数は名前空間 i_type に配置されます。各エクストラクタ関数は inline として宣言され、形式の幅を保持する最も狭い uint_t 型を取り、抽出されたフィールド幅を保持する最も狭い int_t(符号付きの場合)または uint_t(符号なしの場合)型を返します。例:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • 命令グループごとのデコード関数。OpcodeEnum 型の値を返します。また、命令グループ形式の幅を保持する最も狭い uint_t 型を受け取ります。

最初のビルドを実行する

ディレクトリを riscv_bin_decoder に変更し、次のコマンドを使用してプロジェクトをビルドします。

$ cd riscv_bin_decoder
$ bazel build :all

ディレクトリをリポジトリ ルートに戻します。生成されたソースを見てみましょう。これを行うには、ディレクトリを bazel-out/k8-fastbuild/bin/riscv_bin_decoder に変更します(x86 ホストを使用していることを前提としています。他のホストの場合、k8-fastbuild は別の文字列になります)。

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

生成されたヘッダー ファイル(.h)

riscv32i_bin_decoder.h を開きます。ファイルの最初の部分には、標準のボイラープレート ガード、インクルード ファイル、名前空間宣言が含まれています。その次に、名前空間 internal にテンプレート化されたヘルパー関数があります。この関数は、64 ビットの C++ 整数に収まらない長い形式からビットフィールドを抽出するために使用されます。

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

#include "third_party/absl/functional/any_invocable.h"


#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
                            int bit_index, int width) {
  if (width == 0) return 0;

  int byte_pos = bit_index >> 3;
  int end_byte = (bit_index + width - 1) >> 3;
  int start_bit = bit_index & 0x7;

  // If it is only from one byte, extract and return.
  if (byte_pos == end_byte) {
    uint8_t mask = 0xff >> start_bit;
    return (mask & data[byte_pos]) >> (8 - start_bit - width);
  }

  // Extract from the first byte.
  T val = 0;
  val = data[byte_pos++] & 0xff >> start_bit;
  int remainder = width - (8 - start_bit);
  while (remainder >= 8) {
    val = (val << 8) | data[byte_pos++];
    remainder -= 8;
  }

  // Extract any remaining bits.
  if (remainder > 0) {
    val <<= remainder;
    int shift = 8 - remainder;
    uint8_t mask = 0b1111'1111 << shift;
    val |= (data[byte_pos] & mask) >> shift;
  }
  return val;
}

}  // namespace internal

最初のセクションの後に、riscv32i.bin_fmt ファイル内の format 宣言ごとに 1 つずつ、3 つの Namespace のセットが続きます。


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

これらの各名前空間には、その形式の各ビットフィールドの inline ビットフィールド抽出関数が定義されています。さらに、基本形式では、1)フィールド名が 1 つのフィールド名でのみ出現する、または 2)フィールド名が出現する各形式で同じ型フィールド(符号付き/符号なし、ビット位置)を参照する子孫形式から抽出関数が重複します。これにより、最上位の形式名前空間の関数を使用して、同じビットを記述するビットフィールドを抽出できます。

i_type 名前空間の関数を以下に示します。

namespace i_type {

inline uint8_t ExtractFunc3(uint32_t value) {
  return  (value >> 12) & 0x7;
}

inline int16_t ExtractImm12(uint32_t value) {
  int16_t result = ( (value >> 20) & 0xfff) << 4;
  result = result >> 4;
  return result;
}

inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}

inline uint8_t ExtractRd(uint32_t value) {
  return  (value >> 7) & 0x1f;
}

inline uint8_t ExtractRs1(uint32_t value) {
  return  (value >> 15) & 0x1f;
}

}  // namespace i_type

最後に、命令グループ RiscVInst32 のデコーダ関数の関数宣言が宣言されます。32 ビット符号なしを命令語の値として受け取り、一致する OpcodeEnum 列挙型クラス メンバーを返します。一致しない場合は OpcodeEnum::kNone を返します。

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

生成されたソースファイル(.cc)

riscv32i_bin_decoder.cc を開きます。ファイルの最初の部分には、#include と名前空間の宣言が含まれ、その後にデコーダ関数の宣言が続きます。

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);

DecodeRiscVInst32None は、空のデコード アクション(OpcodeEnum::kNone を返すアクション)に使用されます。他の 3 つの関数は、生成されたデコーダを構成します。全体的なデコーダは階層型で動作します。命令語内のビットセットが計算され、最上位レベルの命令または命令グループを区別します。ビットは連続している必要はありません。ビット数は、第 2 レベルのデコーダ関数で入力されるルックアップ テーブルのサイズを決定します。これは、ファイルの次のセクションに表示されます。

absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,

    ...

    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,

    ...
};

最後に、デコーダ関数を定義します。

OpcodeEnum DecodeRiscVInst32None(uint32_t) {
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
  if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 2) & 0x1f;
  index |= (inst_word >> 7) & 0x60;
  return parse_group_RiscVInst32_0[index](inst_word);
}

OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
  return OpcodeEnum::kFence;
}

OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
  if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
  return OpcodeEnum::kCsrwNr;
}

OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
  uint32_t rs1_value = (inst_word >> 15) & 0x1f;
  if (rs1_value != 0x0)
    return OpcodeEnum::kCsrs;
  if (rs1_value == 0x0)
    return OpcodeEnum::kCsrsNw;
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
  OpcodeEnum opcode;
  opcode = DecodeRiscVInst32_0(inst_word);
  return opcode;
}

この場合、定義されている命令は 4 つしかないため、デコード レベルは 1 つだけで、非常にスパースなルックアップ テーブルがあります。命令が追加されると、デコーダの構造が変更され、デコーダ テーブル階層のレベル数が増加する可能性があります。


レジスタ間 ALU 命令を追加

次に、riscv32i.bin_fmt ファイルに新しい指示を追加します。命令の最初のグループは、addand などのレジスタ レジスタ ALU 命令です。RiscV32 では、これらはすべて R 型のバイナリ命令形式を使用します。

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd オペコード

まず、形式を追加します。任意のエディタで riscv32i.bin_fmt を開きます。Inst32Format の直後に、Inst32Format から派生した RType という形式を追加します。RType のすべてのビットフィールドは unsigned です。上の表の名前、ビット幅、順序(左から右)を使用して形式を定義します。ヒントや解決策の全文は、こちらをクリックしてください。

次に、手順を追加する必要があります。手順は次のとおりです。

  • add - 整数の加算。
  • and - ビット演算 AND。
  • or - ビット演算 OR。
  • sll - 左にシフトします。
  • sltu - 未符号の「小なり」を設定します。
  • sub - 整数の減算。
  • xor - ビット演算 XOR。

エンコードは次のとおりです。

31..25 24..20 19..15 14..12 11..7 6..0 オペコード名
000 0000 rs2 rs1 000 rd 011 0011 追加
000,000 rs2 rs1 111 rd 011 0011
000 0000 rs2 rs1 110 rd 011 0011 または
000 0000 rs2 rs1 001 rd 011 0011 sll
000,000 rs2 rs1 011 rd 011 0011 sltu
010 0000 rs2 rs1 000 rd 011 0011 Pub/Subです
000 0000 rs2 rs1 100 011 0011 xor
func7 func3 オペコード

これらの命令定義は、RiscVInst32 命令グループ内の他の命令の前に追加します。バイナリ文字列は、先頭に 0b という接頭辞を付けて指定します(16 進数の 0x と同様です)。2 進数の長い文字列を読みやすくするために、適当な場所に数字の区切りとして単一引用符 ' を挿入することもできます。

これらの各命令定義には、func7func3opcode の 3 つの制約があります。sub を除くすべての場合、func7 制約は次のようになります。

func7 == 0b000'0000

func3 制約は、ほとんどの命令によって異なります。addsub の場合:

func3 == 0b000

opcode 制約は、これらの各命令で同じです。

opcode == 0b011'0011

各行をセミコロン ; で終了してください。

完成したソリューションはこちらです。

先ほどと同じようにプロジェクトをビルドし、生成された riscv32i_bin_decoder.cc ファイルを開きます。新しい命令を処理するために、追加のデコーダ関数が生成されていることがわかります。ほとんどは以前に生成されたものと似ていますが、add / sub デコードに使用される DecodeRiscVInst32_0_c をご覧ください。

OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
  static constexpr OpcodeEnum opcodes[2] = {
    OpcodeEnum::kAdd,
    OpcodeEnum::kSub,
  };
  if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 30) & 0x1;
  return opcodes[index];
}

この関数では、静的デコード テーブルが生成され、命令ワードからルックアップ値が抽出されて適切なインデックスが選択されます。これにより、命令デコーダ階層に 2 番目のレイヤが追加されますが、オペコードは追加の比較なしでテーブルで直接検索できるため、別の関数呼び出しを必要とせずに、この関数にインライン化されます。


即値を含む ALU 命令を追加

次に追加する命令セットは、レジスタの代わりに即値を使用する ALU 命令です。これらの命令には、即値フィールドに基づいて 3 つのグループがあります。12 ビットの符号付き即値を使用する I-Type 即値命令、シフト専用の I-Type 即値命令、20 ビットの符号なし即値を使用する U-Type 即値です。形式は次のとおりです。

I-Type 即時形式:

31~20 19..15 14..12 11..7 6 ~ 0
12 5 3 5 7
imm12 rs1 func3 rd オペコード

特殊な I-Type 即値形式:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd オペコード

U タイプの即時形式:

31 ~ 12 11 ~ 7 6..0
20 5 7
uimm20 rd オペコード

I-Type 形式はすでに riscv32i.bin_fmt に存在するため、この形式を追加する必要はありません。

特殊な I タイプの形式を、前回の演習で定義した R タイプの形式と比較すると、唯一の違いは rs2 フィールドの名前が uimm5 に変更されていることです。まったく新しい形式を追加する代わりに、R-Type 形式を拡張できます。別のフィールドを追加するとフォーマットの幅が広がるため、追加できませんが、オーバーレイを追加することはできます。オーバーレイは、形式の一連のビットのエイリアスであり、形式の複数のサブシーケンスを別の名前付きエンティティに結合するために使用できます。副作用として、生成されるコードには、フィールドの抽出関数に加えて、オーバーレイの抽出関数も含まれるようになります。この場合、rs2uimm5 の両方が符号なしの場合、フィールドが即値として使用されることを明示する以外に、大きな違いはありません。uimm5 という名前のオーバーレイを R-Type 形式に追加するには、最後のフィールドの後に次のように追加します。

  overlays:
    unsigned uimm5[5] = rs2;

追加する必要がある新しい形式は U タイプ形式のみです。形式を追加する前に、その形式を使用する 2 つの命令(auipclui)について考えてみましょう。どちらも、20 ビットの即値を 12 左にシフトしてから、PC を追加するか(auipc)、レジスタに直接書き込みます(lui)。オーバーレイを使用すると、即値の前にシフトされたバージョンを提供できるため、命令実行から命令デコードに計算を少しシフトできます。まず、上記の表に指定されているフィールドに従って形式を追加します。次に、次のオーバーレイを追加します。

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

オーバーレイ構文を使用すると、フィールドだけでなくリテラルも連結できます。この例では、12 個のゼロと連結して、実質的に 12 桁左にシフトしています。

追加する I-Type の手順は次のとおりです。

  • addi - 即時を追加します。
  • andi - 即値を使用したビット演算 AND。
  • ori - 即値を使用したビット演算 OR。
  • xori - 即値とのビット演算 XOR。

エンコードは次のとおりです。

31 ~ 20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 001 0011 addi
imm12 rs1 111 rd 001 0011 andi
imm12 rs1 110 001 0011 ori
imm12 rs1 100 rd 001 0011 xori
func3 オペコード

追加する必要がある R-Type(特殊な I-Type)命令は次のとおりです。

  • slli - 即値で論理左シフトします。
  • srai - 即値による右シフト演算。
  • srli - 即値で論理右シフト。

エンコードは次のとおりです。

31..25 24..20 19..15 14..12 11..7 6..0 オペコード名
000,000 uimm5 rs1 001 001 0011 slli
010 0000 uimm5 rs1 101 001 0011 スライ
000 0000 uimm5 rs1 101 001 0011 srli
func7 func3 オペコード

追加する必要がある U-Type 命令は次のとおりです。

  • auipc - 上位即値を PC に追加。
  • lui - 上位の即時値を読み込みます。

エンコードは次のとおりです。

31..12 11 ~ 7 6..0 オペコード名
uimm20 001 0111 auipc
uimm20 011 0111 lui
オペコード

変更を加えてビルドします。生成された出力を確認します。前回と同様に、riscv32i.bin_fmt と照らし合わせて作業を確認できます。


次に定義する命令セットは、条件付き分岐命令、ジャンプ アンド リンク命令、ジャンプ アンド リンク レジスタ命令です。

追加する条件分岐はすべて B 型エンコードを使用します。

31 ~ 25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 オペコード

B タイプのエンコードは R タイプのエンコードとレイアウトが同じですが、RiscV ドキュメントに合わせて新しい形式タイプを使用するようにしています。ただし、オーバーレイを追加して、R-Type エンコードの func7 フィールドと rd フィールドを使用して、適切な分岐ディスプレースメント即時値を取得することもできます。

上記のフィールドを含む形式 BType を追加することは必要ですが、それだけでは不十分です。ご覧のとおり、即値は 2 つの命令フィールドに分割されています。さらに、分岐命令では、これを 2 つのフィールドの単純な連結として扱いません。代わりに、各フィールドがさらにパーティショニングされ、これらのパーティションが異なる順序で連結されます。最後に、その値を 1 つ左にシフトして、16 ビットで調整されたオフセットを取得します。

即値の形成に使用される命令ワード内のビットの順序は、31、7、30~25、11~8 です。これは次のサブフィールド参照に対応します。インデックスまたは範囲はフィールドのビットを右から左へ番号付けします(つまり、imm7[6]imm7 の msb を指し、imm5[0]imm5 の lsb を指します。

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

このビット操作を分岐命令自体に組み込むことには、2 つの大きな欠点があります。まず、セマンティック関数の実装をバイナリ命令表現の詳細に関連付けます。2 つ目は、ランタイムのオーバーヘッドが増加することです。解決策は、BType 形式にオーバーレイを追加し、左シフトを考慮して末尾に「0」を追加することです。

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

オーバーレイは符号付きであるため、命令ワードから抽出されると自動的に符号拡張されます。

ジャンプ アンド リンク(即時)命令は、J タイプのエンコードを使用します。

31..12 11 ~ 7 6..0
20 5 7
imm20 オペコード

この形式も追加するのは簡単ですが、命令で使用される即値も思ったほど単純ではありません。完全な即値の形成に使用されるビットシーケンスは、31、19~12、20、30~21 です。最終的な即値は、半ワードの位置合わせのために 1 つ左にシフトされます。解決策は、別のオーバーレイ(左シフトを考慮して 21 ビット)を形式に追加することです。

  overlays:
    signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;

ご覧のとおり、オーバーレイの構文では、フィールド内の複数の範囲を省略形式で指定できます。また、フィールド名が使用されていない場合、ビット数は命令語自体を参照するため、上記は次のように記述することもできます。

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

最後に、ジャンプ アンド リンク(レジスタ)は、以前に使用された I 型形式を使用します。

I-Type 即値形式:

31~20 19..15 14..12 11..7 6 ~ 0
12 5 3 5 7
imm12 rs1 func3 rd オペコード

今回の変更では、フォーマットを変更する必要はありません。

追加する分岐命令は次のとおりです。

  • beq - 等しい場合に分岐します。
  • bge - 以上の場合分岐します。
  • bgeu - 無符号で等しい場合分岐します。
  • blt - 小さい場合の分岐。
  • bltu - 符号なしで小さい場合、分岐します。
  • bne - 等しくない場合に分岐します。

エンコードは次のとおりです。

31..25 24..20 19..15 14..12 11..7 6..0 オペコード名
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 オペコード

jal 命令は次のようにエンコードされます。

31 ~ 12 11 ~ 7 6..0 オペコード名
imm20 110 1111 jal
オペコード

jalr 命令は次のようにエンコードされます。

31~20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 110 0111 ジャル
func3 オペコード

変更を加えてからビルドします。生成された出力を確認します。前回と同様に、riscv32i.bin_fmt と照らし合わせて作業を確認できます。


店舗の手順を追加する

ストア命令は S 型エンコードを使用します。これは、即値の構成を除き、分岐命令で使用される B 型エンコードと同じです。RiscV のドキュメントに沿って、SType 形式を追加することを選択しました。

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 オペコード

SType 形式の場合、即値が 2 つの即値フィールドを単純に連結したものであるため、オーバーレイ指定は次のようになります。

  overlays:
    signed s_imm[12] = imm7, imm5;

フィールド全体を連結する場合、ビット範囲指定子が必要ないことに注意してください。

ストア インストラクションは次のようにエンコードされます。

31 ~ 25 24..20 19..15 14..12 11..7 6..0 オペコード名
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 sh
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 オペコード

変更を加えてビルドします。生成された出力を確認します。前回と同様に、riscv32i.bin_fmt と照らし合わせて作業を確認できます。


読み込み手順を追加

読み込み命令は I-Type 形式を使用します。そこで変更を行う必要はありません。

エンコードは次のとおりです。

31~20 19..15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 000 0011 lb
imm12 rs1 100 rd 000 0011 lbu
imm12 rs1 001 rd 000 0011 LH
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 000 0011 lw
func3 オペコード

変更を加えてビルドします。生成された出力を確認します。前回と同様に、riscv32i.bin_fmt と照らし合わせて作業を確認できます。

チュートリアルは以上です。ご参考になれば幸いです。