FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(16. 関数コールをサポートする)

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

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

などを実装していく。

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

  • func_args.cpp
int sum_global;
int sum_i(int x1, int x2, int x3, int x4, int x5, int x6)
{
  sum_global = x1 + x2 + x3 + x4 + x5 + x6;

  return 0;
}
./bin/clang -target riscv32-unknown-linux-gnu -c func_args.cpp -emit-llvm
./bin/llc -march=myriscvx32 -filetype=asm func_args.bc -o -
    llc: /home/msyksphinz/work/llvm/llvm-myriscvx80/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.

最終的に関数をコンパイルして命令を出力するためには、これらの引数を処理し、適切なレジスタ・スタックに収め、命令をデータの流れをコントロールする必要がある。これらの処理について説明する。

f:id:msyksphinz:20191007021543p:plain

MYRISCVXのCalling Conventionについて

命令セットアーキテクチャには、命令の定義ともに、関数呼び出しのルールを規定するCalling Conventionが定義されている。 MYRISCVXにもCalling Conventionが存在し、基本的にRISC-VのCalling Conventionに基づいている。 このルールに基づき、引数を渡す際にも特定のレジスタを使用する必要がある。

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

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

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

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

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

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

MYRISCVXの場合はRISC-Vと同様に整数の引数は汎用レジスタA0-A7に格納して渡すモードもあるのだが、解説のためにもう一つ、すべての引数をスタック経由で渡すモードも用意している。

それぞれのこの関数呼び出しの規約をLP32, STACK32(32-bit版), LP64, STACK64(64-bit版)と呼びる。

Calling Convention名 説明
LP32 / LP64 最初の8個の引数は、A0-A7に格納し、それ以降はスタック経由で引数を渡す。
STACK32 / STACK64 すべての引数をスタック経由で渡す。

このCalling Conventionを実現するのがMYRISCVXCallingConv.tdに記述していく。 これらを制御するために、CCIfTypeおよびCCAssignToRegを使って制御する。

f:id:msyksphinz:20191007021625p:plain
CC_LP{32,64} 関数呼び出し規約
f:id:msyksphinz:20191007021647p:plain
CC_STACK{32,64} 関数呼び出し規約

関数に入るときのCalling Convention

まず、呼び出された関数が実行されるときのCalling Conventionから考えていく(関数を呼ぶ側、つまりCaller側の処理は以降の章で考えていく)。

関数が呼び出されると、まずはCallee Savedレジスタを退避する。つまり、関数側が管理すべきレジスタをスタックに退避し、関数から戻るときにレジスタを汚してしまうことを防止する。

そして、関数内で使用する変数の分だけスタックを移動し、関数本体に入っていく。

MYRISCVXでは2種類のCalling Conventionを用意すると説明した。 LP{32,64}とSTACK{32,64}だ。LPはA0-A7までのレジスタに引数の最初の8つまでを格納し、それ以降はスタックに格納する。一方で、STACKはすべての引数をスタックに格納する。

以下の記述では、関数の引数を渡す際のルールを決めている。Calling ConventionであるCC_MYRISCVXを定義している。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
//===----------------------------------------------------------------------===//
// MYRISCVX LP32/STACK32 Calling Convention
//===----------------------------------------------------------------------===//

def CC_LP32 : CallingConv<[
  // Promote i8/i16 arguments to i32.
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // Integer arguments are passed in integer registers.
  CCIfType<[i32], CCAssignToReg<[A0, A1, A2, A3, A4, A5, A6, A7]>>,

  // Integer values get stored in stack slots that are 4 bytes in
  // size and 4-byte aligned.
  CCIfType<[i32], CCAssignToStack<4, 4>>
]>;

def CC_STACK32 : CallingConv<[
  // Promote i8/i16 arguments to i32.
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // Integer values get stored in stack slots that are 4 bytes in
  // size and 4-byte aligned.
  CCIfType<[i32], CCAssignToStack<4, 4>>
]>;

//===----------------------------------------------------------------------===//
// MYRISCVX LP64/STACK64 Calling Convention
//===----------------------------------------------------------------------===//

def CC_LP64 : CallingConv<[
  // Promote i8/i16/i32 arguments to i64.
  CCIfType<[i1, i8, i16, i32], CCPromoteToType<i64>>,

  // Integer arguments are passed in integer registers.
  CCIfType<[i64], CCAssignToReg<[A0, A1, A2, A3, A4, A5, A6, A7]>>,

  // Integer values get stored in stack slots that are 4 bytes in
  // size and 4-byte aligned.
  CCIfType<[i64], CCAssignToStack<8, 8>>
]>;

def CC_STACK64 : CallingConv<[
  // Promote i8/i16/i32 arguments to i64.
  CCIfType<[i1, i8, i16, i32], CCPromoteToType<i64>>,

  // Integer values get stored in stack slots that are 8 bytes in
  // size and 8-byte aligned.
  CCIfType<[i64], CCAssignToStack<8, 8>>
]>;

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

まず、4種類のCalling Convention、

  • CC_LP32
  • CCL_STACK32
  • CC_LP64
  • CC_STACK64

を定義していることが分かりる。CCIfTypeCCPromoteToTypeを使って、XLENよりも小さな値はすべてXLENのサイズまでビット幅を拡張するように設定している。 CC_LP{32,64}は、CCAssignToRegによってA0-A7までのレジスタを引数わたしに使用できる。 それ以降の引数はCCAssignToStackによりスタックに割り当てる。

CC_STACK{32,64}では、CCAssignToRegを使用せず、すべてCCAssignToStackによって引数をスタックに割り当てている。

さて、この4種類のCalling Conventionからどれを選ぶのか、ですがこれをひとまとめにしたCC_MYRISCVXというCalling Conventionを定義している。 CCIfSubTargetという命令を独自に追加し、サブターゲット内の条件によってCalling Conventionを切り替えるようにしている。

/// 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>;

Calling Conventionの4種類の中から1種類を選択する。isABI_STACK64(), isABI_LP64(), isABI_STACK32()は、それぞれMYRISCVXSubTarget.hで定義されている。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXSubtarget.h
  class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
    virtual void anchor();
...
    bool isABI_LP32()    const { return getXLenVT() == MVT::i32 && getABI().IsLP();    }
    bool isABI_STACK32() const { return getXLenVT() == MVT::i32 && getABI().IsSTACK(); }
    bool isABI_LP64()    const { return getXLenVT() == MVT::i64 && getABI().IsLP();    }
    bool isABI_STACK64() const { return getXLenVT() == MVT::i64 && getABI().IsSTACK(); }
...

それぞれ、現在のABIと現在のレジスタサイズgetXLenVT()によりどのモードを使うのかを判定している。これにより、現在のサブターゲットの設定によりどのCalling Conventionを使うのかを1つに決定している。