FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

LLVMのバックエンドを作るための第一歩 (5. 命令の定義)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

LLVMバックエンドを追加するにあたり、MYRISCVXアーキテクチャを定義するためのtdファイルを作成する必要がある。 tdファイルはLLVMのバックエンドを定義するためのDSLで、バックエンドを理解するためには避けては通れないものだ。

命令を定義する

MYRISCVXInstrInfo.tdには先ほど作成したMYRISCVXInstrFormats.tdをincludeする。

//===----------------------------------------------------------------------===//
// Instruction format superclass
//===----------------------------------------------------------------------===//

include "MYRISCVXInstrFormats.td"
//===----------------------------------------------------------------------===//
// MYRISCVX Operand, Complex Patterns and Transformations Definitions.
//===----------------------------------------------------------------------===//

// Instruction operand types
// Signed Operand
def simm12 : Operand<i32> {
  let DecoderMethod= "DecodeSimm12";
}
def immSExt12 : PatLeaf<(imm), [{ return isInt<12>(N->getSExtValue()); }]>;
// Arithmetic and logical instructions with 2 register operands.
class ArithLogicR<bits<7> opcode, bits<3> funct3, bits<7>funct7,
                  string instr_asm, SDNode OpNode,
                  RegisterClass RC> :
  MYRISCVX_R<opcode, funct3, funct7, (outs RC:$rd), (ins RC:$rs1, RC:$rs2),
  !strconcat(instr_asm, "\t$rd, $rs1, $rs2"),
  [(set RC:$rd, (OpNode RC:$rs1, RC:$rs2))], IIAlu> {
    let isReMaterializable = 1;
}


// Arithmetic and logical instructions with 2 register operands.
class ArithLogicI<bits<7> opcode, bits<3> funct3,
                  string instr_asm, SDNode OpNode,
                  Operand Od, PatLeaf imm_type, RegisterClass RC> :
  MYRISCVX_I<opcode, funct3, (outs RC:$rd), (ins RC:$rs1, Od:$imm12),
  !strconcat(instr_asm, "\t$rd, $rs1, $imm12"),
  [(set RC:$rd, (OpNode RC:$rs1, imm_type:$imm12))], IIAlu> {
    let isReMaterializable = 1;
}

class ArithLogicU<bits<7> opcode,
                  string instr_asm, RegisterClass RC, Operand Od, PatLeaf imm_type> :
  MYRISCVX_U<opcode, (outs RC:$rd), (ins Od:$imm20),
    !strconcat(instr_asm, "\t$rd, $imm20"), [], IIAlu> {
      let isReMaterializable = 1;
}
//===----------------------------------------------------------------------===//
// Instruction definition
//===----------------------------------------------------------------------===//

//===----------------------------------------------------------------------===//
// MYRISCVX Instructions
//===----------------------------------------------------------------------===//

def ADDI : ArithLogicI<0b0010011, 0b000, "addi", add, simm12, immSExt12, GPR>;

def LUI  : ArithLogicU<0b0110111, "lui", GPR, simm20, immSExt12>;
def ADD  : ArithLogicR<0b0110011, 0b000, 0b0000000, "add", add, GPR>;
def SUB  : ArithLogicR<0b0110011, 0b000, 0b0100000, "sub", sub, GPR>;

とりあえずこれだけだ。LUI, ADD, SUB, ADDIに加えてさらにこれらの定義をラップするためのArithLogicR, ArithLogicI, ArithLogicUを定義した。 これらは、似たような種類の命令を何度も定義するにあたり、同じような定義を何度も繰り返さないように作られたラップ用のクラスだ。

f:id:msyksphinz:20190501235430p:plain
ArithLogicI, ArithLogicR, ArithLogicUを定義した。

それぞれ、

  • ArithLogicR : MYRISCVX_Rを継承
  • ArithLogicI : MYRISCVX_Iを継承
  • ArithLogicU : MYRISCVX_Uを継承

となっており、その意味はすぐに分かると思う。 一つADDI命令に着目してみると、

def ADDI : ArithLogicI<0b0010011, 0b000, "addi", add, simm12, immSExt12, GPR>;

ArithLogicIでは、

  • opcode = 0b0010011
  • funct3 = 0b000
  • instr_asm = "addi"
  • OpNode = add
  • Od = simm12
  • imm_type = immSExt12
  • RC = GPR

となっている。opcodefunct3は命令デコードをそのまま示しており、これは分かりやすいと思う。 そして、MYRISCVX_Iクラスにそのままこの値が渡されて、命令でコードに使用される。 instr_asmは命令のニーモニックの表記だ。 ArithLogicI内で!strconcat(instr_asm, "\t$rd, $rs1, $imm12"),で、"addi"と"\t$rd, $rs1, $imm12"がconcatされて文字列が接合され、addi\t$rd, $rs1, $imm12が生成されていることが分かる。

RCレジスタクラスで、MYRISCVXRegisterInfo.tdで定義したGPRを設定している。 このGPR内のどれかのレジスタ割り当てられることになる。

つぎにOdだが、これはどのような即値情報が渡されるかを示している。 ADDIの定義ではsimm12が設定されている。 simm12は、12ビットの即値だが、tdの定義を見ればわかるように、32ビット整数のクラスを継承している。 これは後で登場するが、ディスアセンブラを定義するとき、DecodeSimm12というメソッドを定義してこの12ビットの即値をどのように表記するかを決定する。

imm_typePatLeafというクラスを継承して定義されたimmSExt12が設定されている。 これは即値を定義するためのHelperクラスで、その内容を見ればわかるが、オペランド値を受け取り、それを12ビット目をMSBにして符号拡張するパタンになる。

最後に、OpNodeとして設定されているaddは、この命令が加算操作を行う命令であるということを示している。

ArithLogicI内で、入力ノード、出力ノード、演算ノードを使用してどのような演算が作られるのか定義されている。

  MYRISCVX_I<opcode, funct3, (outs RC:$rd), (ins RC:$rs1, Od:$imm12),
  !strconcat(instr_asm, "\t$rd, $rs1, $imm12"),
  [(set RC:$rd, (OpNode RC:$rs1, imm_type:$imm12))], IIAlu> {
...

つまり、

  • (outs RC:$rd) : RCレジスタクラスの一員として$rdを定義する。これは出力ノードである。
  • (ins RC:$rs1, Od:$imm12) : RCレジスタクラスの一員として$rs1レジスタを定義する。そしてOdクラスの一員として$imm12クラスを定義する。これらは、入力ノードである。
  • この命令は、$rs1レジスタと即値$imm12に対してOpNodeに基づく演算を実行し、その実行結果を$dに設定する命令である。

ということが定義されました。ArithLogicRについても同様だ。

  MYRISCVX_R<opcode, funct3, funct7, (outs RC:$rd), (ins RC:$rs1, RC:$rs2),
  !strconcat(instr_asm, "\t$rd, $rs1, $rs2"),
  [(set RC:$rd, (OpNode RC:$rs1, RC:$rs2))], IIAlu> {

MYRISCVX_Uについては、パタンの定義が行われていない([]で空白となっている)。 これはMYRISCVX_Uで定義される命令はLUIAUIPCなど1つのテンプレートとして表現することができないので、とりあえず省略しているのだ。

  MYRISCVX_U<opcode, (outs RC:$rd), (ins Od:$imm20),
    !strconcat(instr_asm, "\t$rd, $imm20"), [], IIAlu> {