Cpu0のバックエンドをLLVMに追加するプロジェクト、9章では、さまざまな形の関数コールをサポートする。
今回も非常にファイル量が多くて、とりあえずLLVMをビルドするためだけにパッチを作って当てているが、LLVM 7.0は未サポートになっている部分が多く、ソースコードを書き直す必要があった。
関数コールについて。まずは、MIPSのスタックフレームの構造について見ていく。
Mipsのスタックフレーム
まずはCpu0の関数コールの方法について見ていく。Cpu0の関数コールについては2つの方法が存在する。
- 全ての引数をスタックに積み上げていく。
- 関数の引数用に予約されたレジスタを経由して渡す。入りきらなかった関数はスタックに積み上げられる。
例えば、Mipsは最初の4つの引数をレジスタ$a0, $a1, $a2, $a3に格納し、それ以外はスタックに積み上げる。
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番目の引数の格納場所である。
スタックフレームから引数をロードする
ここでは、スタックフレームから引数を渡すための方法を実装する必要がある。
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 ...