RISC向けのアセンブラ の1

JITブランチ( http://code.google.com/p/mosh-scheme/source/browse/branches/jit/ )をチェックしていたらu8-list->c-procedureという手続きが準備されていることに気づいた。libffiが却下された今、RISCFFIするにはこれしかない!*1
要するに、libffiで生成するようなトランポリンコードをScheme側で生成し、あとは従来FFIでしか使えなかった各種XXX->pointer操作を非FFIビルドでも使えるようにすれば良い。mosh本体へのロジックの追加は無くて済む。

TableGenフォーマット

FFIに使うだけなら直接16進でマシンコードを書けば済むが、どうせ信号処理ライブラリで必要になるのでアセンブラも準備することにする。
もちろん、命令リファレンスを見ながら作るのも良いが、他所のマシン記述を流用するのが早い。
この手の事にはLLVMを使う。LLVMアルゴリズムと各種定義が良く分離して書かれていて、各種定義はTableGenと呼ばれるツールで生成される。FFIやJITCのためのアセンブラを作るにはTableGen形式で定義される、以下を使う。

  • 呼び出し規約
  • レジスタのリスト
  • OPコードフォーマット

これらは、個々のファイルに記述され、大本のtd(Target Description)ファイルからIncludeされる。今回はちゃんとしたパーサを作るのが面倒なので、tblgenコマンド(これはLLVMのビルド過程で生成される)をつかって、

../../../../llvm/Release/bin/tblgen -print-records -I /ext/build/llvm25/llvm-2.5/include/ X86.td

のようにして定義のリスト( http://llvm.org/docs/TableGenFundamentals.html#example )だけを得る。.tdはLLVMのツリーのTarget/*/に有る。

例えばPowerPCのaddi命令は、

def ADDI8 {     // Instruction I DForm_base DForm_2
  field bits<32> Inst = { C{0}, C{1}, C{2}, C{3}, C{4}, 
                          C{5}, C{6}, C{7}, C{8}, C{9}, 
                          C{10}, C{11}, C{12}, C{13}, 
                          C{14}, C{15}, B{0}, B{1}, 
                          B{2}, B{3}, B{4}, A{0}, A{1}, A{2}, A{3}, A{4}, 0, 1, 1, 1, 0, 0 };
  string Namespace = "PPC";
  dag OutOperandList = (outs G8RC:$rD);
  dag InOperandList = (ins G8RC:$rA, s16imm64:$imm);
  string AsmString = "addi $rD, $rA, $imm";
  list<dag> Pattern = [(set G8RC:$rD, (add G8RC:$rA, immSExt16:$imm))];
...

のようなレコードとして得られ、確かにこれはaddiのビットパターンを表していることがわかる。あとはAsmStringとInst、レジスタフォーマットのような必要な情報だけを抜き出してくれば良いことになる。

(マニュアルは逆順に書いていることに注意。14→0b001110 ビット逆順にすると0, 1, 1, 1, 0, 0。38→0011(3) 10xx(8)。要するにレジスタによっては38以上になることもある。)
命令の動作はPatternに書かれている。今回は信号処理用にSSEやAltiVecの命令セットを使いたいのがプライマリな要求なのでこれらはほぼそのまま使える。複雑な命令ではあまり役に立たないかも知れない。また、Patternはアーキテクチャごとに多少定義が異なる。言うまでもなくLLVMは生成されたTableとC++のコードを組み合わせてコンパイラを作っている。

*1:ExecutableMemoryにFlushが無いという微妙な問題もあるが。。通常のRISCプロセサは、命令を書き込んだあと命令キャッシュをflushする必要がある。