FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (29. 関数コールのサポート: Calling Conventionの定義)

今までのバックエンドの実装では、関数の取り扱いについていろいろとさぼっている部分があった。今回は、関数の定義と関数コールをきちんとサポートしようと思う。このためには、

  • スタックフレームの定義
  • 引数の処理

などを実装していく。

現状では、引数のある関数を定義してllcに入力すると以下のようなエラーが発生する。

  • ch9_1.cpp
int gI = 100;

int sum_i(int x1, int x2, int x3, int x4, int x5, int x6)
{
  int sum = gI + x1 + x2 + x3 + x4 + x5 + x6;

  return sum;
}

int main()
{
  int a = sum_i(1, 2, 3, 4, 5, 6);

  return a;
}
./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch9_1.cpp -emit-llvm
./bin/llc -march=myriscvx32 -mcpu=simple32 -relocation-model=pic -stats -filetype=asm ch9_1.bc -o -

llc: /home/msyksphinz/work/llvm/llvm-myriscvx/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp:9135: void llvm::SelectionDAGISel::LowerArguments(const llvm::Function&): Assertion `InVals.size() == Ins.size() && "LowerFormalArguments didn't emit the correct number of values!"' failed.

そこで、

  • MYRISCVXTargetLowering::analyzeFormalArguments()
  • MYRISCVXTargetLowering::LowerFormalArguments()
  • MYRISCVXTargetLowering::MYRISCVXCC::analyzeFormalArguments()
  • MYRISCVXTargetLowering::MYRISCVXCC::handleByValArg()
  • MYRISCVXTargetLowering::MYRISCVXCC::numIntArgRegs()
  • MYRISCVXTargetLowering::MYRISCVXCC::intArgRegs()
  • MYRISCVXTargetLowering::MYRISCVXCC::fixedArgFn()
  • MYRISCVXTargetLowering::MYRISCVXCC::allocateRegs()
  • MYRISCVXTargetLowering::LowerCall

の変更を加える。

MYRISCVXの関数呼び出し規約

関数コールをサポートするにあたり、まず決めなければならないのはサブルーチンコールのルール、つまり呼び出し規約だ。

呼び出し規約では、関数呼び出しの規約、つまり関数コールの際の引数をどのような形で渡すのか、戻り値はどのような形式で返すのかと言う事を規定している。呼び出し規約はISAと直接関係ある訳ではなく、ソフトウェアによって独自に決めることができる。しかし、呼び出し規約が異なるバイナリどうしは互いに呼び出すことができないため、ISA毎に標準的な呼び出し規約が決められているのが一般的だ。RISC-Vでは複数の呼び出し規約が決められている。その中で、整数レジスタの動作について定義しているのはILP32だ。これは、

  • 整数値の引数渡しはA0-A7レジスタを経由して渡す。それよりも多い引数はスタックを経由して渡す。
  • 戻り値はA0-A1レジスタを経由して呼び出し元に戻す。

というルールだ。これをMYRISCVXの呼び出し規約としてそのまま採用する。

もう一つ、LLVMの動作を理解するために、独自の呼び出し規約を定義する。

  • 全ての引数はスタックを経由して渡す。
  • 戻り値はA0-A1レジスタを経由して呼び出し元に戻す。

というものである。この呼び出し規約をSTACK32と呼ぶことにする。MYRISCVXでは、この2つの呼び出し規約を実装することにする。

f:id:msyksphinz:20190625224140p:plain
MYRISCVXの関数引数を渡すための2つのABI

Target Descriptionに呼び出し規約を定義する

MYRISCVXの呼び出し規約をLLVMに教えるためには、Target Description(.td)ファイルにルールを記述する必要がある。 tdファイルではMYRISCVXのレジスタなどの要素を定義しているので、これらを使ってルールを作る。 呼び出し規約は、一般的に[アーキテクチャ名]_CallingConv.tdというファイルに記述する。

まず、LP32 呼び出し規約を定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
//===----------------------------------------------------------------------===//
// MYRISCVX LP32 呼び出し規約
//===----------------------------------------------------------------------===//

def CC_LP32 : CallingConv<[
  // i1, i8, i16型(i32)よりも小さい型の値は、i32として取り扱う。
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // i32型の値は、A0-A7のレジスタを使って値渡しを行う。
  CCIfType<[i32], CCAssignToReg<[A0, A1, A2, A3, A4, A5, A6, A7]>>,

  // i32型のスタックを経由して渡す引数は、4バイトのスタックスロットを使用し、アドレスは4バイトにアラインする。
  CCIfType<[i32], CCAssignToStack<4, 4>>
]>;

LP32 呼び出し規約のミソは、CCIfTypeCCAssignToRegを使用してレジスタ渡しに使用できるレジスタを指定している点だ。 A0-A7までは引数渡しのために使用できる。一方で、それ以上の引数を渡す場合はスタックを経由して渡す。 このときのルールとして、1データ分のスタックのサイズと、スタックのアドレスアラインのルールを決めている。

では、次にSTACK32 呼び出し規約を定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
//===----------------------------------------------------------------------===//
// MYRISCVX STACK32 呼び出し規約
//===----------------------------------------------------------------------===//

def CC_STACK32 : CallingConv<[
  // i1, i8, i16型(i32)よりも小さい型の値は、i32として取り扱う。
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // i32型のスタックを経由して渡す引数は、4バイトのスタックスロットを使用し、アドレスは4バイトにアラインする。
  CCIfType<[i32], CCAssignToStack<4, 4>>
]>;

STACK32 呼び出し規約は、LP32 呼び出し規約に比べてCCAssignToRegを使用している。 関数呼び出しの時に引数渡しをすべてスタック経由で行うのでこのような実装になっている。

最後に、2つの呼び出し規約を定義したので、これをまとめた1つの呼び出し規約を定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
/// CCIfSubtarget - Match if the current subtarget has a feature F.
class CCIfSubtarget<string F, CCAction A, string Invert = "">
    : CCIf<!strconcat(Invert,
                      "static_cast<const MYRISCVXSubtarget&>"
            "(State.getMachineFunction().getSubtarget()).",
                      F),
           A>;

def CC_MYRISCVX : CallingConv<[
  CCIfSubtarget<"isABI_STACK32()", CCDelegateTo<CC_STACK32>>,
  CCDelegateTo<CC_LP32>
]>;

CCIfSubtargetは、MIPSの実装から引き抜いてきた。 条件によって呼び出し規約を切り替えるためのものである。 ここでは、isABI_STACK32()がTrueならばSTACK32が有効となり、そうでなければLP32が有効となる。