FPGA開発日記

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

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(14. 関数コール)

f:id:msyksphinz:20181123225150p:plain

Cpu0のバックエンドをLLVMに追加するプロジェクト、9章では、さまざまな形の関数コールをサポートする。

今回も非常にファイル量が多くて、とりあえずLLVMをビルドするためだけにパッチを作って当てているが、LLVM 7.0は未サポートになっている部分が多く、ソースコードを書き直す必要があった。

github.com

関数コールについて。まずは、MIPSのスタックフレームの構造について見ていく。

Mipsのスタックフレーム

まずはCpu0の関数コールの方法について見ていく。Cpu0の関数コールについては2つの方法が存在する。

  • 全ての引数をスタックに積み上げていく。
  • 関数の引数用に予約されたレジスタを経由して渡す。入りきらなかった関数はスタックに積み上げられる。

例えば、Mipsは最初の4つの引数をレジスタ$a0, $a1, $a2, $a3に格納し、それ以外はスタックに積み上げる。

https://jonathan2251.github.io/lbd/_images/13.png
図. Mipsのスタックフレームの構造(https://jonathan2251.github.io/lbd/_images/13.png より引用)
  • lbdex/input/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;
}

最初の4つの引数はレジスタ$a0, - $a3 に格納されることが分かる。残りの2つの引数は 16($sp) と 20($sp) に格納される。以下の図は、引数の渡される方式を示している。 関数の呼び出される側にとって、5番目の引数は48($sp)をロードすることにより呼ばれる。sum_i()のスタックサイズは32であり、16+32($sp)が5番目の引数の格納場所である。

f:id:msyksphinz:20181230205201p:plain
Mipsのスタックフレーム(https://jonathan2251.github.io/lbd/_images/21.pngより引用)

スタックフレームから引数をロードする

ここでは、スタックフレームから引数を渡すための方法を実装する必要がある。 Chapter8_2では、LoawerFormalArguments()を空にしていたためエラーが発生した。Cpu0ではレジスタ経由で2つの引数を渡すことができる。 llc -cpu0-s32-calls=falseだとこの設定が有効になり、llc -cpu0-s32-calls=trueでは、全ての引数がスタック経由で渡されるようになる。

  • analyzeFormalArguments()を定義する。

ArgLocs.size()は6となり、書く引数の情報はArgLocs[i]に格納されている。bool IsRegLoc = VA.isRegLoc();がTrueならば、引数はレジスタ渡しされる。VA.isMemLoc()がTrueならば、引数はスタック経由でアクセスされる。

さらに、呼び出し規約に基づいて、以下の命令を追加する。

  • swi(Software Interrupt)
  • jsub(Jump to subroutine)
  • jalr(Indirect Jump)

/// Jump & link and Return Instructions
let Predicates = [Ch9_1] in {
def JSUB    : JumpLink<0x3b, "jsub">;
}

let Predicates = [Ch9_1] in {
def JALR    : JumpLinkReg<0x39, "jalr", GPROut>;
}

命令の生成は以下となる。関数のジャンプアドレスがtglobaladdrまたはtexternalsymにマッチするとJSUBが呼び出される。

  • lbdex/chapters/Chapter9_1/Cpu0InstrInfo.td

let Predicates = [Ch9_1] in {
def : Pat<(Cpu0JmpLink (i32 tglobaladdr:$dst)),
          (JSUB tglobaladdr:$dst)>;
def : Pat<(Cpu0JmpLink (i32 texternalsym:$dst)),
          (JSUB texternalsym:$dst)>;

入りきらない引数をスタックフレームに格納する。

引数の数が多すぎるとき、入りきらない引数をメモリに格納し、呼び出された側の関数でメモリに格納された引数を取り出す。

Pseudo hook instruction ADJCALLSTACKDOWN and ADJCALLSTACKUP

DAG.getCALLSEQ_START()

Graphvizを使いながらLowercall()を読む

DAGを使うと、lowerCall()の挙動を確認することができる。

文字列の初期化

以下のような文字列の初期化に対しての処理を考える。初期化する文字列が長い場合は、memcpyを使って初期値を設定する。短めの文字列の場合は、初期値をメモリにストアする方式をとる。

  • lbdex/input/ch9_1_2.cpp

int main()
{
  char str[81] = "Hello world";
  char s[6] = "Hello";

return 0; }

$ ./bin/clang  -target mips-unknown-linux-gnu -c ../lbdex/input/ch9_1_2.cpp -emit-llvm -o ch9_1_2.bc
$ ./bin/llvm-dis ch9_1_2.bc -o -
...
  %0 = bitcast [81 x i8] %str to i8
  call void @llvm.memcpy.p0i8.p0i8.i32(i8 align 1 %0, i8 align 1 getelementptr inbounds ([81 x i8], [81 x i8] @_ZZ4mainE3str, i32 0, i32 0), i32 81, i1 false)
  %1 = bitcast [6 x i8] %s to i8
  call void @llvm.memcpy.p0i8.p0i8.i32(i8 align 1 %1, i8 align 1 getelementptr inbounds ([6 x i8], [6 x i8] @_ZZ4mainE1s, i32 0, i32 0), i32 6, i1 false)
...

$ ./bin/llc -march=cpu0 -mcpu=cpu032II -filetype=asm ch9_1_2.bc -o -
...
        lui     $2, %hi($ZZ4mainE3str)
        ori     $5, $2, %lo($ZZ4mainE3str)
        addiu   $4, $sp, 24
        jsub    memcpy
        nop
...
        lbu     $3, 5($2)
        lbu     $4, 4($2)
        shl     $4, $4, 8
        or      $3, $4, $3
        sh      $3, 20($sp)
        lbu     $3, 3($2)
        lbu     $4, 2($2)
        shl     $4, $4, 8
        or      $3, $4, $3
        lbu     $4, 1($2)
        lbu     $2, 0($2)
        shl     $2, $2, 8
        or      $2, $2, $4
        shl     $2, $2, 16
        or      $2, $2, $3
        st      $2, 16($sp)
        addu    $2, $zero, $9
...