引数のある関数を変換するにあたり、関数本体側の引数を取り込む処理は完成したのだが、次は関数を呼び出す側だ。関数コールにあたり、引数を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]に保存する。
また、jalr
とjal
の特殊な形式として以下の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
命令を定義するのだが、jal
とjr
命令も定義しておく。
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
命令を想定している。JumpLink
クラス。ノードtarget
のアドレスにジャンプする。今回は関数コールのために使用するので、制約条件としてrd = 1(=x1)
としている。命令フォーマットとしてはMYRISCVX_J
を使用しており、jalr
命令を想定している。- このクラスのPredicationとして
MYRISCVXCall
を登録している。つまりこのJumpLink
クラスを使用して定義した命令は、ターゲットノードをオペランドに持つMYRISCVXCall
ノードを使用することになる。
- このクラスのPredicationとして
これらのクラスを使用して命令を定義する。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種類、tglobaladdr
とtexternalsym
を使用している。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に変換していく。