FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(6. 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:20190920013022p:plain
f:id:msyksphinz:20190920013039p:plain

関数に入るときのCalling Convention

まず、呼び出された関数が実行されるときのCalling Conventionから考えていく。

関数が呼び出されると、まずは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つに決定している。

関数からも戻るときのCalling Convention

次に、戻り値に関するCalling Conventionの実装から始める。

以下の記述では、関数の戻り値が渡される場合に、CCAssignToRegで示されるレジスタのどれかに引数が格納されるルールが追加されている。そしてCalling ConventionであるRetCC_MYRISCVXを定義する。

commit:84bc7aa2b1b Add MYRISCVX Calling Convention LP32/STACK32/LP64/STACK64

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

def RetCC_LP32 : CallingConv<[
  CCIfType<[i32], CCAssignToReg<[A0, A1]>>
]>;

def RetCC_STACK32 : CallingConv<[
  CCIfType<[i32], CCAssignToStack<4, 4>>
]>;


//===----------------------------------------------------------------------===//
// MYRISCVX LP64/STACK64 Return Convention
//===----------------------------------------------------------------------===//

def RetCC_LP64 : CallingConv<[
  CCIfType<[i64], CCAssignToReg<[A0, A1]>>
]>;

def RetCC_STACK64 : CallingConv<[
  CCIfType<[i64], CCAssignToStack<8, 8>>
]>;

def RetCC_MYRISCVX : CallingConv<[
  CCIfSubtarget<"isABI_STACK64()", CCDelegateTo<RetCC_STACK64>>,
  CCIfSubtarget<"isABI_LP64   ()", CCDelegateTo<RetCC_LP64>>,
  CCIfSubtarget<"isABI_STACK32()", CCDelegateTo<RetCC_STACK32>>,
  CCDelegateTo<RetCC_LP32>
]>;

このRetCC_MYRISCVXはInstruction Selection時のAnalyzeReturnで使用される。AnalyzeReturnLowerReturnから呼び出される。

それぞれの中身は、見てみると簡単だ。 LP32/LP64のCalling Conventionの場合は戻り値をレジスタA0, A1に割り当てる。これをCCAssignToRegという命令によって表現している。 また、STACK32/STACK64のCalling Conventionの場合は、CCAssignToStackという命令を使用してスタックに割り当てる。 文法はCCAssignToStack<Size, Align>となっており、最初の数字はデータサイズ、次の数字はアドレスアラインを示している。

f:id:msyksphinz:20190920013114p:plain
関数戻り時の戻り値を特定のレジスタ経由で行う仕組み。

そして、LP64/STACK64, LP32/STACK32をそれぞれオプションで切り替えるための、大元となるRetCC_MYRISCVXを定義している。