今までのバックエンドの実装では、関数の取り扱いについていろいろとさぼっている部分があった。今回は、関数の定義と関数コールをきちんとサポートしようと思う。このためには、
- スタックフレームの定義
- 引数の処理
などを実装していく。
現状では、引数のある関数を定義して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だ。これは、
というルールだ。これをMYRISCVXの呼び出し規約としてそのまま採用する。
もう一つ、LLVMの動作を理解するために、独自の呼び出し規約を定義する。
- 全ての引数はスタックを経由して渡す。
- 戻り値はA0-A1レジスタを経由して呼び出し元に戻す。
というものである。この呼び出し規約をSTACK32と呼ぶことにする。MYRISCVXでは、この2つの呼び出し規約を実装することにする。
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 呼び出し規約のミソは、CCIfType
とCCAssignToReg
を使用してレジスタ渡しに使用できるレジスタを指定している点だ。
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が有効となる。