FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

LLVMのバックエンドを作るための第一歩 (32. 関数を呼び出す側のDAGの作成:ノードの定義)

f:id:msyksphinz:20190425001356p:plain

引数のある関数を変換するにあたり、関数本体側の引数を取り込む処理は完成したのだが、次は関数を呼び出す側だ。関数コールにあたり、引数をABIのルールに則ってレジスタもしくはスタックに配置する必要がある。

関数呼び出し側の処理を行うためには、MYRISCVXTargetLoweringクラスのLowerCallをオーバーライドする必要がある。xxx

ここまでで、関数コールに必要なLowerメソッドについてまとめる。

  • 関数呼び出し側 : 引数の設定、戻り値の処理 : MYRISCVXTargetLowering::LowerCall
  • 関数本体
    • 引数の取り出し : MYRISCVXTargetLowering::LowerFormalArguments
    • 戻り値の設定 : MYRISCVXTargetLowering::LowerReturn

関数コールを行うためのノードを作成する

MYRISCVXで関数コールを行うのには、RISC-Vと同様にjalr命令もしくはjal命令を使用する。これらの命令の仕様は以下のようになっている。

  • jalr rd, rs1, imm : GPR[rs1] + immのPCアドレスにジャンプする。現在のPC値+4をGPR[rd]に保存する。
  • jal rd, imm : PC + immのPCアドレスにジャンプする。現在のPC値+4をGPR[rd]に保存する。

また、jalrjalの特殊な形式として以下の2つの命令も定義されている(というかエイリアスだね)。

  • jr rs1 : jalr x0, rs1, 0 : rs1のアドレスにジャンプする。rd = 0なので、リンクレジスタには何も保存しない。
  • j imm : jal x0, imm : PC + immのアドレスにジャンプする。rd = 0なので、リンクレジスタには何も保存しない。

関数コールを行うためにはこれらの命令が活用できる。jal命令で関数ターゲットまでジャンプするか、jalr命令でrs1に関数ターゲットのアドレスを格納し、imm=0でジャンプするという方法で実現できそうだ。

まず、関数コールを行うための専用ノードをLLVM IRで定義する。MYRISCVXInstrInfo.tdに、関数コールのためのノードMYRISCVXCallノードを定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
def SDT_MYRISCVXCall       : SDTypeProfile<0, 1, [SDTCisVT<0, iPTR>]>;
...
def MYRISCVXCall : SDNode<"MYRISCVXISD::CALL", SDT_MYRISCVXCall,
                          [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue,
                           SDNPVariadic]>;

前回も説明した通り、MYRISCVXCallが関数コールのためのノードで、SDT_MYRISCVXCallがノードの制約条件を記述している。ノードには1つの入力が与えられ、出力はない。入力ノードの型はポインタとなる。

関数コールを行う命令の定義と推論パタンの登録

では、MYRISCVXCallを使用して命令を定義する。ここでは上記のjalr命令とjal命令を定義するのだが、jaljr命令も定義しておく。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
let isCall=1, hasDelaySlot=0, Defs = [RA], isCodeGenOnly = 0 in {
  class JumpRegister<bits<7> opcode, bits<3> funct3,
        string instr_asm>:
    MYRISCVX_I<opcode, funct3, (outs), (ins GPR:$rs1),
      !strconcat(instr_asm, "\tx1, $rs1, 0"),
      [(MYRISCVXCall GPR:$rs1)], IIAlu> {
      let rd = 1;
      let imm12 = 0;
      let isBranch = 1;
      let isTerminator = 1;
      let isBarrier = 1;
      let hasDelaySlot = 0;
  }

  // Jump and Link (Call)
  class JumpLink<bits<7> opcode, string opstr, DAGOperand opnd> :
    MYRISCVX_J<opcode, (outs), (ins opnd:$target), !strconcat(opstr, "\t$target"),
                   [(MYRISCVXCall tglobaladdr:$target)], IIAlu> {
    let rd = 1;
    let DecoderMethod = "DecodeJumpTarget";
  }
}

ここでは、3つのクラスを定義した。

  • JumpRegister クラス。レジスタrs1に格納されているアドレスにジャンプする。ここでは関数コールのために使用するので、制約条件としてrd = 1(=x1)imm12 = 0としている。命令フォーマットとしてはMYRISCVX_Iを使用しており、jal, jr命令を想定している。
    • このクラスのPredicationとしてMYRISCVXCallを登録している。つまりこのJumpRegisterクラスを使用して定義した命令は、汎用レジスタオペランドに持つMYRISCVXCallノードを使用することになる。
  • JumpLinkクラス。ノードtargetのアドレスにジャンプする。今回は関数コールのために使用するので、制約条件としてrd = 1(=x1)としている。命令フォーマットとしてはMYRISCVX_Jを使用しており、jalr命令を想定している。
    • このクラスのPredicationとしてMYRISCVXCallを登録している。つまりこのJumpLinkクラスを使用して定義した命令は、ターゲットノードをオペランドに持つMYRISCVXCallノードを使用することになる。

これらのクラスを使用して命令を定義する。jal, jalr, jr命令を定義する。命令のマシンコードはRISC-Vのものに則っている。

def JAL : JumpLink<0b1101111, "jal", calltarget>;
def JALR : JumpRegister <0b1100111, 0b000, "jalr">;
def JR   : JumpRegisterX0<0b1100111, 0b000, "jr">;

念のため、もう一つj命令を定義しておく。j命令はjal命令のrd=0版なので新しくJumpOffestX0クラスを定義する。MYRISCVX_Jをベースとし、rd=0という制約を加えます。

class JumpOffsetX0<bits<7> opcode, string instr_asm>:
  MYRISCVX_J<opcode, (outs), (ins brtarget20:$addr),
  !strconcat(instr_asm, "\t$addr"), [], IIAlu> {
    let rd = 0;
    let isBranch = 1;
    let isTerminator = 1;
    let isBarrier = 1;
    let hasDelaySlot = 0;
}

def J   : JumpOffsetX0<0b1101111, "j">;

最後に、MYRISCVXCallの命令生成パタンを登録する。jalを使用した場合のパタン2つと、jalrを使用した場合のパタン2つを登録しておく。

def : Pat<(MYRISCVXCall (i32 tglobaladdr:$dst)),  (JAL tglobaladdr:$dst)>;
def : Pat<(MYRISCVXCall (i32 texternalsym:$dst)), (JAL texternalsym:$dst)>;
def : Pat<(MYRISCVXCall (i32 tglobaladdr:$dst)),  (JALR tglobaladdr:$dst)>;
def : Pat<(MYRISCVXCall (i32 texternalsym:$dst)), (JALR texternalsym:$dst)>;

MYRISCVXCallオペランドとして2種類、tglobaladdrtexternalsymを使用している。tglobaladdrはグローバルアドレスを示すノード、外部シンボルを示すためのノードだ。

  • llvm-myriscvx/include/llvm/Target/TargetSelectionDAG.td
def tglobaladdr : SDNode<"ISD::TargetGlobalAddress",  SDTPtrLeaf, [],
                         "GlobalAddressSDNode">;
...
def texternalsym: SDNode<"ISD::TargetExternalSymbol", SDTPtrLeaf, [],
                         "ExternalSymbolSDNode">;

ここまでで関数ジャンプを生成するための準備は整った。次に、LowerCallメソッドを実装して、関数コールに関するLLVM IRをDAGに変換していく。