diff --git a/.travis.yml b/.travis.yml index 6310a0a..7fbb85a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,9 @@ script: - pwd - mkdir build - asciidoctor/bin/asciidoctor main.asciidoc -o build/index.html + - asciidoctor/bin/asciidoctor draft-cahpv3.asciidoc -o build/draft-cahpv3.html + - asciidoctor/bin/asciidoctor draft-rv16kv2.asciidoc -o build/draft-rv16kv2.html + - asciidoctor/bin/asciidoctor draft-rv32kv1.asciidoc -o build/draft-rv32kv1.html deploy: provider: pages:git token: $GITHUB_TOKEN diff --git a/README.md b/README.md index b50bf4a..ec83914 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ [ここからmasterが読めます。](https://ushitora-anqou.github.io/write-your-llvm-backend/) +## 下書き + +この文章は、2019年度に艮 鮟鱇が作成したLLVMバックエンドの、自分用メモがベースになっています。 +このメモをブラッシュアップしてまともな文章として公開する予定でしたが、 +その作業が遅れているため、一旦メモのまま公開します。 + +- [RV32Kv1](https://ushitora-anqou.github.io/write-your-llvm-backend/draft-rv32kv1.html) +- [RV16Kv2](https://ushitora-anqou.github.io/write-your-llvm-backend/draft-rv16kv2.html) +- [CAHPv3](https://ushitora-anqou.github.io/write-your-llvm-backend/draft-cahpv3.html) + ## ビルド方法 Asciidoctorのmasterを持ってきて`asciidoctor main.asciidoc`とかする。 diff --git a/draft-cahpv3.asciidoc b/draft-cahpv3.asciidoc new file mode 100644 index 0000000..b28d967 --- /dev/null +++ b/draft-cahpv3.asciidoc @@ -0,0 +1,1469 @@ += [下書き] LLVMバックエンド開発文書 for CAHPv3 +艮鮟鱇 +:toc: left + +== これはなに + +2019年にCAHPv3という自作ISA用のLLVMバックエンドを作ったときの自分とメンバ用のメモ。 +メモなので当然読みにくい。これをブラッシュアップしてまともな文章にする予定だったが、 +その作業が遅れているので、一旦メモのまま公開する。内容について質問したい場合は +Twitter https://twitter.com/ushitora_anqou[@ushitora_anqou]までリプライなどを貰えれば反応するかもしれない。 + +この文章は、前に作ったRV16Kv2及びRV32Kv1用LLVMバックエンドで得た知識を前提にして書かれている。 +RV16Kv2のメモは<>を参照のこと。 +RV32Kv1のメモは<>を参照のこと。 + +ソースコードはlink:https://github.com/virtualsecureplatform/llvm-cahp[GitHub]にある。 + +ブラッシュアップはlink:https://github.com/ushitora-anqou/write-your-llvm-backend[GitHub]にて行っている。 + +このLLVMバックエンドの開発は、もともと2019年度未踏事業において +link:https://github.com/virtualsecureplatform/kvsp[Virtual Secure Platform]を開発するために行われた。 + + +== 簡単使い方 + +=== ビルドする + +とりあえずビルドする。ビルドには + +* `cmake` +* `ninja` +* `clang` +* `clang++` +* `make` +* `lld` + +が必要。 + +これらを入れた後 `cmake` を次のように走らせる。 + + $ cd /path/to/llvm-project + $ mkdir build + $ cd build + $ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS="lld;clang" \ + -DCMAKE_BUILD_TYPE="Release" \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="CAHP" \ + ../llvm + $ cmake --build . + +=== アセンブラを使う + +アセンブラを起動する。アセンブラは `build/bin/llvm-mc` である。 + +.... +# オブジェクトファイルにアセンブル +$ bin/llvm-mc -arch=cahp -filetype=obj foo.s | od -tx1z -Ax -v + +# コメント表示の機械語にアセンブル +$ bin/llvm-mc -arch=cahp -show-encoding foo.s + +# オブジェクトファイルにアセンブルしたものを逆アセンブル +$ bin/llvm-mc -filetype=obj -triple=cahp foo.s | bin/llvm-objdump -d - +.... + +=== コンパイラを起動する + +まずランタイムライブラリをビルドする必要がある。cahp-rtレポジトリを `git clone` し +`CC=/path/to/bin/clang` をつけて `make` する。 + +.... +# cahp-rt レポジトリをcloneする。 +$ git clone git@github.com:ushitora-anqou/cahp-rt.git + +# cahp-rt をビルドする。 CC 環境変数で、先程ビルドしたclangを指定する。 +$ cd cahp-rt +$ CC=/path/to/bin/clang make +.... + +以下のようなCプログラム `foo.c` を `clang` を用いてコンパイルする。 +コンパイル時に `--sysroot` オプションを用いて、先程ビルドしたcahp-rtのディレクトリを指定する。 +なおバイナリサイズを小さくしたい場合は `-Oz` オプションを指定するなどすればよい。 + +.... +$ cat foo.c +int hoge; + +int main() +{ + hoge = 42; + return hoge; +} + +$ bin/clang -target cahp foo.c -o foo.exe --sysroot=/path/to/cahp-rt +.... + +`llvm-readelf` を用いて `.text` その他のサイズが分かる。 +これがROMサイズ( `0x200 = 512` )未満であることを確認する。 + +実行結果はRISC-Vのもの。TODO + +.... +$ bin/llvm-readelf -S foo.exe +There are 7 section headers, starting at offset 0x10f0: + +Section Headers: + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + [ 0] NULL 00000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 00000000 001000 00002e 00 AX 0 0 4 + [ 2] .bss NOBITS 00010000 00102e 000002 00 WA 0 0 2 + [ 3] .comment PROGBITS 00000000 00102e 000028 01 MS 0 0 1 + [ 4] .symtab SYMTAB 00000000 001058 000050 10 6 2 4 + [ 5] .shstrtab STRTAB 00000000 0010a8 00002f 00 0 0 1 + [ 6] .strtab STRTAB 00000000 0010d7 000018 00 0 0 1 +Key to Flags: + W (write), A (alloc), X (execute), M (merge), S (strings), l (large) + I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) + O (extra OS processing required) o (OS specific), p (processor specific) +.... + +`llvm-objdump` を用いて逆アセンブルを行うことができる。 + +実行結果はRISC-Vのもの。TODO + +.... +$ bin/llvm-objdump -d foo.exe + +foo.exe: file format ELF32-cahp + +Disassembly of section .text: +0000000000000000 _start: + 0: 00 73 06 00 jal 6 + 4: 00 52 fe ff j -2 + +0000000000000008 main: + 8: c1 f2 addi sp, -4 + a: 21 80 swsp fp, 2(sp) + c: 12 e0 mov fp, sp + e: 42 f2 addi fp, 4 + 10: 08 78 00 00 li a0, 0 + 14: 82 92 fc ff sw a0, -4(fp) + 18: 08 78 00 00 li a0, 0 + 1c: 88 b2 00 00 lw a0, 0(a0) + 20: 12 a0 lwsp fp, 2(sp) + 22: 41 f2 addi sp, 4 + 24: 00 40 jr ra +.... + +`cahp-sim` を使ってシミュレーションを行う。 + +実行結果はRISC-Vのもの。TODO + +.... +$ /path/to/cahp-sim/main foo.exe 20 +ROM: 0000 0073 +ROM: 0002 0600 +ROM: 0004 0052 +ROM: 0006 FEFF +ROM: 0008 C1F2 +ROM: 000A 2180 +ROM: 000C 12E0 +ROM: 000E 42F2 +ROM: 0010 0878 +ROM: 0012 0000 +ROM: 0014 8292 +ROM: 0016 FCFF +ROM: 0018 0878 +ROM: 001A 0000 +ROM: 001C 88B2 +ROM: 001E 0000 +ROM: 0020 12A0 +ROM: 0022 41F2 +ROM: 0024 0040 + +RAM: 0000 2A00 + +Inst:JAL PC <= 0x0002 Reg x0 <= 0x0004 PC <= 0x0008 FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x1 <= 0x01FA PC <= 0x000A FLAGS(SZCV) <= 0000 +Inst:SWSP DataRam[0x01FC] <= 0x0000 DataRam[0x01FD] <= 0x0000 PC <= 0x000C FLAGS(SZCV) <= 0010 +Inst:MOV Reg x2 <= 0x01FA PC <= 0x000E FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x2 <= 0x01FE PC <= 0x0010 FLAGS(SZCV) <= 0010 +Inst:LI PC <= 0x0012 Reg x8 <= 0x0000 PC <= 0x0014 FLAGS(SZCV) <= 0100 +Inst:SW PC <= 0x0016 DataRam[0x01FA] <= 0x0000 DataRam[0x01FB] <= 0x0000 PC <= 0x0018 FLAGS(SZCV) <= 0000 +Inst:LI PC <= 0x001A Reg x8 <= 0x0000 PC <= 0x001C FLAGS(SZCV) <= 0100 +Inst:LW PC <= 0x001E Reg x8 <= 0x002A PC <= 0x0020 FLAGS(SZCV) <= 0110 +Inst:LWSP Reg x2 <= 0x0000 PC <= 0x0022 FLAGS(SZCV) <= 0010 +Inst:ADDI Reg x1 <= 0x01FE PC <= 0x0024 FLAGS(SZCV) <= 0010 +Inst:JR PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +x0=4 x1=510 x2=0 x3=0 x4=0 x5=0 x6=0 x7=0 x8=42 x9=0 x10=0 x11=0 x12=0 x13=0 x14=0 x15=0 +.... + +`x8=42` とあるので、正しく実行されていることが分かる。 + +== 概要 + +これを読めば自作アーキテクチャ(CAHPv3)の機械語を出力するLLVMバックエンドを作成することができる。 +VSP開発のバス係数を高める意義がある。 + +この文書はAsciiDocを用いて記述されている。 +記述方法についてはリファレンス<><>を参照のこと。 + +もう3回目なので差分しかかかない。 + +== ところで + +一度もコンパイラを書いたことがない人は、この文書を読む前に +『低レイヤを知りたい人のためのCコンパイラ作成入門』<>などで一度 +フルスクラッチからコンパイラを書くことをおすすめします。 + +また<>などを参考に、 +LLVMではなくGCCにバックエンドを追加することも検討してみてはいかがでしょうか。 +意外とGCCのほうが楽かもしれませんよ? + +== 参考にすべき資料 + +=== Webページ + +* Writing an LLVM Backend<> +** 分かりにくく読みにくい。正直あんまり見ていないが、たまに眺めると有益な情報を見つけたりもする。 +* The LLVM Target-Independent Code Generator<> +** <>よりもよほど参考になる。LLVMバックエンドがどのようにLLVM IRをアセンブリに落とすかが明記されている。必読。 +* TableGenのLLVMのドキュメント<> +** 情報量が少ない。これを読むよりも各種バックエンドのTableGenファイルを読むほうが良い。 +* LLVM Language Reference Manual<> +** LLVM IRについての言語リファレンス。LLVM IRの仕様などを参照できる。必要に応じて読む。 +* Architecture & Platform Information for Compiler Writers<> +** LLVMで公式に実装されているバックエンドに関するISAの情報が集約されている。Lanaiの言語仕様へのリンクが貴重。 +* RISC-V support for LLVM projects<> +** **どちゃくそに参考になる**。以下の開発はこれに基づいて行う。 +** LLVMにRISC-Vサポートを追加するパッチ群。バックエンドを開発するためのチュートリアルも兼ねているらしく `docs/` 及びそれと対応したpatchが参考になる。 +** またこれについて、開発者が2018 LLVM Developers' Meetingで登壇したときの動画は<>より閲覧できる。スライドは<>より閲覧できる。 +** そのときのCoding Labは<>より閲覧できる。 +* Create an LLVM Backend for the Cpu0 Architecture<> +** Cpu0という独自アーキテクチャのLLVMバックエンドを作成するチュートリアル。多少古いが、内容が網羅的で参考になる。英語が怪しい。 +* FPGA開発日記<> +** Cpu0の資料<>をもとに1からRISC-Vバックエンドを作成する過程がブログエントリとして公開されている。GitHubに実装も公開されている<>。 +* ELVMバックエンド<> +** 限られた命令でLLVM IRの機能を達成する例として貴重。でも意外とISAはリッチだったりする。 +** 作成者のスライドも参考になる<>。 +* 2018年度東大CPU実験で開発されたLLVM Backend<> +** これについて書かれたAdCのエントリもある<>。 +* Tutorial: Building a backend in 24 hours<> +** LLVMバックエンドの大まかな動きについてざっとまとめたあと、 `ret` だけが定義された最低限のLLVMバックエンド ("stub backend") を構成している。 +** Instruction Selection の説明にある *Does bunch of magic and crazy pattern-matching* が好き。 +* 2017 LLVM Developers’ Meeting: M. Braun "Welcome to the back-end: The LLVM machine representation"<> +** スライドも公開されている<>。 +** 命令選択が終わったあとの中間表現であるLLVM MIR +( `MachineFunction` や `MachineInstr` など)や、それに対する操作の解説。 +RegStateやframe index・register scavengerなどの説明が貴重。 +* Howto: Implementing LLVM Integrated Assembler<> +** LLVM上でアセンブラを書くためのチュートリアル。アセンブラ単体に焦点を絞ったものは珍しい。 +* Building an LLVM Backend<> +** 対応するレポジトリが<>にある。 +* [LLVMdev] backend documentation<> +** llvm-devメーリングリストのバックエンドのよいドキュメントは無いかというスレッド。Cpu0とTriCoreが挙げられているが、深くまで記述したものは無いという回答。 +* TriCore Backend<> +** TriCoreというアーキテクチャ用のバックエンドを書いたという論文。スライドもある<>。ソースコードもGitHub上に上がっているが、どれが公式かわからないfootnote:[論文とスライドも怪しいものだが、著者が一致しているので多分正しいだろう。]。 +* Life of an instruction in LLVM<> +** Cコードからassemblyまでの流れを概観。 +* LLVM Backendの紹介<> +** 「コンパイラ勉強会」footnote:[これとは別の発表で「コンパイラ開発してない人生はFAKE」という名言が飛び出した勉強会<>。]での、LLVMバックエンドの大きな流れ(特に命令選択)について概観した日本語スライド。 + +=== 書籍 + +* 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』<> +** 数少ない日本語資料。Passやバックエンドの各クラスについて説明している。<>と合わせて大まかな流れを掴むのに良い。 + +なおLLVMについてGoogleで検索していると"LLVM Cookbook"なる謎の書籍(の電子コピー)が +見つかるが、内容はLLVM公式文書のパクリのようだ<>。 + +=== バックエンド + +* RISC-V<> +** パッチ群が開発ドキュメントとともに公開されている<>。以降の開発はこれをベースに行う。 +* Lanai<> +** Googleが開発した32bit RISCの謎アーキテクチャ。全く実用されていないが、バックエンドが単純に設計されておりコメントも豊富のためかなり参考になるfootnote:[LLVMバックエンドの開発を円滑にするためのアーキテクチャなのではと思うほどに分かりやすい。]。footnote:[後のSparcについて<> +にて指摘されているように、商業的に成功しなかったアーキテクチャほどコードが単純で分かりやすい。] +* Sparc +** <>でも説明に使われており、コメントが豊富。 +* x86 +** みんな大好きx86。貴重なCISCの資料であり、かつ2オペランド方式を採用する場合に実装例を与えてくれる。あと `EFLAGS` の取り回しなども参考になるが、全体的にコードは読みにくい。ただLLVMの命名規則には従うため、他のバックエンドからある程度推論をして読むのが良い。 + +== CAHPv3アーキテクチャ仕様 + +https://docs.google.com/spreadsheets/d/1Q9JPoZLVyqJEC3p_LBYiPLMTjGZgVoNlu-Jx3CFrLNQ/edit?usp=sharing + +== LLVMをテストする + +`llvm-lit` を使用してLLVMをテストできる。 + +.... +$ bin/llvm-lit test -s # 全てのテストを実行する +$ bin/llvm-lit -s --filter 'CAHP' test # CAHPを含むテストを実行する +$ bin/llvm-lit -as --filter 'CAHP' test # テスト結果を詳細に表示する +$ bin/llvm-lit -as --filter 'CAHP' --debug test # デバッグ情報を表示する +.... + +== LLVMバックエンドの流れ + +`CAHP*` はオーバーライドできるメンバ関数を表す。 + +.... + +LLVM IR code + +| +| +v + +SelectionDAG (SDNode); CAHPで扱えない型・操作を含む (not legal)。 + +| +| <-- CAHPTargetLowering::CAHPTargetLowering +| <-- CAHPTargetLowering::Lower* +v + +SelectionDAG (SDNode); CAHPで扱える型・操作のみを含む (legal)。 + +| +| <-- CAHPDAGToDAGISel, CAHPInstrInfo +v + +SelectionDAG (MachineSDNode); ノードの命令は全てCAHPのもの。 + +| +| <-- CAHPInstrInfo; 命令スケジューリング +v + +LLVM MIR (MachineInstr); スケジューリングされた命令列 + +| (以下の流れは TargetPassConfig::addMachinePasses に記述されている) +| +| <-- CAHPTargetLowering::EmitInstrWithCustomInserter; +| usesCustomInserter フラグが立っている ある MachineInstr の代わりに +| 複数の MachineInstr を挿入したり MachineBasicBlock を追加したりする。 +| +| <-- SSA上での最適化 +| +| <-- レジスタ割り付け +v + +LLVM MIR (MachineInstr); 物理レジスタのみを含む命令列(仮想レジスタを含まない) + +| +| <-- CAHPInstrInfo::expandPostRAPseudo +| +| <-- CAHPFrameLowering::processFunctionBeforeFrameFinalized +| +| <-- スタックサイズの確定 +| +| <-- CAHPFrameLowering::emitPrologue; 関数プロローグの挿入 +| <-- CAHPFrameLowering::emitEpilogue; 関数エピローグの挿入 +| <-- CAHPRegisterInfo::eliminateFrameIndex; frame indexの消去 +| +| <-- llvm::scavengeFrameVirtualRegs; +| frame lowering中に必要になった仮想レジスタをscavengeする +v + +LLVM MIR (MachineInstr); frame index が削除された命令列 + +| +| <-- CAHPPassConfig::addPreEmitPass +| <-- CAHPPassConfig::addPreEmitPass2 +| +| +| <-- CAHPAsmPrinter +| <-- PseudoInstExpansion により指定された擬似命令展開の実行 +v + +MC (MCInst); アセンブリと等価な中間表現 +.... + +LLVM MIRについては<>に詳しい。 +各フェーズでの `MachineInstr` をデバッグ出力させる場合は `llc` に `-print-machineinstrs` を +渡せば良い。 + +== LLVMのソースコードを用意する + +LLVMのソースコードを取得する。今回の開発ではv9.0.0をベースとする。 +Git上でcahpブランチを作り、その上で開発する。 + +.... +$ git clone https://github.com/llvm/llvm-project.git +$ cd llvm-project +$ git switch llvmorg-9.0.0 +$ git checkout -b cahp +.... + +== スケルトンバックエンドを追加する + +`isRISCV` などの関数が `Triple.h` に追加されていた。ただしLanaiのものは無かった。 +無くとも問題ないと思われるので実装は省略。TODO + +ビルドする。RISC-Vはもはやexperimentalではない。 + +.... +$ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS="clang;lld" \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86;RISCV" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="CAHP" \ + ../llvm +$ cmake --build . +.... + +CAHPバックエンドが追加された。 + +.... +$ bin/llc --version +LLVM (http://llvm.org/): + LLVM version 9.0.0 + DEBUG build with assertions. + Default target: x86_64-unknown-linux-gnu + Host CPU: skylake + + Registered Targets: + cahp - CAHP + riscv32 - 32-bit RISC-V + riscv64 - 64-bit RISC-V + x86 - 32-bit X86: Pentium-Pro and above + x86-64 - 64-bit X86: EM64T and AMD64 +.... + +== 簡易的なアセンブラを実装する + +やるだけ。メモリ演算以外は正しくエンコードされることを確認。 + +invalid operandのエラーメッセージを正しく出す変更をここでしておく。 + +== `CAHPInstPrinter` を実装する + +やるだけ。 `tablegen(LLVM CAHPGenAsmWriter.inc -gen-asm-writer)` を +CMakeFilesに追加しなくても `-show-encoding` オプションは動いた。 + +== テストを書く + +やるだけ。 + +== メモリ演算を追加する + +やるだけ。やっぱり `AsmWriter` は必要だった。 + +== 属性を指定する + +やるだけ。RISC-Vのlui/addi/xori/oriに +`let isReMaterializable = 1, isAsCheapAsAMove = 1` がついていた。 +要調査TODO. + +== ディスアセンブラを実装する + +やるだけ。24/16bit命令の判定がRV16Kよりも楽。 + +== relocationとfixupに対応する + +relocationは何が必要なのか良くわからない。RISC-Vなどでは +`R_RISCV_32` を定義しているが、32bitの即値を直接読み込める命令など +存在しないはずである。とりあえずfixupで対処し、関数呼び出しを実装する時点で +再び考えることにする。 + +RV16Kのときとは異なり、CAHPは16bit即値を直接読み込むことはできない。 +上位6bitと下位10bitを分けて読み込むことになるが、 +そのためには `%hi/%lo/%pcrel_hi/%pcrel_lo` の実装が必要である。 +これはRISC-Vを参考にして実装する。 + +即値の取り扱い方でだいぶ迷ったがおおよそ理解した。基本的にはRISC-Vに従う。 + +まず `lui` は下位ビットをclearし、addiと補完して使用するほうが良い。 +こうすることで次のように `lw` などとの連携がとれる。 + +.... +lui a0, %hi(foo) +lw a1, %lo(foo)(a0) +.... + +ここで使用している `%hi` は `foo` の上位6bitという意味では**ない**。 +というのも `%lo` が使用されるのは符号つき即値フィールドのため +符号拡張が行われる。そのため `%lo` の10bit目が符号bitと見なされ、 +不用意に負数になる可能性がある。そこでCAHP(と参照したRISC-V)では +「 `%lo` の10bit目が1の場合は `1 << 10` を足す」という動作を行う +必要がある。 + +またCAHPv3に `auipc` は必要ない。当初関数ポインタを正しく扱うためには +`auipc` が必要だと考えていたが、実際には次のようにすればよい。 + +.... +# 関数名を指定した関数呼び出し +jal hoge + +# 関数ポインタを経由した関数呼び出し +lui a0, %hi(hoge) +addi a0, a0, %lo(hoge) +jalr +.... + +RISC-Vにおいて `auipc` を必要とするのは `j` や `jal` 命令などが32bit即値を +とれないためである。CAHPでは `j` 及び `jal` が16bit即値を取れるため問題ない。 + +とりあえず `%hi/%lo` を含まないfixupに対応した。 +relocation対応は後回し。 + +== `%hi/%lo` に対応する + +アセンブリ中で使用できるmodifierである `%hi/%lo` に対応する。 +例えば次のように動作する。 + +.... +lui a0, %hi(0xFFFF) # lui a0, 0 +addi a0, a0, %lo(0xFFFF) # addi a0, a0, 0x3FF +.... + +値の下位10bitが負数になる場合には `%hi` は単に上位6bitを返すのではなく、 +それに `1 << 10` を足した値を返すことに注意が必要である。 + +基本的な実装の流れは次のようになる。まず `CAHPMCExpr` を定義する。 +`CAHPMCExpr` は `MCExpr` をラップすると同時に、 +この式が `%lo/%hi` などのmodifierのうち、どれがついているか(あるいはついていないか)を +`VariantKind` 列挙体として保持する。fixupの生成はこの `VariantKind` を目印に操作を行う。 + +次に `AsmParser` で `%` を読み込んだ場合に `parseOperandWithModifier` を呼出し、 +`CAHPMCExpr` を作成する。 + +`isSImm10` などでは `CAHPMCExpr` が即値として現れることを想定する必要がある。 +この場合、まず i) 定数式として評価できるならばビット幅を確認し ii) そうでなければ +そもそもその式がvalidであるかどうかとmodifierについて調べ( `classifySymbolRef` )、 +validかつ適切なmodifierであればtrueを返す。なおここでいう「適切なmodifier」とは、 +例えば `isSImm10` に `%lo` が来ることは認められるが `%hi` は認められない、 +といったことを意味している。 + +`getImmOpValue` にてfixupを作る際にも `CAHPMCExpr` を考慮する必要がある。 +fixupでは命令そのものを書き換える必要があるため、即値がどのようにバイト中に +配置されるかを知る必要がある。したがって同じbit幅でも格納方法が違う場合は +異なるfixupの種類としなければならない。RISC-Vでは実際このために `InstFormat` を +導入して対処しているが、幸いなことにCAHPではそのようなことがない。よかったね。 + +`AsmBackend` で `%hi/lo` 用のfixupに対応する。 + +ここまでで `lui/addi` はちゃんと動くようになった。 +問題はその他の `ori` などで、例えば次のようなコードはエラーになってしまう。 + + ori a0, a0, %lo(0xffff) + +原因は `%lo(0xffff)` が `-1` となって符号なし即値でなくなってしまうためである。 +ではRISC-Vはどうしているかというと、なんと `ori` などビット演算にも符号付き即値を要求している。 +よくよく仕様を見てみるとこれらは符号付き即値を要求するのだ。これによって `xor a0, a0, -1` で +`not` の代わりになるなどのメリットがあるらしい。なるほど。 + +ということでISAを変更したfootnote:[どんどんCAHPがRISC-Vになっていく。]。 + +`AsmParser` の `addExpr` で `CAHPMCExpr` について `evaluateAsConstant` をすることにより、 +これが定数式の場合はfixupを作ることなく `%lo/%hi` を評価できる。 + +== コンパイラのスケルトンを作成する + +ここでいう「スケルトン」とはおおよそ「何もせずただreturnするだけ」の +LLVM IRをコンパイルできる程度のものである。それでも関数のコンパイルなどが +必要になるため、変更量は多い。 + +やるだけ。 + +これによって次のような変換を実行できるようになる。 + +.... +$ cat foo.ll +define void @foo() nounwind { + ret void +} + +$ bin/llc -mtriple=cahp -verify-machineinstrs < foo.ll + .text + .file "" + .globl foo # -- Begin function foo + .p2align 1 + .type foo,@function +foo: # @foo +# %bb.0: + jr ra +.Lfunc_end0: + .size foo, .Lfunc_end0-foo + # -- End function + + .section ".note.GNU-stack","",@progbits +.... + +`ret` が `jr` に変換されていることが分かる。 + +== 基本的な演算に対応する + +いわゆるALUで実行される演算に対応する。RV16Kまでは「スケルトン」に含めていたのだが +やっぱり分けたほうが見通しが良いと思う。 + +やるだけ。 + +== 定数の実体化に対応する + +materialization<>に対応する。16bit整数は `lui` と `addi` を組み合わせて +読み込む必要があるため、TableGenファイルに `HI6` と `LO10Sext` という `SDNode` を +追記する。 + +ついでに上位6bitで表現できる数値のときは `lui` のみを使用するという +最適化も取り込んでおく。RISC-Vではあとの方のコミット(3ff2022bb94)で +ぬるっと実装されている。 + +== メモリ演算に対応する + +やるだけ。 `copyPhysReg` の実装で `kill` フラグを立てているが、 +<>によればこれは不要であるので削除しておく。 + +== relocationに対応する + +やるだけ。とりあえず `%hi/%lo` に対応するものと関数呼び出しに対応するものだけ。 + +== 条件分岐に対応する + +CAHPはほとんどRISC-Vなのでfootnote:[要出典]、本家<>と +同様に `brcond` によるパターンマッチを行えば良い……と思いきや、そうではない。 +後々 `setcc` に対応する際、RISC-Vでは `setcc` に対応する命令があるので問題ないが、 +CAHPではRV16Kのときと同様にこれをexpandしなければならない。 +これが問題で、愚直にやると `setcc` が `brcond` とセットのときにもexpandされてしまう。 +回避方法があるのかもしれないfootnote:[一つは `custom` でよしなにする方法だが面倒。]が +分からないので `BR_CC` を採用する。 + +`BR_CC` をまともに使っているバックエンドは少ない。BPFがその1つ。 +またBPFをもとに開発されたELVMもこれに従う。 +`CC_EQ` などの述語を作成し、これによってcond codeをパターンマッチする。 + +== 関数呼び出しに対応する + +`PseudoCALL` を導入せずに初めから `Pat` を使用して `jalr` に置き換える。 +そのため `MO_RegisterMask` の対応が必要。なお `jal` による関数呼び出しにしようとすると +エラーになるので一旦放置(TODO)。 + +オブジェクトファイルを生成しようとすると(アセンブラを通そうとすると)次のような +エラーがでた。 + + LLVM ERROR: unable to write nop sequence of 1 bytes + +これは関数自体のアラインメントが2に設定されている( `.p2align 1` )に起因するようだ。 +`CAHPMCAsmInfo::CAHPMCAsmInfo` にて `HasFunctionAlignment = false;` とすることで回避したが、 +これは単に `.p2align` を表示させないだけなので正当ではない。 +`CAHPTargetLowering::CAHPTargetLowering` で次のように関数のアラインメントを設定する。 + + setMinFunctionAlignment(0); + setPrefFunctionAlignment(0); + +== 関数プロローグ・エピローグを実装する + +`llvm.frameaddress` と `llvm.returnaddress` が正常に動作するように、 +`ra` と `fp` は無条件に保存する。 + +半分以上のテストが動作しなくなるが、とりあえず放置する。 + +== frame pointer eliminationを実装する + +テストを書き換えるのが面倒なので、さっさとframe pointer eliminationを実装してしまう。 + +== `select` に対応する + +やるだけ。 + +== `FrameIndex` をlowerする。 + +`select` の前にやるべきだったかも。RV16Kのときのframe indexへの対応は3箇所に分かれているので、 +その全てを統合する。 + +途中混乱したが `Select` に `ISD::FrameIndex` が来た場合は単にaddiに還元してよい。 +ここで指定する即値は `0` である。後々具体的な即値が求まったときにビット幅に収まらない場合の処理は +`eliminateFrameIndex` で行う。 + +== 大きなスタックフレームに対応する + +RISC-Vの実装を参考にしつつ `RegState::Kill` を片っ端から消すfootnote:[これほんまに大丈夫なんやろな……]。 + +== `SETCC` に対応する + +expandするだけ。 + +== `ExternalSymbol` に対応する + +やるだけ。これで `frame.ll` が動くようになったfootnote:[後から見直したら、前から通るはずのテストっぽい。要確認。TODO]。 + +== jump tableを無効化する + +やるだけ。ここでbrainfuckのLLVM IRコードがコンパイルできるようになった。 + +== lldにバックエンドを追加する + +やるだけ。hi6/lo10に対応するのが面倒だったが、特別変わったところはない。 +RISC-Vのものを参考にして、アセンブリから作ったオブジェクトファイルをリンクした結果を +確認するテストを追加した。 + +== 16bit命令を活用する + +=== `CompressPat` の調査 + +現状24bit命令で表現されているところで、変換できる部分は16bit命令を使用するようにしたい。 +これにはRISC-V LLVMバックエンドにて導入された `CompressPat` の仕組みを利用する。 +`CompressPat` を導入したコミット(c1b0e66b586b6898ee73efe445fe6af4125bf998) +<>を参考にする。 + +`CompressPat` の仕組みは `utils/TableGen/RISCVCompressInstEmitter.cpp` にて実装されている。 +エントリポイントは `llvm::EmitCompressInst` で、ここから呼ばれる +`RISCVCompressInstEmitter::run` が `RISCVCompressInstEmitter` クラスの関数を呼び出すことにより +処理が行われる。まず `CompressPat` を親に持つ定義( `def` )をすべて取り出し、 +これらを `evaluateCompressPat` に渡す。 +その後ファイルヘッダの出力、 `compressInst` 関数のソースコードの出力、 +`uncompressInst` 関数のソースコードの出力と続く。 + +`CompressPat` 自体は次のように定義される。ここで `Input/Output` は入力・出力を表すDAGをとり、 +`Predicates` は `HasStdExtC` などの述語をとる。 + +`evaluateCompressPat` では i) まずパターンとして記述された内容が正しいか否かを +判断し、正しければ ii) 変換元から変換先へのパターンを登録する。このときに対象のDAGを解析し、 +「元のどのオペランドが先のどのオペランドに対応するか」という情報 +( `SourceOperandMap/DestOperandMap` )を得る必要がある。 +なおそのあとに `PatReqFeatures` を構成しているが、これは `Predicates` を操作しているようだ。 + +`emitCompressInstEmitter` では `CompressPatterns` を使用して +`compressInst` 関数及び `uncompressInst` 関数の出力を行う。 +どちらの関数が出力されるかは `Compress` 引数によってきまる。 +この関数では、現在注目している `MachineInstr` を変換すべきか否か・変換するならば何に変換するべきか +を決める巨大な `switch` 文を作成する。 +各々の `case` は `if ( cond ) { code }` という形になっており、 `cond` の部分を `CondString` に、 +`code` の部分を `CodeString` に構築している。おおよそ `cond` の部分には +「変換元・変換後のオペランドのパターンが現在見ている `MachineInstr` と一致しているか」を +調べる条件式が入り、 `code` の部分には「現在見ている `MachineInstr` のオペランドを変換後の +ものに置き換える」ためのコードが入る。 + +`switch` 文の条件式には `MI.getOpcode()` を戻り値を使っている。1つのopcodeは +一般に複数のパターンにマッチする場合もあるfootnote:[ただしもちろんオペランドの +内容によって一意に定まる必要はある(多分;TODO)。]。そのようなケースは +(C\++の `switch` が同名のラベルを複数個持てないという言語仕様により)一つにまとめる必要が +ある。ここでは関数冒頭で `std::stable_sort` を呼び出したうえで、今見ているopcodeと +前に処理したopcodeが同じか否かによって判断している。なお `std::stable_sort` は安定ソートを +行うため、先に定義されたパターンがより早く試されることになる。 +その後 `Predicates` を満たしているか否かを判断するコードを出力する。 + +それから変換元オペランドのパターンマッチに入る。まずtied operandの場合(2アドレス方式の +命令など)は、その結び付けられたオペランド同士が等しいかどうか確認する。 +その上で、いま見ているオペランドがパターンの変換元オペランドと等しいかどうかを確認する。 +ただし実際に確認できるのはfixed immediate( `OpData::Imm` )とfixed register( `OpData::Reg` ) +の場合のみである。つまり固定されていないレジスタや即値の場合( `OpData::Operand` )は +変換元オペランドに関するチェックのコードは生成されないfootnote:[これはおそらく、 +すでにその段階に到達する時点で通常のパターンマッチにおけるチェックが済んでいるからであると +推察されるが未確認。TODO]。 + +ここから変換後オペランドのパターンマッチに入る。fixed registerの場合はすでにチェックが +終わっているため、置換のコードのみを出力する。それ以外の場合にはチェックのコードを出力する。 +なお即値チェックで使用されている `getMCOpPredicate` 関数は、 +`ValidateMCOperand` に渡すindexを返却する。このindexによってどのオペランドかを識別し、 +その型に設定された `MCOperandPredicate` の内容を出力する。 + +各即値型の( `simm12` など) `MCOperandPredicate` を見ると、定数値として計算できる場合は +計算した後にビット幅を確かめている一方で、bare symbolの場合(何のmodifierも付されておらず +単にシンボルがある場合)には無条件でチェックを通している。これは一見問題に見えるが、 +ここで入力されるアセンブリは全てcodegenによって生成され、かつopcodeによって +区別されたものである。したがって「変換元の命令の条件を満たしている」という +意味でwell-formedであって、例えば `simm12` にbare symbolが入力された場合に +対応する命令は `JAL` のみで `ADDI` などではない。したがって問題にならない。 +逆に「他のシンボルを通す必要がないのか」という点は良くわからない。TODO +footnote:[実際RISC-Vの `MCOperandPredicate` で使用される `isBareSymbolRef` を +全て `true` としてみたところ、圧縮命令に関するテストは全てパスしたように見えた。 +一方で落ちるテストも2件あったことから、 `CompressPat` 以外でも `MCOperandPredicate` が +使用されていることが伺える。LLVMでは複数の箇所に記述されたプログラムの断片が +合わさってパターンマッチを行うため、全貌を把握することが難しいように思える。] + +=== `CompressPat` をCAHPに導入する + +`utils/TableGen/RISCVCompressInstEmitter.cpp` をコピーして `CAHPCompressInstEmitter.cpp` を作る。 +`RVInst` を参照するところは `CAHPInst` を参照するように変更する。 +なお `Predicates` に関する処理はCAHPには不要だが面倒なので放置する。 +また `TableGen.cpp` を変更し `-gen-cahp-compress-inst-emitter` オプションを作成する。 + +`CAHPAsmWriter` を作成し `int PassSubtarget = 1` とする必要があった。 +RISC-Vのパッチを参考にする。 + +RISC-Vは `addi x1, x1, 10` のようなアセンブリが入力された場合にも `c.addi` に変換する。 +つまりアセンブラも `compressInst` を呼ぶが、CAHPではこのようなことは行わない。 +そのため `compressInst` を呼ぶのは `AsmPrinter` に限られfootnote:[ちょうど +疑似命令の展開と同じような操作である。]、また `uncompressInst` は全く呼ぶ必要がない。 + +なおTableGenのコードを書き換えてビルドしようとするとエラーが発生する。 +これはLLVMのコードをビルドするために使用するTableGen( `NATIVE/bin/llvm-tblgen` )が +再コンパイルされないためである。これを解決するためにはフルビルドするか、 +フルビルドの際のビルドスケジュール( `cmake -nv` で得られるログ)を参考にして +`NATIVE/bin/llvm-tblgen` を次のように再コンパイルする必要がある。 + + cd /path/to/llvm-project/build/NATIVE && \ + /usr/bin/cmake --build /path/to/llvm-project/build/NATIVE --target llvm-tblgen --config Release + +テストが大幅に壊れるので修正する。RISC-Vの場合は圧縮命令を有効化するか否かを表す +オプションが存在する( `-mattr=+c` )が、CAHPの場合は常時有効化されるため、 +24bitの命令と16bit命令の両方をテストするには次のようにひと工夫必要である。 + + define i16 @addi(i16 %a, i16 %b) nounwind { + ; CAHP-LABEL: addi: + ; CAHP: # %bb.0: + ; CAHP-NEXT: addi a0, a1, 1 + ; CAHP-NEXT: jr ra + %1 = add i16 %b, 1 + ret i16 %1 + } + define i16 @addi2(i16 %a) nounwind { + ; CAHP-LABEL: addi2: + ; CAHP: # %bb.0: + ; CAHP-NEXT: addi2 a0, 1 + ; CAHP-NEXT: jr ra + %1 = add i16 %a, 1 + ret i16 %1 + } + +== ClangをCAHPに対応させる + +やるだけ。すでにlldがCAHPに対応しているので `ld.lld` を呼ぶようにしておく。 + +== 分岐解析に対応する + +RISC-Vのパッチを参考にしながら分岐解析に対応する。やるだけ。 + +== インラインアセンブリに対応する + +branch relaxationのテストを書くためにはインラインアセンブリに対応しておく +必要がある。忘れていた。 + +== branch relaxationに対応する + +やるだけ。 + +== 単体の `sext/zext/trunc` に対応する + +やるだけ。 + +== fastccに対応する + +RISC-Vと同じようにcccと同様にしておく。やるだけ。 + +== `jal` を活用する + +現状のCAHPではROMのサイズに512B以下という制限があるため、 +全ての関数呼び出しは `jal` によって解決できる。これを反映し、現在 `jalr` によって +行っている関数呼び出しを `jal` によって行いたい。 + +`LowerCall` での `LowerGlobalAddress` と `LowerExternalSymbol` の呼び出しをやめ、 +`%hi/%lo` で包むことなく `TargetGlobalAddress/TagetExternalSymbol` に変換する。 +これで `LowerExternalSymbol` は不要になった。 + +次いでこれに対するパターンマッチをTableGenにて記述する。 +ここで `tglobaladdr` と `texternalsym` は `OtherVT` ではなく `i16` にマッチすることに +注意する。そのため `OtherVT` の11bit即値を表す `simm11_branch` と +`i16` の11bit即値を表す `simm11` を分ける必要がある。 `js` は `simm11_branch` を +とり `jsal` は `simm11` をとる。 + +ここで気がついたが、実は `ExternalSymbol` を利用したテストは一つも存在しなかった。 +したがって上の `ExternalSymbol` に対する変更は正しいかどうか判断がつかない。 +仕方がないので、このあとに行う乗算の導入で確認することにする。 + +以上の変更を加えて `-filetype=obj` を有効化すると `invalid fixup kind` の +assertで落ちてしまう。直接の原因は `CAHPMCExpr::VK_CAHP_None` を持った +`CAHPMCExpr` が `CAHPMCCodeEmitter::getImmOpValue` に渡されてしまうことである。 +デバッガを使って確認すると、この渡されてくる式の中身は `MCExpr::SymbolRef` であって、 +関数名のシンボルが入っている。すなわちこれは本来 `MCSymbolRefExpr` として中身単体で +渡されてくるべきものであって `CAHPMCExpr` でラップしているのは余計なのだ。 + +ではどこで余計にラップしているのか。ここで `CAHPMCExpr` を生成する箇所は二箇所あることに +注意する必要がある。一つは `AsmParser` で、ここはアセンブリが入力として +与えられたときに動くため、今回は関係がない。もう一つは `MachineInstr` から `MCInst` に変換する +`llvm::LowerCAHPMachineInstrToMCInst` である。コード生成から直接オブジェクトファイルを +生成する際にはこれが使われる。これまでの実装では、ここから呼び出される +`LowerSymbolOperand` で、対象がどのようなシンボルであっても `CAHPMCExpr::create` を +用いて `CAHPMCExpr` でラップしていた。これが原因である。 +RISC-Vを見習い、作成したい式が `CAHPMCExpr::VK_CAHP_None` 以外であるときのみに +これを限定すれば解決した。 + +== 乗算に対応する + +`__mulhi3/__musi3/` が適宜出力されるようにする。やるだけ。 + +== 除算・剰余に対応する + +`__udivhi3/__udivsi3/__divhi3/__divsi3/__umodhi3/__modhi3` +が適宜出力されるようにする。やるだけ。 + +== `hlt` 疑似命令を追加する + +`js 0` のエイリアスとして `hlt` 疑似命令を追加する。やるだけ。 + +== `crt0.o` と `cahp.lds` の導入 + +スタートアップのためのオブジェクトファイル `crt0.o` と、 +リンカスクリプト `cahp.lds` を導入し、これが `sysroot` から読み込まれるように +Clangの `CAHPToolChain` を改変する。なおこれらが `sysroot` にあるのは本来おかしいのだが、 +CAHPがベアメタル専用アーキテクチャのようになっている現状、 +これらのファイルをどこに置けばよいかは判然としない。TODO + +`cahp.lds` と、 `crt0.o` の元になる `crt0.s` は `cahp-rt` という +レポジトリで管理することにする。このあたりはRV16Kと変わらない。 + +== `--nmagic` の有効化 + +セクションのページサイズでのアラインメントを無効化して、 +リンク後のバイナリサイズを小さくする。RV16Kのときには `--omagic` を使用していたが、 +これは `.text` に書き込み可フラグを立てるためにセキュリティ上問題がある。 +LLVM 9.0.0にてLLDに導入された `--nmagic` を使えばこの問題は発生しない。 + +実装はやるだけ。 + +== libcの有効化 + +`-nostdlib` や `-nodefaultlibs` が指定されない限りにおいて `-lc` を自動的に指定する。 +やるだけ。 `cahp-rt` と合わせて、これで掛け算や割り算を使用できるようになった。 + +== `li a0, foo` をエラーにする + +`li` や `addi` などの即値オペランドには10bit符号付き即値が指定される。 +ここにシンボルが指定される場合、そのシンボルは `%lo(...)` という形をとる +必要がある。つまり何もmodifierが付与されていないシンボル(bare symbol)を +受理してはいけない。例えば次のような入力をエラーとする必要がある。 + + li a0, foo + +RISC-Vでは<>にてこれに対応している。 +このコミットを参考にして修正する。 + +`AsmParser` の `isSImm10` にてシンボルを扱う場合には i) +そのシンボルに `%lo` が付されている、あるいは ii) bare symbolでかつ定数式である +ときのみ `true` を返し符号付き10bit即値として認める。 +なお、条件分岐命令のオペランドも符号付き10bit即値を受け取るが、 +こちらはbare symbolでなければならない。そこで `simm10_branch` には +`isBareSImm10` という新しい関数を参照させ、単にbare symbolであるか否かを +調べることにしておく。 + +`%hi` についても `isSImm6` について同様の処理を行う。 + +== `llvm-objdump` の調査 + +`llvm-objdump -D hoge` として `.text` セクション以外でデコードできなくて死ぬ。 + + llvm-objdump: /llvm-project/llvm/lib/Target/CAHP/Disassembler/CAHPDisassembler.cpp:73: DecodeStatus decodeUImmOperand(llvm::MCInst &, uint64_t, int64_t, const void *) [N = 4]: Assertion `isUInt(Imm) && "Invalid immediate"' failed. + +リリース版ビルドだと発生しない。謎。 + +24bit命令の10bit即値と4bit即値、及び16bit命令の6bit即値と4bit即値を、 +同じ命令のクラスとしてTableGenにて記述していたことが原因だった。 +すなわち次のような `CAHPInst24I` クラスで10bit/4bit即値を受け取る命令の両方を +処理していたことが原因だった。 + + class CAHPInst24I opcode, dag outs, dag ins, string opcodestr, string argstr> + : CAHPInst24 { + bits<4> rd; + bits<4> rs1; + bits<10> imm; + + let Inst{23-16} = imm{7-0}; + let Inst{15-12} = rs1; + let Inst{11-8} = rd; + let Inst{7-6} = imm{9-8}; + let Inst{5-0} = opcode; + } + +このとき「まともな」ELFバイナリであれば、4bit即値を受け取る命令( `lsri` など)の +6-7ビット目と20-23ビット目には0が入っているため `imm` は正しく4bit即値となる。 +しかし実際にはこれらのビットはdon't careであり、0が入っているとは限らないうえ、 +不正なバイナリであれば何が入っているかわからない。上の `llvm-objdump` を使った際には +これらのビットが0ではなく、結果として4bitよりも大きい値が `imm` に入ってしまった。 + +これを防ぐためには、4bit即値と10bit即値を受け取る命令のクラスを分ければ良い。 + +.... +// 24-bit I-instruction format for 10bit immediate +class CAHPInst24I_10 opcode, dag outs, dag ins, string opcodestr, string argstr> +: CAHPInst24 { + bits<4> rd; + bits<4> rs1; + bits<10> imm; + + let Inst{23-16} = imm{7-0}; + let Inst{15-12} = rs1; + let Inst{11-8} = rd; + let Inst{7-6} = imm{9-8}; + let Inst{5-0} = opcode; +} + +// 24-bit I-instruction format for 4bit immediate +class CAHPInst24I_4 opcode, dag outs, dag ins, string opcodestr, string argstr> +: CAHPInst24 { + bits<4> rd; + bits<4> rs1; + bits<4> imm; + + let Inst{23-20} = 0; + let Inst{19-16} = imm{3-0}; + let Inst{15-12} = rs1; + let Inst{11-8} = rd; + let Inst{7-6} = 0; + let Inst{5-0} = opcode; +} +.... + +16bit命令の `CAHPInst16I` についても同様である。 + +せっかくなので、回帰バグを防ぐためにテストを書く。 +不正なバイト列footnote:[ISA上はdont' careのbitが0でないだけで不正ではないが、 +LLVMバックエンドとしてはこれらを0として扱うことにする。]に対して +正しくunknownが出力されるかをチェックする。 + +どこに書くのがLLVMとして正当なのかわからないが、 +とりあえずllvm-objdumpのテストとして書くことにする。x86の +`disassemble-invalid-byte-sequences.test` を参考にする。 +`yaml2obj` を使えばすきなELFバイナリを作ることができるので便利だ。 + +== スタックを利用した引数渡し + +やるだけ。先達はなんとやら。 + +== `byval` の対応 + +やるだけ。 `byval` が絡むのは関数呼び出しの引数だけで、 +呼ばれる側や戻り値には関係がないことに注意。 +呼ばれる側はポインタが渡される場合と変わりなく、 +戻り値は `sret` として引数に組み込まれる。 + +== 命令スケジューリングの実装 + +cahp-emerald以降はスーパースカラに対応するらしいので、 +LLVM側でもスーパスカラで効率的に動作するアセンブリを出力できるように +調整する。具体的には命令スケジューリングの設定をする。 +残念ながらRISC-Vではこの設定は為されていないようだfootnote:[単一の +プロセッサをターゲットとしているわけではないからだろうか。]。 +LanaiやSparc・ARMなどのバックエンドを参考にする。 +また<>にも記述がある。 +<>も参考になる。 + +`include/llvm/Target/TargetSchedule.td` によると、 +命令スケジューリングにはいくつかの方法があり、 +さらにこれらが有機的に構成されているようだ<>。 + +<>によればinstruction itinerariesを利用する場合、 +TableGenファイルに各命令が属する命令スケジュールを記述する。 +スケジュール自体は `CAHPSchedule.td` に定義し、これを `CAHPInstrFormats.td` +や `CAHPInstrInfo.td` で使う。 + +`FuncUnit` のインスタンスとして機能ユニットを定義し、 +`InstrItinClass` のインスタンスとして命令スケジュールを定義する。 +各命令はいずれかの `InstrItinClass` に属する。 + +どの `InstrItinClass` がどのように共有リソース(機能ユニット)を利用するかを +記述するために `ProcessorItineraries` のインスタンスを定義する。 +ここでは `InstrStage` を用いて、各命令がその処理を完了するまでに何サイクルかかり、 +どの機能ユニットを使用するかを記述する。 + +ある命令がどの `InstrItinClass` に属するかは `Instruction` クラスの +`Itinerary` 属性に `InstrItinClass` を入れておくことによって記述される。 + +しかし上記のようなやり方は古いものとなっているようだ。(要確認; TODO +footnote:[LLVMのスケジューリング手法は一度大きく変わっている +<><><>。 +`InstrItinClass` などを使用する方法が古いスケジューリング手法と新しいスケジューリング手法の +どちらに入るのか良くわからない。 +ただし `TargetSchedule.td` にて `TargetItinerary.td` を `include` する箇所には +"Include legacy support for instruction itineraries."とコメントされているので、 +古いほうである可能性が高い。]) +<><><> +<>を参考にし、 `SchedMachineModel` をベースとして実装する。 +このとき参考になるのはAArch64で、特に `AArch64SchedA53.td` である<>。 + +次の4ステップで実装する<>。 +まずi) `SchedWrite` と `SchedRead` を用いてtargetごとにoperand categoryを定義し、ii) +その後それらを実際の命令と結びつける。これは命令に `Sched` を継承させることで実現する。 +`Sched` の引数にはオペランドに対応するoperand categoryを順に渡す。 +例えばADDならwrite, read, readのように並ぶことになる。 + +次にiii) sub-target毎に `SchedMachineModel` を用いてモデルを定義するfootnote:[ここでは、 +「基本的なISAが同じ(単一のtarget)で、それを実装するプロセッサ毎にスペックが異なる +(sub-target)」というコンセプトが採用されている。]。 +ここで「一度にどれだけの命令を発行できるか」などを決める。 +最後にiv) `ProcResource` を用いてそのsub-targetがいくつの共有リソースを持っているか決め、 +`WriteRes` を用いてそれらをoperand categoryと結びつける。同時に、その命令を実行するのに +何サイクルかかるかを `Latency` として記述する。 + +以上で記述した情報を用いて、LLVM core(の `MachineScheduler` )は命令列をシミュレーションし、 +ヒューリスティックを用いてよしなに命令をスケジュールしてくれるらしい。 +ほかにも `ReadAdvance` を用いてフォワーディングを表現したりできるfootnote:[ただしこれは +`Latency` で表現できることが多いように思う。要検討;TODO]。 +詳しくは<>を参考のこと。 + +`Latency` の単位が良くわからない。Cortex-A53のパイプライン図<> +と比較すると `AArch64SchedA53.td` の記述はfull latencyを4とするなど、 +明らかに間違っているように見える。 +また `WriteRes` と `ReadAdvance` の両方でフォワーディングを考慮するのは二重でreduced +cycleをカウントしているようにも見える。わけが分からん。 + +`include/llvm/MC/MCSchedule.h` を読む。Latencyの概念については +`struct MCSchedModel` のコメントが(多少)参考になる。 + +.... +/// The abstract pipeline is built around the notion of an "issue point". This +/// is merely a reference point for counting machine cycles. The physical +/// machine will have pipeline stages that delay execution. The scheduler does +/// not model those delays because they are irrelevant as long as they are +/// consistent. Inaccuracies arise when instructions have different execution +/// delays relative to each other, in addition to their intrinsic latency. Those +/// special cases can be handled by TableGen constructs such as, ReadAdvance, +/// which reduces latency when reading data, and ResourceCycles, which consumes +/// a processor resource when writing data for a number of abstract +/// cycles. +.... + +TableGenコードのデバッグをする際には次のようにすればよいらしい<>。 + + $ llvm-tblgen --debug-only=subtarget-emitter --print-records -I=/work/llvm.org/llvm/include/... + +これまでのプロセッサ(generic; スーパースカラなし)と +これからのプロセッサ(emerald; スーパースカラあり)を +区別して扱うためにsubtargetを追加する。これによってARMのように +`-mcpu=generic` ・ `-mcpu=emerald` などとオプションとして +指定できるようになる。 + +コード上は `ProcessorModel` を新たに追加するだけである。 +ARMでは `ProcA53` という `SubtargetFeature` を定義しているが、 +特別いじる属性などはないためこれは作成しない。 +ただしこれだけでは `llc` のオプションとしては `-mcpu` が機能するが、 +`clang` に渡すと `argument unused during compilation: '-mcpu=emerald'` +というエラーが出てしまう。 + +これに対応するためには `clang` でのオプション解析を行う必要がある。 +すなわち `Driver/ToolChains/CommonArgs.cpp` の `tools::getCPUName` をいじって +`Driver/ToolChains/Arch/CAHP.cpp` の `cahp::getCAHPTargetCPU` が呼ばれるようにする +foonote:[ちなみにtarget featuresをいじる場合は `Driver/ToolChains/Clang.cpp` の +`getTargetFeatures` をいじれば良いようだ。]。 +さらにClangの `CAHPTargetInfo` をいじって `isValidCPUName` などを正しく実装する。 +ARMだとClang側からLLVM coreのsupport関数を呼び出すなどして大変なことになっているが、 +その本質はLanaiのバックエンドが分かりやすい。要するに `StringSwitch` を使って、 +引数の文字列がCPUの名前として正しいかどうかを振り分けているだけである。 +この実装によって「 `-mcpu` が渡された場合にはその引数をcpu nameとして後の処理に回す」 +「渡されたcpu nameが正しいものであるかを判断し、正しければLLVM coreに渡す」という +処理が実装でき、無事Clangでも `-mcpu` が使用できるようになる。 + +次のようにすれば `generic` の場合と `emerald` の場合の差を見ることができる。 + + $ ls bf.c | while read line; do \ + diff <(bin/clang -target cahp -mcpu=generic -c -S -o - -Oz $line) \ + <(bin/clang -target cahp -mcpu=emerald -c -S -o - -Oz $line); done + +スケジューリングの詳細を知りたい場合は次のように `llc` を実行する。 + + $ bin/llc -enable-misched -debug-only=machine-scheduler + +なお `clang` で間接的に `llc` を実行したい場合は `-mllvm` オプションにつなげれば良い +<>。(未確認;TODO) + + $ clang ... -mllvm -enable-misched -mllvm -enable-post-misched -mllvm -misched-postra + +ただこれらを見ても、なにをもってLLVMがスケジューリングしているのかは +そこまで自明ではないfooatnote:[`SU` は `SUnit` の略で、多分これはschedule unitの略で、 +つまりスケジューリングの単位なので、各々の命令のことのようだ。]。 +emeraldのエミュレーションで評価するのが一番適切である。TODO + +`CAHPSubtarget` にて `enableMachineScheduler` をオーバーライドし +`true` を返すようにしなければ新しいスケジューラである +MISchedulerを使用してくれないようだ<>footnote:[これによって +`llc` に `-enable-misched` オプションが渡る。]footnote:[これによってテストが壊れるので +修正する。]。 +また同様に `enablePostRAScheduler` から `true` を返すようにしなければ、 +レジスタ割り付け後のスケジューリングは行ってくれないようだが、 +こちらは実行時エラーが出てしまった。 + +`ReadALU` のような `ReadSched` は、命令の `Sched` に指定するだけではエラーに +なってしまう。 `ReadAdvance` などで使用しなければいけない。逆に言えば、 +特別な属性を指定する必要が無いのであれば作る必要はない。 +また複数の `ReadAdvance` を同じ `Read*` に対して定義することはできない。 +この制限により「 `WriteALU` には `2` で `WriteLdSt` には `1` 」のようなことはできないようだ。 + +emeraldを「正しく」モデル化しようとすると `WriteALU/WriteLdSt` のlatencyはともに3、 +`ReadAdvance` で `WriteALU` は2, `WriteLdSt` は1とするのが筋のように思えるが、 +上記の理由からこれは不可能である。仕方がないので `WriteALU` のlatencyを1, `WriteLdSt` を2 +とする。 + +`enablePostRAScheduler` を `true` にしたい。 `enablePostRAScheduler` のコメントを読むと +`SchedMachineModel` にて `let PostRAScheduler = 1;` としろと +書いてあるfootnote:[`PostRAScheduling` が原文ママだがこれは `PostRAScheduler` の誤植のようだ。] +のでそうするが、同じエラーが出る。 + + clang-9: /llvm-project/llvm/lib/CodeGen/MachineBasicBlock.cpp:1494: MachineBasicBlock::livein_iterator llvm::MachineBasicBlock::livein_begin() const: Assertion `getParent()->getProperties().hasProperty( MachineFunctionProperties::Property::TracksLiveness) && "Liveness information is accurate"' failed. + +どうやら一部のbasic blockに `TracksLiveness` というフラグが立っていないことが原因のようだ。 +このフラグについては `MachineFunctionProperties` のコメントに次のようにある。 + + // TracksLiveness: True when tracking register liveness accurately. + // While this property is set, register liveness information in basic block + // live-in lists and machine instruction operands (e.g. kill flags, implicit + // defs) is accurate. This means it can be used to change the code in ways + // that affect the values in registers, for example by the register + // scavenger. + // When this property is clear, liveness is no longer reliable. + +AVRやWebAssemblyはこれを次のように明示的に立てている場所がある。 + + MF.getProperties().set(MachineFunctionProperties::Property::TracksLiveness); + +とりあえず次のように `assert` をコメントアウトすれば動作した。 + + MachineBasicBlock::livein_iterator MachineBasicBlock::livein_begin() const { + //assert(getParent()->getProperties().hasProperty( + // MachineFunctionProperties::Property::TracksLiveness) && + // "Liveness information is accurate"); + return LiveIns.begin(); + } + +`BranchFolder::OptimizeFunction` で `MRI.invalidateLiveness()` が呼び出されることが +原因のようだ。これを抑制するためには `TargetRegisterInfo::trackLivenessAfterRegAlloc` を +`CAHPRegisterInfo` でオーバーライドして `true` を返すようにすれば良い +footnote:[RISC-Vではbranch relaxationに対応させる際に実装されていた<>。]。 +これで `let PostRAScheduler = 1;` とできるようになった。 + +== 動的なスタック領域確保に対応する + +やるだけ。 + +== `frameaddr/returnaddr` に対応する + +やるだけ。 `frameaddr-returnaddr.ll` の `test_frameaddress_3_alloca` は +存在価値がよく分からなかったので削った。 + +== emergency spillに対応する + +RV16Kのときには即値幅が小さすぎたために対応しなかったが、 +今回は `li` の即値幅が10bitあることもあり対応したほうがよさそうだ。 +RISC-Vの実装にならい、スタックサイズが符号付き9bitに収まらない(256バイト以上)ときに +emergency spill slotをスタック上に用意し、どんなときでもレジスタを対比できるようにしておく。 +emergency spillの実装そのものは<>を参考にすればよく、 +それほど難しくない。 + +むしろ同実装のテストケースが、その意味を理解するという点では難解である。 +CAHPの場合は次のようなテストになった。 + + %data = alloca [ 3000 x i16 ], align 2 + %ptr = getelementptr inbounds [3000 x i16], [3000 x i16]* %data, i16 0, i16 1000 + %1 = tail call { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } asm sideeffect "nop", "=r,=r,=r,=r,=r,=r,=r,=r,=r,=r,=r,=r,=r"() + %asmresult0 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 0 + %asmresult1 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 1 + %asmresult2 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 2 + %asmresult3 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 3 + %asmresult4 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 4 + %asmresult5 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 5 + %asmresult6 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 6 + %asmresult7 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 7 + %asmresult8 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 8 + %asmresult9 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 9 + %asmresult10 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 10 + %asmresult11 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 11 + %asmresult12 = extractvalue { i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16, i16 } %1, 12 + store volatile i16 %a, i16* %ptr + tail call void asm sideeffect "nop", "r,r,r,r,r,r,r,r,r,r,r,r,r"(i16 %asmresult0, i16 %asmresult1, i16 %asmresult2, i16 %asmresult3, i16 %asmresult4, i16 %asmresult5, i16 %asmresult6, i16 %asmresult7, i16 %asmresult8, i16 %asmresult9, i16 %asmresult10, i16 %asmresult11, i16 %asmresult12) + +まず冒頭で大きくスタック上に領域を確保することにより、 +emergency spill slotの確保を実行させると同時に、以降のレジスタのspillに +複数命令(典型的には `lui` と `addi` )を要するようにする。 +この領域は、以降のコードを最適化によって +消されること無く確実に実行するためにも用いられるfootnote:[本当にそうかは未確認;TODO]。 + +次にインラインアセンブリを用いて `nop` を呼び出す。この `nop` は13個footnote:[この値は +実験的に求めた。この値を超えるとレジスタ割り付けを行うことができない。 +この値が理論的にどこから来ているのかは未確認;TODO]のレジスタに値を +書き込む命令として扱う。これによって大量のレジスタを消費し、コード生成部にregister pressureを +かけることがこのテストの本質である。すなわちこの `nop` を実行する際には大量のレジスタのspillが +発生しfootnote:[これは生成されるアセンブリを見れば明らかである。]、しかもそれらは +一命令で行うことができない。したがってemergency spillが発生する。 +以降のコードは、この `nop` が最適化によって +消されること無く確実に実行するためのコードであると推察されるfootnote:[本当にそうかは未確認 +;TODO]。 + +== -----DONE LINE ----- +== 末尾再帰の対応 +== 落ち穂拾い +=== `ROTL/ROTR/BSWAP/CTTZ/CTLZ/CTPOP` に対応する +=== 32bitのシフトに対応する +=== 間接ジャンプに対応する +=== `BlockAddress` のlowerに対応する +== 可変長引数関数 +== デバッグ情報の出力 + +== `MachineBlockPlacement` のrollback<> + +これ違うっぽい。 + +[bibliography] +== 参照 + +- [[[github_riscv-llvm_docs_01,1]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/01-intro-and-building-llvm.mkd +- [[[llvm_getting-started,2]]] https://llvm.org/docs/GettingStarted.html +- [[[clang_gettings-started,3]]] https://clang.llvm.org/get_started.html +- [[[asciidoctor_user-manual,4]]] https://asciidoctor.org/docs/user-manual/ +- [[[riscv,5]]] https://riscv.org/ +- [[[riscv_specifications,6]]] https://riscv.org/specifications/ +- [[[fox-llvm,7]]] 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』(柏木 餅子・風薬・矢上 栄一、株式会社インプレス、2013年) +- [[[github_riscv-llvm_docs_02,8]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/02-starting-the-backend.mkd +- [[[github_riscv-llvm_patch_02,9]]] https://github.com/lowRISC/riscv-llvm/blob/master/0002-RISCV-Recognise-riscv32-and-riscv64-in-triple-parsin.patch +- [[[github_riscv-llvm,10]]] https://github.com/lowRISC/riscv-llvm +- [[[youtube_llvm-backend-development-by-example,11]]] https://www.youtube.com/watch?v=AFaIP-dF-RA +- [[[msyksphinz_try-riscv64-llvm-backend,12]]] http://msyksphinz.hatenablog.com/entry/2019/01/02/040000_1 +- [[[github_riscv-llvm_patch_03,13]]] https://github.com/lowRISC/riscv-llvm/blob/master/0003-RISCV-Add-RISC-V-ELF-defines.patch +- [[[github_riscv-llvm_patch_04,14]]] https://github.com/lowRISC/riscv-llvm/blob/master/0004-RISCV-Add-stub-backend.patch +- [[[github_riscv-llvm_patch_06,15]]] https://github.com/lowRISC/riscv-llvm/blob/master/0006-RISCV-Add-bare-bones-RISC-V-MCTargetDesc.patch +- [[[github_riscv-llvm_patch_10,16]]] https://github.com/lowRISC/riscv-llvm/blob/master/0010-RISCV-Add-support-for-disassembly.patch +- [[[llvm-writing_backend-operand_mapping,17]]] https://llvm.org/docs/WritingAnLLVMBackend.html#instruction-operand-mapping +- [[[llvm-writing_backend,18]]] https://llvm.org/docs/WritingAnLLVMBackend.html +- [[[github_riscv-llvm_patch_07,19]]] https://github.com/lowRISC/riscv-llvm/blob/master/0007-RISCV-Add-basic-RISCVAsmParser.patch +- [[[github_riscv-llvm_patch_08,20]]] https://github.com/lowRISC/riscv-llvm/blob/master/0008-RISCV-Add-RISCVInstPrinter-and-basic-MC-assembler-te.patch +- [[[llvm-tablegen,21]]] https://llvm.org/docs/TableGen/index.html +- [[[github_riscv-llvm_patch_09,22]]] https://github.com/lowRISC/riscv-llvm/blob/master/0009-RISCV-Add-support-for-all-RV32I-instructions.patch +- [[[llvm_dev_ml-tablegen_definition_question,23]]] http://lists.llvm.org/pipermail/llvm-dev/2015-December/093310.html +- [[[llvm_doxygen-twine,24]]] https://llvm.org/doxygen/classllvm_1_1Twine.html +- [[[llvm-tablegen-langref,25]]] https://llvm.org/docs/TableGen/LangRef.html +- [[[github_riscv-llvm_docs_05,26]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/05-disassembly.mkd +- [[[github_riscv-llvm_patch_11,27]]] https://github.com/lowRISC/riscv-llvm/blob/master/0011-RISCV-Add-common-fixups-and-relocations.patch +- [[[github_riscv-llvm_docs_06,28]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/06-relocations-and-fixups.mkd +- [[[github_riscv-llvm_patch_13,29]]] https://github.com/lowRISC/riscv-llvm/blob/master/0013-RISCV-Initial-codegen-support-for-ALU-operations.patch +- [[[speakerdeck-llvm_backend_development,30]]] https://speakerdeck.com/asb/llvm-backend-development-by-example-risc-v +- [[[llvm-code_generator,31]]] https://llvm.org/docs/CodeGenerator.html +- [[[llvm-code_generator-target_independent_code_gen_alg,32]]] https://llvm.org/docs/CodeGenerator.html#target-independent-code-generation-algorithms +- [[[llvm-code_generator-selectiondag_instruction_selection,33]]] https://llvm.org/docs/CodeGenerator.html#selectiondag-instruction-selection-process +- [[[github_riscv-llvm_patch_15,34]]] https://github.com/lowRISC/riscv-llvm/blob/master/0015-RISCV-Codegen-support-for-memory-operations.patch +- [[[cpu0,35]]] https://jonathan2251.github.io/lbd/ +- [[[elvm-llvm_backend,36]]] https://github.com/shinh/llvm/tree/elvm +- [[[elvm-slide,37]]] http://shinh.skr.jp/slide/llel/000.html +- [[[github_riscv-llvm_patch_16,38]]] https://github.com/lowRISC/riscv-llvm/blob/master/0016-RISCV-Codegen-support-for-memory-operations-on-globa.patch +- [[[github_riscv-llvm_patch_17,39]]] https://github.com/lowRISC/riscv-llvm/blob/master/0017-RISCV-Codegen-for-conditional-branches.patch +- [[[todai_llvm_backend,40]]] https://github.com/cpu-experiment-2018-2/llvm/tree/master/lib/Target/ELMO +- [[[todai_llvm_backend-article,41]]] http://uenoku.hatenablog.com/entry/2018/12/25/044244 +- [[[github_riscv-llvm_patch_18,42]]] https://github.com/lowRISC/riscv-llvm/blob/master/0018-RISCV-Support-for-function-calls.patch +- [[[llvm-langref,43]]] http://llvm.org/docs/LangRef.html +- [[[fpga_develop_diary,44]]] http://msyksphinz.hatenablog.com/ +- [[[llvm-anton_korobeynikov_2012,45]]] https://llvm.org/devmtg/2012-04-12/Slides/Workshops/Anton_Korobeynikov.pdf +- [[[llvm-welcome_to_the_back_end_2017,46]]] https://www.youtube.com/watch?v=objxlZg01D0 +- [[[ean10-howto-llvmas,47]]] https://www.embecosm.com/appnotes/ean10/ean10-howto-llvmas-1.0.html +- [[[lowrisc-devmtg18,48]]] https://www.lowrisc.org/llvm/devmtg18/ +- [[[LLVMBackend_2015_03_26_v2,49]]] http://www.inf.ed.ac.uk/teaching/courses/ct/other/LLVMBackend-2015-03-26_v2.pdf +- [[[rui-compilerbook,50]]] https://www.sigbus.info/compilerbook +- [[[krister-writing_gcc_backend,51]]] https://kristerw.blogspot.com/2017/08/writing-gcc-backend_4.html +- [[[llvm-ml-129089,52]]] http://lists.llvm.org/pipermail/llvm-dev/2019-January/129089.html +- [[[llvm-langref-datalayout,53]]] https://llvm.org/docs/LangRef.html#langref-datalayout +- [[[github-frasercrmck_llvm_leg,54]]] https://github.com/frasercrmck/llvm-leg/tree/master/lib/Target/LEG +- [[[llvm_doxygen-InitMCRegisterInfo,55]]] https://llvm.org/doxygen/classllvm_1_1MCRegisterInfo.html#a989859615fcb74989b4f978c4d227a03 +- [[[llvm-programmers_manual,56]]] http://llvm.org/docs/ProgrammersManual.html +- [[[llvm-writing_backend-calling_conventions,57]]] https://llvm.org/docs/WritingAnLLVMBackend.html#calling-conventions +- [[[riscv-calling,58]]] https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf +- [[[llvm_dev_ml-how_to_debug_instruction_selection,59]]] http://lists.llvm.org/pipermail/llvm-dev/2017-August/116501.html +- [[[fpga_develop_diary-20190612040000,60]]] http://msyksphinz.hatenablog.com/entry/2019/06/12/040000 +- [[[llvm_dev_ml-br_cc_questions,61]]] http://lists.llvm.org/pipermail/llvm-dev/2014-August/075303.html +- [[[llvm_dev_ml-multiple_result_instrs,62]]] https://groups.google.com/forum/#!topic/llvm-dev/8kPOj-_lbGk +- [[[stackoverflow-frame_lowering,63]]] https://stackoverflow.com/questions/32872946/what-is-stack-frame-lowering-in-llvm +- [[[llvm_dev_ml-selecting_frame_index,64]]] https://groups.google.com/d/msg/llvm-dev/QXwtqgau-jA/PwnHDF0gG_oJ +- [[[fpga_develop_diary-llvm,65]]] https://github.com/msyksphinz/llvm/tree/myriscvx/impl90/lib/Target/MYRISCVX +- [[[llvm-github_cd44ae,66]]] https://github.com/llvm/llvm-project/commit/cd44aee3da22f9a618f2e63c226bebf615fa8cf8 +- [[[llvm_phabricator-d43752,67]]] https://reviews.llvm.org/D43752 +- [[[llvm-compilerwriterinfo,68]]] https://llvm.org/docs/CompilerWriterInfo.html +- [[[wikipedia-The_Gleaners,69]]] https://en.wikipedia.org/wiki/The_Gleaners +- [[[github_riscv-llvm_patch_20,70]]] https://github.com/lowRISC/riscv-llvm/blob/master/0020-RISCV-Support-and-tests-for-a-variety-of-additional-.patch +- [[[llvm_phabricator-d47422,71]]] https://reviews.llvm.org/D47422 +- [[[llvm-extendingllvm,72]]] https://llvm.org/docs/ExtendingLLVM.html +- [[[llvm_dev_ml-001264,73]]] http://lists.llvm.org/pipermail/llvm-dev/2004-June/001264.html +- [[[llvm_phabricator-d42958,74]]] https://reviews.llvm.org/D42958 +- [[[compiler_rt,75]]] https://compiler-rt.llvm.org/ +- [[[github-riscv_compiler_rt,76]]] https://github.com/andestech/riscv-compiler-rt +- [[[github_riscv-llvm_patch_27,77]]] https://github.com/lowRISC/riscv-llvm/blob/master/0027-RISCV-Support-stack-frames-and-offsets-up-to-32-bits.patch +- [[[llvm_phabricator-d44885,78]]] https://reviews.llvm.org/D44885 +- [[[llvm_phabricator-d45859,79]]] https://reviews.llvm.org/D45859 +- [[[llvm-langref-poison_value,80]]] http://llvm.org/docs/LangRef.html#poisonvalues +- [[[github-emscripten-issues-34,81]]] https://github.com/emscripten-core/emscripten/issues/34 +- [[[switch_lowering_in_llvm,82]]] http://fileadmin.cs.lth.se/cs/education/edan75/part2.pdf +- [[[github-avr_llvm-issues-88,83]]] https://github.com/avr-llvm/llvm/issues/88 +- [[[asciidoctor-quickref,84]]] https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/ +- [[[llvm_phabricator-d56351,85]]] https://reviews.llvm.org/D56351 +- [[[hatenablog-rhysd-230119,86]]] https://rhysd.hatenablog.com/entry/2017/03/13/230119 +- [[[llvm_dev_ml-115805,87]]] http://lists.llvm.org/pipermail/llvm-dev/2017-July/115805.html +- [[[github_riscv-llvm_patch_29,88]]] https://github.com/lowRISC/riscv-llvm/blob/master/0029-RISCV-Add-support-for-llvm.-frameaddress-returnaddre.patch +- [[[github-riscv_llvm-clang,89]]] https://github.com/lowRISC/riscv-llvm/tree/master/clang +- [[[github-elvm_clang,90]]] https://github.com/shinh/clang/tree/elvm +- [[[github_riscv-llvm_patch_22,91]]] https://github.com/lowRISC/riscv-llvm/blob/master/0022-RISCV-Support-lowering-FrameIndex.patch +- [[[llvm_dev_ml-087879,92]]] http://lists.llvm.org/pipermail/llvm-dev/2015-July/087879.html +- [[[stackoverflow-27467293,93]]] https://stackoverflow.com/questions/27467293/how-to-force-clang-use-llvm-assembler-instead-of-system +- [[[github-riscv_llvm-clang-03,94]]] https://github.com/lowRISC/riscv-llvm/blob/master/clang/0003-RISCV-Implement-clang-driver-for-the-baremetal-RISCV.patch +- [[[github_riscv-llvm_patch_25,95]]] https://github.com/lowRISC/riscv-llvm/blob/master/0025-RISCV-Add-custom-CC_RISCV-calling-convention-and-imp.patch +- [[[llvm_dev_ml-106187,96]]] http://lists.llvm.org/pipermail/llvm-dev/2016-October/106187.html +- [[[llvm_phabricator-d39322,97]]] https://reviews.llvm.org/D39322 +- [[[cpu0-lld,98]]] http://jonathan2251.github.io/lbt/lld.html +- [[[youtube-how_to_add_a_new_target_to_lld,99]]] https://www.youtube.com/watch?v=FIXaeRU31Ww +- [[[llvm-smith_newlldtargetpdf,100]]] https://llvm.org/devmtg/2016-09/slides/Smith-NewLLDTarget.pdf +- [[[llvm-lld,101]]] https://lld.llvm.org/index.html +- [[[note-n9948f0cc3ed3,102]]] https://note.mu/ruiu/n/n9948f0cc3ed3 +- [[[lanai-isa,103]]] https://docs.google.com/document/d/1jwAc-Rbw1Mn7Dbn2oEB3-0FQNOwqNPslZa-NDy8wGRo/pub +- [[[github-blog_os-issues-370,104]]] https://github.com/phil-opp/blog_os/issues/370 +- [[[llvm_phabricator-d61688,105]]] https://reviews.llvm.org/D61688 +- [[[man-xtensa_linux_gnu_ld,106]]] https://linux.die.net/man/1/xtensa-linux-gnu-ld +- [[[man-elf,107]]] https://linuxjm.osdn.jp/html/LDP_man-pages/man5/elf.5.html +- [[[llvm_phabricator-d45385,108]]] https://reviews.llvm.org/D45385 +- [[[llvm_phabricator-d47882,109]]] https://reviews.llvm.org/D47882 +- [[[llvm_dev_ml-128257,110]]] https://lists.llvm.org/pipermail/llvm-dev/2018-December/128257.html +- [[[github_riscv-llvm_patch_31,111]]] https://github.com/lowRISC/riscv-llvm/blob/master/0031-RISCV-Implement-support-for-the-BranchRelaxation-pas.patch +- [[[github_riscv-llvm_patch_30,112]]] https://github.com/lowRISC/riscv-llvm/blob/master/0030-RISCV-Implement-branch-analysis.patch +- [[[stackoverflow-5789806,113]]] https://stackoverflow.com/questions/5789806/meaning-of-and-in-c +- [[[compiler_study_report,114]]] https://proc-cpuinfo.fixstars.com/2018/11/compiler_study_report/ +- [[[github-llvm-bcb36be8e3f5dced36710ba1a2e2206071ccc7ba,115]]] https://github.com/llvm/llvm-project/commit/bcb36be8e3f5dced36710ba1a2e2206071ccc7ba +- [[[llvm_dev_ml-059799,116]]] http://lists.llvm.org/pipermail/llvm-dev/2013-February/059799.html +- [[[tricore-llvm-slides,117]]] https://reup.dmcs.pl/wiki/images/7/7a/Tricore-llvm-slides.pdf +- [[[tricore-llvm,118]]] https://opus4.kobv.de/opus4-fau/files/1108/tricore_llvm.pdf +- [[[llvm_dev_ml-111697,119]]] http://lists.llvm.org/pipermail/llvm-dev/2017-April/111697.html +- [[[takayuki-no09,120]]] http://www.ertl.jp/~takayuki/readings/c/no09.html +- [[[hwenginner-linker,121]]] https://hwengineer.github.io/linker/ +- [[[koikikukan-000300,122]]] http://www.koikikukan.com/archives/2017/04/05-000300.php +- [[[stackoverflow-57735654_34997577,123]]] https://stackoverflow.com/questions/34997577/linker-script-allocation-of-bss-section#comment57735654_34997577 +- [[[redhat-ld_simple_example,124]]] https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/Using_ld_the_GNU_Linker/simple-example.html +- [[[llvm_phabricator-d45395,125]]] https://reviews.llvm.org/D45395 +- [[[llvm_phabricator-d45395-398662,126]]] https://reviews.llvm.org/D45395#inline-398662 +- [[[llvm-langref-inline_asm,127]]] http://llvm.org/docs/LangRef.html#inline-assembler-expressions +- [[[hazymoon-gcc_inline_asm,128]]] http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html +- [[[github_riscv-llvm_patch_28,129]]] https://github.com/lowRISC/riscv-llvm/blob/master/0028-RISCV-Add-basic-support-for-inline-asm-constraints.patch +- [[[llvm-langref-inline_asm-asm_template_argument_modifier,130]]] http://llvm.org/docs/LangRef.html#asm-template-argument-modifiers +- [[[github-llvm-0715d35ed5ac2312951976bee2a0d2587f98f39f,131]]] https://github.com/llvm/llvm-project/commit/0715d35ed5ac2312951976bee2a0d2587f98f39f +- [[[github_riscv-llvm_patch_32,132]]] https://github.com/lowRISC/riscv-llvm/blob/master/0032-RISCV-Reserve-an-emergency-spill-slot-for-the-regist.patch +- [[[github_riscv-llvm_patch_26,133]]] https://github.com/lowRISC/riscv-llvm/blob/master/0026-RISCV-Support-for-varargs.patch +- [[[github-fracture-wiki-how-dagisel-works,134]]] https://github.com/draperlaboratory/fracture/wiki/How-TableGen%27s-DAGISel-Backend-Works +- [[[welcome_to_the_back_end-slides,135]]] http://llvm.org/devmtg/2017-10/slides/Braun-Welcome%20to%20the%20Back%20End.pdf +- [[[life_of_an_instruction,136]]] https://eli.thegreenplace.net/2012/11/24/life-of-an-instruction-in-llvm/ +- [[[shinh-blog-010637,137]]] http://shinh.hatenablog.com/entry/2014/10/03/010637 +- [[[llvm_backend_intro,138]]] https://www.slideshare.net/AkiraMaruoka/llvm-backend +- [[[amazon-llvm_cookbook-customer_review,139]]] https://www.amazon.co.jp/dp/178528598X#customer_review-R28L2NAL8T9M2H +- [[[llvm_dev_ml-117139,140]]] https://lists.llvm.org/pipermail/llvm-dev/2017-September/117139.html +- [[[github_riscv-llvm_patch_85,141]]] https://github.com/lowRISC/riscv-llvm/blob/master/0085-RISCV-Set-AllowRegisterRenaming-1.patch +- [[[llvm_dev_ml-135337,142]]] https://lists.llvm.org/pipermail/llvm-dev/2019-September/135337.html +- [[[wikipedia-weak_symbol,143]]] https://en.wikipedia.org/wiki/Weak_symbol +- [[[wikipedia-remat,144]]] https://en.wikipedia.org/wiki/Rematerialization +- [[[llvm_phabricator-d46182,145]]] https://reviews.llvm.org/D46182 +- [[[nakata-compiler,146]]] 『コンパイラの構成と最適化(第2版)』(中田育男、朝倉書店、2009) +- [[[fpga_develop_diary-to_llvm9,147]]] http://msyksphinz.hatenablog.com/entry/2019/08/17/040000 +- [[[llvm_phabricator-d60488,148]]] https://reviews.llvm.org/D60488 +- [[[llvm_phabricator-rl364191,149]]] https://reviews.llvm.org/rL364191 +- [[[llvm_phabricator-d64121,150]]] https://reviews.llvm.org/D64121 +- [[[llvm-codingstandards,151]]] https://llvm.org/docs/CodingStandards.html +- [[[llvm_dev_ml-134921,152]]] https://lists.llvm.org/pipermail/llvm-dev/2019-September/134921.html +- [[[llvm_phabricator-d43256,153]]] https://reviews.llvm.org/D43256 +- [[[llvm_dev_ml-114675,154]]] http://lists.llvm.org/pipermail/llvm-dev/2017-June/114675.html +- [[[llvm_phabricator-d42780,155]]] https://reviews.llvm.org/D42780 +- [[[llvm_phabricator-d51732,156]]] https://reviews.llvm.org/D51732 +- [[[llvm_devmtg-schedmachinemodel,157]]] http://llvm.org/devmtg/2014-10/Slides/Estes-MISchedulerTutorial.pdf +- [[[llvm_dev_ml-098535,158]]] https://lists.llvm.org/pipermail/llvm-dev/2016-April/098535.html +- [[[llvm_devmtg-writinggreatsched,159]]] https://www.youtube.com/watch?v=brpomKUynEA +- [[[anandtech-11441,160]]] https://www.anandtech.com/show/11441/dynamiq-and-arms-new-cpus-cortex-a75-a55/4 +- [[[llvm_devmtg-larintrick,161]]] https://llvm.org/devmtg/2012-11/Larin-Trick-Scheduling.pdf +- [[[llvm-schedinorder,162]]] https://llvm.org/devmtg/2016-09/slides/Absar-SchedulingInOrder.pdf diff --git a/draft-rv16kv2.asciidoc b/draft-rv16kv2.asciidoc new file mode 100644 index 0000000..6a7c30e --- /dev/null +++ b/draft-rv16kv2.asciidoc @@ -0,0 +1,5433 @@ += [下書き] LLVMバックエンド for RV16Kv2 開発文書 +艮鮟鱇 +:toc: left + +== これはなに + +2019年にRV16Kv2という自作ISA用のLLVMバックエンドを作ったときの自分とメンバ用のメモ。 +メモなので当然読みにくい。これをブラッシュアップしてまともな文章にする予定だったが、 +その作業が遅れているので、一旦メモのまま公開する。内容について質問したい場合は +Twitter https://twitter.com/ushitora_anqou[@ushitora_anqou]までリプライなどを貰えれば反応するかもしれない。 + +この文章は、前に作ったRV32Kv1用LLVMバックエンドで得た知識を前提にして書かれている。 +RV32Kv1のメモは<>を参照のこと。 + +ソースコードはlink:https://github.com/virtualsecureplatform/llvm-rv16k[GitHub]にある。 + +ブラッシュアップはlink:https://github.com/ushitora-anqou/write-your-llvm-backend[GitHub]にて行っている。 + +このLLVMバックエンドの開発は、もともと2019年度未踏事業において +link:https://github.com/virtualsecureplatform/kvsp[Virtual Secure Platform]を開発するために行われた。 + + +== 簡単使い方 + +=== ビルドする + +とりあえずビルドする。ビルドには + +* `cmake` +* `ninja` +* `clang` +* `clang++` +* `make` +* `lld` + +が必要。 + +これらを入れた後 `cmake` を次のように走らせる。 + + $ cd /path/to/llvm-project + $ mkdir build + $ cd build + $ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS="clang;lld" \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RV16K" \ + ../llvm + $ cmake --build . + +=== アセンブラを使う + +アセンブラを起動する。アセンブラは `build/bin/llvm-mc` である。 + +.... +$ cat foo.s +hoge: + li a0, 55 + mov a1, a0 + add a1, a0 + j hoge + +$ bin/llvm-mc -arch=rv16k -filetype=obj foo.s | od -tx1z -Ax -v +000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< +000010 01 00 f6 00 01 00 00 00 00 00 00 00 00 00 00 00 >................< +000020 7c 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 >|.......4.....(.< +000030 04 00 01 00 08 78 37 00 89 c0 89 e2 00 52 f6 ff >.....x7......R..< +000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000050 07 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 >................< +000060 00 2e 74 65 78 74 00 68 6f 67 65 00 2e 73 74 72 >..text.hoge..str< +000070 74 61 62 00 2e 73 79 6d 74 61 62 00 00 00 00 00 >tab..symtab.....< +000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +0000a0 00 00 00 00 0c 00 00 00 03 00 00 00 00 00 00 00 >................< +0000b0 00 00 00 00 60 00 00 00 1c 00 00 00 00 00 00 00 >....`...........< +0000c0 00 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 >................< +0000d0 01 00 00 00 06 00 00 00 00 00 00 00 34 00 00 00 >............4...< +0000e0 0c 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 >................< +0000f0 00 00 00 00 14 00 00 00 02 00 00 00 00 00 00 00 >................< +000100 00 00 00 00 40 00 00 00 20 00 00 00 01 00 00 00 >....@... .......< +000110 02 00 00 00 04 00 00 00 10 00 00 00 >............< +00011c + +$ bin/llvm-mc -arch=rv16k -show-encoding foo.s + .text +hoge: + li a0, 55 # encoding: [0x08,0x78,0x37,0x00] + mov a1, a0 # encoding: [0x89,0xc0] + add a1, a0 # encoding: [0x89,0xe2] + j hoge # encoding: [0x00,0x52,A,A] + # fixup A - offset: 2, value: hoge, kind: fixup_rv16k_pcrel_16bit + +$ bin/llvm-mc -filetype=obj -triple=rv16k foo.s | bin/llvm-objdump -d - + +: file format ELF32-rv16k + +Disassembly of section .text: +0000000000000000 hoge: + 0: 08 78 37 00 li a0, 55 + 4: 89 c0 mov a1, a0 + 6: 89 e2 add a1, a0 + 8: 00 52 f6 ff j -10 + +.... + +=== コンパイラを起動する + +まずランタイムライブラリをビルドする必要がある。rv16k-rtレポジトリを `git clone` し +`CC=/path/to/bin/clang` をつけて `make` する。 + +.... +# rv16k-rt レポジトリをcloneする。 +$ git clone git@github.com:ushitora-anqou/rv16k-rt.git + +# rv16k-rt をビルドする。 CC 環境変数で、先程ビルドしたclangを指定する。 +$ cd rv16k-rt +$ CC=/path/to/bin/clang make +.... + + +以下のようなCプログラム `foo.c` を `clang` を用いてコンパイルする。 +コンパイル時に `--sysroot` オプションを用いて、先程ビルドしたrv16k-rtのディレクトリを指定する。 +なおバイナリサイズを小さくしたい場合は `-Oz` オプションを指定するなどすればよい。 + +.... +$ cat foo.c +int hoge; + +int main() +{ + hoge = 42; + return hoge; +} + +$ bin/clang -target rv16k foo.c -o foo.exe --sysroot=/path/to/rv16k-rt +.... + +`llvm-readelf` を用いて `.text` その他のサイズが分かる。 +これがROMサイズ( `0x200 = 512` )未満であることを確認する。 + +.... +$ bin/llvm-readelf -S foo.exe +There are 7 section headers, starting at offset 0x10f0: + +Section Headers: + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + [ 0] NULL 00000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 00000000 001000 00002e 00 AX 0 0 4 + [ 2] .bss NOBITS 00010000 00102e 000002 00 WA 0 0 2 + [ 3] .comment PROGBITS 00000000 00102e 000028 01 MS 0 0 1 + [ 4] .symtab SYMTAB 00000000 001058 000050 10 6 2 4 + [ 5] .shstrtab STRTAB 00000000 0010a8 00002f 00 0 0 1 + [ 6] .strtab STRTAB 00000000 0010d7 000018 00 0 0 1 +Key to Flags: + W (write), A (alloc), X (execute), M (merge), S (strings), l (large) + I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) + O (extra OS processing required) o (OS specific), p (processor specific) +.... + +`llvm-objdump` を用いて逆アセンブルを行うことができる。 + +.... +$ bin/llvm-objdump -d foo.exe + +foo.exe: file format ELF32-rv16k + +Disassembly of section .text: +0000000000000000 _start: + 0: 00 73 06 00 jal 6 + 4: 00 52 fe ff j -2 + +0000000000000008 main: + 8: c1 f2 addi sp, -4 + a: 21 80 swsp fp, 2(sp) + c: 12 e0 mov fp, sp + e: 42 f2 addi fp, 4 + 10: 08 78 00 00 li a0, 0 + 14: 82 92 fc ff sw a0, -4(fp) + 18: 08 78 00 00 li a0, 0 + 1c: 88 b2 00 00 lw a0, 0(a0) + 20: 12 a0 lwsp fp, 2(sp) + 22: 41 f2 addi sp, 4 + 24: 00 40 jr ra +.... + +`rv16k-sim` を使ってシミュレーションを行う。 + +.... +$ ~/ano/secure_vm/rv16k-sim/main foo.exe 20 +ROM: 0000 0073 +ROM: 0002 0600 +ROM: 0004 0052 +ROM: 0006 FEFF +ROM: 0008 C1F2 +ROM: 000A 2180 +ROM: 000C 12E0 +ROM: 000E 42F2 +ROM: 0010 0878 +ROM: 0012 0000 +ROM: 0014 8292 +ROM: 0016 FCFF +ROM: 0018 0878 +ROM: 001A 0000 +ROM: 001C 88B2 +ROM: 001E 0000 +ROM: 0020 12A0 +ROM: 0022 41F2 +ROM: 0024 0040 + +RAM: 0000 2A00 + +Inst:JAL PC <= 0x0002 Reg x0 <= 0x0004 PC <= 0x0008 FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x1 <= 0x01FA PC <= 0x000A FLAGS(SZCV) <= 0000 +Inst:SWSP DataRam[0x01FC] <= 0x0000 DataRam[0x01FD] <= 0x0000 PC <= 0x000C FLAGS(SZCV) <= 0010 +Inst:MOV Reg x2 <= 0x01FA PC <= 0x000E FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x2 <= 0x01FE PC <= 0x0010 FLAGS(SZCV) <= 0010 +Inst:LI PC <= 0x0012 Reg x8 <= 0x0000 PC <= 0x0014 FLAGS(SZCV) <= 0100 +Inst:SW PC <= 0x0016 DataRam[0x01FA] <= 0x0000 DataRam[0x01FB] <= 0x0000 PC <= 0x0018 FLAGS(SZCV) <= 0000 +Inst:LI PC <= 0x001A Reg x8 <= 0x0000 PC <= 0x001C FLAGS(SZCV) <= 0100 +Inst:LW PC <= 0x001E Reg x8 <= 0x002A PC <= 0x0020 FLAGS(SZCV) <= 0110 +Inst:LWSP Reg x2 <= 0x0000 PC <= 0x0022 FLAGS(SZCV) <= 0010 +Inst:ADDI Reg x1 <= 0x01FE PC <= 0x0024 FLAGS(SZCV) <= 0010 +Inst:JR PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +x0=4 x1=510 x2=0 x3=0 x4=0 x5=0 x6=0 x7=0 x8=42 x9=0 x10=0 x11=0 x12=0 x13=0 x14=0 x15=0 +.... + +`x8=42` とあるので、正しく実行されていることが分かる。 + +== 概要 + +これを読めば自作アーキテクチャ(RV16Kv2)の機械語を出力するLLVMバックエンドを作成することができる。 +VSP開発のバス係数を高める意義がある。 + +この文書はAsciiDocを用いて記述されている。 +記述方法についてはリファレンス<><>を参照のこと。 + +== ところで + +一度もコンパイラを書いたことがない人は、この文書を読む前に +『低レイヤを知りたい人のためのCコンパイラ作成入門』<>などで一度 +フルスクラッチからコンパイラを書くことをおすすめします。 + +また<>などを参考に、 +LLVMではなくGCCにバックエンドを追加することも検討してみてはいかがでしょうか。 +意外とGCCのほうが楽かもしれませんよ? + +== 参考にすべき資料 + +=== Webページ + +* Writing an LLVM Backend<> +** 分かりにくく読みにくい。正直あんまり見ていないが、たまに眺めると有益な情報を見つけたりもする。 +* The LLVM Target-Independent Code Generator<> +** <>よりもよほど参考になる。LLVMバックエンドがどのようにLLVM IRをアセンブリに落とすかが明記されている。必読。 +* TableGenのLLVMのドキュメント<> +** 情報量が少ない。これを読むよりも各種バックエンドのTableGenファイルを読むほうが良い。 +* LLVM Language Reference Manual<> +** LLVM IRについての言語リファレンス。LLVM IRの仕様などを参照できる。必要に応じて読む。 +* Architecture & Platform Information for Compiler Writers<> +** LLVMで公式に実装されているバックエンドに関するISAの情報が集約されている。Lanaiの言語仕様へのリンクが貴重。 +* RISC-V support for LLVM projects<> +** **どちゃくそに参考になる**。以下の開発はこれに基づいて行う。 +** LLVMにRISC-Vサポートを追加するパッチ群。バックエンドを開発するためのチュートリアルも兼ねているらしく `docs/` 及びそれと対応したpatchが参考になる。 +** またこれについて、開発者が2018 LLVM Developers' Meetingで登壇したときの動画は<>より閲覧できる。スライドは<>より閲覧できる。 +** そのときのCoding Labは<>より閲覧できる。 +* Create an LLVM Backend for the Cpu0 Architecture<> +** Cpu0という独自アーキテクチャのLLVMバックエンドを作成するチュートリアル。多少古いが、内容が網羅的で参考になる。英語が怪しい。 +* FPGA開発日記<> +** Cpu0の資料<>をもとに1からRISC-Vバックエンドを作成する過程がブログエントリとして公開されている。GitHubに実装も公開されている<>。 +* ELVMバックエンド<> +** 限られた命令でLLVM IRの機能を達成する例として貴重。でも意外とISAはリッチだったりする。 +** 作成者のスライドも参考になる<>。 +* 2018年度東大CPU実験で開発されたLLVM Backend<> +** これについて書かれたAdCのエントリもある<>。 +* Tutorial: Building a backend in 24 hours<> +** LLVMバックエンドの大まかな動きについてざっとまとめたあと、 `ret` だけが定義された最低限のLLVMバックエンド ("stub backend") を構成している。 +** Instruction Selection の説明にある *Does bunch of magic and crazy pattern-matching* が好き。 +* 2017 LLVM Developers’ Meeting: M. Braun "Welcome to the back-end: The LLVM machine representation"<> +** スライドも公開されている<>。 +** 命令選択が終わったあとの中間表現であるLLVM MIR +( `MachineFunction` や `MachineInstr` など)や、それに対する操作の解説。 +RegStateやframe index・register scavengerなどの説明が貴重。 +* Howto: Implementing LLVM Integrated Assembler<> +** LLVM上でアセンブラを書くためのチュートリアル。アセンブラ単体に焦点を絞ったものは珍しい。 +* Building an LLVM Backend<> +** 対応するレポジトリが<>にある。 +* [LLVMdev] backend documentation<> +** llvm-devメーリングリストのバックエンドのよいドキュメントは無いかというスレッド。Cpu0とTriCoreが挙げられているが、深くまで記述したものは無いという回答。 +* TriCore Backend<> +** TriCoreというアーキテクチャ用のバックエンドを書いたという論文。スライドもある<>。ソースコードもGitHub上に上がっているが、どれが公式かわからないfootnote:[論文とスライドも怪しいものだが、著者が一致しているので多分正しいだろう。]。 +* Life of an instruction in LLVM<> +** Cコードからassemblyまでの流れを概観。 +* LLVM Backendの紹介<> +** 「コンパイラ勉強会」footnote:[これとは別の発表で「コンパイラ開発してない人生はFAKE」という名言が飛び出した勉強会<>。]での、LLVMバックエンドの大きな流れ(特に命令選択)について概観した日本語スライド。 + +=== 書籍 + +* 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』<> +** 数少ない日本語資料。Passやバックエンドの各クラスについて説明している。<>と合わせて大まかな流れを掴むのに良い。 + +なおLLVMについてGoogleで検索していると"LLVM Cookbook"なる謎の書籍(の電子コピー)が +見つかるが、内容はLLVM公式文書のパクリのようだ<>。 + +=== バックエンド + +* RISC-V<> +** パッチ群が開発ドキュメントとともに公開されている<>。移行の開発はこれをベースに行う。 +* Lanai<> +** Googleが開発した32bit RISCの謎アーキテクチャ。全く実用されていないが、バックエンドが単純に設計されておりコメントも豊富のためかなり参考になるfootnote:[LLVMバックエンドの開発を円滑にするためのアーキテクチャなのではと思うほどに分かりやすい。]。footnote:[後のSparcについて<> +にて指摘されているように、商業的に成功しなかったアーキテクチャほどコードが単純で分かりやすい。] +* Sparc +** <>でも説明に使われており、コメントが豊富。 +* x86 +** みんな大好きx86。貴重なCISCの資料であり、かつ2オペランド方式を採用する場合に貴重な実装例を与えてくれる。あと `EFLAGS` の取り回しなども参考になるが、全体的にコードは読みにくい。ただLLVMの命名規則には従うため、他のバックエンドからある程度推論をして読むのが良い。 + +== RV16Kv2アーキテクチャ仕様 + +https://docs.google.com/spreadsheets/d/1Q9JPoZLVyqJEC3p_LBYiPLMTjGZgVoNlu-Jx3CFrLNQ/edit?usp=sharing + +== LLVMをテストする + +`llvm-lit` を使用してLLVMをテストできる。 + +.... +$ bin/llvm-lit test -s # 全てのテストを実行する +$ bin/llvm-lit -s --filter 'RV16K' test # RV16Kを含むテストを実行する +$ bin/llvm-lit -as --filter 'RV16K' test # テスト結果を詳細に表示する +$ bin/llvm-lit -as --filter 'RV16K' --debug test # デバッグ情報を表示する +.... + +== LLVMバックエンドの流れ + +`RV16K*` はオーバーライドできるメンバ関数を表す。 + +.... + +LLVM IR code + +| +| +v + +SelectionDAG (SDNode); RV16Kで扱えない型・操作を含む (not legal)。 + +| +| <-- RV16KTargetLowering::RV16KTargetLowering +| <-- RV16KTargetLowering::Lower* +v + +SelectionDAG (SDNode); RV16Kで扱える型・操作のみを含む (legal)。 + +| +| <-- RV16KDAGToDAGISel, RV16KInstrInfo +v + +SelectionDAG (MachineSDNode); ノードの命令は全てRV16Kのもの。 + +| +| <-- RV16KInstrInfo; 命令スケジューリング +v + +LLVM MIR (MachineInstr); スケジューリングされた命令列 + +| (以下の流れは TargetPassConfig::addMachinePasses に記述されている) +| +| <-- RV16KTargetLowering::EmitInstrWithCustomInserter; +| usesCustomInserter フラグが立っている ある MachineInstr の代わりに +| 複数の MachineInstr を挿入したり MachineBasicBlock を追加したりする。 +| +| <-- SSA上での最適化 +| +| <-- レジスタ割り付け +v + +LLVM MIR (MachineInstr); 物理レジスタのみを含む命令列(仮想レジスタを含まない) + +| +| <-- RV16KInstrInfo::expandPostRAPseudo +| +| <-- RV16KFrameLowering::processFunctionBeforeFrameFinalized +| +| <-- スタックサイズの確定 +| +| <-- RV16KFrameLowering::emitPrologue; 関数プロローグの挿入 +| <-- RV16KFrameLowering::emitEpilogue; 関数エピローグの挿入 +| <-- RV16KRegisterInfo::eliminateFrameIndex; frame indexの消去 +| +| <-- llvm::scavengeFrameVirtualRegs; +| frame lowering中に必要になった仮想レジスタをscavengeする +v + +LLVM MIR (MachineInstr); frame index が削除された命令列 + +| +| <-- RV16KPassConfig::addPreEmitPass +| <-- RV16KPassConfig::addPreEmitPass2 +| +| +| <-- RV16KAsmPrinter +| <-- PseudoInstExpansion により指定された擬似命令展開の実行 +v + +MC (MCInst); アセンブリと等価な中間表現 +.... + +LLVM MIRについては<>に詳しい。 +各フェーズでの `MachineInstr` をデバッグ出力させる場合は `llc` に `-print-machineinstrs` を +渡せば良い。 + +== LLVMのソースコードを用意する + +LLVMのソースコードを取得する。今回の開発ではv8.0.0をベースとする。 +Git上でrv16kブランチを作り、その上で開発する。 + +.... +$ git clone https://github.com/llvm/llvm-project.git +$ cd llvm-project +$ git checkout llvmorg-8.0.0 +$ git checkout -b rv16k +.... + +== スケルトンバックエンドを追加する + +=== RV16KをTripleに追加する + +RV16Kは名実ともに16bitアーキテクチャなので、RV32Kとの変更点がままある。 +RISC Vは32bitか64bitで参考にならないので、例えばavrなどを参考にすると次のようになる。 + +.... + T.setArch(Triple::rv16k); + EXPECT_TRUE(T.isArch16Bit()); + EXPECT_FALSE(T.isArch32Bit()); + EXPECT_FALSE(T.isArch64Bit()); +.... + +ちなみにavrは**8bit**アーキテクチャだが、8bitを引数に取る命令をLLVMがうまく扱えないらしく +C\++コードを大量に書いてよしなにしているらしい<>。 + +ところでRV32Kは32bitアーキテクチャとして登録していたが、一方で +`get32BitArchVariant` で `UnknownArch` を返しており、ねじれていたのだと分かる。 +結局の所このあたりは32bit/64bitで一部同じ命令を使うとか、ユーザーインターフェースの部分とかで +関係する話のようで、とりあえず開発を行う分にはあまり関係なさそうだ。 + +=== RV16KのELF定義を追加する + +`RV16K.def` でリロケーションの情報を記載できる。 + +`ELFObjectFile.h` では `ELF32` としておく。AVRもこうなっている。 +多分名前だけの問題だと思う。 + +`elf-flags.yaml` は作らない。 + +=== バックエンドを追加する + +Apache 2.0 Licenseを明記。 + +Data Layoutの文字列を正確に指定する必要がある。詳細はLLVM IRの言語仕様<> +に載っている。 `P` を指定してやることでハーバード・アーキテクチャの場合のプログラムメモリ(ROM) +の位置を指定できるようだ。 + +ビルドする。 + +.... +$ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS="clang;lld" \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCV;RV16K" \ + ../llvm +$ cmake --build . +.... + +大量のオプションはビルドを早くするためのものである<>。 + +RV16Kバックエンドが追加された。 + +.... +$ bin/llc --version +LLVM (http://llvm.org/): + LLVM version 8.0.0 + DEBUG build with assertions. + Default target: x86_64-unknown-linux-gnu + Host CPU: skylake + + Registered Targets: + riscv32 - 32-bit RISC-V + riscv64 - 64-bit RISC-V + rv16k - RV16K + x86 - 32-bit X86: Pentium-Pro and above + x86-64 - 64-bit X86: EM64T and AMD64 +.... + +== 簡易的なアセンブラを実装する + +=== TableGenファイルを追加する + +`llvm/include/llvm/Target/Target.td` に 主なclassが定義されているので、 +overrideしたいフィールドなどは、コメントなどを見ながらここで確認する。 + +.... +//===----------------------------------------------------------------------===// +// Instruction set description - These classes correspond to the C++ classes in +// the Target/TargetInstrInfo.h file. +// +class Instruction { + string Namespace = ""; + + dag OutOperandList; // An dag containing the MI def operand list. + dag InOperandList; // An dag containing the MI use operand list. + string AsmString = ""; // The .s format to print the instruction with. + + // Pattern - Set to the DAG pattern for this instruction, if we know of one, + // otherwise, uninitialized. + list Pattern; + + // The follow state will eventually be inferred automatically from the + // instruction pattern. + + list Uses = []; // Default to using no non-operand registers + list Defs = []; // Default to modifying no non-operand registers + + // Predicates - List of predicates which will be turned into isel matching + // code. + list Predicates = []; + + // Size - Size of encoded instruction, or zero if the size cannot be determined + // from the opcode. + int Size = 0; + + // DecoderNamespace - The "namespace" in which this instruction exists, on + // targets like ARM which multiple ISA namespaces exist. + string DecoderNamespace = ""; + + // Code size, for instruction selection. + // FIXME: What does this actually mean? + int CodeSize = 0; + + // Added complexity passed onto matching pattern. + int AddedComplexity = 0; + + // These bits capture information about the high-level semantics of the + // instruction. + bit isReturn = 0; // Is this instruction a return instruction? + bit isBranch = 0; // Is this instruction a branch instruction? + bit isEHScopeReturn = 0; // Does this instruction end an EH scope? + bit isIndirectBranch = 0; // Is this instruction an indirect branch? + bit isCompare = 0; // Is this instruction a comparison instruction? + bit isMoveImm = 0; // Is this instruction a move immediate instruction? + bit isMoveReg = 0; // Is this instruction a move register instruction? + bit isBitcast = 0; // Is this instruction a bitcast instruction? + bit isSelect = 0; // Is this instruction a select instruction? + bit isBarrier = 0; // Can control flow fall through this instruction? + bit isCall = 0; // Is this instruction a call instruction? + bit isAdd = 0; // Is this instruction an add instruction? + bit isTrap = 0; // Is this instruction a trap instruction? + bit canFoldAsLoad = 0; // Can this be folded as a simple memory operand? + bit mayLoad = ?; // Is it possible for this inst to read memory? + bit mayStore = ?; // Is it possible for this inst to write memory? + bit isConvertibleToThreeAddress = 0; // Can this 2-addr instruction promote? + bit isCommutable = 0; // Is this 3 operand instruction commutable? + bit isTerminator = 0; // Is this part of the terminator for a basic block? + bit isReMaterializable = 0; // Is this instruction re-materializable? + bit isPredicable = 0; // Is this instruction predicable? + bit hasDelaySlot = 0; // Does this instruction have an delay slot? + bit usesCustomInserter = 0; // Pseudo instr needing special help. + bit hasPostISelHook = 0; // To be *adjusted* after isel by target hook. + bit hasCtrlDep = 0; // Does this instruction r/w ctrl-flow chains? + bit isNotDuplicable = 0; // Is it unsafe to duplicate this instruction? + bit isConvergent = 0; // Is this instruction convergent? + bit isAsCheapAsAMove = 0; // As cheap (or cheaper) than a move instruction. + bit hasExtraSrcRegAllocReq = 0; // Sources have special regalloc requirement? + bit hasExtraDefRegAllocReq = 0; // Defs have special regalloc requirement? + bit isRegSequence = 0; // Is this instruction a kind of reg sequence? + // If so, make sure to override + // TargetInstrInfo::getRegSequenceLikeInputs. + bit isPseudo = 0; // Is this instruction a pseudo-instruction? + // If so, won't have encoding information for + // the [MC]CodeEmitter stuff. + bit isExtractSubreg = 0; // Is this instruction a kind of extract subreg? + // If so, make sure to override + // TargetInstrInfo::getExtractSubregLikeInputs. + bit isInsertSubreg = 0; // Is this instruction a kind of insert subreg? + // If so, make sure to override + // TargetInstrInfo::getInsertSubregLikeInputs. + bit variadicOpsAreDefs = 0; // Are variadic operands definitions? + + // Does the instruction have side effects that are not captured by any + // operands of the instruction or other flags? + bit hasSideEffects = ?; + + // Is this instruction a "real" instruction (with a distinct machine + // encoding), or is it a pseudo instruction used for codegen modeling + // purposes. + // FIXME: For now this is distinct from isPseudo, above, as code-gen-only + // instructions can (and often do) still have encoding information + // associated with them. Once we've migrated all of them over to true + // pseudo-instructions that are lowered to real instructions prior to + // the printer/emitter, we can remove this attribute and just use isPseudo. + // + // The intended use is: + // isPseudo: Does not have encoding information and should be expanded, + // at the latest, during lowering to MCInst. + // + // isCodeGenOnly: Does have encoding information and can go through to the + // CodeEmitter unchanged, but duplicates a canonical instruction + // definition's encoding and should be ignored when constructing the + // assembler match tables. + bit isCodeGenOnly = 0; + + // Is this instruction a pseudo instruction for use by the assembler parser. + bit isAsmParserOnly = 0; + + // This instruction is not expected to be queried for scheduling latencies + // and therefore needs no scheduling information even for a complete + // scheduling model. + bit hasNoSchedulingInfo = 0; + + InstrItinClass Itinerary = NoItinerary;// Execution steps used for scheduling. + + // Scheduling information from TargetSchedule.td. + list SchedRW; + + string Constraints = ""; // OperandConstraint, e.g. $src = $dst. + + /// DisableEncoding - List of operand names (e.g. "$op1,$op2") that should not + /// be encoded into the output machineinstr. + string DisableEncoding = ""; + + string PostEncoderMethod = ""; + string DecoderMethod = ""; + + // Is the instruction decoder method able to completely determine if the + // given instruction is valid or not. If the TableGen definition of the + // instruction specifies bitpattern A??B where A and B are static bits, the + // hasCompleteDecoder flag says whether the decoder method fully handles the + // ?? space, i.e. if it is a final arbiter for the instruction validity. + // If not then the decoder attempts to continue decoding when the decoder + // method fails. + // + // This allows to handle situations where the encoding is not fully + // orthogonal. Example: + // * InstA with bitpattern 0b0000????, + // * InstB with bitpattern 0b000000?? but the associated decoder method + // DecodeInstB() returns Fail when ?? is 0b00 or 0b11. + // + // The decoder tries to decode a bitpattern that matches both InstA and + // InstB bitpatterns first as InstB (because it is the most specific + // encoding). In the default case (hasCompleteDecoder = 1), when + // DecodeInstB() returns Fail the bitpattern gets rejected. By setting + // hasCompleteDecoder = 0 in InstB, the decoder is informed that + // DecodeInstB() is not able to determine if all possible values of ?? are + // valid or not. If DecodeInstB() returns Fail the decoder will attempt to + // decode the bitpattern as InstA too. + bit hasCompleteDecoder = 1; + + /// Target-specific flags. This becomes the TSFlags field in TargetInstrDesc. + bits<64> TSFlags = 0; + + ///@name Assembler Parser Support + ///@{ + + string AsmMatchConverter = ""; + + /// TwoOperandAliasConstraint - Enable TableGen to auto-generate a + /// two-operand matcher inst-alias for a three operand instruction. + /// For example, the arm instruction "add r3, r3, r5" can be written + /// as "add r3, r5". The constraint is of the same form as a tied-operand + /// constraint. For example, "$Rn = $Rd". + string TwoOperandAliasConstraint = ""; + + /// Assembler variant name to use for this instruction. If specified then + /// instruction will be presented only in MatchTable for this variant. If + /// not specified then assembler variants will be determined based on + /// AsmString + string AsmVariantName = ""; + + ///@} + + /// UseNamedOperandTable - If set, the operand indices of this instruction + /// can be queried via the getNamedOperandIdx() function which is generated + /// by TableGen. + bit UseNamedOperandTable = 0; + + /// Should FastISel ignore this instruction. For certain ISAs, they have + /// instructions which map to the same ISD Opcode, value type operands and + /// instruction selection predicates. FastISel cannot handle such cases, but + /// SelectionDAG can. + bit FastISelShouldIgnore = 0; +} +.... + +RISC Vがx0〜x7について `CostPerUse` を `1` に設定しているのは、 +圧縮命令セット(RV32C)の命令のオペランドにも使えるレジスタを優先的に使ってほしいかららしい。 +この設定はRV32Cが有効になっていないとき( `llvm-mc` に `-mattr=c` を渡さないとき)にも +反映されるが、とくに不都合はないようだ。 + +`field bits<16> Inst` はどこからも参照されていないが、なぜか命令のエンコードとしてはたらく。 +ハードコードされているようだ(TODO 未確認)。 `Inst` を使う場合は +`MCCodeEmitter::encodeInstrution` で `getBinaryCodeForInstr` を呼ぶだけでエンコードが原則完了する。 +一方使わない場合(x86など)は自前でコード生成を行う必要があるようだ。 + +`field` がついている場合といない場合の違いは良くわからない(TODO)が、 +フィールドであることを明示する以上の意味はなさそうだ。 + +TableGenファイルを書いた後はそれが正しいかテストをしておく。 + +.... +$ bin/llvm-tblgen -I ../llvm/lib/Target/RV16K/ -I ../llvm/include/ -I ../llvm/lib/Target/ ../llvm/lib/Target/RV16K/RV16K.td +.... + +またTableGenが仕事をするように `CMakeLists.txt` を書き換えておく。 + +=== RV16K用の `MCTargetDesc` を追加する + +`createRV16KMCRegisterInfo` 内で呼び出す `InitRV16KMCRegisterInfo` はTableGenが生成する関数で、 +内部で `llvm::MCRegisterInfo::InitMCRegisterInfo` <> +を呼び出している。したがって第一引数に `MCRegisterInfo *` をとり、 +第二引数はreturn addressが入っているレジスタ(RV16Kv2では `x0` )を渡せば良いfootnote:[ +ちなみにreturn addressをスタックに積むx86では `eip` を(x86_64では `rip` を)返している。 +なぜかは良くわからない。TODO]。また第五引数にはPCを表すレジスタを渡すことができるが、 +指定しなければ `0` が渡される。レジスタ番号 `0` は無指定であることを意味するようだ(未確認;TODO)。 + +`RV16KMCCodeEmitter::encodeInstruction` では + +.... +const MCInstrDesc &Desc = MCII.get(MI.getOpcode()); +unsigned Size = Desc.getSize(); +.... + +とすることで、命令の大きさを取得できる。ここで `getOpcode` で得られる値は +TableGenのフィールドとして指定する `Opcode` ではなく、LLVMが自動的につける通し番号が返ってくる +(多分。どこかにそう書いてあった気がするが見つけられない。;TODO)。 + +buildしようとするとTableGenがSegmentation falutで落ちた。どうやらCodeEmitter用の +ファイル生成に失敗しているらしい。原因は `NOP` の `rs` と `rd` に `0` を入れ忘れていたことだった。 + +そうこうしていると命令を限った簡易的なアセンブラができる。 + +.... +$ cat foo.s +mov x2, x1 +add x2, x1 +sub x2, x1 +and x2, x1 +or x2, x1 +xor x2, x1 +lsl x2, x1 +lsr x2, x1 +asr x2, x1 +cmp x2, x1 +li x1, 0x7fff +li x1, -0x8000 +addi x1, 7 +addi x1, -8 +cmpi x1, 7 +cmpi x1, -8 +nop + +$ bin/llvm-mc -arch=rv16k -filetype=obj foo.s | od -tx1z -Ax -v +000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< +000010 01 00 f6 00 01 00 00 00 00 00 00 00 00 00 00 00 >................< +000020 84 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 >........4.....(.< +000030 04 00 01 00 12 c0 12 e2 12 e3 12 e4 12 e5 12 e6 >................< +000040 12 ea 12 ea 12 ed 12 c3 01 78 ff 7f 01 78 00 80 >.........x...x..< +000050 71 f2 81 f2 71 d3 81 d3 00 00 00 00 00 00 00 00 >q...q...........< +000060 00 00 00 00 00 00 00 00 00 00 00 00 00 2e 74 65 >..............te< +000070 78 74 00 2e 73 74 72 74 61 62 00 2e 73 79 6d 74 >xt..strtab..symt< +000080 61 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >ab..............< +000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +0000a0 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 >................< +0000b0 03 00 00 00 00 00 00 00 00 00 00 00 6c 00 00 00 >............l...< +0000c0 17 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 >................< +0000d0 00 00 00 00 01 00 00 00 01 00 00 00 06 00 00 00 >................< +0000e0 00 00 00 00 34 00 00 00 26 00 00 00 00 00 00 00 >....4...&.......< +0000f0 00 00 00 00 04 00 00 00 00 00 00 00 0f 00 00 00 >................< +000100 02 00 00 00 00 00 00 00 00 00 00 00 5c 00 00 00 >............\...< +000110 10 00 00 00 01 00 00 00 01 00 00 00 04 00 00 00 >................< +000120 10 00 00 00 >....< +000124 +.... + +== 簡易アセンブラのテストを書く + +=== `RV16KInstPrinter` を実装する + +`AltNames` でアセンブリに出力させる場合、 `AltNames` が指定されていないレジスタが +あるとエラーが出る。そこで `FLAGS` にもダミーの `AltNames` を与えた。 + +.... +def FLAGS : RV16KReg<0, "flags", ["flags"]>; +.... + +次のように `-show-encoding` オプションが動くようになった。 + +.... +$ bin/llvm-mc -arch=rv16k -show-encoding foo.s + .text + mov fp, sp # encoding: [0x12,0xc0] + add fp, sp # encoding: [0x12,0xe2] + sub fp, sp # encoding: [0x12,0xe3] + and fp, sp # encoding: [0x12,0xe4] + or fp, sp # encoding: [0x12,0xe5] + xor fp, sp # encoding: [0x12,0xe6] + lsl fp, sp # encoding: [0x12,0xea] + lsr fp, sp # encoding: [0x12,0xea] + asr fp, sp # encoding: [0x12,0xed] + cmp fp, sp # encoding: [0x12,0xc3] + li sp, 32767 # encoding: [0x01,0x78,0xff,0x7f] + li sp, -32768 # encoding: [0x01,0x78,0x00,0x80] + addi sp, 7 # encoding: [0x71,0xf2] + addi sp, -8 # encoding: [0x81,0xf2] + cmpi sp, 7 # encoding: [0x71,0xd3] + cmpi sp, -8 # encoding: [0x81,0xd3] + nop # encoding: [0x00,0x00] +.... + +=== テストを書く + +すべての命令を網羅するようにテストを書く。 + +== アセンブラに残りの命令を追加する + +=== `lw/lbu/lb/sw/sb` 命令を追加する + +TableGenにエントリを増やし、またオペランドにメモリ表記をとれるように +`AsmParser` を変更すれば良い。この時点では `getImmOpValue` を実装する必要はない。 + +ここで `li x11, x12` を入力すると、期待しているエラーメッセージ +`invalid operand for instruction` ではなく +`immediate must be an integer in the range [-32768, 32767]` が出てしまう。 +これはアセンブリをパーズするときに使用する関数 `RV16KAsmParser::MatchAndEmitInstruction` 内で +呼んでいる、TableGenが生成する `MatchInstructionImpl` の挙動のためである。 +すなわち `MatchInstructionImpl` が呼ぶ `validateOperandClass` では、例えば符号付き16bit即値を +期待しているときにi) まず `isSImm16` を用いて即値として正しいかを調べii) +正しければ `Match_Success` を返しiii) +正しくなければ `Match_InvalidSImm16` を返している。そしてiv) これが `MatchAndEmitInstruction` で +補足されて、即値幅のエラーとして表示される。すなわち「そもそもオペランドが即値か」という +条件分岐が行われていないことが、この挙動の原因である。 +そこで `MatchInstructionImpl` から即値エラーが返ってきた場合には +「そもそもオペランドが即値か」を次のように判定し、即値でない場合に `invalid operand` エラーを +返すことで、この問題を解決できる。 + +.... +RV16KOperand &Operand = (RV16KOperand &)*Operands[ErrorInfo]; +if (!Operand.isImm()) + return Error(Operand.getStartLoc(), "invalid operand for instruction"); +.... + +=== `lwsp/swsp` 命令を追加する + +やるだけ。 + +=== `j/jal/jalr/jr/jl/jle/je/jne/jb/jbe` 命令を追加する + +やるだけ。先達はあらまほしきことなり。 + +=== 属性を指定する + +`llvm/include/llvm/Target/Target.td` を参考に、必要なフィールドを `let` を使って上書きする。 + +どの程度の粒度で設定すべきなのか良くわからない。例えば `isAdd` というフラグがあるが、 +誰も使っていない。また `isCommutable` はx86やLanaiは使っているが、RISC Vは使っていない。 +おそらくTableGenが推論してくれる部分などがあるのだろうが、どれを指定してどれを指定するべきでないのか +全然わからない。 + +x86を見るとmov命令には `Defs = [EFLAGS]` の記載がない。Intel SDMを見ると、 +実はx86のMOVやJccはEFLAGSを書き換えないことが分かる。意外だ。 + +とりあえず分かる範囲で設定して先に進む。不都合が起きたら戻ってこよう。 + +== ディスアセンブラを実装する + +途中でバグらせてつらかったが、LLVM流printfデバッグの方法が分かった。 +`LLVM_DEBUG(dbgs() << "message");` とすることでデバッグ情報を出力できる。 +`message` の部分に変数などを仕込みたい場合は `formatv` を使うのが安全である。 +このようにして出力したデバッグ情報は、ツールの +コマンドライン引数に `-debug` を渡すことで見ることができる。 +例えばディスアセンブラの挙動についてデバッグしたい場合は、まず情報を表示するように仕込み + +.... +LLVM_DEBUG(dbgs() << formatv("HOGEHOGE {0} {1} {2} {3}\n", bit15, bit14, + bit12, isNIAI)); +.... + +つぎにそれを表示させる。 + +.... +$ bin/llvm-mc -filetype=obj -triple=rv16k < foo.s | bin/llvm-objdump -debug -d - +.... + +なおこのあたりの技法についてはLLVM Programmer's Manual<>が詳しい。 + +== relocationとfixupに対応する + +=== Fixupを定義する + +`j` や `jal` のために `fixup_rv16k_pcrel_16bit` を、 `jl` や `jle` のために `fixup_rv16k_pcrel_8bit` を +定義した。なおグローバル変数などのリロケーションに必要なfixupは `FK_Data_2` を使えば良いようだ。 + +=== Fixupを適用する関数を定義する + +やるだけ。RV32Kと比べて即値の埋め込み方が簡単なため、これ自体はそれほど難しくない。 + +=== アセンブラにFixupを生成させる + +Fixupは `RV16KMCCodeEmitter::getImmOpValue` が即値の `EncoderMethod` として呼ばれることにより +生成される。すなわち `getImmOpValue` の中で、当該オペランドの値が `MCSymbolRefExpr` の場合には +Fixupを生成するようにすればよい。また `isSImm16Lsb0` などは `MCSymbolRefExpr` に対して `true` +を返すようにしておくfootnote:[このあたり、即値とは `MCExpr` である、というデータ構造が効いている。]。 + +またそもそもアセンブリをパーズするときに文字列が `MCSymbolRefExpr` として読み込まれるように、 +`AsmParser` を変更する必要がある。 +そこで `RV16KAsmParser::parseImmediate` を変更する。 + +作り終わったらFixupのテストを書く。RV16Kv2仕様の詰めが甘かったので若干修正した。 + +=== Relocationに対応する + +最後まで修正できずに残ったFixupをRelocationに変換してELFに埋め込み、 +リンカに解決させるようにする。 `RV16KELFObjectWriter::getRelocType` にFixupとRelocationの +対応を書けば良いが、そのまえにどのようなRelocationが存在するかについて +`include/llvm/BinaryFormat/ELFRelocs/RISCV.def` に記述する必要がある。 + +`.int foo` として `li x2, foo` とすると `FK_Data_4` が生成されてしまう。 +解決策を探して他のターゲットの `RV16KELFObjectWriter.cpp` を見たが、 +どれも `FK_Data_4` を捕捉し処理していた。とりあえず `.2byte foo` とすることで回避する。 + +`llvm-readobj` でできたELFバイナリを見ると `AddressSize` が `32bit` となっているが、 +変え方がわからない。と思ったが、どうやらELFバイナリそのものが32bit/64bit限定のようだ。 + +全体的にLLVMは32bit以上を念頭において開発されているように思う。 + +=== 即値に定数式を書けるようにする + +すなわち `.2byte foo` があったときに `li x3, foo+3` などと書けるようにするということである。 +これのためには `AsmParser` に手を入れて適当な箇所で `parseExpression` を呼びfootnote:[この +`parseExpression` がどの程度の「式」をパーズしてくれるかは定かでない。TODO]、 +また `isSImm16` など即値判定の関数でこのような式に対して `true` を返すようにする。 +なおFixup時の式の評価などはLLVMが勝手にやってくれるようだfootnote:[もっとも +これは即値部分にシンボルのみが書けたときから(よく考えれば)そうだったのだが。]。 + +`CodeEmitter` では、即値の `Expr` に対して適切にFixupを生成する必要がある。 +「いつ、どのFixupを作成するべきか」を判断するためには +命令のハードコーディングが必要になる。それよりはむしろRISC Vが導入しているような `InstFormat` を +命令に埋め込むほうが後々の拡張性に優れるが、とりあえずこのままにしておくfootnote:[面倒なので。]。 + +RISC Vの実装では `jal` のみを別枠として処理している。おそらくこれは `R_RISCV_JAL` などが +addendに対応しないためである。RV16Kの場合も `R_RV16K_16` のみがaddendに対応するのでfootnote:[今決めた。]これに従う。 +したがって結局、単なるシンボルのみだけでなく式を受け取れるのは `li` 命令のみとなるfootnote:[仕組みさえ作っておけば、今後他の命令で同様の操作が必要になったときでも変更が容易である。]。 + +ところでLanaiの実装では、二項演算について左辺のみを取得してFixupに追加している。 +これはaddendなどについて誤ったfixupを生成すると思われるが、調べていない(TODO)。 + +結局即値に定数式を書けるようにするためにはi) `AsmParser` に手を入れて式をパーズするようにしii) +`isSImm16` などで正しい定数式か否かを判断するようにしiii) `CodeEmitter` に手を入れて +正しいfixupを作成するようにする必要がある。 + +== コンパイラのスケルトンを作成する + +=== `RV16KAsmPrinter` と `RV16KMCInstLower` を追加 + +やるだけ + +=== `RV16KInstrInfo` を追加 + +やるだけ + +=== `RV16KRegisterInfo` を追加 + +`RV16KGenRegisterInfo` の第一引数にはreturn addressを渡せば良い。 + +frame pointerはx86で言うところのrbpのことのようだ。 + +=== `RV16KSubtarget` を追加 + +やるだけ + +=== `RV16KPassConfig` を追加 + +やるだけ + +=== `RV16KISelDAGToDAG` を追加 + +SelectionDAGをMachineDAGに変換するときに使用される。 + +=== `RV16KCallingConv.td` を追加 + +関数呼び出し規約について記述する。ここの記法については<>が +詳しい。ここで `CSR` としてcallee-savedなレジスタを指定する必要がある。 + +RISC Vの実装を参考にするとreturn addressを格納するレジスタはこれに**指定する**一方で、 +stack pointer registerは**指定しない**。RISC Vの仕様<>によれば +`ra` はcaller-saved、 `sp` はcallee-savedとなっており良くわからない。 + +=== `RV16KInstrInfo.td` を修正 + +まず `RetFlag` を `SDNode` として追加する。 +これはすなわち `RetFlag` がSelectionDAGのノード(命令)として新たに使えるようになる +ということを意味する。 +実際 `RV16KTargetLowering::LowerReturn` では `DAG.getNode` に `RV16KISD::RET_FLAG` を渡すことで +このノードを作成している。 +ここで第一引数に渡す `RV16KISD::RET_FLAG` は別のところ( `RV16KISelLowering.h` )で +定義する必要がある。また第二引数に `SDTNone` が渡されているため +オペランドを取らないことがわかる(ほんまか?;TODO) + +その他命令を変換するための `Pat` を追加する。 + +=== その他 + +`RV32K.td` の `RV16KInstrInfo` では `guessInstructionProperties` に `0` を +設定している。これによって `mayLoad, mayStore, hasSideEffects` に対する +自動的な値の推論がなくなり、 +明示的に設定していない場合にはエラーが出力されるようになるfootnote:[と `Target.td` の +コメントに記載がある。]。 + +== 定数に対応する + +やるだけ + +== メモリ操作に対応する + +`load` が16bitの操作に対応することに注意。 +具体的にどのようなパターンを書けばよいかわからないときは `-debug-only=isel` +を使用すると便利である<>。 +例えば `load` を実装した後では、次のようにLLVM IRが変換されていくことが分かる。 +一方で途中で変換が失敗する場合は、パターンに誤りがあることが示唆される。 + +.... +$ cat foo.ll +define i16 @lw(i16 *%a) nounwind { + %1 = getelementptr i16, i16* %a, i16 3 + %2 = load i16, i16* %1 + %3 = load volatile i16, i16* %a + ret i16 %2 +} + +$ bin/llc -mtriple=rv16k -verify-machineinstrs -debug-only=isel < foo.ll + .text + .file "" + + + +=== lw +Initial selection DAG: %bb.0 'lw:' +SelectionDAG has 12 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t5: i16 = Constant<0> + t4: i16 = add t2, Constant:i16<6> + t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 + t8: i16,ch = load<(volatile load 2 from %ir.a)> t7:1, t2, undef:i16 + t10: ch,glue = CopyToReg t8:1, Register:i16 $x8, t7 + t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 + + +Optimized lowered selection DAG: %bb.0 'lw:' +SelectionDAG has 12 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t4: i16 = add t2, Constant:i16<6> + t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 + t12: i16,ch = load<(volatile load 2 from %ir.a)> t0, t2, undef:i16 + t13: ch = TokenFactor t7:1, t12:1 + t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 + + +Type-legalized selection DAG: %bb.0 'lw:' +SelectionDAG has 12 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t4: i16 = add t2, Constant:i16<6> + t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 + t12: i16,ch = load<(volatile load 2 from %ir.a)> t0, t2, undef:i16 + t13: ch = TokenFactor t7:1, t12:1 + t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 + + +Legalized selection DAG: %bb.0 'lw:' +SelectionDAG has 12 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t4: i16 = add t2, Constant:i16<6> + t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 + t12: i16,ch = load<(volatile load 2 from %ir.a)> t0, t2, undef:i16 + t13: ch = TokenFactor t7:1, t12:1 + t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 + + +Optimized legalized selection DAG: %bb.0 'lw:' +SelectionDAG has 12 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t4: i16 = add t2, Constant:i16<6> + t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 + t12: i16,ch = load<(volatile load 2 from %ir.a)> t0, t2, undef:i16 + t13: ch = TokenFactor t7:1, t12:1 + t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 + + +===== Instruction selection begins: %bb.0 '' + +ISEL: Starting selection on root node: t11: ch = RV16KISD::RET_FLAG t10, Register:i16 $x8, t10:1 +ISEL: Starting pattern match + Morphed node: t11: ch = PseudoRET Register:i16 $x8, t10, t10:1 +ISEL: Match complete! + +ISEL: Starting selection on root node: t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + +ISEL: Starting selection on root node: t13: ch = TokenFactor t7:1, t12:1 + +ISEL: Starting selection on root node: t7: i16,ch = load<(load 2 from %ir.1)> t0, t4, undef:i16 +ISEL: Starting pattern match + Initial Opcode index to 5 + Morphed node: t7: i16,i16,ch = LW t2, TargetConstant:i16<6>, t0 +ISEL: Match complete! + +ISEL: Starting selection on root node: t12: i16,ch = load<(volatile load 2 from %ir.a)> t0, t2, undef:i16 +ISEL: Starting pattern match + Initial Opcode index to 5 + Match failed at index 10 + Continuing at 95 + Morphed node: t12: i16,i16,ch = LW t2, TargetConstant:i16<0>, t0 +ISEL: Match complete! + +ISEL: Starting selection on root node: t2: i16,ch = CopyFromReg t0, Register:i16 %0 + +ISEL: Starting selection on root node: t9: i16 = Register $x8 + +ISEL: Starting selection on root node: t1: i16 = Register %0 + +ISEL: Starting selection on root node: t0: ch = EntryToken + +===== Instruction selection ends: +Selected selection DAG: %bb.0 'lw:' +SelectionDAG has 11 nodes: + t0: ch = EntryToken + t2: i16,ch = CopyFromReg t0, Register:i16 %0 + t7: i16,i16,ch = LW t2, TargetConstant:i16<6>, t0 + t12: i16,i16,ch = LW t2, TargetConstant:i16<0>, t0 + t13: ch = TokenFactor t7:2, t12:2 + t10: ch,glue = CopyToReg t13, Register:i16 $x8, t7 + t11: ch = PseudoRET Register:i16 $x8, t10, t10:1 + + +Total amount of phi nodes to update: 0 +*** MachineFunction at end of ISel *** +# Machine code for function lw: IsSSA, TracksLiveness +Function Live Ins: $x8 in %0 + +bb.0 (%ir-block.0): + liveins: $x8 + %0:gpr = COPY $x8 + %1:gpr = LW %0:gpr, 0, implicit-def dead $flags :: (volatile load 2 from %ir.a) + %2:gpr = LW %0:gpr, 6, implicit-def dead $flags :: (load 2 from %ir.1) + $x8 = COPY %2:gpr + PseudoRET implicit $x8 + +# End machine code for function lw. + + .globl lw # -- Begin function lw + .p2align 1 + .type lw,@function +lw: # @lw +# %bb.0: + lw a1, 0(a0) + lw a0, 6(a0) + jr ra +.Lfunc_end0: + .size lw, .Lfunc_end0-lw + # -- End function + + .section ".note.GNU-stack","",@progbits +.... + +`RV16KTargetLowering` で次のように `Promote` を指定すると + +.... +for (auto N : {ISD::EXTLOAD, ISD::SEXTLOAD, ISD::ZEXTLOAD}) + setLoadExtAction(N, MVT::i16, MVT::i1, Promote); +.... + +`i1` に対する `extload, sextload, zextload` が `i16` のそれに変換される(TODO;多分)。 + +== グローバル変数に対応する + +グローバル変数に対するlw/swに対応する<><>。 +SelectionDAGノードである `GlobalAddress` を処理するために + +.... +setOperationAction(ISD::GlobalAddress, MVT::i16, Custom); +.... + +とし `lowerOperation` から `lowerGlobalAddress` を呼び出す。ここでは即値を `LI` を利用して +読み込むためのDAGを作成する。 + +とりあえず次のようなLLVM IRを読み込ませる。 + +.... +@G = global i16 0 + +define i16 @lw_sw_global(i16 %a) nounwind { + %1 = load volatile i16, i16* @G + store i16 %a, i16* @G + %2 = getelementptr i16, i16* @G, i16 9 + %3 = load volatile i16, i16* %2 + store i16 %a, i16* %2 + ret i16 0 +} +.... + +すると `LLVM ERROR: LowerRV16KMachineInstrToMCInst: unknown operand type` +とエラーがでるので修正する。 + +アセンブリを出力するために `MachineInstr` を `MCInst` に変換するときに、 +`MachineInstr` に含まれる `MachineOperand` の `MO_GlobalAddress` に対応する必要がある。 +これはこのオペランドのシンボル(名前)を取得し、オフセットを含めた `MCExpr` として構築しなおし、 +`MCOperand::createExpr` に渡して `MCOperand` とすればよい。 +これで上記のLLVM IRが正しくコンパイルされるようになる。 + +一方で `ret i16 0` を `ret i16 %1` としたLLVM IRは、次のようなエラーが出力され +コンパイルされない。 + +.... +$ bin/llc -mtriple=rv16k -verify-machineinstrs < foo.ll + .text + .file "" + +# After Post-RA pseudo instruction expansion pass +# Machine code for function lw_sw_global: NoPHIs, NoVRegs +Function Live Ins: $x8 + +bb.0 (%ir-block.0): + $x10 = LI @G, implicit-def dead $flags + $x9 = LW $x10, 0, implicit-def dead $flags :: (volatile dereferenceable load 2 from @G) + SW $x8, killed $x10, 0, implicit-def dead $flags :: (store 2 into @G) + $x10 = LI @G + 18, implicit-def dead $flags + dead $x11 = LW $x10, 0, implicit-def dead $flags :: (volatile load 2 from %ir.2) + SW killed $x8, killed $x10, 0, implicit-def dead $flags :: (store 2 into %ir.2) + $x8 = MOV killed $x9(tied-def 0), implicit-def $flags + PseudoRET implicit $x8 + +# End machine code for function lw_sw_global. + +*** Bad machine code: Tied physical registers must match. *** +- function: lw_sw_global +- basic block: %bb.0 (0xf02c98) +- instruction: $x8 = MOV killed $x9(tied-def 0), implicit-def $flags +- operand 0: $x8 + +*** Bad machine code: Two-address instruction operands must be identical *** +- function: lw_sw_global +- basic block: %bb.0 (0xf02c98) +- instruction: $x8 = MOV killed $x9(tied-def 0), implicit-def $flags +- operand 1: killed $x9(tied-def 0) + +*** Bad machine code: Explicit operand marked as def *** +- function: lw_sw_global +- basic block: %bb.0 (0xf02c98) +- instruction: $x8 = MOV killed $x9(tied-def 0), implicit-def $flags +- operand 2: implicit-def $flags + +*** Bad machine code: Explicit operand marked as implicit *** +- function: lw_sw_global +- basic block: %bb.0 (0xf02c98) +- instruction: $x8 = MOV killed $x9(tied-def 0), implicit-def $flags +- operand 2: implicit-def $flags + +*** Bad machine code: Illegal physical register for instruction *** +- function: lw_sw_global +- basic block: %bb.0 (0xf02c98) +- instruction: $x8 = MOV killed $x9(tied-def 0), implicit-def $flags +- operand 2: implicit-def $flags +$flags is not a GPR register. +LLVM ERROR: Found 5 machine code errors. +.... + +これは `MOV` の定義が入力を2つ受け取っていたためである +footnote:[ここまで問題が明らかにならなかった理由は良くわからない。]。 +正しい形式となるように変更しておく。 + +.... +let hasSideEffects = 0, mayLoad = 0, mayStore = 0, Defs = [FLAGS] in +def MOV : RV16KInstRR16<0b11000000, (outs GPR:$rd), (ins GPR:$rs), + "mov", "$rd, $rs">; +.... + +== 条件分岐に対応する + +これが複雑。LLVMはネイティブで `CMP` 命令相当のSelectionDAGノードを有しないため、 +独自に実装する必要がある。これをやっている例はいまのところx86しか見つけていない +footnote:[ただし条件分岐周りは各アーキ工夫をこらすところのようで、TableGenに頼らず +C\++コードをがりがり書いているバックエンドは多く存在する。]。 + +x86ではまず `ISD::BRCOND` に対して `Custom` 指定をし `LowerOperation` から +`LowerBRCOND` を呼び出す。ここでは`brcond` を `X86ISD::BRCOND` に変換する。 + +.... +def SDTX86BrCond : SDTypeProfile<0, 3, + [SDTCisVT<0, OtherVT>, + SDTCisVT<1, i8>, SDTCisVT<2, i32>]>; +def X86brcond : SDNode<"X86ISD::BRCOND", SDTX86BrCond, + [SDNPHasChain]>; +.... + +`X86ISD::BRCOND` はオペランドを3個とり、順に飛ぶ先・条件の種類・ `X86ISD::CMP` ノードと +なっているようだが、詳細不明。 + +`LowerBRCOND` 内ではかなりの最適化が図られているが `ISD::SETCC` に対する処理のみ +追いかければ当分問題ないはず。TODO + +他の簡易な実装としてLEG<>があった。 +ここでは `LEGDAGToDAGISel::Select` で割り込み `ISD::BR_CC` に対して `CMP` と `Bcc` の +MachineNodeを作成している。これは <>と同じ手法のようだ。 +ここの依存関係を表現するために `SDValue` の配列を作成しているが詳細不明。 + +.... +SDNode *LEGDAGToDAGISel::SelectConditionalBranch(SDNode *N) { + SDValue Chain = N->getOperand(0); + SDValue Cond = N->getOperand(1); + SDValue LHS = N->getOperand(2); + SDValue RHS = N->getOperand(3); + SDValue Target = N->getOperand(4); + + // Generate a comparison instruction. + EVT CompareTys[] = { MVT::Other, MVT::Glue }; + SDVTList CompareVT = CurDAG->getVTList(CompareTys); + SDValue CompareOps[] = {LHS, RHS, Chain}; + SDNode *Compare = CurDAG->getMachineNode(LEG::CMP, N, CompareVT, CompareOps); + + // Generate a predicated branch instruction. + CondCodeSDNode *CC = cast(Cond.getNode()); + SDValue CCVal = CurDAG->getTargetConstant(CC->get(), N, MVT::i32); + SDValue BranchOps[] = {CCVal, Target, SDValue(Compare, 0), + SDValue(Compare, 1)}; + return CurDAG->getMachineNode(LEG::Bcc, N, MVT::Other, BranchOps); +} +.... + +結局 `BR_CC` や `BRCOND` に対して `CMP` と `Jcc` を同時に発行できればよく、 +問題はここに依存関係が存在することである。すなわちこの間に `EFLAGS` を +いじるような命令が来てはいけないのである。ただこの関係は `Defs` の +定義によってよしなになるような気はする。 + +TableGenのパターンでは複数命令を発行することはできないようなので +<>、どこかでフックする他ない。 + +`SDValue(hoge, 1)` と書くと `hoge` がdefするものの2番めを取ってくるようだ。 +ただしこれは要確認だが、例えばx86で + +.... + SDValue Sub = DAG.getNode(X86ISD::SUB, dl, VTs, Op0, Op1); + return SDValue(Sub.getNode(), 1); +.... + +とかくと `EFLAGS` を返していることになりそうだ。 + +ここからの類推で `Uses` に書けばオペランドとして渡せるのではないかと思い次のように書いたが + +.... +def : Pat<(brcond (i16 (seteq GPR:$rs1, GPR:$rs2)), bb:$imm), + (JE simm8_lsb0:$imm, (CMP GPR:$rs1, GPR:$rs2))>; +.... + +次のようにエラーが出た。 + +.... +anonymous_978: (JE simm8_lsb0:{ *:[i16] }:$imm, (CMP:{}:{} GPR:{}:$rs1, GPR:{}:$rs2)) +Included from /home/anqou/ano/secure_vm/llvm-project/llvm/lib/Target/RV16K/RV16K.td:23: +/home/anqou/ano/secure_vm/llvm-project/llvm/lib/Target/RV16K/RV16KInstrInfo.td:215:1: error: In anonymous_978: Instruction 'JE' was provided 2 operands but expected only 1! +def : Pat<(brcond (i16 (seteq GPR:$rs1, GPR:$rs2)), bb:$imm), +.... + +せやろなfootnote:[かなしい。]。 + +Lanaiがよく見ると `CMP` と `Jcc` パターンだった。 +`CMP` に相当するのは `SFSUB_F` で、条件分岐自体は `BRCC` で行う。 +全体としてx86の方式を `BR_CC` に変換しつつ簡易化したような構成になっている。 +RV16Kv2でもこの方式を採用することにする。 + +まず `BRCOND` はexpandするようにし `BR_CC` はcustom指定を行う。 +ここで `BRCOND` には `MVT::Other` を指定することに注意する。 + +.... +setOperationAction(ISD::BRCOND, MVT::Other, Expand); +setOperationAction(ISD::BR_CC, MVT::i16, Custom); +.... + +こうすることで、次のように条件が別の場所で作られて `br` に持ち込まれる場合に対応できる。 +なぜかは良くわからない(TODO)。 + +.... +test11: + %val11 = load volatile i16, i16* %b + br i1 %c, label %end, label %test12 +.... + +さて `BR_CC` を処理するための関数 `LowerBR_CC` を作成し作業を行う。 +ここでは `BR_CC` を `RV16KISD::CMP` と `RV16KISD::BR_CC` に構成し直す。 +すなわち `ISD::CondCode` を `RV16KISD::CondCode` に変換し、 +これを子ノードに持つような `RV16KISD::CMP` を作り、さらにそれを持つような `RV16KISD::BR_CC` を +作成する。 + +`RV16KISD::CMP` と `RV16KISD::BR_CC` は `RV16KInstrInfo.td` で新しいSelectionDAGの +ノード( `SDNode` )として定義する。 + +.... +def SDT_RV16KCmp : SDTypeProfile<0, 2, [SDTCisSameAs<0, 1>]>; +def SDT_RV16KBrCC : SDTypeProfile<0, 2, [SDTCisVT<0, OtherVT>, + SDTCisVT<1, i16>]>; + +def RV16KCmp : SDNode<"RV16KISD::CMP", SDT_RV16KCmp, [SDNPOutGlue]>; +def RV16BrCC : SDNode<"RV16KISD::BR_CC", SDT_RV16KBrCC, [SDNPHasChain, SDNPInGlue]>; +.... + +`SDTCisSameAs` は指定するオペランドの型が全く同一であることを意味する。 + +この構成では `RV16KISD::BR_CC` は第二オペランドに条件分岐の種類を持つため、 +それを利用してTableGen上でパターンマッチが可能である。 + +.... +def : Pat<(RV16KCmp GPR:$rs1, GPR:$rs2), (CMP GPR:$rs1, GPR:$rs2)>; +def : Pat<(RV16KBrCC bb:$imm7, RV16K_COND_E), (JE simm8_lsb0:$imm7)>; +.... + +`br` を `J` に対応させるために `simm16_lsb0` が `Operand` を継承するようにすると、 +同じオペランドをとる `load` が正しく動かなくなってしまう。 +そこで `simm16_lsb0_j` という新しい `def` を用意し、これは `Operand` を +継承するようにした上で `j` や `jal` 命令はこれを参照するようにする。 + +`MachineInstr` のオペランド中に `MachineOperand::MO_MachineBasicBlock` が登場するようになるので +`LowerRV32KMachineOperandToMCOperand` の修正が必要である。 + +== 関数呼び出しを実装する + +関数呼び出しをサポートするためにi) `PseudoCALL` を実装しii) `RV16KTargetLowering::LowerCall` を実装する。 +`PseudoCALL` は `RV16KISD::CALL` に対してパターンマッチし `JALR` に伸張されるfootnote:[本当は `JAL` になるべきだが、 +とりあえずここでは飛ぶ先の関数のアドレスをレジスタに入れて `JALR` で飛ぶ仕組みになっている。]。 +`ra` を変更するため `Defs` を上書きする必要がある。 + +`RV16KISD::CALL` は `LowerCall` にて挿入される。これの前後には `ISD::CALLSEQ_START` と +`ISD::CALLSEQ_END` が挿入される。 `ISD::CALLSEQ_START` は `ADJCALLSTACKDOWN` に、 +`ISD::CALLSEQ_END` は `ADJCALLSTACKUP` にパターンマッチで変換される。 +これらは `RV16KGenInstrInfo` のコンストラクタに渡され処理されるようだ(詳細要確認;TODO)[[adjcallstack]]。 + +これらが具体的に何をやっているかはよくわからない(TODO)が、 +`RV16KFrameLowering::eliminateCallFramePseudoInstr` にて `ADJCALLSTACKDOWN` と +`ADJCALLSTACKUP` は削除する際にフックを仕掛け、関数呼び出しの前後で行う処理を記述することが +可能のようだ<>。 + +`ISDOpcodes.h` にはこうある。 + +.... +/// CALLSEQ_START/CALLSEQ_END - These operators mark the beginning and end +/// of a call sequence, and carry arbitrary information that target might +/// want to know. The first operand is a chain, the rest are specified by +/// the target and not touched by the DAG optimizers. +/// Targets that may use stack to pass call arguments define additional +/// operands: +/// - size of the call frame part that must be set up within the +/// CALLSEQ_START..CALLSEQ_END pair, +/// - part of the call frame prepared prior to CALLSEQ_START. +/// Both these parameters must be constants, their sum is the total call +/// frame size. +/// CALLSEQ_START..CALLSEQ_END pairs may not be nested. +CALLSEQ_START, // Beginning of a call sequence +CALLSEQ_END, // End of a call sequence +.... + +スタックサイズを渡している? + +`AnalyzeCallOperands` で、各々のオペランドをどの位置に置くか(どのレジスタか・スタックの何番目か) +を決める。この関数はTableGenによって生成される。 + +`NumBytes` はスタックに積まれる引数のサイズを表す(多分;要確認;TODO)。 + +引数が `ByVal` であるというのは、その引数がレジスタに収まらないサイズの値渡しであることを意味するはず。 +例えば構造体を値渡しする場合などが含まれる。(詳細な条件を要確認;TODO)。 +ここではとりあえず対応しない。 + +可変長引数にはここではとりあえず対応しない。 + +`LowerCall` はおおまかにi) 引数の解析を行いii) `CALLSEQ_START` を発行しiii) +各引数について `CopyToReg` を発行して仮想レジスタから物理レジスタへの +割付を行いiv) 呼出し先がグローバルアドレスの場合はfootnote:[普通そうだ。むしろそうでない場合が思い当たらない。] +このアドレスをレジスタに読み込むために `li` を発行しfootnote:[これは +`LowerGlobalAddress` において行われる。]iv) +引数に使用するレジスタ及びcallee-savedなレジスタは関数呼び出し中も継続して +生存していることを表すためにオペランドに追加しfootnote:[本来必要なオペランド以外もオペランドに追加することで +生存期間を引き伸ばせるようだ。RISC Vのコードから類推しただけなので確認が必要。TODO]) +`RV16KISD::CALL` を発行しvi) `RV16KISD::CALLSEQ_END` を発行しvii) +戻り値を解析して戻り値のどの部分がどのレジスタ/スタックに載って返ってくるかを求めviii) +`CopyFromReg` を発行して物理レジスタから仮想レジスタへの割付を行うfootnote:[これらの +DAGは `Glue` を用いて1つにまとめられている。これによって間に余計な命令が挟まることを +防いでいる<>。]。 + +途中で `getCallPreservedMask` の戻り値をオペランドに追加している。 +これによってcaller-savedなレジスタが関数呼び出しによって上書きされることが +指定されている。 + +関数呼び出しの前後で `ra` を保存するために、スタックへのレジスタの退避と復帰を各々 +`RV16KInstrInfo::storeRegToStackSlot` と `RV16KInstrInfo::loadRegFromStackSlot` として +実装する必要がある。このとき `addFrameIndex` を利用して `fp` からの距離で `lw/sw` を +発行する。このときの距離は `RV16KRegisterInfo::eliminateFrameIndex` にて正しい値に変更する。 +将来的にはこの操作は `sp` からの距離に変更し `lwsp/swsp` 命令を使うように +最適化したいが、とりあえずはこのままにしておく。どの部分で `fp` を `sp` に変換しているのかは +要調査(TODO)。 + +実装すると次のようなLLVM IRが + +.... +define i16 @defined_function(i16 %a) nounwind { + %1 = add i16 %a, 1 + ret i16 %1 +} + +define i16 @test_call_defined(i16 %a) nounwind { + %1 = call i16 @defined_function(i16 %a) nounwind + %2 = add i16 %a, %1 + ret i16 %2 +} +.... + +次のようなアセンブリに変換される。 + +.... + .text + .file "" + .globl defined_function # -- Begin function defined_function + .p2align 1 + .type defined_function,@function +defined_function: # @defined_function +# %bb.0: + addi a0, 1 + jr ra +.Lfunc_end0: + .size defined_function, .Lfunc_end0-defined_function + # -- End function + .globl test_call_defined # -- Begin function test_call_defined + .p2align 1 + .type test_call_defined,@function +test_call_defined: # @test_call_defined +# %bb.0: + sw ra, 2(fp) + sw s0, 0(fp) + mov s0, a0 + li a1, defined_function + jalr a1 + add s0, a0 + mov a0, s0 + lw s0, 0(fp) + lw ra, 2(fp) + jr ra +.Lfunc_end1: + .size test_call_defined, .Lfunc_end1-test_call_defined + # -- End function + + .section ".note.GNU-stack","",@progbits +.... + +ここで `2(fp)` などと出ているのは明確に**間違っており** `2(sp)` と出るべきである。 +というのも `fp` はアドレス上高位に位置する一方、 `sp` は低位に存在するからであるfootnote:[ +関数プロローグではi) `sp` を押し下げii) **`fp` を含む**全てのcallee-savedレジスタを +スタック上に保存した後iii) `fp` を設定する。したがって +`sp` からの相対アドレスでないと正しく指定ができないという事情もあるようだ。 +結局callee-savedなレジスタについては `sp` からの正のoffsetで参照され、 +それ以外のレジスタについては `fp` からの負のoffsetで参照される。]。 +これは `getFrameIndexReference` をオーバーライドすることによって +将来的に解決されるfootnote:[x86の実装などを見ると、 `fp` を使用せず全て +`sp` で対応する場合( `hasFP` が `false` の場合) +などもここでoffset計算を行うようだが、挙動を追いきれていない。TODO]footnote:[ +RISC Vの実装では `12(fp)` などと、実際に必要な値以上に深くスタックフレームが確保されている +ように見える。これはRISC Vの16バイトアラインを考慮しているためと推察される。要確認TODO]。 + +またこの段階では関数プロローグ・エピローグの出力には対応していないため、 +複雑な関数をネストして呼び出すような場合については対応していない。 + +さてRISC Vではこの段階で `fastcc` 関数呼び出し規約に対応している +footnote:[正確には「対応している」というより「許容している」が正しく、 +特に特別な処理は行っていない(ように見える;要確認;TODO)。]。 +だがここではとりあえず放置する。 + +== `getFrameIndexReference` をオーバーライドする + +さて `RV16KFrameLowering::getFrameIndexReference` では次のようにして +オフセットを求める。これはframe pointer相対になっている。 + +.... + int Offset = MFI.getObjectOffset(FI) - getOffsetOfLocalArea() + + MFI.getOffsetAdjustment(); +.... + +`getObjectOffset` はスタック中のオフセットを返す。基本的にはこれでよい。 +他に `getOffsetOfLocalArea()` はローカル変数の保存場所へのオフセットを返し(多分;TODO)、 +`getOffsetAdjustment` はそれ以外の調整値のようだfootnote:[正直これら2つが +必要なのかどうかはよくわからない。スタックフレームの定義によりそうだが、 +例えばLanaiでは両方考慮していない。]。 + +.... + explicit RV16KFrameLowering(const RV16KSubtarget &STI) + : TargetFrameLowering(StackGrowsDown, + /*StackAlignment=*/2, + /*LocalAreaOffset=*/0) {} +.... + +現状このように定義されるため `LocalAreaOffset` は `0` である。 +また `OffsetAdjustment` の定義には次のようなコメントが記載されている。 + +.... + /// The amount that a frame offset needs to be adjusted to + /// have the actual offset from the stack/frame pointer. The exact usage of + /// this is target-dependent, but it is typically used to adjust between + /// SP-relative and FP-relative offsets. E.G., if objects are accessed via + /// SP then OffsetAdjustment is zero; if FP is used, OffsetAdjustment is set + /// to the distance between the initial SP and the value in FP. For many + /// targets, this value is only used when generating debug info (via + /// TargetRegisterInfo::getFrameIndexReference); when generating code, the + /// corresponding adjustments are performed directly. +.... + +デバッグ情報などを出力する際に使用されるようだ。 + +== 関数プロローグとエピローグを出力する + +関数プロローグではi) その関数中で利用するスタックフレームのサイズを算出しii) +`sp` をその分押し下げiii) callee-savedなレジスタをスタックに保存する +命令分イテレータを先に進めfootnote:[この処理で飛ばされる命令の意味がよくわからない。 +TODO]iii) `fp` を `sp` にスタックサイズを加算したものとして算出する。 +関数エピローグではi) callee-savedなレジスタをスタックから復帰する命令分 +イテレータを戻しii) `sp` がアセンブリ中で変更された場合にはこれを `fp` から復帰しiii) +`sp` にスタックサイズを足して元の位置に戻す。 +これらの動作は教科書通りのものであるから、結局問題になるのはi) どのように +スタックサイズを算出するかii) どこで・いつcallee-savedなレジスタのスタック退避が +行われるかである。 + +`determineFrameLayout` では `MaxCallFrameSize` と `StackSize` について +正しい値を求める。 + +`MaxCallFrameSize` は事前に `computeMaxCallFrameSize` footnote:[ +この計算は `CallFrameSetupOpcode` と `CallFrameDestroyOpcode` が定義されていればよく、 +これは<>によってなされているようだ。]が呼ばれるなどしなければ +デフォルトの値 `0` となる。 + +`StackSize` は使用するスタックのバイト数である。 + +現在のRISC V実装ではより簡素になっている<>。 +またstack realignment(とは?;TODO)については2019年8月現在でもWIPのようだ<> +footnote:[<>はC言語の可変長配列(VLA; Variable Length Array)に +対応するためのパッチのようだ。関数中にVLAが存在する場合 `sp` が適切に +アラインメントされないまま関数呼び出しが行われてしまう場合がある。 +それを防ぐために `ADJCALLSTACKDOWN` と `ADJCALLSTACKUP` を利用して +`sp` の操作を行っている。]。 + +.... +// Determines the size of the frame and maximum call frame size. +void RISCVFrameLowering::determineFrameLayout(MachineFunction &MF) const { + MachineFrameInfo &MFI = MF.getFrameInfo(); + const RISCVRegisterInfo *RI = STI.getRegisterInfo(); + + // Get the number of bytes to allocate from the FrameInfo. + uint64_t FrameSize = MFI.getStackSize(); + + // Get the alignment. + uint64_t StackAlign = RI->needsStackRealignment(MF) ? MFI.getMaxAlignment() + : getStackAlignment(); + + // Make sure the frame is aligned. + FrameSize = alignTo(FrameSize, StackAlign); + + // Update frame info. + MFI.setStackSize(FrameSize); +} +.... + +`RV16KFrameLowering::determineCalleeSaves` ではcallee-savedなレジスタを特定し、 +`SavedRegs` に追加する。基本的には `TargetFrameLowering::determineCalleeSaves` を +内部で呼ぶことによってi) `TargetRegisterInfo::getCalleeSavedRegs()` にてcallee-savedと +指定されていてii) 実際に関数中で編集がなされるレジスタが特定される。 +一方で `fp` は `emitPrologue` 内で**callee-savedなレジスタ退避のためのコード生成が終わった後に** +`fp` を設定するコードが出力される。したがってこのデフォルトの仕組みでは保存するべきレジスタと +判断されない。そこで `RV16KFrameLowering::determineCalleeSaves` 内で明示的に +指定する必要があるfootnote:[実際この指定を外してコード生成を行うと `fp` の +スタック退避のためのコードは生成されない。一方で関数呼び出しなどが +行われている場合には `ra` のためのコードは生成されるが、 +RISC-Vでは `ra` と `fp` の両方を明示的に退避している。これによって +最適化が行われない場合には必ず `ra` と `fp` がスタック上に退避されることになり、 +`llvm.frameaddress` と `llvm.returnaddress` が動作することを保証している。] +footnote:[異なる手法でcallee-savedレジスタ退避を行うアーキテクチャもある。 +Lanaiの `determineCalleeSaves` では +`RCA` と `FP` のための領域のみを `MachineFrameInfo` に確保し +`SavedRegs` への追加は行わない。そして `emitPrologue` にて、 +この領域に `FP` を退避するコードを生成する。]footnote:[RV16K(ないし +実装を参照したRISC V)の `emitPrologue` では `MBBI` を `std::advance` +を用いて先に進める部分がある。 +これはここで特定し生成されたcallee-savedレジスタ退避のコードの後に `fp` +の設定を行うためである。]。 + +これによって次のようなLLVM IRが + +.... +define i16 @defined_function(i16 %a) nounwind { + %1 = add i16 %a, 1 + ret i16 %1 +} + +define i16 @test_call_defined(i16 %a) nounwind { + %1 = call i16 @defined_function(i16 %a) nounwind + %2 = add i16 %a, %1 + ret i16 %2 +} +.... + +次のようなアセンブリに化けるようになった。 + +.... + .text + .file "" + .globl defined_function # -- Begin function defined_function + .p2align 1 + .type defined_function,@function +defined_function: # @defined_function +# %bb.0: + addi sp, -2 + sw fp, 0(sp) + mov fp, sp + addi fp, 2 + addi a0, 1 + lw fp, 0(sp) + addi sp, 2 + jr ra +.Lfunc_end0: + .size defined_function, .Lfunc_end0-defined_function + # -- End function + .globl test_call_defined # -- Begin function test_call_defined + .p2align 1 + .type test_call_defined,@function +test_call_defined: # @test_call_defined +# %bb.0: + addi sp, -6 + sw ra, 4(sp) + sw fp, 2(sp) + sw s0, 0(sp) + mov fp, sp + addi fp, 6 + mov s0, a0 + li a1, defined_function + jalr a1 + add s0, a0 + mov a0, s0 + lw s0, 0(sp) + lw fp, 2(sp) + lw ra, 4(sp) + addi sp, 6 + jr ra +.Lfunc_end1: + .size test_call_defined, .Lfunc_end1-test_call_defined + # -- End function + + .section ".note.GNU-stack","",@progbits +.... + +== `select` に対応する + +`select` は条件に応じて2つの値の一方を選択する命令である。 +C言語の条件演算子footnote:[またの名を三項演算sうわなにをするやめr(静粛されました)] +`?:` に対応するようだfootnote:[意味的にはそうだが、実際にClangがそうコンパイルするかは +要確認TODO]。対応するSelectionDAGノードは `SELECT` と `SELECT_CC` で、 +次のように定義される。 + +.... +/// Select(COND, TRUEVAL, FALSEVAL). If the type of the boolean COND is not +/// i1 then the high bits must conform to getBooleanContents. +SELECT, + +/// Select with condition operator - This selects between a true value and +/// a false value (ops #2 and #3) based on the boolean result of comparing +/// the lhs and rhs (ops #0 and #1) of a conditional expression with the +/// condition code in op #4, a CondCodeSDNode. +SELECT_CC, +.... + +RISC Vでは `SELECT_CC` をexpandした上で `SELECT` をcustomにlowerして +`RISCVISD::SELECT_CC` とし、これを擬似命令 `Select_GPR_Using_CC_GPR` で捕捉している。 +この擬似命令にはcustom inserterが付随していて、 +`RISCVTargetLowering::EmitInstrWithCustomInserter` にてこの擬似命令を +`MachineInstr` に変換している。 + +`EmitInstrWithCustomInserter` は `usesCustomInserter` フラグが立っている +`MachineInstr` に対してフックし、これを適切な命令に変換・伸張する。 +すなわち `MachineInstr` を異なる `MachineInstr` に変換するということである。 + +SparcもRISC Vと同様の手法を取っているfootnote:[歴史的には順序が逆だが。]が、 +`SELECT` ではなく `SELECT_CC` をlowerしている。 +そのため `LowerSELECT_CC` で比較命令と `SPISD::SELECT_ICC` を作成し、 +これを `EmitInstrWithCustomInserter` で処理している。 +なお `setcc` が使用された場合に冗長なコードが出力されるのを防ぐためSparc +バックエンドでは `LookThroughSetCC` という関数を定義し、 +`(cmpLHS < cmpRHS ? 1 : 0) != 0 ? TrueVal : FalseVal` というノードが現れた +場合は `cmpLHS < cmpRHS ? TrueVal : FalseVal` と変換する処理を行っているfootnote:[これは +expandの処理に含まれないのだろうか。要調査TODO]。 + +Lanaiでは `SELECT_CC` のハードウェアサポートが存在するため、 +`ISD::SELECT_CC` を `LanaiISD::SELECT_CC` に置換した後、その命令にパターンマッチさせて +いる。 + +x86では `SELECT_CC` をexpandしたうえで `SELECT` を捕捉し、 +`Select (COND, TRUEVAL, FALSEVAL)` に対し、 `COND` が真ならば `TRUEVAL` を +`FALSEVAL` に代入するような `X86ISD::CMOV` を発行している。 +これはパターンマッチによって `CMOVcc` footnote:[条件が成立する場合に `MOV` +を行うx86の命令。]に変換される。 + +RV16KではRISC Vの手法をとる。すなわち `SELECT_CC` はexpandした上で `SELECT` と +それに付随する `setcc` を `RV16KISD::SELECT_CC` にlowerする。 +`setcc` がない場合は定数 `0` を補う。その後 `RV16KISD::SELECT_CC` を +擬似命令 `SelectCCrr` に変換し、これに対してcustom inserterを適用して +処理を行う。 + +`getNode` に `SDVTList` を渡しているのは `RV16KISD::SELECT_CC` の出力の型として +`i16` とGlueの2種類があるからのようだが、判然としない(要確認;TODO)。 + +`EmitInstrWithCustomInserter` では新たに制御構造を追加し、 +`SelectCCrr` を具体的な `MachineInstr` に変換する。 +`BuildMI` を用いて比較命令とジャンプ命令の2つを生成する。 +この比較命令とジャンプ命令の間に、後々のパスで別の命令が入る可能性は +`Defs` と `Uses` の適切な指定によって排除されている(要確認;TODO)。 + +新たに擬似命令として `SelectCCri` を追加し、右辺が即値の場合には `cmpi` を出力するように +した。 + +ところでわざわざ `EmitInstrWithCustomInserter` など使わずとも、 +`LowerSELECT` で適当なSelectionDAGを作り `SELECT` の挙動を実現することは +できないのだろうか。調べた限りでは、LLVMのバックエンドにおいて新たなbasic blockを +作成することはできずfootnote:[一方で `MachineBasicBlock` を作成することは +(既に見てきたように)可能である。これによって、1つのbasic blockに複数の +`MachineBasicBlock` が対応することになる。]、そのためにジャンプ先の +`SDValue` を作ることができないように見えるが、判然としない(要確認;TODO)。 + +== 落穂拾いをする + +image::img/gleaners.jpg[] +__The Gleaners__ -- Jean-François Millet<> + +上記で最低限のコード生成は行えるようになったが、未だ多くの(雑多な)IRに対応していない。 +そこで今までに作成した部分を利用してこれらに対応する。 +またその途中で必要となる未サポートの機能に対応する。 +参考にするRISC Vのパッチは主に<>である。 + +以下では `setOperationAction` を利用してコード生成を可能にする場合が多い。 +関数定義に付されたコメントによれば、この関数に指定する型は入力と出力のどちらをも +表す場合があるようだfootnote:[`setOperationAction` の指定によって、結果どのような +アセンブリが生成されるかについてまとめた情報(実装以外に)はない(要確認;TODO)。 +結局、他のバックエンドを参考にしながら指定するほかない。 +なお実装を見る場合は `DAGTypeLegalizer` や `SelectionDAGLegalize` +などを参考するとよさそうだ。]。 + +.... +/// Indicate that the specified operation does not work with the specified +/// type and indicate what to do about it. Note that VT may refer to either +/// the type of a result or that of an operand of Op. +void setOperationAction(unsigned Op, MVT VT, + LegalizeAction Action) { + assert(Op < array_lengthof(OpActions[0]) && "Table isn't big enough!"); + OpActions[(unsigned)VT.SimpleTy][Op] = Action; +} +.... + +=== 大きなスタックフレームに対応する + +関数プロローグでは `sp` を押し下げるコードを出力する必要がある。 +このとき小さいスタックフレームであれば `addi` のみで対応できるため問題にならない。 + +.... +define void @test() nounwind { + %tmp = alloca [ 3 x i8 ] , align 2 + ret void +} +.... + +.... +$ bin/llc -mtriple=rv16k -verify-machineinstrs -print-after=prologepilog < foo.ll > /dev/null +# *** IR Dump After Prologue/Epilogue Insertion & Frame Finalization ***: +# Machine code for function test: NoPHIs, TracksLiveness, NoVRegs +Frame Objects: + fi#0: size=3, align=2, at location [SP-6] + fi#1: size=2, align=2, at location [SP-2] + +bb.0 (%ir-block.0): + $x1 = frame-setup ADDI $x1(tied-def 0), -6, implicit-def $flags + SW killed $x2, $x1, 4, implicit-def $flags + $x2 = MOV $x1, implicit-def $flags + $x2 = frame-setup ADDI $x2(tied-def 0), 6, implicit-def $flags + $x2 = LW $x1, 4, implicit-def $flags + $x1 = frame-destroy ADDI $x1(tied-def 0), 6, implicit-def $flags + PseudoRET + +# End machine code for function test. +.... + +一方で大きなスタックフレームでは `li` と `add` footnote:[実際には +状況によって `sub` も用いられる。] を組み合わせる必要がある。 +このとき一度使い捨てのレジスタに `li` で即値を読み込み、 +その上で `add` しなければならない。これをナイーブに実装するfootnote:[以前の実装は +このナイーブな手法であったが、その部分を使用する適切なテストが存在しなかったため +エラーにならなかった。ここでの修正とともにテストに `large-stack.ll` +<>を追加した。] +と次のようなエラーが出る。 + +.... +define void @test() nounwind { + %tmp = alloca [ 30000 x i8 ] , align 2 + ret void +} +.... + +.... +$ bin/llc -mtriple=rv16k -verify-machineinstrs < foo.ll + .text + .file "" + +# After Prologue/Epilogue Insertion & Frame Finalization +# Machine code for function test: NoPHIs, TracksLiveness, NoVRegs +Frame Objects: + fi#0: size=30000, align=2, at location [SP-30002] + fi#1: size=2, align=2, at location [SP-2] + +bb.0 (%ir-block.0): + %0:gpr = frame-setup LI 30002, implicit-def $flags + $x1 = frame-setup SUB $x1(tied-def 0), killed %0:gpr, implicit-def $flags + SW killed $x2, $x1, 30000, implicit-def $flags + $x2 = MOV $x1, implicit-def $flags + %1:gpr = frame-setup LI 30002, implicit-def $flags + $x2 = frame-setup ADD $x2(tied-def 0), killed %1:gpr, implicit-def $flags + $x2 = LW $x1, 30000, implicit-def $flags + %2:gpr = frame-destroy LI 30002, implicit-def $flags + $x1 = frame-destroy ADD $x1(tied-def 0), killed %2:gpr, implicit-def $flags + PseudoRET + +# End machine code for function test. + +*** Bad machine code: Function has NoVRegs property but there are VReg operands *** +- function: test +LLVM ERROR: Found 1 machine code errors. +.... + +これはおそらく `frame-setup/frame-destroy` に仮想レジスタ `%0/%1/%2` が登場していることが +原因である。これらは本来物理レジスタに割り付けられなければならないためエラーが +発生しているfootnote:[このエラーは `-verify-machineinstrs` オプションを +指定しているために発生している。これを指定しない場合はそのまま処理が継続し、 +別の場所でエラーが出る。]。 +スタックフレームの調整値が大きい場合 `addi` を使用することが出来ず `li` と `add` を +組み合わせて実現する必要がある。ここで使用する使い捨ての仮想レジスタに物理レジスタが +割り付けられていないようだ。これに対応するためには `RV16KRegisterInfo` にて +`requiresRegisterScavenging` と `requiresFrameIndexScavenging` から `true` を返すよう +オーバーライドする必要がある<>。 +LLVMはレジスタ割付をした後に関数プロローグ・エピローグの挿入を行う +<>。そのため関数プロローグ・エピローグで挿入される +仮想レジスタに正しく物理レジスタを割り付ける特別な仕組みが必要となるようだ +footnote:[それを行っているのがregister scavengerのようだが、詳細は要調査;TODO]。 + +=== 命令に対するフラグを追加する + +2オペランド命令のうち左右辺を入れ替えられるものについては `isCommutable` を `1` にする。 +これによって生成される命令が効率化されるfootnote:[例えば +`and x1, x0` 後に `mov x0, x1` とする場合 `and` に `isCommutable` +フラグが立っていれば、2つの命令をまとめて `and x0, x1` とできる(場合がある)。]。 +また `isMoveImm` や `isMoveReg` ・ `isAdd` フラグなども(おまじない程度に)立てておく。 + +=== `SETCC` に対応する + +条件分岐や `SELECT` に付随する `SETCC` には対応したが、 +単発の `SETCC` には対応できていない。RV16Kには対応する命令がないため、 +これはexpandして `SELECT` として扱うfootnote:[Sparcも同様の手法を採用している。]。 + +=== キャリー付き加減算に対応する + +<>では `ADDC/ADDE/SUBC/SUBE` に対応している。 +これらは他倍長の加減算に対応するための命令でcarryを(明示的に) +受け取り・出力する加減を行う。このような命令はRV16Kにはないためexpandする必要がある。 + +しかし試してみると、expandしなくともすでにi32の演算に対応していたfootnote:[ただし +`i32` の結果を関数から返却するために関数呼び出し規則を変更する必要がある。]。実際 +現在のRISC V実装でもこれらに対するexpandは明示されていない。 +どうやらLLVM core側で指定されるようになったようだ<>。 + +=== `LowerCall` 内で `ExternalSymbolSDNode` を処理する + +すでに `GlobalAddress` については対応しているが、同様に `ExternalSymbol` についても +対応する。 `TargetExternalSymbol` に変換した上で `LI` にて値を読み込むようにする。 +これによって `MachineOperand::MO_ExternalSymbol` が `MachineInstr` 中に出現するようになるため、 +`LowerRV16KMachineOperandToMCOperand` にてこれに対処する。 + +なお `ExternalSymbol` は、そのシンボルが指す関数をLLVMが把握していないときに用いられる点で +`GlobalAddress` と異なる<>footnote:[LLVMが把握していない関数を呼ぶとは +これいかに。参照先ではランタイムライブラリを呼ぶ場合などが説明されているがよくわからなかった。 +ランタイムライブラリの関数(後に登場する `__mulsi3` など)はコンパイラが挿入するため、 +具体的な関数プロトタイプを埋め込むことは困難ということか? 実際アセンブリ中にシンボル名のまま +出力すれば目的は達成されるため、無理にこれを特定する必要はない。要調査;TODO]。 + +=== 掛け算に対応する + +後の `CTLZ` の実装などでも必要となる掛け算を実装する。RV16Kv2では掛け算命令が存在しないため +`MUL/SMUL_LOHI/UMUL_LOHI/MULHS/MULHU` についてexpandする。ここで `SMUL_LOHI/UMUL_LOHI` は +Nビット二数を掛けて得られる2Nビットの結果を2つのNビットレジスタにて返す。 +また `MULHS/MULHU` は同様の2Nビットの結果のうち、上位Nビットのみを返す。 +なおLLVMにおいて `mul` は符号の有無を問わないfootnote:[逆に言えば +すべて符号なしとして積を計算してよい。これはi) LLVMが2の補数表現を採用していることとii) +結果とオペランドが同じビット幅を持っていることから従うようだ<>。]。 + +さてこれらの命令は `__mulhi3` と `__mulsi3` を呼ぶようにexpandされる。 +`__mulhi3` は16bit整数の掛け算を行い `__mulsi3` は32bit整数の掛け算を行う(ぽい;要確認 TODO)。 +したがってこれらの関数がリンクされる必要がある。たとえばRISC Vではcompiler-rtに +`__mulsi3` を追加して目的を達成している<><>。 +compiler-rtは主にClangやLLVMのためのランタイムライブラリである<>。 + +とりあえずここではcompiler-rtまで踏み込むことは後回しにし、 +文字列として `__mulhi3` などを出すまでに留める。 + +=== `ROTL/ROTR/BSWAP/CTTZ/CTLZ/CTPOP` に対応する + +特殊なfootnote:[ISAや人によっては特殊でないかもしれないが。]ビット演算に対応する。 +RV16Kにはこれらに対応する命令が直接は存在しないためexpandする。 +各々の操作の意味については<>に詳しい。 + +`rotl` のようなLLVM IRレベルで対応する関数のことをintrinsic functionと呼ぶ。 +the intrinsicと呼称される場合もある。ようするにビルトイン関数のことのようだ。 +拡張方法が<>にある。 + +=== 除算・剰余に対応する + +RV16Kv2には除算・剰余のための命令が存在しないため +`SREM/SDIVREM/SDIV/UREM/UDIVREM/UDIV` についてexpandする。 + +=== 32bitのシフトに対応する + +`SHL_PARTS/SRL_PARTS/SRA_PARTS` についてexpandする。 +これによって `__lshrsi3/__ashrsi3/__ashlsi3` を各々呼ぶように伸張される。 + +=== 単体の `sext/zext/trunc` に対応する + +`SIGN_EXTEND_INREG` を `i1` と `i8` についてexpandするfootnote:[`i16` はレジスタが16bitで +あるためか勝手にexpandされるようだ。]。 + +=== 間接ジャンプに対応する + +`brind` に対するパターンをTableGenファイルに記述する。 + +間接ジャンプに関するRISC Vのテストでは `getelementptr inbounds` が使用されている<>。 +LLVM IRの言語仕様<>によれば、 `inbounds` が指定される場合、 +第二引数として渡すbase pointerが適切なアドレスを指して +いないときには結果の値としてpoison value footnote:[LLVM IRではオペランドに不適切な +値が渡された場合poison valueを返却し、命令に依存関係がある間はこれを伝播させる。 +その後その値が別の場所で使用される(例えばメモリロードのアドレスとして用いられる等) +に際してundefに変換され、結果未定義の挙動を行うことになっているようだ<>が、 +ちゃんと読んでいない TODO。これがアセンブリとどう対応するのかわからない。 +とりあえずinvalidな値が出力されるという認識でよさそうだ。]が返される。 +ここでどのようにその適切性を調べるのかは判然としない。 +実際関数の引数が第二引数にくるような今回の場合では、実行時にならなければ +判断がつかないように思われるfootnote:[というかこれはまさにGCの挙動であって、 +コンパイル時に完全に把握することは(ライスの定理的に(言いたかっただけ))不可能ではないのか。]。 +要確認;TODO。実際この例では `inbounds` がなくとも正常に動作したが、おまじない程度につけておく。 + +=== `BlockAddress` のlowerに対応する + +`BlockAddress` はbasic blockのアドレスを表す。C言語においてラベルのアドレスを取得し +`goto` する場合などに用いられる<>footnote:[使いどころが無いように +思えるが、Ruby VMで実際に使用されているらしい<>。]。 +LLVM IRでは `blockaddress` 定数を利用することで取得できる<>。 + +=== ジャンプテーブルに対応しない + +LLVM IRには `switch` という命令があり、意味論はC言語の `switch` 文と対応するfootnote:[実際にClangが +`switch` 文 を `switch` 命令 に対応させているかは要調査;TODO。]。 +これに対応したい。 + +LLVMは `switch` をコンパイルする際にjump tableを用いる場合がある<>。 +これに対応するためには `ISD::BR_JT` と `ISD::JumpTable` を適切に処理する必要がある。 +しかしRISC Vではこれを無効化している。AVRも同様である<>。 +Lanaiの実装を参考にすると `GlobalAddress` などと同様に処理すれば良いように見える。 + +とりあえずここではRISC V・AVRと同様に `setMinimumJumpTableEntries(INT_MAX);` とすることで +ジャンプテーブルの生成を抑止することで解決するfootnote:[あとの作業と直交していて +手を抜けるところはとりあえず抜いておく。]。これによって `switch` は当面 +`if-else` の連続としてコンパイルされることになるようだfootnote:[実際の出力と +<>からの推論。詳細は要調査;TODO]。 + +== frame pointer eliminationに対応する + +現在の実装では `sp` と `fp` の両方を使用してスタックの操作を行っている。 +これはスタックに対する操作全てをカバーするためには必要だfootnote:[後に登場する +`hasFP` の実装を見ると、具体的にどのような場合に `fp` が必要となるのか分かる。]が、 +一方で `sp` のみで十分である関数も多い。そこでそのような場合に限って +`fp` の使用をやめ、全て `sp` を通じてスタックを操作することで、 +レジスタを1つ多く使用することができる。これを実装する。 +参考にするパッチは<>。 + +まず `RV16KFrameLowering::hasFP` を実装する。この関数は引数に `MachineFunction` をとり、 +この関数に `fp` が必要か否かを返す。その後prologue/epilogueやRegisterInfoなどを適切に +実装すればよい。 + +この変更によってテストの正解コードが多く変更されるので頑張って修正する。 +Vimで置換すると楽だった。コマンドラインウィンドウはいいぞ。 + +幾つかのテスト( `large-stack.ll` )については、これによってテストしたい内容が +最終的なアセンブリから消えてしまう場合があるため `-frame-pointer=all` オプションを +つけてframe pointer eliminationを無効化した場合の結果もテストするようにする。 +なお<>ではこれのために `-disable-fp-elim` オプションが +使われているが、オプションの名前が最近変更されたようだ<>。 + +== 動的なスタック領域確保に対応する + +C99の可変長配列(VLA; Variable-Length Array)のような、スタック領域の動的確保に対応する。 +領域の確保そのものは `alloca` 命令によって行われる。したがってこの命令が動的な値をオペランドに +取れるように `DYNAMIC_STACKALLOC` をexpandする。 + +また一般的に、動的スタック領域確保にはスタック状態の退避・復帰footnote:[要するに `sp` を +スタックにpush/popするということである。]が必要である +<><>。 +このために `llvm.stacksave` と `llvm.stackrestore` 命令が用いられるため、これらに対応する +`STACKSAVE` と `STACKRESTORE` をexpandする。命令の詳細は<>を参照する。 + +== 関数呼び出しを `jal` で行うようにする + +現状関数呼び出しは、まず `li` で関数のアドレスをレジスタに格納し、 +その後に `jalr` を実行して関数にジャンプしている。しかしRV16Kv2では +`jal` 命令が16bitのアドレスをそのままとることができるため、 +この2命令を `jal` 1命令に置き換えることが可能である。 + +RISC Vのアセンブリ上では、関数呼び出しを `call` という一命令で表現している。 +これは `PseudoCALL` と1対1に対応する。 +しかしこのような命令はRISC Vに存在せず、 +内部的には `auipc` と `jalr` の二命令を用いて表現される。 +一方で、RISC Vには `jal` 命令が存在し、 +これを用いれば一命令で関数呼び出しを実現できるように思えるが、 +実際にはビット幅の制限からすべての関数呼び出しにおいて `jal` を使えるわけではない。 +さらに悪いことに `jal` が適用できるか否か、一般的にはリンク時まで判断できない。 +したがってRISC Vのバックエンドは、すべての `PseudoCALL` を `auipc` と `jalr` に置き換える手法を +採用している<><>。 + +対してRV16Kはすべての関数呼び出しを `jal` で扱うことができ、 +またアセンブリ上も `jal` と表記される。したがって `PseudoCALL` 疑似命令を導入するメリットは +少なく、単にパターンマッチを行えば良い。 + +そこで従来の `PseudoCALL` の代わりに `def : Pat<(Call GPR:$rs), (JALR GPR:$rs)>;` を追加すると +`LowerRV16KMachineInstrToMCInst` が `MO_RegisterMask` を扱えないというエラーが出る。 +`MO_RegisterMask` は関数呼び出しを行う際などに指定され、 +関数呼び出しなどによって非明示的に書き換わるレジスタを表す<>footnote:[正確には +書き換わらないレジスタについて記述する。記述しなかったそれ以外のレジスタが +書き換えられるとLLVMは認識する。]。 +これはレジスタのimplicit defと同じ役割のため `MCInst` +においては無視するように変更する<>。 + +続いて `jalr` のオペランドに直接シンボルを指定するように変更する。これが本題である。 +シンボルというのは結局 `GlobalAddress` と `ExternalSymbol` のことなので、 +これらについてパターンマッチを追加する。次いで、現在 `LowerCall` でfootnote:[実際には +`LowerGlobalAddress` と `LowerExternalSymbol` を呼び出すことで行われている。]行っている +`LI` ノードの作成をやめ、 `TargetGlobalAddress` と `TargetExternalSymbol` の変換のみを +行うようにする。 + +ここで次のように指定すると正しく動作する。 + +.... +def : Pat<(Call GPR:$rs), (JALR GPR:$rs)>; +def : Pat<(Call tglobaladdr:$dst), (JAL tglobaladdr:$dst)>; +def : Pat<(Call texternalsym:$dst), (JAL texternalsym:$dst)>; +.... + +ここで下2つをコメントアウトすると `jal` の代わりに `jalr` が使用される。 +どうやら `tglobaladdr` が `GPR` にマッチするようだが、原因不明(TODO)。 + +いままでのテストが壊れるので頑張って直す。 + +== `FrameIndex` をlowerする + +現在のバックエンドで次のようなLLVM IRをコンパイルしようとするとエラーが出力される。 + +.... +$ cat foo.ll +define void @test() { +entry: + %retval = alloca i16, align 2 + store i16 0, i16* %retval, align 2 + ret void +} + +$ bin/llc -mtriple=rv16k -verify-machineinstrs < foo.ll + .text + .file "" +LLVM ERROR: Cannot select: t2: i16 = FrameIndex\<0\> +In function: test +.... + +SelectionDAGの `FrameIndex` に対してパターンマッチが存在しないために +`MachineInstr` を選択できないというエラーである。 + +そもそも `FrameIndex` とは何か。<>では次のように説明される。 + +[quote] +____ +When variables on the stack are referred to before the prologue-epilogue insertion (PEI) step, they are addressed using "frame indexes", an arbitrary name for a location that will eventually resolve to a stack-pointer relative offset. +____ + +`TargetSelectionDAG.td` には次のように定義される。 + +.... +def frameindex : SDNode<"ISD::FrameIndex", SDTPtrLeaf, [], + "FrameIndexSDNode">; +.... + +<><>や +上記によれば、 +frame indexとはフレーム上に割り付けられた変数の位置を表す抽象的なアドレスのこと +footnote:[アドレスそのものであって、それが指す値ではないことに注意が必要である。]であり、 +関数プロローグ・エピローグ挿入時に具体的な値( `sp` や `fp` からの相対アドレスなど) +に置き換えられる。 +逆に言えば命令選択時などはこのframe indexを各種命令のオペランドとすることになる。 +そのような状態に対処するために `ISD::FrameIndex` が用意されている。 +これについて独自アーキテクチャ側で対応するためには、まずi) `frameindex` をオペランドとして受け取る +ような命令を定義する。ただし直接 `frameindex` +をオペランドに指定することはLLVMの仕様上できない<>ため、 +`frameindex` をtarget frame indexに変換するような `ComplexPattern` を定義し、 +これをオペランドの型として指定するfootnote:[パターンを型として指定するというのは若干違和感がある。 +要調査。TODO]。これによってオペランドの `frameindex` は適切に処理されることになる。 +ついで `frameindex` (を含む `ComplexPattern` )をオペランドとして持たない場所で +`frameindex` が使用された場合に対処するため `RV16KDAGToDAGISel::Select` で `ISD::FrameIndex` を +トラップする。ここではレジスタに格納されたframe indexに即値0を足すようなDAGノードを +RISC VやLanaiは返している。よくわからない。TODO +とりあえず `llvm_unreachable` を仕込んで、エラーになるまで +放置するfootnote:[これを俗に「バグを泳がせる」と言ったり言わなかったりする。]。 + +.... +//===----------------------------------------------------------------------===// +// Complex pattern definitions. +// + +// Complex patterns, e.g. X86 addressing mode, requires pattern matching code +// in C++. NumOperands is the number of operands returned by the select function; +// SelectFunc is the name of the function used to pattern match the max. pattern; +// RootNodes are the list of possible root nodes of the sub-dags to match. +// e.g. X86 addressing mode - def addr : ComplexPattern<4, "SelectAddr", [add]>; +// +class ComplexPattern roots = [], list props = [], + int complexity = -1> { + ValueType Ty = ty; + int NumOperands = numops; + string SelectFunc = fn; + list RootNodes = roots; + list Properties = props; + int Complexity = complexity; +} +.... + +`ComplexPattern` に指定するroot nodesは、パターンマッチが試行されるべきノードを +表している。例えばx86の `lea32addr` では次のように定義され、 +通常の四則演算を `lea` 命令に変更できることを示唆しているfootnote:[実際には +`selectLEAAddr` 関数内で「 `lea` 命令に置き換える価値があるか」を判断し( `Complexity` ) +ているようだ(要確認;TODO)。]。 + +.... +def lea32addr : ComplexPattern; +.... + +ここで、これはSelectionDAGからMachineDAGに変換する部分に影響することに注意が必要である。 +例えば `RV16KInstrInfo::storeRegToStackSlot` などで `addFrameIndex` を使用して +frame indexを指定するが、これはすでに `MachineDAG` ノードであるため、 +frame indexを処理する機構を必要としない。 + +ここで登場した抽象的なスタックは `MachineFrameInfo` によって管理される。 + +`add` のみならず `add` と等価な `or` についてもパターンが定義される。 +これは存在価値が良くわからないのと、他の命令中でやらない理由がわからない。 +とりあえずやめておく。TODO + +== ClangをRV16Kに対応させる + +今までRV16KバックエンドはLLVM IRを受け取りアセンブリ(やオブジェクトファイル)を出力する +ものとして作成してきた。しかし我々が本当にほしいものはCコンパイラであり、 +その入力はC言語ソースコードである。 +そこでClangをRV16Kに対応させ、本当の「コンパイラ」として駆動するようにする。 + +これに際して参照するRISC Vのパッチは `clang` ディレクトリにまとめられている +<>。また単純な32bitRISCアーキテクチャでない +バックエンドのClang対応としてAVRのものや、 +ELVMバックエンドのClang対応<>も参考になる。 + +=== ClangのターゲットにRV16Kを追加する + +`clang/lib/Basic/Targets.cpp` の `AllocateTarget` にて +`rv16k` のtripleが渡された場合には `RV16KTargetInfo` のインスタンスを返すようにし、 +`clang/lib/Basic/Targets/RV16K.{cpp,h}` にその定義を記述する。 + +ここで整数や浮動小数点などのビット幅を指定する<>。 +`RV16KTargetInfo` が継承する `TargetInfo` は32bit RISCアーキテクチャを +念頭においた値をデフォルトとしているため、これを16bit用に適宜変更する。 +似たようなアーキテクチャとしてAVRのバックエンドが参考になる。 + +以上により `clang` が一応動作するようになる。動かすときには次のように +`-target rv16k` を指定することでrv16kを指定するfootnote:[あるいは `-triple rv16k--` としてもよい。]。 + +.... +$ bin/clang -S -c -target rv16k foo.c -o foo.s +.... + +=== ClangのdriverにRV16Kを追加する + +先程のコマンドで、直接オブジェクトファイルを出力させるために +`-S` オプションを外すとエラーとなる。これはclangが内部的にGNU asを呼ぶためである。 +これをllvm-mcに変更するためには `-fintegrated-as` オプションを指定すればよいが、 +いちいちユーザが指定するよりはむしろ内部的にデフォルトの設定とすれば良い +<><>。 + +そのために `clang/lib/Driver/Driver.cpp` を書き換えrv16kのtripleに対して +`RV16KToolChain` のインスタンスを返すようにし、またその定義を +`clang/lib/Driver/ToolChains/RV16K.h` に記述する。 +RISC Vの対応するパッチ<>では +`RV16K.cpp` も記述し、コンパイル時に必要なinclude flagsの設定や +リンカのための設定を行っているが、RV16Kv2では現状そこまでの記述は +必要ないfootnote:[仕様が固まっていない。]。そこでLanaiバックエンドの +記述を参考にし `-fintegrated-as` に対応する `IsIntegratedAssemblerDefault` のみを +記述し、その他の関数は空にしておく。 + +=== frame pointer eliminationを最適化時に有効にする + +`clang/lib/Driver/ToolChains/Clang.cpp` の `useFramePointerForTargetByDefault` +に `Triple::rv16k` を追加すれば良い。このファイルでは他にターゲットに依存した +コマンドラインオプション等を追加できるが、RV16Kv2では存在しないのでその変更は +行わない。 + +また同じファイルに `char` 型が `signed` か否かを決める `isSignedCharDefault` という +関数がある。デフォルトでは `signed` になるようだが、RISC Vは `unsigned` に +変更している。RV16Kv2では `signed` なのでfootnote:[今決めた。]このままにしておく。 + +このようなLLVM IRを入力してみる。 + +.... +char add(char a, char b) +{ + return a + b; +} +.... + +signedの場合。 + +.... +define dso_local signext i8 @add(i8 signext %a, i8 signext %b) local_unnamed_addr #0 { +entry: + %add = add i8 %b, %a + ret i8 %add +} +.... + +unsignedの場合。 + +.... +define dso_local zeroext i8 @add(i8 zeroext %a, i8 zeroext %b) local_unnamed_addr #0 { +entry: + %add = add i8 %b, %a + ret i8 %add +} +.... + +`signedext/zeroext` が切り替わっていることが分かる。 + +以上のClangへの変更により、次のように動くようになったfootnote:[ようやく +「コンパイラ」という感じがしてきた。]。 + +.... +$ cat foo.c +int main() +{ + return 42; +} + +$ bin/clang -c -O2 -target rv16k foo.c -o foo.o + +$ bin/llvm-objdump -d foo.o + +foo.o: file format ELF32-rv16k + +Disassembly of section .text: +0000000000000000 main: + 0: 08 78 2a 00 li a0, 42 + 4: 00 40 jr ra +.... + +== スタック経由の引数渡しに対応する + +関数引数の数の制限を取り払い、レジスタに収まらないものはスタックを経由して渡す。 + +RISC Vでは、TableGenが生成する呼び出し規則解決のコードではRV32Iの +呼び出し規則を達成できないため、新たにフルスクラッチで書き直している +<>。 + +`LowerFormalArguments` は `SelectionDAGISel::LowerArguments` から呼ばれる。 +これは `SmallVectorImpl Ins` を作るときにsplitのフラグを +設定する。必要なレジスタ数は `TargetLowering::getNumRegisters` によって見積もられる。 +したがって `Ins` にはすでにsplitされた入力が保存されている。 + +`CC_RISCV` は `ISD::InputArg` の内容を引数の順に受け取り、 +それをレジスタに保存する(/に保存されているfootnote:[`CC_RISCV` は +`analyze{Input,Output}Args` を通して引数と戻り値の両方に +対応している。])かメモリに保存するかを決め `CCState::addLoc` を呼ぶ。 +このとき、複数の引数がまとめて一つの値を示す場合があるため、 +それを検知し処理する必要がある。2つに分かれている場合は +各々をレジスタないしスタックに積む。それ以上に別れている場合は +スタックにすべて積み、その先頭アドレスをレジスタないしスタックに積む。 +このアドレスのみを渡す手法は `CCValAssign::Indirect` に対応する。 + +RV16KではLanaiに準じたナイーブな手法でスタックを使用する。すなわち +2バイト毎に引数を区切り順にスタックに格納する。 +このためには `LowerFormalArguments/LowerCall` にてスタック上に +領域が割り振られた場合に `DAG.getLoad/DAG.getStore` を使用して `load/store` 命令を +出力する必要がある。また `LowerCall` における `store` 命令は互いに関係がなく +順序に意味はないため `Chain` で結ぶことはせず `ISD::TokenFactor` で +一つにまとめ挿入する。 + +.... +/// TokenFactor - This node takes multiple tokens as input and produces a +/// single token result. This is used to represent the fact that the operand +/// operators are independent of each other. +TokenFactor, +.... + +ここで `SDValue` まわりの疑問が出てきた。現状の `LowerCall` の最後には +戻り値に `CopyFromReg` を適用するため次のようなコード片がある。 + +.... + // Copy all of the result registers out of their specified physreg. + for (auto &VA : RVLocs) { + // Copy the value out, gluing the copy to the end of the call sequence. + SDValue RetValue = + DAG.getCopyFromReg(Chain, DL, VA.getLocReg(), VA.getLocVT(), Glue); + Chain = RetValue.getValue(1); + Glue = RetValue.getValue(2); + + InVals.push_back(Chain.getValue(0)); + } +.... + +RISC Vでは最後を `InVals.push_back(RetValue)` と書き換えている。 +この意味を理解したい。 + +上のコードは物理レジスタで戻される結果を仮想レジスタに引き込むための +`CopyFromReg` ノードを作成するものである。実際にDAGを出力させると +次のようになる。 + +image::img/sdvalue_copyfromreg.png[] + +コードと対照させると `DAG.getNode` はそのノードへの**最初の入力**を +`SDValue` として返すことが分かる。これはしばしば `Chain` である。 +逆に言えば、子ノードの `Chain` を `DAG.getNode` に渡すことによって +DAGをより根の方へ伸ばすことができる。 + +その使われ方から分かるように `SDValue` は `SDNode` と添字を持つ。 +これによって「どのノードの何番目の入力か」を表している。 + +.... +/// Unlike LLVM values, Selection DAG nodes may return multiple +/// values as the result of a computation. Many nodes return multiple values, +/// from loads (which define a token and a return value) to ADDC (which returns +/// a result and a carry value), to calls (which may return an arbitrary number +/// of values). +/// +/// As such, each use of a SelectionDAG computation must indicate the node that +/// computes it as well as which return value to use from that node. This pair +/// of information is represented with the SDValue value type. +/// +class SDValue { + friend struct DenseMapInfo; + + SDNode *Node = nullptr; // The node defining the value we are using. + unsigned ResNo = 0; // Which return value of the node we are using. +.... + +ただしここで「入力」とは「DAGの矢印が入ってくる方」という意味で用いている。 +実際のコード実行はDAGの葉の方から根の方へ、つまり矢印の方向とは逆の方向に +行われることに注意が必要であるfootnote:[示した `SDValue` クラスの定義に付された +コメントで `return value to use from that node` という書かれ方がされているのは +このためであると推察される。]。 + +ここで `SDValue::getValue` の定義を見ると次のようになっている。 + +.... + SDValue getValue(unsigned R) const { + return SDValue(Node, R); + } +.... + +よって `getValue` が返す `SDValue` は、そのインスタンスが持つノードの +**兄弟ノード**であるfootnote:[`getValue` と言われるとなんとなく親ノードを指すような +気がしてしまうのは私だけだろうか。]。 + +さてここで再び `LowerCall` のコードを抜粋する。 + +.... +SDValue RetValue = + DAG.getCopyFromReg(Chain, DL, VA.getLocReg(), VA.getLocVT(), Glue); +Chain = RetValue.getValue(1); +Glue = RetValue.getValue(2); + +InVals.push_back(Chain.getValue(0)); +.... + +以上の調査から `Chain` は `RetValue` が指すノードの2番めの入力を表していることが +分かる。また `Chain.getValue(0)` は `Chain` が指すノードの0番目の入力を表し、 +それはとりもなおさず `RetValue` である。 +したがって `InVals.push_back(Chain.getValue(0));` を +`InVals.push_back(RetValue);` に書き換えても何ら問題はない。 + +== `HLT` 擬似命令を追加する + +RV16Kv2には明示的な `hlt` 命令が存在しないが、実用的なプログラムを書く上ではかかせない。 +RV16Kv2実行時には必ず動作するサイクル数が指定されるfootnote:[準同型暗号上で動く +プロセッサでは、レジスタやメモリその他全てのデータが暗号化されているため、 +外から終了条件を判定することができない。そのため、プロセッサを動作させるサイクル数 +を予め指定することで有限時間で終了することを保証する。もちろん、 +そのクロック数までに計算が終わらなければ、正しい結果は得られない。]ため、 +結局 `hlt` は無限ループと対応付ければ良いことが分かる。 + +このためにまず `HLT` 擬似命令をTableGenに追加し、 +これを `encodeInstruction` で `j -2` footnote:[`j` 命令は `pc` を `pc + imm + 2` に +更新する。従って自分自身にジャンプするためには `-2` を `imm` として指定する +必要がある。]に展開する。こうすることによって +アセンブリ上で `hlt` 擬似命令を使用することができる。 +なおこの手法はRISC Vの `PseudoCALL` と同じである<>。 + +ここで `PseudoHLT` に `isCodeGenOnly = 0` を指定した上で `AsmString` を +上書きしなければ、アセンブリ中の `hlt` がパーズされないことに注意が必要である。 + +== 実行可能ファイルを作る + +lld<>footnote:[LLVMが作成しているリンカ。GNU ldやgoldよりも動作速度が速い・ +コードベースが簡素化されている・新しいターゲットの追加が容易などの利点を有する<>。] +に変更を加えてRV16Kのオブジェクトファイルから実行可能ファイルを +生成するようにする。RISC Vのlld対応<>や +Cpu0のlld対応<>が参考になる。 +またLLVM Cauldron 2016において"How to add a new target to LLD"と +銘打たれた講演があり、YouTubeで公開されている +<><>。 + +=== ELFのセクション構造を考える + +image::img/elfmap.png[] + +ここでRV16KのアーキテクチャとELFのセクション構造を厳密に考える必要が出てきた。 +すなわちRV16KはROMとRAMの両方を持つアーキテクチャであり、プログラム部分( `.text` セクション)は +ROMに配置し、データ部分( `.data` や `.rodata` セクションfootnote:[`.rodata` セクションは +実行中変更されることがないためROMで良いように思われるが、そもそもRV16Kでは +ROMに配置されたデータにアクセスする方法がないため、実行中参照されるデータは +すべてRAMに置かなければいけない。])はRAMに配置しなければならない。 +すなわちRV16Kでは**考慮するべきアドレス空間が2つある**。しかしELFはこのような構造に +対応しておらずfootnote:[多分。ほんまか? 要調査;TODO]一つの32bit仮想 +アドレス空間footnote:[一般には、ヘッダの作り方により64bitアドレス空間でもあり得る。] +を持つのみである。 +そこでRV16KではELFにおける仮想アドレス空間のうち `0x00000000` から `0x0000FFFF` を +ROMの領域(すなわち `.text` の領域)としfootnote:[実はエントリポイントが `0` はELFファイルが +エントリポイントを保持しないことを意味する<>ため、この設計は +若干危うい。RV16Kの使われ方からは問題ないはずだが、要調査;TODO]、 +`0x00010000` から `0x0001FFFF` をRAMの領域とする。 +これにより後々ELFからROM/RAM用のバイナリを切り出すことが容易となるfootnote:[`objcopy` などが使える? + 要調査;TODO]。 + +問題は、このようなアドレス配置でリンカのrelocationが正しく動作するかである。 +まずPC相対のrelocationを考えよう。例えば `jal` 命令はリンク時に関数へのPC相対アドレスを +計算し当該箇所に埋め込む必要がある。指し示す関数と `jal` 命令は両者ともにROM上・ `.text` 上にある。 +ここで、仮想アドレスにおいて0番地から始まる `.text` のデータはそのままROMの0番地から +配置されるfootnote:[これは(暗黙の)仮定。]ため、 +`.text` 中でのPC相対アドレスとROM上でのPC相対アドレスは一致する。 +したがって問題ない。 + +次に絶対値によるrelocationを考える。例えばグローバル変数 `hoge` を `lw` 命令を用いて読み込む +場合、 `hoge` のアドレスはRAM上の絶対アドレスによって指定される。 +ここで `hoge` は `0x0001xxxx` のようなアドレスをリンカによって +指定されるfootnote:[`.data` セクションが `0x00010000` から始まるとしたため。]。 +これは正しいROM上の絶対アドレスではないfootnote:[そもそもこの値は32bitのため +`lw` がオペランドとする16bitのスペースに書き込むことができない。]。 +そこで下位16bitのみを有効なアドレスとみなし、この値をELFファイルに書き込む。 + +以上から、ROMとRAMの両方にvalidなアドレスをリンカの再配置機構を使用して +得られることが分かった。 + +=== LLDにRV16Kターゲットを追加する + +`lld/ELF/Arch/RV16K.cpp` ファイルを追加し `getRV16KTargetInfo` 関数を +追加する。これはこのファイル中で定義する `RV16K` クラスのインスタンスを返す。 +`RV16K` クラスは `TargetInfo` クラスを継承し `relocateOne` と `getRelExpr` 関数を +オーバーライドする。 +`relocateOne` は再配置処理の本体で、再配置のタイプ( `R_RV16K_16` など) +に応じて値の書き込みを行う。 +このときに書き込む値はLLD側から与えられる。その値の種類(絶対値かPC相対かなど)を +指定するために `RV16K::getRelExpr` を記述するfootnote:[多分。記述と処理からの +類推。要確認;TODO]。 + +RV16Kが使用する再配置の種類は既存のものばかりで、それほど困難ではない。 +逆に独自の再配置タイプを作成する場合はRISC Vの対応<>が +参考になりそうだ。 + +さてELFの再配置情報には `.rel` と `.rela` の2種類の形式があり、 +どちらを利用するかはABIによって決まっている。LLDでは `Driver.cpp` において +次のようにハードコーディングされている。 + +.... +// ELF defines two different ways to store relocation addends as shown below: +// +// Rel: Addends are stored to the location where relocations are applied. +// Rela: Addends are stored as part of relocation entry. +// +// In other words, Rela makes it easy to read addends at the price of extra +// 4 or 8 byte for each relocation entry. We don't know why ELF defined two +// different mechanisms in the first place, but this is how the spec is +// defined. +// +// You cannot choose which one, Rel or Rela, you want to use. Instead each +// ABI defines which one you need to use. The following expression expresses +// that. +Config->IsRela = M == EM_AARCH64 || M == EM_AMDGPU || M == EM_HEXAGON || + M == EM_PPC || M == EM_PPC64 || M == EM_RISCV || + M == EM_X86_64; +.... + +LLVMバックエンド のレベルでは `.rel` と `.rela` のいずれを使用するかは +`MCELFObjectTargetWriter` の第四引数に `false/true` のいずれを +渡すかに対応している。 + +.... +RV16KELFObjectWriter::RV16KELFObjectWriter(uint8_t OSABI) + : MCELFObjectTargetWriter(false, OSABI, ELF::EM_RV16K, + /*HasRelocationAddend*/ true) {} +.... + +RV16Kv2では `.rela` を使用するfootnote:[今決めた。]ので `true` と +なっている。したがってLLDの当該箇所も更新する必要がある。 + +その他RV16Kを認識させるために `Driver.cpp` と `Target.cpp` を修正する。 + +コンパイルのために `cmake -DLLVM_ENABLE_PROJECTS="lld;clang"` として +再コンパイルしようとするとうまくいかない。途中のproject追加は対応していないようだ? +footnote:[そんなわけはないと思うので要調査;TODO]とりあえずスクラッチから +lldを含めたビルドを行う。 + +具体的に実行可能ファイルを作成するために +エントリポイント `_start` をアセンブリで作成し `runtime.s` という +名前で保存する。中身は次のように `main` を呼出した後 `hlt` +するfootnote:[コマンドライン引数などを実装すればここで処理を行う必要がある。]。 + +.... +.global _start +_start: + jal main + hlt +.... + +これをコンパイルしてオブジェクトファイル `runtime.o` とし、 +`main` 関数を含む適当なオブジェクトファイル `foo.o` と次のようにリンクする。 + +.... +$ bin/ld.lld -Ttext=0 runtime.o foo.o -o foo.exe +.... + +LLDのAVRのターゲットのコメントに書かれているように、 +`-Ttext=0` を指定することで `.text` セクションをアドレス `0x0` に +配置することができる。これは `llvm-readobj` を利用して確かめられる。 + +.... +$ bin/llvm-readobj -a foo.exe | pbcopy + +File: foo.exe +Format: ELF32-rv16k +Arch: rv16k +AddressSize: 32bit +LoadName: +ElfHeader { + Ident { + Magic: (7F 45 4C 46) + Class: 32-bit (0x1) + DataEncoding: LittleEndian (0x1) + FileVersion: 1 + OS/ABI: SystemV (0x0) + ABIVersion: 0 + Unused: (00 00 00 00 00 00 00) + } + Type: Executable (0x2) + Machine: EM_RV16K (0xF6) + Version: 1 + Entry: 0x0 + ProgramHeaderOffset: 0x34 + SectionHeaderOffset: 0x20A8 + Flags [ (0x0) + ] + HeaderSize: 52 + ProgramHeaderEntrySize: 32 + ProgramHeaderCount: 2 + SectionHeaderEntrySize: 40 + SectionHeaderCount: 6 + StringTableSectionIndex: 4 +} +(以下略) +.... + +テストを追加する。 `lld/test/ELF/basic-rv16k.s` というファイルを作り記述する。 +Sparcのものなどが参考になる。LLDのテストをする際には `ninja check-lld` とすればよいが、 +こうすると全てのテストが走ってしまう。RV16Kに限って実行するには +次のようにすればよい。 + +.... +$ bin/llvm-lit -as --filter 'rv16k' tools/lld/test +.... + +しかしコマンドを手で入力した結果と `llvm-lit` での実行結果が異なる。 +`tee` で内容を取得しテストの正解とした。 +原因不明。 `.comment` のサイズが異なるようだ? TODO + +=== `clang` で実行可能ファイルを出力する + +必要な場合に `clang` が適切に `lld` を呼ぶようにする。 +`RV16KToolChain` の `protected` メンバ関数として `buildLinker` を +オーバーライドし `tools::RV16K::Linker` クラスのインスタンスを +返却する。次いで `tools::RV16K::Linker` を `GnuTool` を継承するクラスとして +定義し、 `GnuTool` のコンストラクタに `ld.lld` を渡すfootnote:[このようにして +リンカを指定するのは本来でないかもしれない。要調査;TODO]。 +`tools::RV16K::Linker::ConstructJob` ではリンカへのオプションなどを指定できる。 +`.text` セクションをアドレス `0x00000000` 番地に割り当てるように `-Ttext=0` を、 +`.data` セクションをアドレス `0x00010000` 番地に割り当てるように `-Tdata=0` を、 +またページサイズ単位のセクションアラインメントを無効化するために `--omagic` +footnote:[デフォルトではldやlldはセクションをページサイズでアラインメントする。 +これは静的ライブラリをリンクするために必要なようだ<>が、 +一方で生成されるバイナリサイズを +大きくしてしまう。そこで `--nmagic` を指定することでこれを無効化できる。しかしこのオプションは +ldには存在するが、我々がベースにしているLLVM 8.0.0のlldには存在しない。 +そこで代わりに `--omagic` を指定する。 +これは `.text` に書き込み可フラグを立てるためセキュリティ上問題があるようだが、 +今回の使用方法では問題にならない<>。なおLLVM 8.0.0の +リリース後に `--nmagic` が追加されているようだ<>。要調査;TODO]を指定する +footnote:[このようにLLDをコマンドとして使用するのは現在のLLDを設計した +Rui Ueyamaさんの決定らしい<>。]。 + +以上の設定により次のように実行ファイルが生成されるようになったfootnote:[ここで +`runtime.s` と `foo.c` の順番を入れ替えるとentry pointの位置が変化する。 +ファイル名の順序が `.text` 中での順序を決定していると推察されるが、 +根拠がない。要調査;TODO]。 + +.... +$ cat runtime.s +.global _start +_start: + jal main + hlt + +$ cat foo.c +int main() +{ + return 42; +} + +$ bin/clang -target rv16k runtime.s foo.c -o foo.exe + +$ bin/llvm-readelf -a foo.exe +ELF Header: + Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 + Class: ELF32 + Data: 2's complement, little endian + Version: 1 (current) + OS/ABI: UNIX - System V + ABI Version: 0x0 + Type: EXEC (Executable file) + Machine: RV16K + Version: 0x1 + Entry point address: 0x0 + Start of program headers: 52 (bytes into file) + Start of section headers: 8360 (bytes into file) + Flags: 0x0 + Size of this header: 52 (bytes) + Size of program headers: 32 (bytes) + Number of program headers: 2 + Size of section headers: 40 (bytes) + Number of section headers: 6 + Section header string table index: 4 +There are 6 section headers, starting at offset 0x20a8: + +Section Headers: + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + [ 0] NULL 00000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 00000000 001000 000026 00 AX 0 0 4 + [ 2] .comment PROGBITS 00000000 002000 000028 01 MS 0 0 1 + [ 3] .symtab SYMTAB 00000000 002028 000040 10 5 2 4 + [ 4] .shstrtab STRTAB 00000000 002068 00002a 00 0 0 1 + [ 5] .strtab STRTAB 00000000 002092 000013 00 0 0 1 +Key to Flags: + W (write), A (alloc), X (execute), M (merge), S (strings), l (large) + I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) + O (extra OS processing required) o (OS specific), p (processor specific) + +There are no relocations in this file. + +Symbol table '.symtab' contains 4 entries: + Num: Value Size Type Bind Vis Ndx Name + 0: 00000000 0 NOTYPE LOCAL DEFAULT UND + 1: 00000000 0 FILE LOCAL DEFAULT ABS foo.c + 2: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start + 3: 00000008 30 FUNC GLOBAL DEFAULT 1 main +UnwindInfo not implemented. + +Elf file type is EXEC (Executable file) +Entry point 0x0 +There are 2 program headers, starting at offset 52 + +Program Headers: + Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align + LOAD 0x001000 0x00000000 0x00000000 0x01000 0x01000 R E 0x1000 + GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x0 + + Section to Segment mapping: + Segment Sections... + 00 .text + 01 +Version symbols { +} +SHT_GNU_verdef { +} +SHT_GNU_verneed { +} +There are no section groups in this file. + +$ ~/workspace/rv16k-sim/main foo.exe 20 +0000 0073 +0002 0600 +0004 0052 +0006 FEFF +0008 C1F2 +000A 2192 +000C 0200 +000E 12E0 +0010 42F2 +0012 0878 +0014 0000 +0016 8292 +0018 FCFF +001A 0878 +001C 2A00 +001E 12B2 +0020 0200 +0022 41F2 +0024 0040 + +Inst:JAL PC <= 0x0002 Reg x0 <= 0x0004 PC <= 0x0008 FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x1 <= 0x01FA PC <= 0x000A FLAGS(SZCV) <= 0000 +Inst:SW PC <= 0x000C DataRam[0x01FC] <= 0x0000 DataRam[0x01FD] <= 0x0000 PC <= 0x000E FLAGS(SZCV) <= 0010 +Inst:MOV Reg x2 <= 0x01FA PC <= 0x0010 FLAGS(SZCV) <= 0000 +Inst:ADDI Reg x2 <= 0x01FE PC <= 0x0012 FLAGS(SZCV) <= 0010 +Inst:LI PC <= 0x0014 Reg x8 <= 0x0000 PC <= 0x0016 FLAGS(SZCV) <= 0100 +Inst:SW PC <= 0x0018 DataRam[0x01FA] <= 0x0000 DataRam[0x01FB] <= 0x0000 PC <= 0x001A FLAGS(SZCV) <= 0000 +Inst:LI PC <= 0x001C Reg x8 <= 0x002A PC <= 0x001E FLAGS(SZCV) <= 0000 +PC <= 0x0020 Inst:LW Reg x2 <= 0x0000 PC <= 0x0022 FLAGS(SZCV) <= 0010 +Inst:ADDI Reg x1 <= 0x01FE PC <= 0x0024 FLAGS(SZCV) <= 0010 +Inst:JR PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +Inst:J PC <= 0x0006 PC <= 0x0004 FLAGS(SZCV) <= 0000 +x0=4 x1=510 x2=0 x3=0 x4=0 x5=0 x6=0 x7=0 x8=42 x9=0 x10=0 x11=0 x12=0 x13=0 x14=0 x15=0 +.... + +== `lw/sw` の代わりに `lwsp/swsp` 命令を使う + +現在 `lw/sw` を用いて行っているスタックの読み書きを、適用できる場合に限って `lwsp/swsp` に変更する。 +結局スタックにアクセスする際には `eliminateFrameIndex` が呼ばれるため、 +ここでi) `lw/sw` を使用しており ii) オペランドが `sp` で、かつ iii) +即値が9bitに収まるのであれば、これを `lwsp/swsp` に各々変換する。 + +`eliminateFrameIndex` で命令の変更を行うバックエンドは少ないが、footnote:[驚いたことにx86では行われていない。 +x86ではアセンブリレベルでは即値に応じて命令を変更する必要がないからかもしれない。要調査;TODO]。 +例えばSparcでは行われている。それによれば `MI.setDesc` を使用して +命令の変更を行えば良いようだ。 + +似たような変更はRISC VのRV32C対応でも行われている。ただしこちらの手法はより大胆で、 +TableGen本体に手をいれることで、TableGenファイルにおいて +RV32Iその他の命令とRV32Cとの対応を取れるようにしている。 + +結局RV16Kでは次のようになった。 + +.... + // Use lwsp whenever possible. + if (MI.getOpcode() == RV16K::LW && FrameReg == RV16K::X1 && + isShiftedUInt<8, 1>(Offset)) { + const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo(); + MI.setDesc(TII.get(RV16K::LWSP)); + } + + // Use swsp whenever possible. + if (MI.getOpcode() == RV16K::SW && FrameReg == RV16K::X1 && + isShiftedUInt<8, 1>(Offset)) { + const TargetInstrInfo &TII = *MF.getSubtarget().getInstrInfo(); + MI.setDesc(TII.get(RV16K::SWSP)); + } +.... + +ちなみにここで `isShiftedUInt<9, 1>` などとしてもエラーにはならない。 +命令選択はすでに終わっておりfootnote:[終わっていなければ `MachineInstr` が +引数として渡されることはない。またframe index ("abstract stack location references") +の削除は関数プロローグ・エピローグの挿入と同時に行われ +るようだ<>。]、 +オペランドの幅を指定するのは命令選択に使用されるTableGenファイルを除いて +他にないからである。従ってそのまま `lwsp a0, 512(sp)` などと平気で +出力される。ではオブジェクトファイルはどうかというと、 +こちらでもエラーは出力されず、逆アセンブルすると `lwsp a0, 0(sp)` と +なっていることが分かる。これはTableGenファイル中の `Inst` の指定が +bitの対応のみになっていることに起因するのだろう。 +これに対して有効な手立てがあるかはちょっと分からない。TODO +footnote:[例えばRISC Vのバックエンドに対して同様の細工をして試してみるなど。] +仕方がないので、とりあえずテストで正しい挙動となることを保証することに +するfootnote:[本当はコンパイル時にシュッとしてほしい。]。 + +ところがこれでは関数の引数をスタックに積む際には適用されないことが分かった。 +この場合は `DAG.getStore` を利用してSelectionDAGレベルでspに対するstoreが +発行され、frame indexはそのオペランド中に含まれないためと推察される(TODO)。 +そこで次のようにパターンを追加した。 + +.... +def : Pat<(store GPR:$rs2, SP:$rs1), (SWSP GPR:$rs2, SP:$rs1, 0)>; +def : Pat<(store GPR:$rs2, (add SP:$rs1, uimm9_lsb0:$imm)), + (SWSP GPR:$rs2, SP:$rs1, uimm9_lsb0:$imm)>; +.... + +これを普通の `store` の上に置くと失敗するテストが発生した( `blockaddress.ll` )。 +詳細不明。TODO `-debug` を付けて確認すると `SP` のレジスタが足りなくなっているような +雰囲気? `sp` をスタックにスピルしてロードして……みたいなことをやっている気がする。 +どうも `a0` を直接 `lw` で読む代わりに、 +`a0` を `sp` に入れて `swsp` を実行するようなアセンブリが吐かれている。恐ろしや。 +しかしこれは命令数的に不利だと思うのだが……。 + +しかしこの手法では、新たにload/storeを挟むたびにlwsp/swspのことを +考える必要があり、少々面倒である。lwsp/swspは条件が揃えば必ずlw/swに +書き換わるべき命令なのだから、もっと後ろ、すなわち +frame index eliminationが終わったあとにpeephole最適化footnote:[というよりただの +置換だが。]にて処理すべきである。このような場合に対応するためにLLVMバックエンドには +"Late Machine Code Optimizations"がある<>が、 +公式ドキュメントには"To Be Written"となっていて情報がない。 + +おそらくこれは `MachineFunctionPass` を継承したような最適化に対応すると +推察される(TODO)。これは `Target/RV16K` 内にて定義し、 +`RV16KPassConfig` にて `addPreEmitPass2` などをオーバライドしたうえで +`addPass` を呼出し利用する<>。 + +.... + /// Methods with trivial inline returns are convenient points in the common + /// codegen pass pipeline where targets may insert passes. Methods with + /// out-of-line standard implementations are major CodeGen stages called by + /// addMachinePasses. Some targets may override major stages when inserting + /// passes is insufficient, but maintaining overriden stages is more work. + /// + + /// addPreISelPasses - This method should add any "last minute" LLVM->LLVM + /// passes (which are run just before instruction selector). + virtual bool addPreISel() { + return true; + } + + /// addMachineSSAOptimization - Add standard passes that optimize machine + /// instructions in SSA form. + virtual void addMachineSSAOptimization(); + + /// Add passes that optimize instruction level parallelism for out-of-order + /// targets. These passes are run while the machine code is still in SSA + /// form, so they can use MachineTraceMetrics to control their heuristics. + /// + /// All passes added here should preserve the MachineDominatorTree, + /// MachineLoopInfo, and MachineTraceMetrics analyses. + virtual bool addILPOpts() { + return false; + } + + /// This method may be implemented by targets that want to run passes + /// immediately before register allocation. + virtual void addPreRegAlloc() { } + + /// createTargetRegisterAllocator - Create the register allocator pass for + /// this target at the current optimization level. + virtual FunctionPass *createTargetRegisterAllocator(bool Optimized); + + /// addFastRegAlloc - Add the minimum set of target-independent passes that + /// are required for fast register allocation. + virtual void addFastRegAlloc(FunctionPass *RegAllocPass); + + /// addOptimizedRegAlloc - Add passes related to register allocation. + /// LLVMTargetMachine provides standard regalloc passes for most targets. + virtual void addOptimizedRegAlloc(FunctionPass *RegAllocPass); + + /// addPreRewrite - Add passes to the optimized register allocation pipeline + /// after register allocation is complete, but before virtual registers are + /// rewritten to physical registers. + /// + /// These passes must preserve VirtRegMap and LiveIntervals, and when running + /// after RABasic or RAGreedy, they should take advantage of LiveRegMatrix. + /// When these passes run, VirtRegMap contains legal physreg assignments for + /// all virtual registers. + virtual bool addPreRewrite() { + return false; + } + + /// This method may be implemented by targets that want to run passes after + /// register allocation pass pipeline but before prolog-epilog insertion. + virtual void addPostRegAlloc() { } + + /// Add passes that optimize machine instructions after register allocation. + virtual void addMachineLateOptimization(); + + /// This method may be implemented by targets that want to run passes after + /// prolog-epilog insertion and before the second instruction scheduling pass. + virtual void addPreSched2() { } + + /// addGCPasses - Add late codegen passes that analyze code for garbage + /// collection. This should return true if GC info should be printed after + /// these passes. + virtual bool addGCPasses(); + + /// Add standard basic block placement passes. + virtual void addBlockPlacement(); + + /// This pass may be implemented by targets that want to run passes + /// immediately before machine code is emitted. + virtual void addPreEmitPass() { } + + /// Targets may add passes immediately before machine code is emitted in this + /// callback. This is called even later than `addPreEmitPass`. + // FIXME: Rename `addPreEmitPass` to something more sensible given its actual + // position and remove the `2` suffix here as this callback is what + // `addPreEmitPass` *should* be but in reality isn't. + virtual void addPreEmitPass2() {} +.... + +`RV16KUseLWSPSWSP` という名前で実際にパスを作る。 `RISCVMergeBaseOffset` が +参考になる。 `MachineFunction` の中に `MachineBasicBlock` があり、 +その中に `MachineInstr` がある。iteratorが実装されているためrange-based forが +使える。 `INITIALIZE_PASS` の第二引数はコマンドラインでの引数。第三引数は名前。 + +その後 `RV16KPassConfig::addPreEmitPass` から `addPass` を呼ぶようにした。 +正直どれから呼べばどの順序で呼ばれるのかよくわからないが、 +`addPreEmitPass` と `addPreEmitPass2` が `MCInst` にlowerされる直前に +相次いで呼ばれるという認識。要調査;TODO + +image::img/gemba_neko.jpg[] + +テストを追加する。 + +== 続・ `FrameIndex` をlowerする + +バグを泳がせた結果、`alloca` で確保したスタック領域へのポインタを +関数の引数に渡すようなLLVM IRを入力すると `RV16KDAGToDAGISel::Select` に +`ISD::FrameIndex` が来ることが分かった。なるほど然り。 + +.... +declare i16 @bar(i16*) + +define i16 @foo() { + %1 = alloca i16 + %2 = call i16 @bar(i16* %1) + ret i16 %2 +} +.... + +これに対応する。オペランドにframe indexが現れた際は `TargetFrameIndex` に +変換しつつそのままオペランドとして扱った。しかし今回はframe indexが +単体で与えられているため、そのままでは `MachineDAG` のノードとして +扱えないfootnote:[`TargetFrameIndex` は命令のオペランドにはなるが、命令そのものには +ならないため、という推論。TODO. ちょうど即値のmaterializeと似ているかもしれない]。 +そこで適当な命令とセットにして `Select` の +結果とする必要がある。ここでframe indexの実体は「レジスタ+即値」である +ことに注意する。すなわち `eliminateFrameIndex` で扱うことができるような +形式でなければならない。RISC-Vでは `addi` がこれに該当するが、 +RV16Kではビット幅が狭く現実的ではない。そこで、とりあえず `Select` では +`addi` を割り振り、後から `eliminateFrameIndex` で調整する手法を採用する。 +そこで<>を参考にし +次のようにコードを書いた。 + +.... + if (Node->getOpcode() == ISD::FrameIndex) { + SDLoc DL(Node); + SDValue Imm = CurDAG->getTargetConstant(0, DL, MVT::i16); + int FI = dyn_cast(Node)->getIndex(); + EVT VT = Node->getValueType(0); + SDValue TFI = CurDAG->getTargetFrameIndex(FI, VT); + ReplaceNode(Node, + CurDAG->getMachineNode(RV16K::ADDI, DL, VT, TFI, Imm)); + return; + } +.... + +するとエラーが出た。 + +.... +$ ~/workspace/llvm-project-build/bin/llc -mtriple=rv16k -verify-machineinstrs < 0003.ll + .text + .file "" + +# After Instruction Selection +# Machine code for function foo: IsSSA, TracksLiveness +Frame Objects: + fi#0: size=2, align=2, at location [SP] + +bb.0 (%ir-block.0): + ADJCALLSTACKDOWN 0, 0, implicit-def dead $x1, implicit $x1 + %0:gpr = ADDI %stack.0, 0, implicit-def dead $flags + $x8 = COPY %0:gpr + JAL @bar, , implicit-def dead $x0, implicit $x8, implicit-def $x1, implicit-def $x8 + ADJCALLSTACKUP 0, 0, implicit-def dead $x1, implicit $x1 + %1:gpr = COPY $x8 + $x8 = COPY %1:gpr + PseudoRET implicit $x8 + +# End machine code for function foo. + +*** Bad machine code: Tied use must be a register *** +- function: foo +- basic block: %bb.0 (0x5647e963da08) +- instruction: %0:gpr = ADDI %stack.0, 0, implicit-def dead $flags +- operand 1: %stack.0 +LLVM ERROR: Found 1 machine code errors. +.... + +どうやら `ADDI` の2オペランド制約( `Constraints = "$rd = $rd_w"` ) +を満たすことが出来ないようだfootnote:[`ADDI` の代わりに +`Constraints` のみを外した `ADDIhoge` を作成して試したところ +エラーは発生しなかった。またAVRのバックエンドのコメントには"This is actually +"load effective address" of the stack slot instruction. +We have only two-address instructions, thus we need to expand it into move + add." +とあるので、おそらくこの理解で正しい。]。 +そこで同様の問題を抱えるAVRのバックエンドを参考にして `FRMIDX` という擬似命令を作成し、 +これを `addi` の代わりに取り回すことにする。 + +まず次のように `FRMIDX` を定義し + +.... +let hasSideEffects = 0, mayLoad = 0, mayStore = 0, Defs = [FLAGS] in +def FRMIDX : Pseudo<(outs GPR:$rd), (ins GPR:$rs, simm16:$imm), []>; +.... + +これを `Select` で作成する。 + +.... + if (Node->getOpcode() == ISD::FrameIndex) { + // Convert the frameindex into a temp instruction that will hold the + // effective address of the final stack slot. + int FI = cast(Node)->getIndex(); + SDValue TFI = CurDAG->getTargetFrameIndex(FI, MVT::i16); + + CurDAG->SelectNodeTo(Node, RV16K::FRMIDX, MVT::i16, TFI, + CurDAG->getTargetConstant(0, SDLoc(Node), MVT::i16)); + return; + } +.... + +なお `Pat` で `FRMIDX` を作成できないかとやってみたが上手く行かなかった。 +そもそもどのようなパターンにマッチさせればいいのか分からなかった。TODO + +ちなみにこの段階でアセンブリを出力させると空文字列で出力される。 +それはそうなのだが、擬似命令でもエラーが出ず出力されるのは意外footnote:[エラーにして +ほしい。方法はないのか。TODO]。 + +最後に `eliminateFrameIndex` で `FRMIDX` をトラップし、適切に処理する。 +4bit幅に収まる場合には `addi` が使えると思ったが、 +実際には `sp` を書き換えるのはまずいと判断し、 +どのような場合でも `li` と `mov` で取り扱うことにした。要検討。TODO + +AVRでは二命令目の最後のオペランドに `RegState::Dead` が適用されていた。 +この `RegState` の意味は `MachineOperand` クラスのコメントに詳しい。 + +TableGenを使用した通常のパスでこれらの属性がどのように付与されるかは要調査;TODO + +.... + /// TiedTo - Non-zero when this register operand is tied to another register + /// operand. The encoding of this field is described in the block comment + /// before MachineInstr::tieOperands(). + unsigned TiedTo : 4; + + /// IsDef - True if this is a def, false if this is a use of the register. + /// This is only valid on register operands. + /// + unsigned IsDef : 1; + + /// IsImp - True if this is an implicit def or use, false if it is explicit. + /// This is only valid on register opderands. + /// + unsigned IsImp : 1; + + /// IsDeadOrKill + /// For uses: IsKill - True if this instruction is the last use of the + /// register on this path through the function. + /// For defs: IsDead - True if this register is never used by a subsequent + /// instruction. + /// This is only valid on register operands. + unsigned IsDeadOrKill : 1; + + /// See isRenamable(). + unsigned IsRenamable : 1; + + /// IsUndef - True if this register operand reads an "undef" value, i.e. the + /// read value doesn't matter. This flag can be set on both use and def + /// operands. On a sub-register def operand, it refers to the part of the + /// register that isn't written. On a full-register def operand, it is a + /// noop. See readsReg(). + /// + /// This is only valid on registers. + /// + /// Note that an instruction may have multiple operands referring to + /// the same register. In that case, the instruction may depend on those + /// operands reading the same dont-care value. For example: + /// + /// %1 = XOR undef %2, undef %2 + /// + /// Any register can be used for %2, and its value doesn't matter, but + /// the two operands must be the same register. + /// + unsigned IsUndef : 1; + + /// IsInternalRead - True if this operand reads a value that was defined + /// inside the same instruction or bundle. This flag can be set on both use + /// and def operands. On a sub-register def operand, it refers to the part + /// of the register that isn't written. On a full-register def operand, it + /// is a noop. + /// + /// When this flag is set, the instruction bundle must contain at least one + /// other def of the register. If multiple instructions in the bundle define + /// the register, the meaning is target-defined. + unsigned IsInternalRead : 1; + + /// IsEarlyClobber - True if this MO_Register 'def' operand is written to + /// by the MachineInstr before all input registers are read. This is used to + /// model the GCC inline asm '&' constraint modifier. + unsigned IsEarlyClobber : 1; + + /// IsDebug - True if this MO_Register 'use' operand is in a debug pseudo, + /// not a real instruction. Such uses should be ignored during codegen. + unsigned IsDebug : 1; +.... + +`Kill` はレジスタのuseのとき、 `Dead` はdefのとき用いられる。 +`Kill/Dead` のフラグがたったレジスタは、その後使用されないことを意味する。 +今回の場合 `FLAGS` がこの後で使用されることはないfootnote:[直観的にはそうだが +形式的に証明できるかというと怪しい。TODO]ので `FLAGS` に `Dead` を適用する必要がある。 + +なおbasic blockを前からではなく後ろから走査することで `Kill` フラグの有無に関わらず +正しいレジスタ生存期間を算出できるfootnote:[これは `llvm::RegScavenger::scavengeRegister` の +コメントからも示唆される。]。したがって `Kill` フラグをつけるか否かは任意であり、 +誤って `Kill` フラグが立つことによるバグの混入 +footnote:[passが重なると起こりやすいらしい<>。] +を避けるためにも、新しく書くコードでは `Kill` +を使用すべきでないらしい<>footnote:[しかしRISC-Vでは +使用しているし良くわからない。TODO]。 + +.... +$ cat 0003.ll +declare i16 @bar(i16*) + +define i16 @foo() { + %1 = alloca i16 + %2 = call i16 @bar(i16* %1) + ret i16 %2 +} + +$ ~/workspace/llvm-project-build/bin/llc -mtriple=rv16k -verify-machineinstrs -print-after=prologepilog < 0003.ll > /dev/null +# *** IR Dump After Prologue/Epilogue Insertion & Frame Finalization ***: +# Machine code for function foo: NoPHIs, TracksLiveness, NoVRegs +Frame Objects: + fi#0: size=2, align=2, at location [SP-4] + fi#1: size=2, align=2, at location [SP-2] + +bb.0 (%ir-block.0): + $x1 = frame-setup ADDI $x1(tied-def 0), -4, implicit-def $flags + SW killed $x0, $x1, 2, implicit-def $flags + $x8 = LI 0, implicit-def dead $flags + $x8 = ADD killed $x8(tied-def 0), $x1, implicit-def dead $flags + JAL @bar, , implicit-def dead $x0, implicit $x8, implicit-def $x1, implicit-def $x8 + $x0 = LW $x1, 2, implicit-def $flags + $x1 = frame-destroy ADDI $x1(tied-def 0), 4, implicit-def $flags + PseudoRET implicit $x8 + +# End machine code for function foo. +.... + +`ADD` に `dead` がついていることが分かる。 +なお `implicit` というのはアセンブリ中( `MCInst` 中)に表現されないということである。 +実際 `flags` は各命令において勝手に書き換えられ、勝手に使用されるため、 +明示的に表現することはない。また `tied` というのは2オペランド形式などにおいて +読み込むレジスタと書き込むレジスタが +一致することを意味している<>。 +上のコードにはないが `undef` はその入力レジスタの値自体は重要でないことfootnote:[例えば +`eax` に `0` をいれるために `xor eax, eax` を実行するとき、 +`eax` に(もともと)何が入っているかは重要ではない。]を意味する。 +これらの `RegState` はレジスタの生存期間を算出するために用いられる。 + +`Offset` が `0` のときは `mov` のみでよいので条件分岐しておく。 + +ところでRISC-VやAVRがframeindexを取り回す方法と、x86やSparcのそれは +明らかに異なる。RISC-Vではアドレスモードらしいモードが無いのに対し、 +Sparcでは `ADDRri` などが存在する。このあたりで `ISD::FrameIndex` を +直接 `Select` で対応する必要があるか否かが決まっているようだが詳細不明。TODO + +== `Bcc` を導入する + +後に行う分岐解析では「ある分岐条件を逆向きにする」という処理を行う必要があるが、 +`CMP` と `Jcc` に分けていると、このとき `Jcc` の情報しか与えられず `CMP` への +変更を加えることが出来ない。そこで `Jcc` と `CMP` をまとめて `Bcc` とした +擬似命令を作成してレジスタ割り付けまでを行いfootnote:[分岐解析はレジスタ割り付け +までに起こっているはずだと仮定している。TODO]、その後これを `CMP` と `Jcc` に +伸長することにする。 +この方式ではRISC-Vの方法<>をそのまま適用することができる。 + +しかし単発の `setcc` を上手く取り扱うことが出来ない。 `SelectCCrr` に変換すれば +良いと思い次のように書いたが、パターンマッチに成功しない。 + +.... +def : Pat<(seteq GPR:$lhs, GPR:$rhs), + (SelectCCrr GPR:$lhs, GPR:$rhs, SETEQ, (LI 0), (LI 1))>; +.... + +調査したが原因不明。何もわからない。TODO + +ところで + +.... +def : Pat<(SelectCC GPR:$lhs, GPR:$rhs, (i16 imm:$imm), GPR:$src, GPR:$src2), + (SelectCCrr GPR:$lhs, GPR:$rhs, i16imm:$imm, GPR:$src, GPR:$src2)>; +.... + +とは書けないらしい。 `imm` の部分が `li` からレジスタ渡しに +変更されてしまう。 `Pseudo` の引数にパターンとして渡さないといけないようだ。 +どのような違いがあるのか全然分からない。TODO + +仕方が無いので `BR_CC` で実装することにする。 +`LowerBR_CC` で直接 `Bcc` を返すことができないかと試したが無理のようだ。 +`DAG.getNode` が引数に受け取るのはあくまで `SDValue` のための引数であって +`MachineSDValue` ではない。ExpandやCustomの処理が終わった後に +TableGenファイルをもとに命令選択が行われるのだから、当然といえば当然である。 +そこにフックしたければ `ComplexPattern` を使えということなのだろう。 + +結局 i) `BR_CC` を `LowerOperation` でトラップして `RV16KISD::BR_CC` を作成し ii) +TableGenファイルで `RV16KBrCC` を捕捉して擬似命令 `Bcc/BccI` とし iii) +`RV16KInstrInfo::expandPostRAPseudo` でこれを `CMP` と `Jcc` に +変換することにしたfootnote:[あちらこちらさまよって一日を費やしたわりには +大したことの無い方法で収まりが付いた。]。 + +== 分岐解析に対応する + +branch analysisに対応することで、分岐に関する種々の最適化を +行うことができる。参考にするRISC-Vのパッチは<>。 + +具体的には `RV16KInstrInfo::analyzeBranch` をオーバーライドし実装する。 +`TargetInstrInfo::analyzeBranch` には次のようにコメントがある。 + +.... + /// Analyze the branching code at the end of MBB, returning + /// true if it cannot be understood (e.g. it's a switch dispatch or isn't + /// implemented for a target). Upon success, this returns false and returns + /// with the following information in various cases: + /// + /// 1. If this block ends with no branches (it just falls through to its succ) + /// just return false, leaving TBB/FBB null. + /// 2. If this block ends with only an unconditional branch, it sets TBB to be + /// the destination block. + /// 3. If this block ends with a conditional branch and it falls through to a + /// successor block, it sets TBB to be the branch destination block and a + /// list of operands that evaluate the condition. These operands can be + /// passed to other TargetInstrInfo methods to create new branches. + /// 4. If this block ends with a conditional branch followed by an + /// unconditional branch, it returns the 'true' destination in TBB, the + /// 'false' destination in FBB, and a list of operands that evaluate the + /// condition. These operands can be passed to other TargetInstrInfo + /// methods to create new branches. + /// + /// Note that removeBranch and insertBranch must be implemented to support + /// cases where this method returns success. + /// + /// If AllowModify is true, then this routine is allowed to modify the basic + /// block (e.g. delete instructions after the unconditional branch). + /// + /// The CFG information in MBB.Predecessors and MBB.Successors must be valid + /// before calling this function. + virtual bool analyzeBranch(MachineBasicBlock &MBB, MachineBasicBlock *&TBB, + MachineBasicBlock *&FBB, + SmallVectorImpl &Cond, + bool AllowModify = false) const { + return true; + } +.... + +`MachineBasicBlock` の中身を調査し、分岐でbasic blockが終わっている場合には +そのtrue/falseの際の分岐先を `TBB` と `FBB` に入れろということのようだ。 +なお `MachineBasicBlock *&` は「ポインタへの参照」を表す<>。 +機能を考えれば妥当だ。 + +TableGenファイルで指定した `isBranch` などはここで使用されるようだ。 +例えば無条件ジャンプか否かは `MachineInstr::isUnconditionalBranch` で +次のように判断される。 + +.... + /// Return true if this is a branch which always + /// transfers control flow to some other block. The + /// TargetInstrInfo::AnalyzeBranch method can be used to get more information + /// about this branch. + bool isUnconditionalBranch(QueryType Type = AnyInBundle) const { + return isBranch(Type) & isBarrier(Type) & !isIndirectBranch(Type); + } +.... + +このあたりを見ながらTableGenファイルを書けば良いようだ。 + +`analyzeBranch` ではおおよそ次のことをやっている。 + +. 無条件分岐か間接分岐がある場合は、その後ろの命令は実行されないので消去するfootnote:[ここから `jalr` を間接分岐と呼ばないことが分かる。]。 +. basic blockを抜け出る命令が1つしかなく、それがbasic block最後の無条件分岐である場合は、その分岐先を `TBB` にセットして `false` を返す。 +. basic blockを抜け出る命令が1つしかなく、それがbasic block最後の条件分岐命令である場合は、その分岐先を `TBB` にセットして `false` を返す。 +. basic blockを抜け出る命令が2つあり、それがbasic block最後の「条件分岐命令+無条件分岐命令」ならば、前者の分岐先を `TBB` に、後者の分岐先を `FBB` にセットして `false` を返す。 +. 上記以外は `true` を返し、解析が不可能であることを示す。 + +LanaiとSparcのものも見たが、RISC-Vと同様のことをやっている気がするfootnote:[同じことをやるならLLVM core側で対応してくれればいいのに。]。 +なお `MachineBasicBlock` の最後には `isTerminator` がついた命令が来ることが +制約として課されているが、これは複数個あっても良い<>。 +これによって条件分岐( `true` ならこちらに飛び、 `false` なら別のところに飛ぶ) +が自然に表現できる。 + +引数の `Cond` に設定したオペランドは `insertBranch` や `reverseBranchCondition` +に付加情報として渡される。 + +`insertBranch` は引数で指示された分岐命令を挿入する。 +そのときに `Cond` に指定した値が同時に渡される。RV16Kでは「どの種類の分岐なのか」 +が分かればよいのでopcodeを `Cond` に `push_back` しておく。 + +`removeBranch` では `MBB` 末尾にある分岐命令を削除する。 +「条件分岐命令+無条件分岐命令」の場合はその両方を削除する。 + +.... + /// Remove the branching code at the end of the specific MBB. + /// This is only invoked in cases where AnalyzeBranch returns success. It + /// returns the number of instructions that were removed. + /// If \p BytesRemoved is non-null, report the change in code size from the + /// removed instructions. + virtual unsigned removeBranch(MachineBasicBlock &MBB, + int *BytesRemoved = nullptr) const { + llvm_unreachable("Target didn't implement TargetInstrInfo::removeBranch!"); + } + + /// Insert branch code into the end of the specified MachineBasicBlock. The + /// operands to this method are the same as those returned by AnalyzeBranch. + /// This is only invoked in cases where AnalyzeBranch returns success. It + /// returns the number of instructions inserted. If \p BytesAdded is non-null, + /// report the change in code size from the added instructions. + /// + /// It is also invoked by tail merging to add unconditional branches in + /// cases where AnalyzeBranch doesn't apply because there was no original + /// branch to analyze. At least this much must be implemented, else tail + /// merging needs to be disabled. + /// + /// The CFG information in MBB.Predecessors and MBB.Successors must be valid + /// before calling this function. + virtual unsigned insertBranch(MachineBasicBlock &MBB, MachineBasicBlock *TBB, + MachineBasicBlock *FBB, + ArrayRef Cond, + const DebugLoc &DL, + int *BytesAdded = nullptr) const { + llvm_unreachable("Target didn't implement TargetInstrInfo::insertBranch!"); + } +.... + +`Bcc/BccI` に対して `reverseBranchCondition` が作用するようにするfootnote:[以前の +設計では `Bcc` を導入していなかったため +`reverseBranchCondition` の実装で問題が発生した。RV16Kには条件分岐の逆の命令は +存在しないのである。これは本質的に `CMP` と `Jcc` を分けていることに起因する。]。 + +実はregister allocationのあとにもbranch analysisが実行されるため +上の設計ではエラーが出るfootnote:[悲しい。]。 +仕方がないのでRISC-Vのatomic命令を見習い<> +コードが出力される直前に +変換することにするfootnote:[peephole最適化などを行うことができないため、 +最適化の観点からは好ましくない。TODO]。 + +と思ったらこれでもだめだった。仕方がないので `MCInst` に変換する直前で +行うことにする。複数命令に展開されるため `AsmPrinter` の方に +変更を加える。 + +ここまでpeephole最適化footnote:[LLVMには実際 `CMP` と `SUB` が重複した際に +これを最適化するようなpeephole最適化の `MachineFunctionPass` である +`lib/CodeGen/PeepholeOptimizer.cpp` が存在する。] +のことを気にしてきたが、よく考えると +`CMP+Jcc` のpeephole最適化とbranch analysisは不可分なのではないか。 +結局peephole最適化と `Bcc` のいずれか一方のみしか対応できないのではないか +と思われるfootnote:[というかイソップ寓話的にそう思い込んだともいう。]。TODO + +しかしこれもつらい。基本的に `AsmPrinter` は一命令を複数の命令に展開することを +想定していないfootnote:[というかそもそも命令を置き換えるコードを手で書くこと +自体が想定されていないような気がする。]。せっかくPassとして整備したのだから、 +Passとして追加したい。 + +結局問題になっているのは `CMP+Jcc` に置換された後に `analyzeBranch` が +実行されてしまうことである。エラーログを見ると、どうやら `-verify-machineinstrs` +がこれを実行しているようだ。 + +`TargetPassConfig::addMachinePasses` の実装を読む。 +本当の最後に実行されるパスは `addPreEmitPass2` で追加されるもののようだ。 +また `addPass` の第一引数に `false` を渡せばmachine instruction verifierが +走ることを抑制できるようだ。ということで、次のように書くとうまくいったfootnote:[長かった。]。 + +.... +void RV16KPassConfig::addPreEmitPass2() { + addPass(createRV16KExpandPseudoPass(), false, true); +} +.... + +ちなみにELVMバックエンド<>には分岐解析は実装されているが +`reverseBranchCondition` は実装されていない。なぜだろう。TODO + +== `jcc` の分岐先が8bitに収まらない場合に対応する + +`je` などの条件分岐命令は、その分岐先を指定するための即値が符号付き7bit以内 +であるという制約がある。従ってPC相対で前後128バイトfootnote:[最下位bitとして +`0` が補充されるため。]にジャンプすることができるが、場合によっては +この幅に収まらないことがありうる。そのような場合には `j` 命令と組み合わせて +用いる。問題は `j` 命令を使うか否かの判断をいつするかということである。 + +RISC-Vではこの問題に対応していることが、次のようなCプログラムを +入力することで分かる。 + +.... +int test(int a, int b) +{ + if (a < 0) { + a += b; + a += b; + // ... 以下 a += b が続く + a += b; + } + return a; +} +.... + +`a += b` の部分を増減させることで `blt` と `bge` 命令の内で適当な方が +出力されることが分かる。 + +これはbranch relaxationと呼ばれるようだ<> + +[quote] +.... +Branch relaxation is needed to support branch displacements that overflow the +instruction's immediate field. +.... + +RISC-Vでの対応<>を参考に実装する。 +まず `addPreEmitPass` に `addPass(&BranchRelaxationPassID);` を追加する。 +ついで `lib/CodeGen/BranchRelaxation.cpp` を見ると、次の関数を `RV16KInstrInfo` に +実装すれば良いことが分かる。 + +* `getInstSizeInBytes` +* `isBranchOffsetInRange` +* `getBranchDestBlock` +* `insertIndirectBranch` + +順に実装する。 `getInstSizeInBytes` は命令のサイズを返す。 +基本的には `MCInstrDesc` の `getSize` を呼べばよいが、 `Bcc` の `Size` をTableGenにて +32bitに設定しておく必要がある。 + +`getBranchDestBlock` の実装では、ジャンプ先のbasic blockを返却する必要がある。 +RISC-Vではこれは必ず最後のオペランドになるのだが、RV16Kでは下手に `Bcc` +疑似命令を作ってしまったためにそうはならない。仕方がないので +命令の種類で分けて処理することにする。TODO + +実際のところRV16Kv2では `insertIndirectBranch` を実装する必要は**ない**。 +`BranchRelaxation` は、即値で飛ぶようなジャンプ命令 +(要するに `isBranchOffsetsInRange` が対象とするような命令) +では対処できないほどの大きなジャンプに対して +`insertIndirectBranch` を呼ぶ。したがって、そもそも +`J` 命令が16bitの即値をとるようなRV16Kv2では、これで対応できないジャンプには +ISAレベルで対応できない。AVRバックエンドには次のようにある。 + +.... +// This method inserts a *direct* branch (JMP), despite its name. +// LLVM calls this method to fixup unconditional branches; it never calls +// insertBranch or some hypothetical "insertDirectBranch". +// See lib/CodeGen/RegisterRelaxation.cpp for details. +// We end up here when a jump is too long for a RJMP instruction. +.... + +次いで `insertBranch/removeBranch` で挿入・削除したバイト数を数え +`BytesAdded/BytesRemoved` に保存する。 `BuildMI` の返却値を間接参照すると +`MachineInstr` への参照が受け取れるらしい。 + +ここでどさくさに紛れて `PseudoBR` をただの `Pat` に直そうとしたところ +`br` にパターンマッチしなかった。 + +.... +// Old +let hasSideEffects = 0, mayStore = 0, mayLoad = 0, + isBarrier = 1, isBranch = 1, isTerminator = 1 in +def PseudoBR : Pseudo<(outs), (ins simm16_lsb0_j:$imm15), [(br bb:$imm15)]>, + PseudoInstExpansion<(J simm16_lsb0_j:$imm15)>; + +// New +def : Pat<(br bb:$imm), (J simm16_lsb0_j:$imm)>; +.... + +それから間接分岐に `jalr` を使うのは、RISC-Vでは正しいがRV16Kでは間違っている。 +RISC-Vでは `jr` は `jalr` へのaliasとなっていて、次のように定義される。 + + def : InstAlias<"jr $rs", (JALR X0, GPR:$rs, 0)>; + +したがって `brind` は `JALR` にパターンマッチさせるべきなのだが、 +一方で `JALR` は**一般には**間接分岐の命令ではないため、 +`isIndirectBranch` などのフラグが立っていない。 +そこで `PseudoBRIND` 疑似命令を作成してフラグを立て、 +その上で `brind` を `PseudoBRIND` にマッチさせているfootnote:[しかし +`PseudoBRIND` に `isCall` や `Defs` がついている理由はわからない。TODO]。 +このように、既存の命令に異なるフラグを立てたい場合に +`PseudoInstExpansion` が使えるようだ。 +<>。 + + + let isCall = 1, Defs=[X1] in + let isBarrier = 1, isBranch = 1, isIndirectBranch = 1, isTerminator = 1 in + def PseudoBRIND : Pseudo<(outs), (ins GPR:$rs1, simm12:$imm12), []>, + PseudoInstExpansion<(JALR X0, GPR:$rs1, simm12:$imm12)>; + + def : Pat<(brind GPR:$rs1), (PseudoBRIND GPR:$rs1, 0)>; + def : Pat<(brind (add GPR:$rs1, simm12:$imm12)), + (PseudoBRIND GPR:$rs1, simm12:$imm12)>; + +さてではRV16Kはどうかというと、 `jalr` と `jr` はまったく異なる +命令である。ただの間接分岐では `jr` を使うことが望ましく、 +一方でレジスタに入った関数の呼び出し時には `jalr` を使うべきである。 +そこで次のようにパターンマッチできる。 + + def : Pat<(brind GPR:$rs), (JR GPR:$rs)>; + def : Pat<(Call GPR:$rs), (JALR GPR:$rs)>; + +先の `Bcc` を導入した際にも実験したが、どうやら `Pseudo` のパターンは +少々違う性質を持っているようだ。TODO + +ところでアセンブラ( `MCInst` )レベルでbranch relaxationに対応するのは +良い考えでは無いらしい<>。 + +大きめのプログラムを入力したところ、正しくrelaxされないことがあった。 +原因は i) 疑似命令のサイズが( `Bcc/BccI` を除いて)すべて16bit +であったことと ii) `Bcc/BccI` のoffset計算では `CMP` の分(2バイト)を +減算する必要があったことであった。特に前者が重要で、 +`PseudoBR` や `PseudoRET` のように `PseudoInstExpansion` を用いて +疑似命令を展開する場合、これらは `AsmPrinter` で展開されるため +<>branch relaxation +時点でもそのまま入力される。したがって命令サイズの情報が必要である。 +さらに遅く展開される `PseudoHLT` についても同様である。 +一方で `FRMIDX` などの疑似命令は +関数エピローグ・プロローグの挿入時点で展開される。 +したがって命令サイズの情報は必要ではないfootnote:[挙動からの類推。 +要調査;TODO]。 + +branch relaxationの結果によっては `BccI imm, reg, ...` のような分岐を +`reverseBranchCondition` が作成し、これを +`insertBranch` で挿入される可能性がある。これは `cmpi imm, reg` に +相当するため、そのままではいけない。まず `ScratchReg` を用意し +`li ScratchReg, imm` としたうえで `Bcc ScratchReg, reg` とする。 +しかしbranch relaxationはregister allocationが終わった後に実行される。 +そのため新しいレジスタを確保するためにはregister scavengerを使用する +必要がある。register scavengerはレジスタ割り付けが終わったあとに +レジスタを確保するためのヘルパである<>。 +RISC-V・AMDバックエンドの `insertIndirectBranch` では実際にこれが +使われている。なおこれらのバックエンドでは、 +仮想アドレスを物理アドレスに置換し終わったあとに +`MRI.clearVirtRegs()` を呼んでいる。これは `insertIndirectBranch` は +BranchRelaxationパスからのみ呼ばれるため、この段階で仮想レジスタが +存在しないことを保証できるためである。一方で `insertBranch` は +他のパスからも呼ばれうるため、ここで呼ぶことは適当でないfootnote:[`clearVirtRegs` +に仕込まれたデバッグ用コードによってエラーが出力される。] + +machine basic blockの終わりにセットし、逆向きにレジスタを探すのが +推奨されるfootnote:[前向きに探す `scavengeRegister` はdeprecatedと +コメントにある。]。 +必要であればemergency spillが行われるようだ(要確認;TODO)footnote:[最新版 +ではこれの有効・無効を第五引数の `AllowSpill` で切り替えられるようだ。TODO]。 + +`BccI imm, reg, ...` となるようなテストを書こうとしたが挫折した。TODO +とりあえずテストなしにしておく。 + +== リンカスクリプトを書く + +=== `.text/.data/.rodata` に対応する + +ROM を `0x00000000` から `0x0000FFFF` まで、 +RAM を `0x00010000` から `0x0001FFFF` までと定めたが、 +現状これに即して設定しているのは `.text` と `.data` のみで +他のセクションには対応していない。これに対応するためには +リンカスクリプトを書いてリンク時に指定する他ないようなので設定する。 + + SECTIONS + { + . = 0x00000000; + .text : { *(.text) } + . = 0x00010000; + .data : { *(.data) } + .rodata : { *(.rodata) } + .bss : { *(.bss) } + } + +<><><>を参考にして、 +`.text` をROMに割り付け `.rodata` と `.data` をRAMに割り付ける +シンプルなスクリプトになった。次のように `-Xlinker` オプションで +`--script` オプションをlldに渡すfootnote:[`clang` のヘルプをみると、直接 +`-T` オプションを渡すことでリンカスクリプトを指定できるように思えるが、 +動かなかった。要調査;TODO]。 + +.... +$ bin/clang -target rv16k runtime.s foo.c -o foo.exe -Xlinker "--script=/home/anqou/workspace/llvm-project-build/rv16k.ld" +.... + +さてこれをClang側で指定したかったのだが、そもそもこのリンカスクリプトを +どこに配置すればいいかわからない。例えばGCCでは内部的にリンカスクリプトを +持っておりこれを利用してリンクを行っている<>が、Clangではそうではなく +lldへのコマンドラインオプションを適切に指定することでこれを達成しているように +思えるfootnote:[ただしOpenMPとMPLのためのリンカスクリプトだけは、 +`tools::AddOpenMPLinkerScript` などで動的に作成しているようだ。]。 +一方で上記のリンカスクリプトのような指定をlldのコマンドラインオプションでは +達成できないように思える。良くわからない。TODO 仕方がないので当分手で `-Xlinker` をつける +ことにする。 + +=== `.bss` に対応する + +`.bss` セクションは `.data` や `.rodata` セクションとは異なり初期値を直接持たず、 +アドレスとサイズだけが格納されているfootnote:[多分。要確認。TODO]非初期化領域である<>。 +「非初期化」といいつつ、一般的なアーキテクチャfootnote:[一般的 :thinking_face:]では +`0` で初期化される。 + +RV16Kでは `.bss` を、メモリ上一番最後に配置されるセクションとすることで、 +他のセクションに使用されないRAM領域をそのまま利用することにするfootnote:[何らかの理由で +`.bss` の後ろに他の領域を配置したい場合は、 +`.bss` が指定する領域を( `.data` のように)ELFバイナリ中に確保するという手もある +<>。] + +== [脇道] Brainfuckインタプリタを動かす + +このあたりでBrainfuckのインタプリタが動くようになる。 + +.... +void jumpFront(const char *str, int *index) +{ + if (str[*index] != '[') return; + for (int count = 0; str[*index] != '\0'; (*index)++) { + if (str[*index] == '[') + count++; + else if (str[*index] == ']') + count--; + if (count == 0) break; + } +} + +void jumpBack(const char *str, int *index) +{ + if (str[*index] != ']') return; + for (int count = 0; *index >= 0; (*index)--) { + if (str[*index] == '[') + count--; + else if (str[*index] == ']') + count++; + if (count == 0) break; + } +} + +int brainfuck(const char *str) +{ + static int data[100]; + + int pointer = sizeof(data) / sizeof(int) / 2; + for (int index = 0; str[index] != '\0'; index++) { + switch (str[index]) { + case '+': + data[pointer]++; + break; + case '-': + data[pointer]--; + break; + case '>': + pointer++; + break; + case '<': + pointer--; + break; + case '.': + break; + case ',': + break; + case '[': + if (data[pointer] == 0) jumpFront(str, &index); + break; + case ']': + if (data[pointer] != 0) jumpBack(str, &index); + break; + } + } + return data[pointer]; +} + +int main() +{ + return brainfuck("+++++[>++++++++++<-]>++"); +} +.... + +AWS Lambdaの出来上がりであるfootnote:[は?]。 + +== byvalに対応する + +構造体などレジスタに収まらない値を関数に値渡しする場合( `isByVal()` が +`true` を返す時)に対応する。 + +次のようなCコードを入力するとbyvalに対応していないために `bar` +についてはエラーになるが、 `foo` と `piyo` については +エラーにならない。 + +.... +struct hoge { + int ary[10]; +}; + +int foo(struct hoge h) +{ + return h.ary[3]; +} + +int bar(void) +{ + struct hoge h; + h.ary[3] = 10; + return foo(h); +} + +struct hoge piyo(int i) +{ + struct hoge h; + h.ary[3] = i; + return h; +} +.... + +まず `foo` のように、構造体の値を引数として渡す場合、 +呼び出し元の関数のスタックに渡す構造体のコピーを作成する。 +次いでこのコピーへのポインタを +値の代わりに引数として渡す。これらは `LowerCall` にて行われる。 + +また `piyo` のように +戻り値として構造体の値を返す場合、呼び出される関数の第一引数に +`sret` がついたポインタが渡されるように関数プロトタイプが +フロントエンドによって変更される。それに合わせて関数呼び出しの +戻り値・引数も変更されるため、バックエンドが特別にすべき +処理はない。 + +結局 `byval` 処理のために変更する必要があるのは `LowerCall` での +「構造体のコピーをスタックに作成し、値の代わりにこれへのポインタを +返す」部分である。実装に際してはRISC-Vのもの<> +を参考にする。 + +最適化なしだと無意味なコードが多く出力されてしまうため、 +次のようなコードを `-O2` をつけてテストするとよい。 + +.... +struct hoge { + int ary[10]; +}; + +int foo(struct hoge); + +struct hoge bar(int i) +{ + struct hoge h; + h.ary[3] = i; + h.ary[3] = foo(h); + return h; +} +.... + +なお構造体のサイズが小さく、レジスタに直接載るような場合footnote:[`int` の変数一つ +がメンバであるなど。]でも、このように迂遠な方法をとる。 +これはおそらく `Clang` 側でcalling conventionとして指定することにより +解決が可能であると推測される。TODO footnote:[例えばx86で「本来 +レジスタに収まるために直接渡されるはずの構造体に `byval` 指定がついた +LLVM IRが渡された場合」などを実験する必要がある。TODO] + +テストでは `getelementptr` を用いる。この命令の +第三引数以降は、構造体なら何番目のメンバか、配列なら何番目の要素かを表している +<>。ちょうどC言語の `.` や `[]` に相当するようだfootnote:[ただし +返ってくるのは当該要素へのポインタである]。 +単なるポインタを間接参照する場合は `0` を指定する必要があることが分かる。 +<>にある例を引用する。 + +.... +struct RT { + char A; + int B[10][20]; + char C; +}; +struct ST { + int X; + double Y; + struct RT Z; +}; + +int *foo(struct ST *s) { + return &s[1].Z.B[5][13]; +} +.... + +このようなCコードに対して、次のようなLLVM IRが対応する。 + +.... +%struct.RT = type { i8, [10 x [20 x i32]], i8 } +%struct.ST = type { i32, double, %struct.RT } + +define i32* @foo(%struct.ST* %s) nounwind uwtable readnone optsize ssp { +entry: + %arrayidx = getelementptr inbounds %struct.ST, %struct.ST* %s, i64 1, i32 2, i32 1, i64 5, i64 13 + ret i32* %arrayidx +} +.... + +なお構造体に対してindexを指定する際には `i32` 定数値のみが使用できる。 +上の例で `i32` となっているところはそれである。 + +RISC-Vのコード<>を参考にしてテストを書くと +`foo+2` のようなコードが生成される。 +`llvm-objdump` で確認すると、一見 `0` が入っていて正しくオブジェクトファイルが +生成されていないように見えるが `llvm-readelf` で確認するとrelocationが +作成されていることが分かる。実際グローバル変数はどこに配置されるか分からないので +これが正しい。 + +byval引数のためにローカルスタックに用意した領域への +ポインタに引数をすり替える必要がある。first tryではこれを忘れていた。 + +== fastccに対応する + +LLVMでは、ABIに定義されたC言語の関数呼び出し規約に則った呼出し規約 (`ccc`; +`The C calling convention`) の他に、 +互換性を無視したより高速な呼出し規約を利用できる。 +これを `fastcc`; `The fast calling convention` という<>。 +これは例えば `static` 指定された関数など、外部との互換性を気にする必要がない +場合に使用されるfootnote:[最適化を有効化した場合に限る。多分;TODO]。 +また、末尾最適化は `fastcc` 指定された関数にのみ適用される。 + +RISC-VではCCCとFastCCに違いがない<>。 + +RV16Kでもあえて違いを出す必要がないため `ccc` と全く同じにしておく。 + +== インラインアセンブリに対応する + +LLVM IRはインラインアセンブリに対応している<>。 +制約などの記述方法はほとんどgccのインラインアセンブリのそれと一致するため、 +gcc向けの資料などを参考にできる<>。 + +RV16Kバックエンドでもインラインアセンブリに対応する。 +ユーザが直接使うことよりも、テストが記述しやすくなることが +モチベーションである。 +参考にするパッチは<>。 + +まず `RV16KTargetLowering::getRegForInlineAsmConstraint` をオーバーライドする。 +この関数では `r` や `{edx}` といったレジスタの制約に応じて、 +対応するレジスタの番号とレジスタのクラスを返す。 +オーバーライド元の `TargetLowering::getRegForInlineAsmConstraint` は +具体的な物理レジスタが制約として与えられた場合(例えば `{edx}` ) +の処理が記述されているため、 +それ以外( `r` )の場合について記述する。特定の物理レジスタに限らず、 +あるレジスタクラスに属するレジスタなら何でも良い場合は、 +そのレジスタクラスと `0` を返却する +footnote:[`TargetLowering::getRegForInlineAsmConstraint` の +コメントに詳しい。]。 + +これによってbranch analysisまでインラインアセンブリが通るようになる。 +`RV16KInstrInfo::getInstSizeInBytes` にてエラーが出る +footnote:[branch analysis実装時に `llvm_unreachable` を仕込んでいたため。]ので、 +`TargetOpcode::INLINEASM` を +ハンドルする。 `getInlineAsmLength` を呼べば良い。 + +すると次のようなエラーがアセンブリ中に出力されるようになる。 + + error: invalid operand in inline asm: 'add $1, $2' + error: invalid operand in inline asm: 'add $1, $2' + +すなわち `$1` などが正しく展開されていない。 +これを展開するためには `RV16KAsmPrinter::PrintAsmOperand` を +実装する必要があるfootnote:[つまり `MachineOperand` のレベルでは +既にレジスタが対応している。通常の `LowerRV16KMachineInstrToMCInst` で +処理できない理由はよくわからない。TODO]。 +なお `AsmPrinter::PrintAsmOperand` は `c` や `n` といったmodifierが +付与された即値オペランドを処理するようだ<>。 + +同様に制約 `m` にも対応する。まず `RV16KDAGToDAGISel::SelectInlineAsmMemoryOperand` +をオーバーライドして実装し、メモリアドレスへのパターンマッチを実現するfootnote:[「メモリアドレスへの +パターンマッチ」とは耳慣れない表現だが、インラインアセンブリ以外のコード生成の場合でも +メモリアドレスを計算する `SelectionDAG` へパターンマッチを行いこれを +lowerしている。ちなみにこの関数を実装していなければ +`Could not match memory address.` というエラーが出る。]。 +とりあえず単純な場合にのみ対応するため左から右へ受け流すfootnote:[参考にしたRISC-V以外の +バックエンドでは `reg + reg` などのアドレッシング・モードが存在 +することもあり、より複雑な処理を行っている。実際RISC-Vの実装では、メモリアクセス時に +足される即値が `0` に固定されている。また `FrameIndex` の取り扱いなども +不透明だ。ナイーブに考えれば `SelectAddrFI` の呼出しが必要なはずである。TODO]。 + +また `$1` などを展開するために `RV16KAsmPrinter::PrintAsmMemoryOperand` を +実装する。 + +== emergency spillに対応しない + +register scavengerが空きレジスタを見つけられない場合にレジスタの内容を +退避するemergency spillに対応しようとした。 +まず `RV16KFrameLowering::processFunctionBeforeFrameFinalized` を +オーバーライドする。この関数はstack frameのファイナライズを行う関数で、 +これが呼ばれた後はスタックのサイズが確定し、 +frame indexを定数値に置き換えられるfootnote:[コメントより。]。 + +emergency spillとはregister scavengerがレジスタを確保する際に行う +spill操作のことで `RegScavenger::spill` 内で行われる。 + +ここでRISC-Vバックエンドのemergency spillに関する考察を行うと、 +RV16Kバックエンドにはemergency spillへの対応は不要であると結論される。 + +そもそも、RISC-Vバックエンドがregister scavengerを必要とするのは次に限られる。 +これは `MRI.createVirtualRegister` を利用しているコードのうち、 +register allocationが実行された後に実行される部分と同一である。 + +1. `RISCVFrameLowering::adjustReg` 内で `Val` を `DestReg` に読み込む際 `Val` が +符号付き12bit即値に収まらない場合は `addi` 1命令では対応できないため、 +`ScratchReg` 仮想レジスタを一時レジスタとして使用してこれを行う。 +このとき `Val` はスタックサイズ(ないしそれに近い値)である。 +2. `RISCVRegisterInfo::eliminateFrameIndex` 内で `Offset` を `FrameReg` と +ともにframe indexの代わりに指定する際 `Offset` が符号付き12bit即値に +収まらない場合は `addi/lw/sw` など1命令では対応できないため、 +`ScratchReg` 仮想レジスタを一時レジスタとして使用してこれを行う。 +このとき `Offset` はスタック上の相対アドレスである。 +3. `RISCVInstrInfo::insertIndirectBranch` 内でジャンプ先のアドレスを +レジスタに格納するために `ScratchReg` 仮想レジスタを一時レジスタとして +使用してこれを行う。 + +`RISCVFrameLowering::processFunctionBeforeFrameFinalized` 内のコメントが +意味しているのは、上記の3について `ScratchReg` に +対応する物理レジスタを見つけられないような場合のemergency spillには +対応しないということだろう。おそらくその理由は、そのような状況を +frame loweringの段階で判断できないからだと推察される。 +すなわちRISC-Vバックエンドにおいては、 +全ての場合についてemergency spillのための領域 (emergency spill slot) +をstack上に確保するのは非効率であるため、 +frame loweringの段階で分かる情報から判断して +emergency spill slotが必要そうなframeに対してのみこの領域を確保しているが、 +上記リストの3の状態でemergency spillが必要か否かをframe loweringの +段階で判断することはほとんど不可能だと推察されるということである。 + +では1と2の場合にはどのように「emergency spill slotが必要か否か」を判断するのか。 +実際のRISC-Vバックエンドでは「フレームが使用すると推測されるスタックサイズが +符号付き11bit整数に収まらない(つまり2048バイト以上)のとき」にこれを確保する。 +なぜこれでよいのか。それは、1と2はどちらも**スタックサイズまたはsp/fpからの +相対アドレスを表す即値が符号付き12bit整数に収まらない場合に、 +register scavengerを利用して一時レジスタを確保する**という共通点があるからである。 +従って一時レジスタを使用するのはスタックサイズが符号付き12bit整数値で +表せないほど大きい場合に限定される。もちろんこれは必要条件で、 +このような状態であったとしても実際にはemergency spillが必要とならないケースが +ほとんどだと推察される。それゆえRISC-Vバックエンドにemergency spillを +導入したコミットのコミットメッセージ +<> +では次のように書いている。 + +[quote] +.... +Although the register scavenger can often find a spare register, an emergency +spill slot is needed to guarantee success. +.... + +では転じてRV16Kではどうか。RV16Kでは `lw/sw` が符号付き16bit即値をとるため、 +上記リストのうち2は問題にならない。一方で1と3については同様の問題をはらんでいる。 +このうち1については、RISC-Vと同様の手法を用いて対応できる。すなわち `addi` が +許容する符号付き4bit即値より大きいスタックサイズとなりそうな場合は +emergency spill slotを確保するのである。しかしこれはほとんどの関数で +この領域を確保することを意味するため、極めて非効率であり「コスパ」が悪い。 +従って当分emergency spillについては実装しないことにする。 + +ちなみにRISC-Vの `large-stack.ll` のテストが<>では +変更されているが、実はこのテストはemergency spillを必要としないため、 +`processFunctionBeforeFrameFinalized` の当該部分をコメントアウトしても +動作する。これはよく分からない。TODO + +なおemergency spillを実装していない( `processFunctionBeforeFrameFinalized` がない) +バックエンドも多い。 +またARCバックエンドでは、スタック領域を使用するならば (`MFI.hasStackObjects()`) +必ずemergency spill slotを確保している。 + +== `FRAMEADDR/RETURNADDR` に対応する + +LLVM IRには `llvm.frameaddress` というintrinsicがある。 +これは指定したスタックフレームのframe pointerの値を計算し返却するビルトイン関数で、 +例えばスタックトレースの取得などに使用される(要確認;TODO)。 +これは `ISD::FRAMEADDR` に対応しておりcustomでのlowerが必要である<> +footnote:[`setOperationAction` にCustomを指定する必要はないようだ。なぜだろう。TODO]。 +具体的には引数で指定された深さまでframe pointerを手繰る必要がある。 +しかしこれはスタックにframe pointerが格納されている場合に限り有効である。 +例えば `hasFP` では、引数で指定する関数中で `llvm.frameaddress` が呼ばれていないかを +考慮できるが、その関数を呼び出す関数のことまではわからない。したがってframe pointer eliminationが +行われている関数から呼び出されるなどした場合は正しいframe pointerを +計算することはできないが、仕様上これで良いことになっている<>。 +すなわち `llvm.frameaddress` を使用すること自体は、これが正しく動かなくなるような +最適化等を抑止しない。 + +`llvm.returnaddress` はその関数のreturn addressを同様に計算し返却するビルトイン関数で、 +`ISD::RETURNADDR` に対応する。これもまた `llvm.frameaddress` と同じ意味において +正しい値を返却することは保証しない<>。 + +ところで `llvm.returnaddress` が正常に動作するためには、スタック上に +`ra` が保存されている必要がある。おそらくこれがRISC-Vバックエンドが +`RISCVFrameLowering::determineCalleeSaves` にて `SavedRegs` に +`ra` を追加する理由だろうと推察される。 + +== `PseudoBR` を消す + +RISC-Vでは `br` にパターンマッチさせるために `PseudoBR` 擬似命令を作成している。 +これは `JAL` 命令を直接使うと `isBranch` などのフラグが立たないためである +と推察される<>。 +RV16Kでは `J` 命令を直接使用すればよいため `PseudoBR` 擬似命令は必要ない。 +そこでこれを削除し次のようにパターンマッチを記述した。 + + def : Pat<(br bb:$imm), (J simm16_lsb0_j:$imm)>; + +すると `branch.ll` を読み込ませると次のようなエラーが出力された。 + + ISEL: Starting selection on root node: t19: ch = br t21, BasicBlock:ch + ISEL: Starting pattern match + Match failed at index 0 + LLVM ERROR: Cannot select: t19: ch = br t21, BasicBlock:ch + In function: foo + +以前にもあったが、どうやら `Instruction` の `Pattern` フィールドに +パターンを指定するのと `Pat` を利用してパターンを指定するのとは +微妙に意味が異なるようなのだ。実際 `J` の定義の際に +`let Pattern = [(br bb:$imm)]` と指定すると正しく動作する。 + +`TargetSelectionDAG.td` 内で `Pat` は `Pattern` クラスであると定義される。 +一方 `Pattern` フィールドの方は `Target.td` の `Instruction` クラス内で +保持される。 + +`br` のための `Pat` がある場合とない場合で生成されるTableGenファイルを比較した +ところ**全く同一**であった。つまりTableGenが `Pat` で指定される `br` に +対応していないようだ。 + +`Pat` で指定するパターンも `Pattern` フィールドで指定するパターンも、 +どちらもTableGenによって `RV16KGenDAGISel.inc` の `SelectCode` という関数の +`MatcherTable` という巨大なテーブルにまとめられる。 +これを生成するのは `DAGISelEmitter.cpp` の `DAGISelEmitter::run` のようだ。 + +`ninja -v` として `RV16KGenDAGISel.inc` を作るコマンドを把握し `-debug` オプションを +仕掛けたが、そもそもこれに対応していなかった。 `gdb` を用いて調べようとしたが +breakpointが設定できない。謎。仕方がないので `DAGISelEmitter::run` 内の +`LLVM_DEBUG` の `errs()` を `std::cerr` に変更してコンパイルし実行すると、 +すでにこの段階で `br` が消えていることが分かった。さてどうするか。 + +このDAGISelの部分がどのように動くかについてまとめた資料を見つけた +<>。 +これによると実際に `Pattern` を解釈しているのは +`CodeGenDAGPatterns::ParsePatterns` のようだ。 +そこでここに次のようにデバッグ表示を仕込むと `br` の文字が見えた。 +ここまでは到達しているようだ。 + + /////////// INSERTED FOR DEBUG FROM HERE ////////// + std::cerr << "PATTERN: "; + Pattern.dump(); + std::cerr << "RESULT: "; + Result.dump(); + /////////// INSERTED FOR DEBUG TO HERE ////////// + + ParseOnePattern(CurPattern, Pattern, Result, InstImpResults); + +次に `CodeGenDAGPatterns::ParseOnePattern` の末尾にこのようにデバッグ表示を +仕込んだ。 + + /////////// INSERTED FOR DEBUG FROM HERE ////////// + std::cerr << "PATTERN: "; + Pattern.dump(); + std::cerr << "RESULT: "; + Result.dump(); + std::cerr << ">>>>>>>>>>>>>>>\t" << std::boolalpha << Temp.getOnlyTree()->hasPossibleType() << std::endl; + /////////// INSERTED FOR DEBUG TO HERE ////////// + + + // A pattern may end up with an "impossible" type, i.e. a situation + // where all types have been eliminated for some node in this pattern. + // This could occur for intrinsics that only make sense for a specific + // value type, and use a specific register class. If, for some mode, + // that register class does not accept that type, the type inference + // will lead to a contradiction, which is not an error however, but + // a sign that this pattern will simply never match. + if (Temp.getOnlyTree()->hasPossibleType()) + for (auto T : Pattern.getTrees()) + if (T->hasPossibleType()) + AddPatternToMatch(&Pattern, + PatternToMatch(TheDef, makePredList(Preds), + T, Temp.getOnlyTree(), + InstImpResults, Complexity, + TheDef->getID())); + +次のように出力された。 + + PATTERN: anonymous_997: (br (bb:{ *:[Other] }):$imm) + RESULT: anonymous_997: (J (imm:{ *:[] })<>:$imm) + >>>>>>>>>>>>>>> false + +すなわち `Temp.getOnlyTree()->hasPossibleType()` が `false` +となっているためにパターンとして登録されないことが分かった。 + +ここで正しく動作する `Pattern` フィールドでの指定を試すと、次のように出力された。 + + PATTERN: J: (br (bb:{ *:[Other] }):$imm) + RESULT: J: (J (bb:{ *:[Other] }):$imm) + >>>>>>>>>>>>>>> true + +そこで次のように `Pat` を指定すると無事動作した。 + + def : Pat<(br bb:$imm), (J bb:$imm)>; + +一方 `bb` ではなく `simm16_lsb0_j` に合わせると `false` となり動作しなかった。 + + PATTERN: anonymous_997: (br (imm:{ *:[] })<>:$imm) + RESULT: anonymous_997: (J (imm:{ *:[] })<>:$imm) + >>>>>>>>>>>>>>> false + +ここで `Bcc` に注目すると、こちらも `bb` から `simm8_lsb0` と同じような +パターンマッチを行っているにも関わらず、次のように `true` が返却されている。 + + PATTERN: anonymous_1003: (RV16KBrCC GPR:{ *:[i16] }:$lhs, GPR:{ *:[i16] }:$rhs, (bb:{ *:[Other] }):$dst, SETLE:{ *:[Other] }) + RESULT: anonymous_1003: (Bcc:{ *:[i16] } GPR:{ *:[i16] }:$lhs, GPR:{ *:[i16] }:$rhs, simm8_lsb0:{ *:[Other] }:$dst, 1:{ *:[i16] }) + >>>>>>>>>>>>>>> true + +そこで `simm16_lsb0_j` の定義を変え `ImmLeaf` の継承をやめたところ、 +`br` のパターンマッチを書き換えずとも動作するようになった。 + + def simm16_lsb0_j : Operand { + +推測するに `ImmLeaf` 継承時に指定する `return isShiftedInt<15, 1>(Imm);` +が `bb` には当てはまらなかった(そもそも即値でない)ために +正しいパターンマッチと認識されなかったのではないだろうか。TODO + +== rv16k-rtを作る + +ランタイムライブラリなど、LLVM バックエンドを使うためのスクリプトを +置く場所を作る。これ自体は単なるディレクトリ(ないしgitレポジトリ)だが、 +これがある前提でClangを設定する。 + +`runtime.s` を `crt0.s` という名前でrv16k-rtに移す。また `rv16k.lds` も +ここに移す。Clangを起動する際には `--sysroot=/path/to/rv16k-rt` とする。 +そのため `--sysroot` が設定された場合には `getFilePaths().push_back(D.SysRoot)` とし、 +そこを探索パスに追加する。 + +ついでに `-L` オプションに対応する。これはそのままリンカに渡す。 +したがって `-L` をつけるだけではClangの探索パスには入らないことに注意が必要である。 +`GetProgramPath` の実装を見ると `LibraryPaths` を検索していることが分かるが、 +これは `-L` とは全く無関係である。 + +さて以上の変更から、Clangを起動する際には次のようにすることになった。 + + $ bin/clang -target rv16k -Oz bf.c -o bf.exe --sysroot=/path/to/rv16k-rt + +== `0` を不正なアドレスにする + +通常 `0` として指定される `NULL` を不正なアドレスにする。 +すなわちRAMの0番地にはいかなるデータも格納しないことにする。 +これはLLVMが暗黙のうちに仮定している条件でもある(どこかで出典を見たが見つけられない; +要調査;TODO)。 + +リンカスクリプトをいじって `.data` が `0x00010002` から始まるようにする。 + +しかし実質的にNULLポインタが発生するのはmalloc/freeを使い始めてからなので、 +それが無い間はさほど問題ではない。 + +== 乗算・除算・剰余に対応 + +RV16Kv2命令セットには掛け算や割り算・余りを計算するための命令がない。 +そこでこれらをソフトウェア的に実装し対応する。 + +LLVMはこれらの演算を `__mulhi3` などのライブラリ関数呼び出しに変換する。 +どの操作がどのライブラリ関数呼び出しに対応しているかは +`llvm/include/llvm/RuntimeLibcalls.def` にて定まっており、 +例えば32bitの掛け算なら `__mulsi3` ・ 16bitなら `__muhi3` と定義されている。 + +これらの関数の実体はLLVMのcompiler-rtプロジェクトなどfootnote:[他にglibcや +newlibなどがある。]で定義される。 +しかしcompiler-rt の `__mulsi3` の実装などをみると `int` が使われている。 +すなわち `int` を32bitの整数型と前提しているのだ。 +一方で16bit用の `__mulhi3` などはそもそも定義されていない。 +したがってこのままでは使用できないため、 +これをフォークし改変して使用することにする。 + +これらの関数は、各々別のオブジェクトファイルとしてコンパイルしfootnote:[リンクは +オブジェクトファイル単位で行われるため、複数の関数を一つの +オブジェクトファイルにまとめてしまうと、不要な関数までリンクされてしまう。]、 +`ar` コマンドを用いて次のように静的ライブラリ `libc` とする。 + + ar rcs libc.a __mulhi3.o __divhi3.o ... + +この `libc` をLLDを用いてリンクする。それには `-lc` オプションを付与すればよいが、 +いちいち手で指定するのは面倒のためClang側でデフォルト設定する。 +このときこのオプションはソースコードをコマンドに並べ終えたあとに指定することが +大切である。また `libc` の他にも静的ライブラリをリンクする必要があり、 +それらが互いに循環依存の関係にある場合は `--start-group` と `--end-group` で +囲むなどする必要がある<>。 + +== `AllowRegisterRenaming` を `1` にする + +TableGenファイルによるTarget定義のところで `AllowRegisterRenaming` を `1` に +設定する<>。これによってテストを若干修正する必要が +ある。 + +`AllowRegisterRenaming` によって、 +レジスタ割り付けが終わった後にレジスタの名前を書き換える処理が許可される。 +ABIやopcodeの制限からレジスタを書き換えることができない場合は +`MachineOperand::isRenamable` が `false` を返すことが要求される。 +これが具体的にどのような場合に相当するのかはよくわからないが、 +とりあえずテストはパスしたのでよしとする。TODO + +このレジスタ書き換えは `MachineCopyPropagation` で行われる。 +このパスは、コピー先レジスタが使われるまでにコピー元レジスタが破壊されていなければ、 +代わりにコピー元レジスタを使用するようにする。 + +== 最適化について調査する + +RV16Kv2にとってベストな最適化が行われるようにしたい。 + +`llvm/lib/Passes/PassBuilder.cpp` にて最適化等のパイプラインが構築されている<>。 +具体的には `PassBuilder::parseModulePass` にてコマンドラインオプションが +パーズされ、通常 `buildPerModuleDefaultPipeline` が呼ばれる。 +このなかでは `buildModuleSimplificationPipeline` によってcore simplificationの +ためのパスが追加され、次いで `buildModuleOptimizationPipeline` によって +optimizationのためのパスが追加される。 + +具体的にどのようなパスが実行されているか、Clangに `-mllvm +-opt-bisect-limit=-1` をオプションとして渡すことでダンプさせることができる。 +また `-mllvm -debug-pass=Arguments` を渡すことで `opt` にどのようなオプションが +渡されるかを見ることができる<>。 + +`TwoAddressInstructionPass` は `A = B op C` を `A = B` と `A op= C` に +分割する。 + +`-Oz` オプションが渡されたときの挙動が良くわからない。TODO +`-O2` のときと比較すると `opt` に渡されているオプションは少なくなっている。 +これは `PassBuilder` にて `isOptimizingForSize` を使って分岐しているところに +対応しているようだ。したがって `-Oz` オプションfootnote:[あるいは `-Os` オプション。 +確認した限りにおいて `-Os` と `-Oz` は同じように扱われている気がする。]をつけた場合、 +次の箇所が**無効化**される。 + +まず標準ライブラリ関数呼び出しの結果が用いられない場合に、 +引数の値の範囲に応じて関数呼び出しを抑制するような `if` を追加する(shrink-wrapする) +パスfootnote:[shrink-wrapの意味がよくわからないが、当該ソースコードを見る限りは +多分こういうこと。]。 + + if (!isOptimizingForSize(Level)) + FPM.addPass(LibCallsShrinkWrapPass()); + +次にソースコードのプロファイルに応じてメモリアクセスの命令を最適化するパス +footnote:[これがいまいち良くわからない。なぜサイズのために抑制する必要があるのか。TODO]。 + + // For PGO use pipeline, try to optimize memory intrinsics such as memcpy + // using the size value profile. Don't perform this when optimizing for size. + if (PGOOpt && PGOOpt->Action == PGOOptions::IRUse && + !isOptimizingForSize(Level)) + FPM.addPass(PGOMemOPSizeOpt()); + +最後に簡素化とインライン展開を行うパス群footnote:[サイズのためにインライン展開を +抑制すべきというのは理解できるが、一方で `static` が付与された関数のインライン展開は +抑制されない。扱う場所が違うのかもしれない。TODO]。 + +.... +// Generally running simplification passes and the inliner with an high +// threshold results in smaller executables, but there may be cases where +// the size grows, so let's be conservative here and skip this simplification +// at -Os/Oz. We will not do this inline for context sensistive PGO (when +// IsCS is true). +if (!isOptimizingForSize(Level) && !IsCS) { + InlineParams IP; + + // In the old pass manager, this is a cl::opt. Should still this be one? + IP.DefaultThreshold = 75; + + // FIXME: The hint threshold has the same value used by the regular inliner. + // This should probably be lowered after performance testing. + // FIXME: this comment is cargo culted from the old pass manager, revisit). + IP.HintThreshold = 325; + + CGSCCPassManager CGPipeline(DebugLogging); + + CGPipeline.addPass(InlinerPass(IP)); + + FunctionPassManager FPM; + FPM.addPass(SROA()); + FPM.addPass(EarlyCSEPass()); // Catch trivial redundancies. + FPM.addPass(SimplifyCFGPass()); // Merge & remove basic blocks. + FPM.addPass(InstCombinePass()); // Combine silly sequences. + invokePeepholeEPCallbacks(FPM, Level); + + CGPipeline.addPass(createCGSCCToFunctionPassAdaptor(std::move(FPM))); + + MPM.addPass(createModuleToPostOrderCGSCCPassAdaptor(std::move(CGPipeline))); +} +.... + +`PassBuilder` で指定されているのはこれだけだが、パス内部で最適化レベルの +処理を行うパスについてはこの限りでない。例えば `LoopUnrollPass` は +`OptLevel` を引数として受け取り、その値に応じて内部的なしきい値を変える。 + +== 即値命令を追加する + +RV16Kv3に向けて `lsi/andi/ori/xori/lsli/lsri/asri` 命令を追加すると +次のように変化した。 + +image::img/rv16kv2_imm_opt.png[] + +4bitに収まるにも関わらず `lsi` ではなく `li` を使用しているのは、 +リンク時に値が決まるためであると推察される。 + +== リンク時最適化(LTO; Link Time Optimization)をかける + +各ファイルをコンパイルする際にLLVM bitcodeでとどめ、 +必要なすべてのファイルをリンク時にconcatした上で機械語へ変換することによって、 +オブジェクトファイル間をまたいで不要なコードの削除などの +最適化を行うことができる。これをLTOという。 +LTOはClangに `-flto` オプションを渡すことで実行することができる。 + +LLDにLLVM bitcodeを渡すため、Clangの `RV16KToolChain` に次を追加。 + + bool HasNativeLLVMSupport() const override { return true; } + +またLLDがLLVM bitcodeから `e_machine` を特定できるように、 +`lld/ELF/InputFiles` の `getBitcodeMachineKind` にRV16Kのための `case` を追加。 + + case Triple::rv16k: + return EM_RV16K; + +これでClangに `-flto` オプションが渡せるようになる。 + +……なるのだが、目立った最適化は得られなかった。むしろ `-Oz` オプションを +つけているにもかかわらずコードサイズは大きくなった。理由不明。TODO + +== 末尾再帰に対応する + +RISC-Vは末尾再帰に対応している<>。 +`PseudoTAIL` 擬似命令をアセンブリレベルで導入した上で、これをコード生成させている。 + +例えば次のようなコードをLLVM IRにコンパイルすると `tail call` が出現する。 + +.... +$ cat foo.c +int factorial(int x) +{ + if (x > 0) + return x * factorial(x - 1); + else + return 1; +} + +int test_tailcall(int a) +{ + return factorial(a); +} + +$ ./rv16k-clang -c -S -O1 foo.c -o foo.ll -emit-llvm + +$ cat foo.s + +... + +; Function Attrs: nounwind readnone +define dso_local i16 @factorial(i16 %x) local_unnamed_addr #0 { +entry: + %cmp3 = icmp sgt i16 %x, 0 + br i1 %cmp3, label %if.then, label %return + +if.then: ; preds = %entry, %if.then + %x.tr5 = phi i16 [ %sub, %if.then ], [ %x, %entry ] + %accumulator.tr4 = phi i16 [ %mul, %if.then ], [ 1, %entry ] + %sub = add nsw i16 %x.tr5, -1 + %mul = mul nsw i16 %x.tr5, %accumulator.tr4 + %cmp = icmp sgt i16 %sub, 0 + br i1 %cmp, label %if.then, label %return + +return: ; preds = %if.then, %entry + %accumulator.tr.lcssa = phi i16 [ 1, %entry ], [ %mul, %if.then ] + ret i16 %accumulator.tr.lcssa +} + +; Function Attrs: nounwind readnone +define dso_local i16 @test_tailcall(i16 %a) local_unnamed_addr #0 { +entry: + %call = tail call i16 @factorial(i16 %a) + ret i16 %call +} + +... +.... + +ちなみに `factorial` の方は(関数呼び出しでなく)単なるループに展開 +されていることにも注意が必要である。また `-O2` で最適化すると +`factorial` がインライン展開されるため `tail call` はなくなってしまう。 + +さてTCOを行うためには、まず i) `tail` が付与された関数呼び出しが +実際に末尾関数呼び出し(tail call)であるかどうかを判断し ii) tail callである場合は +`RV16KISD::CALL` ではなく `RV16KISD::TAIL` を発行し iii) +これに対してパターンマッチを行い `J` ないし `JR` 命令に変換する必要がある。 i) と ii) +は `RV16KTargetLowering::LowerCall` にて行い iii) はTableGenファイルとして記述する。 + +まず iii) から実装する。 +はじめ次のように定義したところ `Tail` にパターンマッチしなかった。 + +.... +def PseudoTAIL : Pseudo<(outs), (ins simm16_lsb0_j:$imm), []>, + PseudoInstExpansion<(J simm16_lsb0_j:$imm)>; + +... + +def : Pat<(Tail (iPTR tglobaladdr:$dst)), + (PseudoTAIL texternalsym:$dst)>; +.... + +`ins simm16_lsb0_j` を `ins simm16_lsb0` としたところうまくいった。 +`PseudoBR` のときと同様の症状に見えるが対処方法が微妙に違う。 +`ImmLeaf` を継承した型でないと `tglobaladdr` などにパターンマッチできない? +しかしRISC-Vの `bare_symbol` などはこの限りでないように見える。 +全くわからない。TODO 結局 `tglobaladdr` などが即値のパターンにマッチするのが +すべての元凶のように思えるが……。 + +RISC-Vでは `Tail` のパターンマッチに `(iPTR ...)` を挟んでいる。 +RV16Kは16bitアーキテクチャなのでfootnote:[RV16Kバックエンドの記述の、具体的に +どの部分がこれを定めているかははっきりしない。TODO]、 +これは `(i16 ...)` と等価であると推察される。 +実際これを `(i16 ...)` と書き換えてもテストに通った。一方で `(i8 ...)` や `(i32 ...)` では +動作しなかった。一方で `iPTR` を単純に削除し `...` の部分のみとしても動作した。 +そこでとりあえずはこれを削除した形で記述した。 + +`Tail` がレジスタを取る場合、パターンマッチによって `PseudoTAILIndirect` に変換される。 +ここで使用できるレジスタはcaller-savedなレジスタに限られる。 +というのも、RISC-Vバックエンドのコメントによれば、tail callが行われる直前には、 +callee-savedなレジスタの値の復帰が行われるため、分岐先のアドレスを保持する +レジスタの中身が打ち消されてしまうからである。 +そこで新たに `GPRTC` というレジスタクラスを定義し `PseudoTAILIndirect` はこれをオペランドと +している。 + +次に ii) を記述する。 `IsTailCall` が立っている場合は、通常の関数呼び出しを表す +`CALLSEQ_START/CALL/CALLSEQ_END` の出力を抑止し、代わりに `TAIL` を出力する。 +なおtail callの場合preserved maskをオペランドに追加する必要はない。 +おそらくこれはtail call後に実行される同関数中のコードが存在しないためであると考えられる。 +したがってtail callが存在する場合のpreserved mask追加も害はない。 +実際Mipsバックエンドは追加している。 +またRV16Kバックエンドで試しても、テストを通過した。 + +最後に i) を実装する。具体的には +`RV16KTargetLowering::IsEligibleForTailCallOptimization` の実装を行う。 +この関数は、LLVM IRでは `tail call` となっている関数について、 +本当にtail callしてよいかを判断する。 + +関数宣言がweak symbol<>の場合tail callの挙動は実装依存となるため +行わない。 + +tail callのcallerが保存するレジスタをcalleeも保存しているかをチェックしている。 + +構造体をbyvalで引数として渡す関数をtail callしたい場合一筋縄ではいかない。 +というのもこのような場合、構造体はcaller側のstackに保存され、 +そのポインタが引数として渡される。しかしtail callでは +calleeがcallerのスタック領域を使用する(これによってメモリ使用量を削減する)ため、 +ナイーブな実装では引数として渡された構造体を壊してしまう。 +実際これを処理するためのwork-aroundは存在し、 +x86向けバックエンドでは使用されているようだが、 +RISC-Vを始めとする他のバックエンドでは、byvalで引数を渡す場合は +単にtail callしないという選択をしている。RV16Kもこれに従う。 + +== 構造体を正しく扱う + +次のようなテストコードを正しく動かしたい。これを動かすと `0` が +返却されるはずだが、現時点では `-1` が返ってきてしまう。 + +.... +struct hoge { + int x, y; +}; + +struct hoge piyo(struct hoge h) +{ + if (h.x < 0) return h; + + h.x -= 1; + h.y -= 1; + struct hoge ret = piyo(h); + return ret; +} + +int main() +{ + struct hoge h = {1, 2}; + return piyo(h).y; +} +.... + +上のコードをコンパイルすると、 +`piyo` の戻り値から `y` を取り出すために「 `piyo` の戻り値の構造体が入った +アドレスに `2` を `or` して `y` のアドレスとする」というアセンブリが +出力される。ここで `or` は `add` の代替として機能することが +期待されている。これは `bin/llc` に `-debug` をつけると、 +次のように変換されていることから分かる。 + +.... +Creating new node: t17: ch,glue = callseq_start t15, TargetConstant:i16<0>, TargetConstant:i16<0> +Creating new node: t19: ch,glue = CopyToReg t17, Register:i16 $x8, FrameIndex:i16<0> +Creating new node: t21: ch,glue = CopyToReg t19, Register:i16 $x9, FrameIndex:i16<1>, t19:1 +Creating new node: t24: ch,glue = RV16KISD::CALL t21, TargetGlobalAddress:i16 0, Register:i16 $x8, Register:i16 $x9, RegisterMask:Untyped, t21:1 +Creating new node: t25: ch,glue = callseq_end t24, TargetConstant:i16<0>, TargetConstant:i16<0>, t24:1 +Creating new node: t26: i16 = add nuw FrameIndex:i16<0>, Constant:i16<2> +Creating new node: t27: i16,ch = load<(dereferenceable load 2 from %ir.y, !tbaa !7)> t25, t26, undef:i16 + +... + +Combining: t26: i16 = add nuw FrameIndex:i16<0>, Constant:i16<2> +Creating new node: t30: i16 = or FrameIndex:i16<0>, Constant:i16<2> + ... into: t30: i16 = or FrameIndex:i16<0>, Constant:i16<2> +.... + +この変換は `llvm/lib/CodeGen/SelectionDAG/DAGCombiner.cpp` の +`visitADD` にて次のように行われている。 + + // fold (a+b) -> (a|b) iff a and b share no bits. + if ((!LegalOperations || TLI.isOperationLegal(ISD::OR, VT)) && + DAG.haveNoCommonBitsSet(N0, N1)) + return DAG.getNode(ISD::OR, DL, VT, N0, N1); + +しかし実際には、RV16Kにおいて `piyo` の戻り値が格納されるアドレスは2バイトで +アラインされるため、このような `add` を `or` に変換する操作は正しく +動かない場合がある。 `visitADD` では +`DAG.haveNoCommonBitsSet` を呼び出すことでこのチェックを +行っているように見えるが、frame indexが対象の場合は、 +このチェックをすり抜けてしまう(後述)footnote:[また +以下のパターンマッチで使用する `isOrEquivalentToAdd` でも +同様のチェックを行っているが、これも後述する同様の理由ですり抜けてしまう。]。 + +これを解決するには i) このような `or` への変換を無効化したり ii) frame indexに +対する即値の `or` をメモリ操作のdisplacementとする などが考えられる。 +RV16Kにおいてはこのような場合に `or` へ変換することはそもそも間違っている +ため i) の対処が本来である。しかしLLVMにおいてこれをどのように +無効化すればよいか判然としない(後述;TODO)。 +そこで今回は ii) によって対処する。 +なおこれはRISC-Vも採用している手法である<> +footnote:[ただし上記と同様のCテストコードをRISC−Vバックエンドに入力した +ところ、同じような問題は発生しなかった。良くわからない。TODO]。 + +具体的には<>を参考にして次のような +パターンを追加する。 + + def : Pat<(load (IsOrAdd AddrFI:$rs1, simm16_lsb0:$imm16)), + (LW AddrFI:$rs1, simm16_lsb0:$imm16)>; + +ここで使用される `IsOrAdd` は次のように定義される述語である。 + + def IsOrAdd: PatFrag<(ops node:$A, node:$B), (or node:$A, node:$B), [{ + return isOrEquivalentToAdd(N); + }]>; + +これでうまく動作する。 + +さてではなぜ上記のようなチェックが機能しないのか。 +それは**具体的な値が確定するまでframe indexは `0` として扱われて +いるから**である(多分)。 + +== RISC-VとRV16Kの比較 + +生成されるオブジェクトのサイズの比較を行いたい。 +これはRV16KがRISC-Vと比べてどの程度今回の目的(ROMが極めて小さい)に +特化出来ているかの指標の1つになりうる。 + +RV16KとRISC-Vのバックエンドの両方でテスト用Cコードを +オブジェクトファイルにコンパイルし、両者の `.text` 部分のサイズを +取得する。掛け算・割り算の扱い方や、構造体値渡しのconventionなど +単純に比較できない要素は数多くあるが、1つの指標にはなる。 + +LLVMをビルドしたディレクトリで、次のようなワンライナーを実行する。 +なおこれの実行には、RISC-Vクロスコンパイル用にビルドしたGCCが必要である。 + + ls ~/ano/secure_vm/rv16k-test/build_and_test/test/00*.c | \ + while read line; do \ + echo $(basename $line); \ + echo "\tRV16K $(bin/clang -target rv16k -Oz -c -o - $line | \ + bin/llvm-readelf -S | \ + grep " .text" | \ + sed -E 's/ +/ /g' | \ + cut -d' ' -f8)"; \ + echo "\tRV32EC (GCC) $(~/workspace/riscv-gnu-toolchain/build/bin/riscv32-unknown-elf-gcc -march=rv32ec -mabi=ilp32e -Os -c -o _tetete.o $line && \ + bin/llvm-readelf -S _tetete.o | \ + sed -E 's/ +/ /g' | \ + grep " .text" | \ + cut -d' ' -f8 | \ + awk '{s+=strtonum("0x"$1)} END {printf "%06x",s}' )"; rm -f _tetete.o; \ + echo "\tRV32IC (LLVM) $(bin/clang -target riscv32 -march=rv32ic -Oz -c -o - $line | \ + bin/llvm-readelf -S | \ + grep " .text" | \ + sed -E 's/ +/ /g' | \ + cut -d' ' -f8)"; \ + echo "\tRV32IC (GCC) $(~/workspace/riscv-gnu-toolchain/build/bin/riscv32-unknown-elf-gcc -march=rv32ic -mabi=ilp32e -Os -c -o _tetete.o $line && \ + bin/llvm-readelf -S _tetete.o | \ + sed -E 's/ +/ /g' | \ + grep " .text" | \ + cut -d' ' -f8 | \ + awk '{s+=strtonum("0x"$1)} END {printf "%06x",s}' )"; rm -f _tetete.o; \ + done + +結果は次のようであった。 + + 0001.c + RV16K 000006 + RV32IC (LLVM) 000006 + RV32EC (GCC) 000006 + 0002-fib.c + RV16K 00003e + RV32IC (LLVM) 000044 + RV32EC (GCC) 00003c + 0003-bf.c + RV16K 000162 + RV32IC (LLVM) 0000f2 + RV32EC (GCC) 0001aa + 0004-lisp.c + RV16K 000176 + RV32IC (LLVM) 00016e + RV32EC (GCC) 00017e + 0005-prime.c + RV16K 00009c + RV32IC (LLVM) 000082 + RV32EC (GCC) 000082 + 0006-editdis.c + RV16K 0000d6 + RV32IC (LLVM) 00009e + RV32EC (GCC) 0000de + 0007-perceptron.c + RV16K 000148 + RV32IC (LLVM) 000132 + RV32EC (GCC) 000134 + 0008-rpn.c + RV16K 000110 + RV32IC (LLVM) 0000e4 + RV32EC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001c4 + RV32IC (LLVM) 000166 + RV32EC (GCC) 000130 + 0010-struct.c + RV16K 000100 + RV32IC (LLVM) 000090 + RV32EC (GCC) 0000a0 + +あるオブジェクトファイルに登場する命令の種類と数を調べるときは +次のようにする。RISC-Vの場合 `-riscv-no-aliases` をつけることで、 +圧縮命令に対して `c.` プレフィクスを出力してくれる。 + + $ bin/llvm-objdump -d -riscv-no-aliases rpn-rv32ec.o | cut -f3 | egrep -v "^$|:" | sort | uniq -c | sort -nr + +=== `lsi/andi/ori/xori/lsli/lsri/asri` など即値命令を追加する + + 0001.c + RV16K 000006 + RV32IC (LLVM) 000006 + RV32EC (GCC) 000006 + 0002-fib.c + RV16K 00003a + RV32IC (LLVM) 000044 + RV32EC (GCC) 00003c + 0003-bf.c + RV16K 000146 + RV32IC (LLVM) 0000f2 + RV32EC (GCC) 0001aa + 0004-lisp.c + RV16K 000158 + RV32IC (LLVM) 00016e + RV32EC (GCC) 00017e + 0005-prime.c + RV16K 00008c + RV32IC (LLVM) 000082 + RV32EC (GCC) 000082 + 0006-editdis.c + RV16K 0000d0 + RV32IC (LLVM) 00009e + RV32EC (GCC) 0000de + 0007-perceptron.c + RV16K 000136 + RV32IC (LLVM) 000132 + RV32EC (GCC) 000134 + 0008-rpn.c + RV16K 000110 + RV32IC (LLVM) 0000e4 + RV32EC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001be + RV32IC (LLVM) 000166 + RV32EC (GCC) 000130 + 0010-struct.c + RV16K 0000fa + RV32IC (LLVM) 000090 + RV32EC (GCC) 0000a0 + +多少下がる。 + +=== `lw0` と `sw0` を追加する + +`lw` や `sw` のoffsetが `0` の場合と同等の2byte命令を追加。 + + 0001.c + RV16K 000006 + RV32IC (LLVM) 000006 + RV32EC (GCC) 000006 + 0002-fib.c + RV16K 00003a + RV32IC (LLVM) 000044 + RV32EC (GCC) 00003c + 0003-bf.c + RV16K 00013e + RV32IC (LLVM) 0000f2 + RV32EC (GCC) 0001aa + 0004-lisp.c + RV16K 000144 + RV32IC (LLVM) 00016e + RV32EC (GCC) 00017e + 0005-prime.c + RV16K 000086 + RV32IC (LLVM) 000082 + RV32EC (GCC) 000082 + 0006-editdis.c + RV16K 0000c8 + RV32IC (LLVM) 00009e + RV32EC (GCC) 0000de + 0007-perceptron.c + RV16K 000120 + RV32IC (LLVM) 000132 + RV32EC (GCC) 000134 + 0008-rpn.c + RV16K 0000f8 + RV32IC (LLVM) 0000e4 + RV32EC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001be + RV32IC (LLVM) 000166 + RV32EC (GCC) 000130 + 0010-struct.c + RV16K 0000e8 + RV32IC (LLVM) 000090 + RV32EC (GCC) 0000a0 + +そこそこ縮んだ。 + +=== `addi` の即値フィールドを6bitに増やす + +新たに `addib` 命令を追加し、即値フィールドを6bitとする。 + + 0001.c + RV16K 000006 + RV32IC (LLVM) 000006 + RV32EC (GCC) 000006 + 0002-fib.c + RV16K 00003a + RV32IC (LLVM) 000044 + RV32EC (GCC) 00003c + 0003-bf.c + RV16K 000136 + RV32IC (LLVM) 0000f2 + RV32EC (GCC) 0001aa + 0004-lisp.c + RV16K 00013c + RV32IC (LLVM) 00016e + RV32EC (GCC) 00017e + 0005-prime.c + RV16K 00007e + RV32IC (LLVM) 000082 + RV32EC (GCC) 000082 + 0006-editdis.c + RV16K 0000c0 + RV32IC (LLVM) 00009e + RV32EC (GCC) 0000de + 0007-perceptron.c + RV16K 000114 + RV32IC (LLVM) 000132 + RV32EC (GCC) 000134 + 0008-rpn.c + RV16K 0000f8 + RV32IC (LLVM) 0000e4 + RV32EC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001be + RV32IC (LLVM) 000166 + RV32EC (GCC) 000130 + 0010-struct.c + RV16K 0000d0 + RV32IC (LLVM) 000090 + RV32EC (GCC) 0000a0 + +微妙。 + +=== `add3 rd, rs1, rs2` を追加する + +3オペランド命令を追加する。次のようにパターンマッチさせる。 + + def : Pat<(add GPRC:$rs1, GPRC:$rs2), (ADD3 GPRC:$rs1, GPRC:$rs2)>; + def : Pat<(add GPR:$rs1, GPR:$rs2), (ADD GPR:$rs1, GPR:$rs2)>; + +結果は次の通り。 + + 0001.c + RV16K 000006 + RV32IC (LLVM) 000006 + RV32EC (GCC) 000006 + 0002-fib.c + RV16K 000038 + RV32IC (LLVM) 000044 + RV32EC (GCC) 00003c + 0003-bf.c + RV16K 000134 + RV32IC (LLVM) 0000f2 + RV32EC (GCC) 0001aa + 0004-lisp.c + RV16K 00013a + RV32IC (LLVM) 00016e + RV32EC (GCC) 00017e + 0005-prime.c + RV16K 000082 + RV32IC (LLVM) 000082 + RV32EC (GCC) 000082 + 0006-editdis.c + RV16K 0000be + RV32IC (LLVM) 00009e + RV32EC (GCC) 0000de + 0007-perceptron.c + RV16K 000110 + RV32IC (LLVM) 000132 + RV32EC (GCC) 000134 + 0008-rpn.c + RV16K 000108 + RV32IC (LLVM) 0000e4 + RV32EC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001a6 + RV32IC (LLVM) 000166 + RV32EC (GCC) 000130 + 0010-struct.c + RV16K 0000d0 + RV32IC (LLVM) 000090 + RV32EC (GCC) 0000a0 + +`gameoflife.c` のサイズは若干小さくなるが、むしろ `rpn.c` などサイズが大きく +なっているバイナリがあることに注意が必要。 +TableGenによるパターンマッチでは最初に書いたパターンを優先して採用するため、 +このような場合では `add3` のみが +使用されfootnote:[ただし関数プロローグ・エピローグでの `sp` の操作など、 +直接 `add` を生成する場合は除く。]、レジスタの使用に制限が発生したことが +原因と思われる。 + +2オペランドの命令を基調としているところに3オペランドの命令を追加するのは +難しい。RISC-VのRV32CはRV32Iのコードサイズを削減することが主目的であるため、 +RV32IからRV32Cへの1:1対応として処理すれば良い。しかしRV16Kは2オペランドの +体系のため、この一部を3オペランドに変換するというのは非自明な処理である。 +というのも、命令選択の時点では、レジスタクラスは分かっても、 +どのレジスタを使用するかはわからない。そもそも `add3` が適用可能かどうかが +わからないのである。 + +=== IRレベルで定数読み込みのコストを考慮に入れる + +より効率的なコード生成のために、IRレベルで行われる変形(transformation)に +対してコード生成時の情報を提供する。具体的にはconstant materializingの +コストを伝えるようにする。これには `TargetTransformInfo` の +RV16K版を定義する必要がある。 + +IRレベルにおける「コスト」は、通常の命令( `add` など)を1として +考える。これは加算乗除ができる値として捉える必要がある。 +`enum TargetCostConstants` のコメントに詳細がある。 + +RISC-VではLLVM9にて `TargetTransformInfo` が定義される。これを参考にする。 + +基本的には即値のサイズと命令の種類ごとに「基本コスト」の何倍かを +記述する。ここで「基本コスト」は実質的に2バイトのことである。 + +`LLVMBuild.txt` の `required_libraries` に `Analysis` を追加する必要があった。 +理由不明。TODO + +サイズベンチマークをしたところ、全く減らなかった。悲しい。 + +=== サイズ最適化のときはloop-unrollingを抑止する + +ARMがloop unrollingを明示的に抑止しているので、それに習う。 + +`RV16KTTIImpl::getUnrollingPreferences` を定義し、この中でloop unrollingを無効化する。 + +全く効果がなかったのでrevertした。 + +=== `GlobalMergePass` を追加する + +ARMが使っているパスで、グローバル変数を一つにまとめることでregister pressureを下げる。 +`lw/sw` のdisplacementを有効活用できるのではないかと追加したが効果なし。revert. + +=== `MachineScheduler` を有効化する + +なんかRISC-Vが最近有効化してたからしてみた。 + + bool enableMachineScheduler() const override { return true; } + +コードサイズが増えた。revert. + +=== `isLegalAddressingMode/isLegalICmpImmediate/isLegalAddImmediate` を定義 + +アドレスモード・ `cmpi` が取れる即値幅・ `addi` が取れる即値幅を、 +LLVM IRレベルで利用できるように定義。コードサイズに効果なし。 +これらに関するRISC-Vのコミットでも「LLVMに対して正しいbackendの情報を +伝えることは間違っていないと思うから追加するけど、このコミットのご利益を表現する +テストは書けないよ」みたいなことが書いてある。 + +=== `isZextFree` を定義 + +符号なし8bitの値をメモリから読み込む際に `zext` が使われないことを保証する。 + +実装したが効果なし。revert. + +=== 再実体化(remat; rematerialization)の実装 + +再実体化とは、複数回使用されるような値を一度だけ計算しレジスタやメモリに格納しておく +共通部分式除去とは**逆に**、毎回計算することにより実行時間を減少させたり、 +レジスタ・メモリ使用量に余裕をもたせる最適化である<> +footnote:[<>では「再計算」と訳されている。このほうが分かりやすい +かもしれない。]。RISC-Vによる実装を参考にする<>。 + +LLVMでは結局、即値を即値ロードの命令に変換することをmaterializeと呼び、 +その逆をrematerializeと読んでいるようだ? + +やることは単純で `li` と `lsi` に `isReMaterializable` フラグを立てれば良い。 +<>でも指摘されているように、 +このフラグが立ったからといって必ずrematが行われるわけではなく、 +あくまでヒントとして使用される。 + +コードサイズは変化しなかった。revert. + +== LLVM9に移行する + +LLVM9が9/19に公開されたfootnote:[もちろんLLVMはOSSのため、9/19以前から +「公開」されていたのだが、言葉の綾である。]。そこで、これまでLLVM8にて開発してきた +RV16KバックエンドをLLVM9に対応させたい。 + +RISC-Vバックエンドを独自に作っている<>では +LLVM8から9にポートした際の記事が公開されている<>。 + +まず `rv16k-toward-v3` ブランチをLLVM9とマージする。これには `git rebase --onto` が使える。 + + $ git rebase --onto llvmorg-9.0.0 llvmorg-8.0.0 rv16k-toward-v3 + +次にこれをビルドする。RISC-Vはもはやexperimentalではない。 + + $ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS="clang;lld" \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86;RISCV" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RV16K" \ + ~/ano/secure_vm/llvm-project/llvm + ../llvm + $ cmake --build . + +もちろんLLVM8と9で異なっている部分があるため、ビルドがコケる。適宜修正する。 + +- LLVM8まではレジスタは単なる `unsigned` の整数値であったが、 +これをラップするものとして新たに `Register` クラスが導入された<>。 +具体的には `MachineOperand::getReg` の戻り値が `Register` に変更され、 +それに依存する他の部分も変更されている。RV16Kバックエンドの場合は +`RV16KRegisterInfo::getFrameRegister` の戻り値の型のみを変更した。 +- `AsmPrinter` の `AsmVariant` を活用しているバックエンドはx86のみだったため +削除されたらしい<>。合わせて削除する。 +- LLDにおけるコーディング規約が変更され、 +変数名が小文字から始まるようになった<> +footnote:[現在LLVMでは大文字始まり(CamlCase)の変数名が +原則使用されている<>が、 +これを小文字始まり(camlBack)に変更しようという動きがある<>。 +その試行として、LLDにおいて実際に変更が行われた。]。これに伴い、 +継承元のメンバ変数を参照する部分などでは書き換えを要する。具体的には +`RV16K::RV16K` で上書きする `NoneRel` メンバ変数などが該当する。 + +以上の変更によりビルドが可能になった。コードサイズのベンチマークは次のように変化した。 + + 0001.c + RV16K 000006 + RV32EC (GCC) 000006 + RV32IC (LLVM) 000008 + RV32IC (GCC) 000006 + 0002-fib.c + RV16K 00003a + RV32EC (GCC) 00003c + RV32IC (LLVM) 000046 + RV32IC (GCC) 00003c + 0003-bf.c + RV16K 00013a + RV32EC (GCC) 0001aa + RV32IC (LLVM) 0000e6 + RV32IC (GCC) 0000ec + 0004-lisp.c + RV16K 00013c + RV32EC (GCC) 00017e + RV32IC (LLVM) 00017a + RV32IC (GCC) 00017e + 0005-prime.c + RV16K 00007a + RV32EC (GCC) 000082 + RV32IC (LLVM) 000082 + RV32IC (GCC) 000082 + 0006-editdis.c + RV16K 0000c2 + RV32EC (GCC) 0000de + RV32IC (LLVM) 0000b0 + RV32IC (GCC) 0000ca + 0007-perceptron.c + RV16K 00013c + RV32EC (GCC) 000134 + RV32IC (LLVM) 000158 + RV32IC (GCC) 000134 + 0008-rpn.c + RV16K 0000fc + RV32EC (GCC) 0000e2 + RV32IC (LLVM) 0000d8 + RV32IC (GCC) 0000e2 + 0009-gameoflife.c + RV16K 0001c6 + RV32EC (GCC) 000130 + RV32IC (LLVM) 000176 + RV32IC (GCC) 000130 + 0010-struct.c + RV16K 0000d0 + RV32EC (GCC) 0000a0 + RV32IC (LLVM) 000092 + RV32IC (GCC) 0000a4 + +悪化したfootnote:[は?]footnote:[LLVMのRV32ICもコードサイズが大きくなっていることから、 +LLVM coreが悪化したように見える。は?]footnote:[キレた。]。 + +`git checkout` を使って、この変更が為される前後での挙動を見てみると、 +`MachineBlockPlacement` への変更<>により +挙動が変わっている。 + +この変更は次のようなbasic blockのスケジューリングを行うパスである。 +例えば次のようなCFGがあるとする。ここで `for.body` や `for.inc` から +2本の矢印が伸びているのは、そのbasic blockの内部または末尾から、 +どちらかのbasic blockに分岐する(fall-throughを含む)ことを意味している。 + + entry + | + V + -->for.body + | |\ + | | \ + | | \ + | | if.then3 + | | / + | | / + | |/ + ---for.inc + | + V + for.cond.cleanup + +このとき、仮に次のように命令をスケジュールしたとする。 + + entry + for.body + if.then3 + for.inc + for.cond.cleanup + +`if.then3` のbasic blockが一度も実行されないとすると、 +ループは次のように回ることになる。 + + ... + -> for.body + -> (ジャンプ) for.inc + -> (ジャンプ) for.body + ... + +したがって、ループ中2回ジャンプする。 + +次に、以下のようにスケジュールしてみる。 + + entry + for.inc + for.body + if.then3 + for.cond.cleanup + +すると、ループは次のように回る。 + + ... + -> for.body + -> (ジャンプ) for.inc + -> for.body + ... + +したがって、ループ中のジャンプは1回で済むことになる。 + +しかし上記の図には**`entry` 末尾から `for.cond.cleanup` への条件分岐が +明記されていない**。この分だけバイナリサイズとして不利である。 + +`rpn.c` のbefore/afterを見てみると、このパスによるメリットは無いように見える。 +過剰に変換を行っているということだろうか? + +`Bcc` の条件が絶対に成り立たないことを見抜いて `J` に変換する +機能がなくなっているっぽい。なんもわからん。TODO + +ところでLLVM9では `llvm-objdump` の出力形式も変更されているようだ。謎。TODO + +== --- DONE LINE --- + +== malloc/freeの実装 + +bss領域の後ろからheapにすればいいが、512Bに収まるようなmalloc/freeが書けるかどうか。 + +== 命令スケジューリングのための情報を命令に付加する + +Instruction itineraries + +命令ごとのサイクル数などを考慮?<> + +== アドレッシング・モード + +SparcやLanaiでは `reg + imm` のアドレッシング・モードを `ComplexPattern` を +使用してパターンマッチしている。すなわち `(load reg0, (add reg1 + imm))` に対する +パターンマッチを `SelectADDRri` などで `(lw reg0, reg1, imm)` に変換している? +これは `MIOperandInfo` と結びついて `ins` として扱われる。 +従って `SelectInlineAsmMemoryOperand` では `(load reg, (add reg + imm))` に +対するパターンマッチを実施する必要がある? のか? 分からん。TODO +これをどうやってビット列に結びつけているかまったくの不明。 + +一方RISC-Vでは明確なアドレッシング・モードの定義はなく、 +TableGenレベルのパターンマッチで対応している。 + +== 関数の可変長引数に対応する + +<> + +== コマンドライン引数対応 + +RAMをいじればいいはずだが面倒そう。 + +== デバッグ情報を出力する + +https://github.com/lowRISC/riscv-llvm/blob/master/0047-RISCV-Initial-support-for-emitting-call-frame-inform.patch + +[bibliography] +== 参照 + +- [[[github_riscv-llvm_docs_01,1]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/01-intro-and-building-llvm.mkd +- [[[llvm_getting-started,2]]] https://llvm.org/docs/GettingStarted.html +- [[[clang_gettings-started,3]]] https://clang.llvm.org/get_started.html +- [[[asciidoctor_user-manual,4]]] https://asciidoctor.org/docs/user-manual/ +- [[[riscv,5]]] https://riscv.org/ +- [[[riscv_specifications,6]]] https://riscv.org/specifications/ +- [[[fox-llvm,7]]] 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』(柏木 餅子・風薬・矢上 栄一、株式会社インプレス、2013年) +- [[[github_riscv-llvm_docs_02,8]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/02-starting-the-backend.mkd +- [[[github_riscv-llvm_patch_02,9]]] https://github.com/lowRISC/riscv-llvm/blob/master/0002-RISCV-Recognise-riscv32-and-riscv64-in-triple-parsin.patch +- [[[github_riscv-llvm,10]]] https://github.com/lowRISC/riscv-llvm +- [[[youtube_llvm-backend-development-by-example,11]]] https://www.youtube.com/watch?v=AFaIP-dF-RA +- [[[msyksphinz_try-riscv64-llvm-backend,12]]] http://msyksphinz.hatenablog.com/entry/2019/01/02/040000_1 +- [[[github_riscv-llvm_patch_03,13]]] https://github.com/lowRISC/riscv-llvm/blob/master/0003-RISCV-Add-RISC-V-ELF-defines.patch +- [[[github_riscv-llvm_patch_04,14]]] https://github.com/lowRISC/riscv-llvm/blob/master/0004-RISCV-Add-stub-backend.patch +- [[[github_riscv-llvm_patch_06,15]]] https://github.com/lowRISC/riscv-llvm/blob/master/0006-RISCV-Add-bare-bones-RISC-V-MCTargetDesc.patch +- [[[github_riscv-llvm_patch_10,16]]] https://github.com/lowRISC/riscv-llvm/blob/master/0010-RISCV-Add-support-for-disassembly.patch +- [[[llvm-writing_backend-operand_mapping,17]]] https://llvm.org/docs/WritingAnLLVMBackend.html#instruction-operand-mapping +- [[[llvm-writing_backend,18]]] https://llvm.org/docs/WritingAnLLVMBackend.html +- [[[github_riscv-llvm_patch_07,19]]] https://github.com/lowRISC/riscv-llvm/blob/master/0007-RISCV-Add-basic-RISCVAsmParser.patch +- [[[github_riscv-llvm_patch_08,20]]] https://github.com/lowRISC/riscv-llvm/blob/master/0008-RISCV-Add-RISCVInstPrinter-and-basic-MC-assembler-te.patch +- [[[llvm-tablegen,21]]] https://llvm.org/docs/TableGen/index.html +- [[[github_riscv-llvm_patch_09,22]]] https://github.com/lowRISC/riscv-llvm/blob/master/0009-RISCV-Add-support-for-all-RV32I-instructions.patch +- [[[llvm_dev_ml-tablegen_definition_question,23]]] http://lists.llvm.org/pipermail/llvm-dev/2015-December/093310.html +- [[[llvm_doxygen-twine,24]]] https://llvm.org/doxygen/classllvm_1_1Twine.html +- [[[llvm-tablegen-langref,25]]] https://llvm.org/docs/TableGen/LangRef.html +- [[[github_riscv-llvm_docs_05,26]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/05-disassembly.mkd +- [[[github_riscv-llvm_patch_11,27]]] https://github.com/lowRISC/riscv-llvm/blob/master/0011-RISCV-Add-common-fixups-and-relocations.patch +- [[[github_riscv-llvm_docs_06,28]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/06-relocations-and-fixups.mkd +- [[[github_riscv-llvm_patch_13,29]]] https://github.com/lowRISC/riscv-llvm/blob/master/0013-RISCV-Initial-codegen-support-for-ALU-operations.patch +- [[[speakerdeck-llvm_backend_development,30]]] https://speakerdeck.com/asb/llvm-backend-development-by-example-risc-v +- [[[llvm-code_generator,31]]] https://llvm.org/docs/CodeGenerator.html +- [[[llvm-code_generator-target_independent_code_gen_alg,32]]] https://llvm.org/docs/CodeGenerator.html#target-independent-code-generation-algorithms +- [[[llvm-code_generator-selectiondag_instruction_selection,33]]] https://llvm.org/docs/CodeGenerator.html#selectiondag-instruction-selection-process +- [[[github_riscv-llvm_patch_15,34]]] https://github.com/lowRISC/riscv-llvm/blob/master/0015-RISCV-Codegen-support-for-memory-operations.patch +- [[[cpu0,35]]] https://jonathan2251.github.io/lbd/ +- [[[elvm-llvm_backend,36]]] https://github.com/shinh/llvm/tree/elvm +- [[[elvm-slide,37]]] http://shinh.skr.jp/slide/llel/000.html +- [[[github_riscv-llvm_patch_16,38]]] https://github.com/lowRISC/riscv-llvm/blob/master/0016-RISCV-Codegen-support-for-memory-operations-on-globa.patch +- [[[github_riscv-llvm_patch_17,39]]] https://github.com/lowRISC/riscv-llvm/blob/master/0017-RISCV-Codegen-for-conditional-branches.patch +- [[[todai_llvm_backend,40]]] https://github.com/cpu-experiment-2018-2/llvm/tree/master/lib/Target/ELMO +- [[[todai_llvm_backend-article,41]]] http://uenoku.hatenablog.com/entry/2018/12/25/044244 +- [[[github_riscv-llvm_patch_18,42]]] https://github.com/lowRISC/riscv-llvm/blob/master/0018-RISCV-Support-for-function-calls.patch +- [[[llvm-langref,43]]] http://llvm.org/docs/LangRef.html +- [[[fpga_develop_diary,44]]] http://msyksphinz.hatenablog.com/ +- [[[llvm-anton_korobeynikov_2012,45]]] https://llvm.org/devmtg/2012-04-12/Slides/Workshops/Anton_Korobeynikov.pdf +- [[[llvm-welcome_to_the_back_end_2017,46]]] https://www.youtube.com/watch?v=objxlZg01D0 +- [[[ean10-howto-llvmas,47]]] https://www.embecosm.com/appnotes/ean10/ean10-howto-llvmas-1.0.html +- [[[lowrisc-devmtg18,48]]] https://www.lowrisc.org/llvm/devmtg18/ +- [[[LLVMBackend_2015_03_26_v2,49]]] http://www.inf.ed.ac.uk/teaching/courses/ct/other/LLVMBackend-2015-03-26_v2.pdf +- [[[rui-compilerbook,50]]] https://www.sigbus.info/compilerbook +- [[[krister-writing_gcc_backend,51]]] https://kristerw.blogspot.com/2017/08/writing-gcc-backend_4.html +- [[[llvm-ml-129089,52]]] http://lists.llvm.org/pipermail/llvm-dev/2019-January/129089.html +- [[[llvm-langref-datalayout,53]]] https://llvm.org/docs/LangRef.html#langref-datalayout +- [[[github-frasercrmck_llvm_leg,54]]] https://github.com/frasercrmck/llvm-leg/tree/master/lib/Target/LEG +- [[[llvm_doxygen-InitMCRegisterInfo,55]]] https://llvm.org/doxygen/classllvm_1_1MCRegisterInfo.html#a989859615fcb74989b4f978c4d227a03 +- [[[llvm-programmers_manual,56]]] http://llvm.org/docs/ProgrammersManual.html +- [[[llvm-writing_backend-calling_conventions,57]]] https://llvm.org/docs/WritingAnLLVMBackend.html#calling-conventions +- [[[riscv-calling,58]]] https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf +- [[[llvm_dev_ml-how_to_debug_instruction_selection,59]]] http://lists.llvm.org/pipermail/llvm-dev/2017-August/116501.html +- [[[fpga_develop_diary-20190612040000,60]]] http://msyksphinz.hatenablog.com/entry/2019/06/12/040000 +- [[[llvm_dev_ml-br_cc_questions,61]]] http://lists.llvm.org/pipermail/llvm-dev/2014-August/075303.html +- [[[llvm_dev_ml-multiple_result_instrs,62]]] https://groups.google.com/forum/#!topic/llvm-dev/8kPOj-_lbGk +- [[[stackoverflow-frame_lowering,63]]] https://stackoverflow.com/questions/32872946/what-is-stack-frame-lowering-in-llvm +- [[[llvm_dev_ml-selecting_frame_index,64]]] https://groups.google.com/d/msg/llvm-dev/QXwtqgau-jA/PwnHDF0gG_oJ +- [[[fpga_develop_diary-llvm,65]]] https://github.com/msyksphinz/llvm/tree/myriscvx/impl90/lib/Target/MYRISCVX +- [[[llvm-github_cd44ae,66]]] https://github.com/llvm/llvm-project/commit/cd44aee3da22f9a618f2e63c226bebf615fa8cf8 +- [[[llvm_phabricator-d43752,67]]] https://reviews.llvm.org/D43752 +- [[[llvm-compilerwriterinfo,68]]] https://llvm.org/docs/CompilerWriterInfo.html +- [[[wikipedia-The_Gleaners,69]]] https://en.wikipedia.org/wiki/The_Gleaners +- [[[github_riscv-llvm_patch_20,70]]] https://github.com/lowRISC/riscv-llvm/blob/master/0020-RISCV-Support-and-tests-for-a-variety-of-additional-.patch +- [[[llvm_phabricator-d47422,71]]] https://reviews.llvm.org/D47422 +- [[[llvm-extendingllvm,72]]] https://llvm.org/docs/ExtendingLLVM.html +- [[[llvm_dev_ml-001264,73]]] http://lists.llvm.org/pipermail/llvm-dev/2004-June/001264.html +- [[[llvm_phabricator-d42958,74]]] https://reviews.llvm.org/D42958 +- [[[compiler_rt,75]]] https://compiler-rt.llvm.org/ +- [[[github-riscv_compiler_rt,76]]] https://github.com/andestech/riscv-compiler-rt +- [[[github_riscv-llvm_patch_27,77]]] https://github.com/lowRISC/riscv-llvm/blob/master/0027-RISCV-Support-stack-frames-and-offsets-up-to-32-bits.patch +- [[[llvm_phabricator-d44885,78]]] https://reviews.llvm.org/D44885 +- [[[llvm_phabricator-d45859,79]]] https://reviews.llvm.org/D45859 +- [[[llvm-langref-poison_value,80]]] http://llvm.org/docs/LangRef.html#poisonvalues +- [[[github-emscripten-issues-34,81]]] https://github.com/emscripten-core/emscripten/issues/34 +- [[[switch_lowering_in_llvm,82]]] http://fileadmin.cs.lth.se/cs/education/edan75/part2.pdf +- [[[github-avr_llvm-issues-88,83]]] https://github.com/avr-llvm/llvm/issues/88 +- [[[asciidoctor-quickref,84]]] https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/ +- [[[llvm_phabricator-d56351,85]]] https://reviews.llvm.org/D56351 +- [[[hatenablog-rhysd-230119,86]]] https://rhysd.hatenablog.com/entry/2017/03/13/230119 +- [[[llvm_dev_ml-115805,87]]] http://lists.llvm.org/pipermail/llvm-dev/2017-July/115805.html +- [[[github_riscv-llvm_patch_29,88]]] https://github.com/lowRISC/riscv-llvm/blob/master/0029-RISCV-Add-support-for-llvm.-frameaddress-returnaddre.patch +- [[[github-riscv_llvm-clang,89]]] https://github.com/lowRISC/riscv-llvm/tree/master/clang +- [[[github-elvm_clang,90]]] https://github.com/shinh/clang/tree/elvm +- [[[github_riscv-llvm_patch_22,91]]] https://github.com/lowRISC/riscv-llvm/blob/master/0022-RISCV-Support-lowering-FrameIndex.patch +- [[[llvm_dev_ml-087879,92]]] http://lists.llvm.org/pipermail/llvm-dev/2015-July/087879.html +- [[[stackoverflow-27467293,93]]] https://stackoverflow.com/questions/27467293/how-to-force-clang-use-llvm-assembler-instead-of-system +- [[[github-riscv_llvm-clang-03,94]]] https://github.com/lowRISC/riscv-llvm/blob/master/clang/0003-RISCV-Implement-clang-driver-for-the-baremetal-RISCV.patch +- [[[github_riscv-llvm_patch_25,95]]] https://github.com/lowRISC/riscv-llvm/blob/master/0025-RISCV-Add-custom-CC_RISCV-calling-convention-and-imp.patch +- [[[llvm_dev_ml-106187,96]]] http://lists.llvm.org/pipermail/llvm-dev/2016-October/106187.html +- [[[llvm_phabricator-d39322,97]]] https://reviews.llvm.org/D39322 +- [[[cpu0-lld,98]]] http://jonathan2251.github.io/lbt/lld.html +- [[[youtube-how_to_add_a_new_target_to_lld,99]]] https://www.youtube.com/watch?v=FIXaeRU31Ww +- [[[llvm-smith_newlldtargetpdf,100]]] https://llvm.org/devmtg/2016-09/slides/Smith-NewLLDTarget.pdf +- [[[llvm-lld,101]]] https://lld.llvm.org/index.html +- [[[note-n9948f0cc3ed3,102]]] https://note.mu/ruiu/n/n9948f0cc3ed3 +- [[[lanai-isa,103]]] https://docs.google.com/document/d/1jwAc-Rbw1Mn7Dbn2oEB3-0FQNOwqNPslZa-NDy8wGRo/pub +- [[[github-blog_os-issues-370,104]]] https://github.com/phil-opp/blog_os/issues/370 +- [[[llvm_phabricator-d61688,105]]] https://reviews.llvm.org/D61688 +- [[[man-xtensa_linux_gnu_ld,106]]] https://linux.die.net/man/1/xtensa-linux-gnu-ld +- [[[man-elf,107]]] https://linuxjm.osdn.jp/html/LDP_man-pages/man5/elf.5.html +- [[[llvm_phabricator-d45385,108]]] https://reviews.llvm.org/D45385 +- [[[llvm_phabricator-d47882,109]]] https://reviews.llvm.org/D47882 +- [[[llvm_dev_ml-128257,110]]] https://lists.llvm.org/pipermail/llvm-dev/2018-December/128257.html +- [[[github_riscv-llvm_patch_31,111]]] https://github.com/lowRISC/riscv-llvm/blob/master/0031-RISCV-Implement-support-for-the-BranchRelaxation-pas.patch +- [[[github_riscv-llvm_patch_30,112]]] https://github.com/lowRISC/riscv-llvm/blob/master/0030-RISCV-Implement-branch-analysis.patch +- [[[stackoverflow-5789806,113]]] https://stackoverflow.com/questions/5789806/meaning-of-and-in-c +- [[[compiler_study_report,114]]] https://proc-cpuinfo.fixstars.com/2018/11/compiler_study_report/ +- [[[github-llvm-bcb36be8e3f5dced36710ba1a2e2206071ccc7ba,115]]] https://github.com/llvm/llvm-project/commit/bcb36be8e3f5dced36710ba1a2e2206071ccc7ba +- [[[llvm_dev_ml-059799,116]]] http://lists.llvm.org/pipermail/llvm-dev/2013-February/059799.html +- [[[tricore-llvm-slides,117]]] https://reup.dmcs.pl/wiki/images/7/7a/Tricore-llvm-slides.pdf +- [[[tricore-llvm,118]]] https://opus4.kobv.de/opus4-fau/files/1108/tricore_llvm.pdf +- [[[llvm_dev_ml-111697,119]]] http://lists.llvm.org/pipermail/llvm-dev/2017-April/111697.html +- [[[takayuki-no09,120]]] http://www.ertl.jp/~takayuki/readings/c/no09.html +- [[[hwenginner-linker,121]]] https://hwengineer.github.io/linker/ +- [[[koikikukan-000300,122]]] http://www.koikikukan.com/archives/2017/04/05-000300.php +- [[[stackoverflow-57735654_34997577,123]]] https://stackoverflow.com/questions/34997577/linker-script-allocation-of-bss-section#comment57735654_34997577 +- [[[redhat-ld_simple_example,124]]] https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/Using_ld_the_GNU_Linker/simple-example.html +- [[[llvm_phabricator-d45395,125]]] https://reviews.llvm.org/D45395 +- [[[llvm_phabricator-d45395-398662,126]]] https://reviews.llvm.org/D45395#inline-398662 +- [[[llvm-langref-inline_asm,127]]] http://llvm.org/docs/LangRef.html#inline-assembler-expressions +- [[[hazymoon-gcc_inline_asm,128]]] http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html +- [[[github_riscv-llvm_patch_28,129]]] https://github.com/lowRISC/riscv-llvm/blob/master/0028-RISCV-Add-basic-support-for-inline-asm-constraints.patch +- [[[llvm-langref-inline_asm-asm_template_argument_modifier,130]]] http://llvm.org/docs/LangRef.html#asm-template-argument-modifiers +- [[[github-llvm-0715d35ed5ac2312951976bee2a0d2587f98f39f,131]]] https://github.com/llvm/llvm-project/commit/0715d35ed5ac2312951976bee2a0d2587f98f39f +- [[[github_riscv-llvm_patch_32,132]]] https://github.com/lowRISC/riscv-llvm/blob/master/0032-RISCV-Reserve-an-emergency-spill-slot-for-the-regist.patch +- [[[github_riscv-llvm_patch_26,133]]] https://github.com/lowRISC/riscv-llvm/blob/master/0026-RISCV-Support-for-varargs.patch +- [[[github-fracture-wiki-how-dagisel-works,134]]] https://github.com/draperlaboratory/fracture/wiki/How-TableGen%27s-DAGISel-Backend-Works +- [[[welcome_to_the_back_end-slides,135]]] http://llvm.org/devmtg/2017-10/slides/Braun-Welcome%20to%20the%20Back%20End.pdf +- [[[life_of_an_instruction,136]]] https://eli.thegreenplace.net/2012/11/24/life-of-an-instruction-in-llvm/ +- [[[shinh-blog-010637,137]]] http://shinh.hatenablog.com/entry/2014/10/03/010637 +- [[[llvm_backend_intro,138]]] https://www.slideshare.net/AkiraMaruoka/llvm-backend +- [[[amazon-llvm_cookbook-customer_review,139]]] https://www.amazon.co.jp/dp/178528598X#customer_review-R28L2NAL8T9M2H +- [[[llvm_dev_ml-117139,140]]] https://lists.llvm.org/pipermail/llvm-dev/2017-September/117139.html +- [[[github_riscv-llvm_patch_85,141]]] https://github.com/lowRISC/riscv-llvm/blob/master/0085-RISCV-Set-AllowRegisterRenaming-1.patch +- [[[llvm_dev_ml-135337,142]]] https://lists.llvm.org/pipermail/llvm-dev/2019-September/135337.html +- [[[wikipedia-weak_symbol,143]]] https://en.wikipedia.org/wiki/Weak_symbol +- [[[wikipedia-remat,144]]] https://en.wikipedia.org/wiki/Rematerialization +- [[[llvm_phabricator-d46182,145]]] https://reviews.llvm.org/D46182 +- [[[nakata-compiler,146]]] 『コンパイラの構成と最適化(第2版)』(中田育男、朝倉書店、2009) +- [[[fpga_develop_diary-to_llvm9,147]]] http://msyksphinz.hatenablog.com/entry/2019/08/17/040000 +- [[[llvm_phabricator-d60488,148]]] https://reviews.llvm.org/D60488 +- [[[llvm_phabricator-rl364191,149]]] https://reviews.llvm.org/rL364191 +- [[[llvm_phabricator-d64121,150]]] https://reviews.llvm.org/D64121 +- [[[llvm-codingstandards,151]]] https://llvm.org/docs/CodingStandards.html +- [[[llvm_dev_ml-134921,152]]] https://lists.llvm.org/pipermail/llvm-dev/2019-September/134921.html +- [[[llvm_phabricator-d43256,153]]] https://reviews.llvm.org/D43256 diff --git a/draft-rv32kv1.asciidoc b/draft-rv32kv1.asciidoc new file mode 100644 index 0000000..25466c1 --- /dev/null +++ b/draft-rv32kv1.asciidoc @@ -0,0 +1,1441 @@ += [下書き] LLVMバックエンド開発文書 for RV32Kv1 +艮鮟鱇 + +== これはなに + +2019年にRV32Kv1という自作ISA用のLLVMバックエンドを作ったときの自分とメンバ用のメモ。 +メモなので当然読みにくい。これをブラッシュアップしてまともな文章にする予定だったが、 +その作業が遅れているので、一旦メモのまま公開する。内容について質問したい場合は +Twitter https://twitter.com/ushitora_anqou[@ushitora_anqou]までリプライなどを貰えれば反応するかもしれない。 + +ソースコードは未公開。 + +ブラッシュアップはlink:https://github.com/ushitora-anqou/write-your-llvm-backend[GitHub]にて行っている。 + +このLLVMバックエンドの開発は、もともと2019年度未踏事業において +link:https://github.com/virtualsecureplatform/kvsp[Virtual Secure Platform]を開発するために行われた。 + +== アセンブラ簡単使い方 + +とりあえずビルドする。ビルドには + +* `cmake` +* `ninja` +* `clang` +* `clang++` + +が必要。 + +これらを入れた後 `cmake` を次のように走らせる。 +.... +$ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCV;RV32K" \ + ../llvm +$ cmake --build . +.... +その後アセンブラを起動する。アセンブラは `build/bin/llvm-mc` である。 +.... +$ cat foo.s +li x9, 3 +mv x11, x1 +sub x9, x10 +add x8, x1 +nop + +$ bin/llvm-mc -arch=rv32k -filetype=obj foo.s | od -tx1z -Ax -v +000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< +000010 01 00 f5 00 01 00 00 00 00 00 00 00 00 00 00 00 >................< +000020 68 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 >h.......4.....(.< +000030 04 00 01 00 8d 44 86 85 89 8c 06 94 01 00 00 00 >.....D..........< +000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000050 00 2e 74 65 78 74 00 2e 73 74 72 74 61 62 00 2e >..text..strtab..< +000060 73 79 6d 74 61 62 00 00 00 00 00 00 00 00 00 00 >symtab..........< +000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000090 07 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 >................< +0000a0 50 00 00 00 17 00 00 00 00 00 00 00 00 00 00 00 >P...............< +0000b0 01 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 >................< +0000c0 06 00 00 00 00 00 00 00 34 00 00 00 0a 00 00 00 >........4.......< +0000d0 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 >................< +0000e0 0f 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 >................< +0000f0 40 00 00 00 10 00 00 00 01 00 00 00 01 00 00 00 >@...............< +000100 04 00 00 00 10 00 00 00 >........< +000108 +.... +0x34から0x3dにある `8d 44 86 85 89 8c 06 94 01 00` が出力である。 + +このような出力の他に `-show-encoding` を用いる方法もある。 +.... +$ bin/llvm-mc -arch=rv32k -show-encoding foo.s + .text + li x9, 3 # encoding: [0x8d,0x44] + mv x11, x1 # encoding: [0x86,0x85] + sub x9, x10 # encoding: [0x89,0x8c] + add x8, x1 # encoding: [0x06,0x94] + nop # encoding: [0x01,0x00] +.... + +== 概要 + +これを読めば自作アーキテクチャ(RV32Kv1)の機械語を出力するLLVMバックエンドを作成することができる。 + +この文書はAsciiDocを用いて記述されている。記述方法については<>を参照のこと。 + +== 参考にすべき資料 + +* Writing an LLVM Backend<> +* 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』<> +* RISC-V support for LLVM projects<> +LLVMにRISC-Vサポートを追加するパッチ群。バックエンドを開発するためのチュートリアルも兼ねているらしく `docs/` 及びそれと対応したpatchが参考になる。 +またこれについて、開発者が2018 LLVM Developers' Meetingで登壇したときの動画は<>より閲覧できる。 +スライドは<>より閲覧できる。 +* TableGenのLLVMのドキュメント<> +* The LLVM Target-Independent Code Generator<> +* Create an LLVM Backend for the Cpu0 Architecture<> +** これをもとにLLVMバックエンドを開発しているブログ<> +* ELVMバックエンド<> +** 限られた命令でLLVM IRの機能を達成する例として貴重。 +*** でも意外とISAはリッチだったりする。 +** Hamajiさんのスライドも参考になる<>。 +* 2018年度東大CPU実験で開発されたLLVM Backend<> +** これについて書かれたAdCのエントリもある<>。 +* LLVM Language Reference Manual<> +** LLVM IRについての言語リファレンス。意外と実装すべき命令が少ないことが分かる。 + +== RV32Kv1アーキテクチャ仕様 + +RV32Kv1はRISC-V<>のISA<>であるRV32Cをベースとして設定する。 +RV32Cからそのまま借用する命令は下記のとおりである。 + +* LWSP +* SWSP +* LW +* SW +* J +* JAL +* JR +* JALR +* BEQZ +* BNEZ +* LI +* MV +* ADD +* SUB +* NOP + +RV32Cに無く、新設する命令は下記のとおりである。 + +* SLT +** 次に示すReservedのうち上の方を使う。 + +image::img/rv32c_reserved_insts.png[] + +エンディアンにはリトルエンディアンを採用する。 + +レジスタは次の通り。 + +* x1/ra : return address register / ABI link register +* x2/sp : ABI stack pointer +* x8-x15 : general purpose register + +分岐時のPC更新は `PC <- PC + d` とする( `PC <- PC + 1 + d` **でない**)。 + +関数呼び出し規約は次の通り。 + +* x8-x15 : 引数0番目〜7番目 +* x8 : 戻り値 +* sp : callee-saved +* x15 : frame register + +== マイルストーン + +* MachineCodeからMCLayerに変換するアセンブラ +** RV32Kアセンブリを受け取ってELFバイナリを出力する。 +** ELFバイナリである必要は微塵もないが、既存のLLVMコードベースを利用するためにはこれが必要。 +*** 最終的には自作バイナリに出力するようにしたい。 +** 実際に食わせるためにはELFバイナリを適当にstripする変換器を必要するかもしれない。 +** というか同一ファイル内でシンボル解決を行うリンカもどきが必要になる。あと `main` もとい `_start` がバイナリの一番最初にないといけないなどありそう。 +** 自作C標準ライブラリも必要かも。 + +== LLVMのソースコードを用意する + +LLVMのソースコードを取得する。今回の開発ではv8.0.0をベースとする。 +Git上でrv32kブランチを作り、その上で開発する。 + +.... +$ git clone https://github.com/llvm/llvm-project.git +$ cd llvm-project +$ git checkout llvmorg-8.0.0 +$ git checkout -b rv32k +.... + +== LLVM・Clangをビルドする + +<>, <>, <>を参考にしてLLVMとClangをNinjaを用いてビルドする。 +以降の開発において参考にするため、x86とRISC VをLLVM・Clangの出力可能ターゲットとして指定する。 + +なお、CPUがIntel Core i5 7200U、メモリが8GBのLet's note上でのビルドには1時間半程度要した。 + +.... +$ mkdir build +$ cd build +$ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCV" \ + ../llvm +$ cmake --build . # OR ninja +.... + +== LLVM・Clangを使う + +RV32バックエンドを使う例をいくつか掲げる<>。 + +.... +$ bin/clang -target riscv32-unknown-linux-gnu -S -o main.s main.c # main.cをRV32のアセンブリにコンパイル +$ bin/clang -target riscv32-unknown-linux-gnu -emit-llvm -o main.bc -c main.c # main.cをRV32用のLLVM IRにコンパイル +$ llvm-project/build/bin/llvm-dis main.bc -o - # LLVM IRをテキスト形式に変換 +.... + +== LLVMをテストする + +`llvm-lit` を使用してLLVMをテストできる。 + +.... +$ bin/llvm-lit test -s # 全てのテストを実行する +$ bin/llvm-lit -s --filter 'RV32K' test # RV32Kを含むテストを実行する +$ bin/llvm-lit -as --filter 'RV32K' test # テスト結果を詳細に表示する +.... + +== スケルトンバックエンドを追加する + +<>を参考に、中身のないスケルトンのバックエンドをLLVMに追加する。 +これによって `llc` などの出力などにRV32Kが追加される。 + +=== RV32KをTripleに追加する + +参照先<>のパッチのとおりに書き換える。 +基本的に `riscv32` という文字列を検索し、都度 `rv32k` の項目を追加していけばよい。 +ただし、RISC-Vには32bit( `riscv32` )と64bit( `riscv64` )の変種があるため変換テーブルを用意する必要があるが、 +RV32Kにはその必要はない。 `UnknownArch` を返せば良い。 + +コードを改変した後 `$ bin/llvm-lit -s --filter "Triple" test` でテストを行う。 + +=== RV32KのELF定義を追加する + +<>を参考にファイルを書き換える。 +途中 `llvm-objdump.cpp` の書き換え該当箇所が見当たらない。とりあえず放置(TODO)。 + +出力するELF形式のテストは、出力すべきELFの細部が決まっていないため、 +とりあえず書かないでおく(TODO)footnote:[この変更は後で行ったため、後ろの記述に齟齬が発生する場合がある(TODO)。]。 + +=== バックエンドを追加する + +<>を参考にファイルを修正・追加する。 +ただし `RV32KTargetMachine::RV32KTargetMachine()` の初期化子で使用している +`getEffectiveCodeModel()` がそのままではコンパイルエラーを出すため修正するfootnote:[同ファイル内にある同名のstatic関数を関数呼び出しの候補に入れていないようだ。原因不明。v8.0.0にて採用されているRISC-Vのコードを参考に、別の場所で定義されている同名関数を利用するよう修正した。]。 + +ファイル中のライセンス条項などは、将来的にはApache 2.0 Licenseを明記する予定だが、 +とりあえず削除しておくfootnote:[LLVMのARMバックエンドはApache 2.0 Licenseに例外条項を +付与したライセンスになっていた。検討の余地あり。]。 + +さて追加したバックエンドも含めてLLVMの再ビルドを行う。 +そのためには再び `cmake` を実行する必要がある: +.... +$ cmake -G Ninja \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DCMAKE_BUILD_TYPE="Debug" \ + -DBUILD_SHARED_LIBS=True \ + -DLLVM_USE_SPLIT_DWARF=True \ + -DLLVM_OPTIMIZED_TABLEGEN=True \ + -DLLVM_BUILD_TESTS=True \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DLLVM_USE_LINKER=lld \ + -DLLVM_TARGETS_TO_BUILD="X86" \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCV;RV32K" \ + ../llvm +$ cmake --build . # OR ninja +.... + +無事 `llc --version` にRV32Kが含まれるようになった: +.... +$ bin/llc --version +LLVM (http://llvm.org/): + LLVM version 8.0.0 + DEBUG build with assertions. + Default target: x86_64-unknown-linux-gnu + Host CPU: skylake + + Registered Targets: + riscv32 - 32-bit RISC-V + riscv64 - 64-bit RISC-V + rv32k - RV32K + x86 - 32-bit X86: Pentium-Pro and above + x86-64 - 64-bit X86: EM64T and AMD64 +.... + +== 簡易的なアセンブラを実装する + +スケルトンバックエンドに実装を追加して、簡易アセンブラを構築する。 +わざわざ「簡易」と銘打っているのは、命令を +`LI`, `MV`, `ADD`, `SUB`, `NOP` のみに限るためであるfootnote:[この方針はアセンブラを書き始めてから決めたため、以下の記述に不整合が生じている場合がある。いずれ修正する(TODO)。]。 +残りの命令については後の章で実装する予定であるfootnote:[蓋し `RV32KInstrInfo.td` を書くのは見た目によらず難しい。ある程度進んでから戻ってくるほうが良いだろう。]。 + +=== TableGenファイルを追加する + +LLVMのDSLであるTableGenを使用し、RV32Kのレジスタや命令について記述する。 +追加すべきTableGenファイルは次の通り: + +* `RV32KRegisterInfo.td`: レジスタ +* `RV32KInstrFormats.td`: 命令形式 +* `RV32KInstrInfo.td`: 命令 +* `RV32K.td`: 全体 + +`RISCV` ディレクトリ以下に `RISCVInstrFormatsC.td` や `RISCVInstrInfoC.td` などがあるので参考にする。 +`LWSP` はエンコードされると `SP` の情報を持たないが、表記上は `lwsp rd, 100(sp)` と書くため `SP` をとる必要がある。 +これのために `RegisterClass` として `SP` を定義している。ただし `SP` には `X2` のみが所属する。 +また、単純に全てのレジスタを `GPR` として1つの括りにしてしまうと、 +`sub` 命令のオペランドとして `x2` などが許容されてしまう。 +これを防ぐため、 `x8` から `x15` を束ねて `GPRC` という名の `RegisterClass` を作成し、これを `sub` 命令のオペランドの型として指定する。 + +即値を単純にとる命令はそのまま記述すればよいが、デコードした値を左シフトするような命令は注意が必要である。 +すなわち即値の `[7:2]` のみを生成コード中に持つような場合は `Instruction` 中に8bitの即値を宣言し、 +その `[7:2]` 部分が `Inst` と対応するという形をとる。 +なお、この点について `RISCVInstrFormatsC.td` に書いてある `RVInst16CJ` の実装が間違っている気がする。 +`offset` は12bitではなかろうか(TODO <>)。また `Bcz` も同様の問題があるように見える(TODO)。 + +文字列リテラル中の変数展開をTableGenはサポートしている。すなわち `"$rd, ${imm}(${rs})"` と書けばレジスタ `rd` や `rs`、 即値 `imm` の中身が展開される。 +なおここで変数名を `{}` で囲む**必要がある**。というのもTableGenでは `(` や `)` +もオペランドの名前として認識されてしまうからだ。 +実際 `$rd, $imm(${rs})` と書くと次のようなエラーになる。 +.... +error: unable to find operand: 'imm(' +.... +なおここでの表記はパーズの際に行うパターンマッチのパターンとしての役割と、 +アセンブリとして出力を行う際のパターンとしての役割があるようだ(TODO: ほんまか?)。 + +//`ins` や `outs` で使用するレジスタ・即値の名前は `RV32KInstrFormats.td` でフィールドとして定義した名前と一致する必要があるようだ。//TODO: ほんまか? +2アドレス方式などで、読み込むレジスタと書き込むレジスタが一致する場合 `let Constraints = "$rd = $rd_wb";` などと書けばよい。 + +必要なTableGenファイルを追加した後、これらのTableGenファイルが正しいかどうか `llvm-tblgen` を用いて確認する: +.... +$ bin/llvm-tblgen -I ../llvm/lib/Target/RV32K/ -I ../llvm/include/ -I ../llvm/lib/Target/ ../llvm/lib/Target/RV32K/RV32K.td +.... +初めて実行すると、おそらくたくさんエラーがでるので頑張って全て修正する。 +変換が成功した場合、生成されたコードが、自分が得たいものになっているかどうかを確認する。 + +=== RV32K用の `MCTargetDesc` を追加する + +RV32KのELF形式オブジェクトファイルを出力できるようにする。そのために `LLVMInitializeRV32KTargetMC()` を実装する。 +`MCTargetDesc` サブディレクトリを作成し、その中に `RV32KMCTargetDesc.{cpp,h}` を作成する。 +他のファイルに依存する部分はコメントアウトして、適当な `CMakeLists.txt` や `LLVMBuild.txt` を作成・編集すると、 +ビルドが通るようになる。そこで次に実装すべき場所を次のように特定する: +.... +$ bin/llvm-mc -arch=rv32k -filetype=obj foo.s +llvm-mc: /home/anqou/workspace/llvm-project/llvm/tools/llvm-mc/llvm-mc.cpp:355: int main(int, char **): Assertion `MAI && "Unable to create target asm info!"' failed. +Stack dump: +0. Program arguments: bin/llvm-mc -arch=rv32k -filetype=obj foo.s + #0 0x00007ff0c8f93e26 llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/anqou/workspace/llvm-project/llvm/lib/Support/Unix/Signals.inc:495:11 + #1 0x00007ff0c8f94029 PrintStackTraceSignalHandler(void*) /home/anqou/workspace/llvm-project/llvm/lib/Support/Unix/Signals.inc:559:1 + #2 0x00007ff0c8f91eb3 llvm::sys::RunSignalHandlers() /home/anqou/workspace/llvm-project/llvm/lib/Support/Signals.cpp:68:5 + #3 0x00007ff0c8f947c4 SignalHandler(int) /home/anqou/workspace/llvm-project/llvm/lib/Support/Unix/Signals.inc:0:3 + #4 0x00007ff0c8af73c0 __restore_rt (/usr/lib/libpthread.so.0+0x123c0) + #5 0x00007ff0c8628d7f __GI_raise (/usr/lib/libc.so.6+0x37d7f) + #6 0x00007ff0c8613672 __GI_abort (/usr/lib/libc.so.6+0x22672) + #7 0x00007ff0c8613548 _nl_load_domain.cold.0 (/usr/lib/libc.so.6+0x22548) + #8 0x00007ff0c8621396 (/usr/lib/libc.so.6+0x30396) + #9 0x00005588f8384783 main /home/anqou/workspace/llvm-project/llvm/tools/llvm-mc/llvm-mc.cpp:357:3 +#10 0x00007ff0c8615223 __libc_start_main (/usr/lib/libc.so.6+0x24223) +#11 0x00005588f838402e _start (bin/llvm-mc+0x2402e) +zsh: abort (core dumped) bin/llvm-mc -arch=rv32k -filetype=obj foo.s +.... +この場合 `AsmInfo` が次に実装すべき場所だと分かる。 + +具体的な編集方法はパッチ<>を参考にする。 + +`RV32KAsmBackend.cpp` の作成時に多くのコンパイルエラーが出た。どうやらこのあたりの仕様変更があったようだ。 +具体的には下記のとおりである。 + +* `applyFixup` の引数変更。 +* `mayNeedRelaxation` の引数変更。 +* `writeNopData` の引数変更。 +* `MCAsmBackend::MCAsmBackend` の引数変更。 +* `createObjectWriter` の `createObjectTargetWriter` への名称・引数・戻り値変更。それに伴う `createRV32KELFObjectWriter` の引数・戻り値変更。 + +以上を実装して動かすとSEGVで落ちる。デバッガで追いかけると、どうやら `MCSubtargetInfo` の生成関数である `createRV32KMCSubtargetInfo` +を実装しなければならないようだ。RISC Vの最新のソースコードを参考に実装する。 +基本的にはTableGenが生成する `createRV32KMCSubtargetInfoImpl` をそのまま使えば良いが、これを使用するためには +`CMakeLists.txt` に `tablegen(LLVM RISCVGenSubtargetInfo.inc -gen-subtarget)` を追加する必要があるため注意が必要だ。 +ちなみにRISC Vのパッチ群では、これらは<>で実装されている。なぜここで実装しなければならなくなったかは、よくわからない。 + +諸々実装すると、次のように出力される: +.... +$ bin/llvm-mc -arch=rv32k -filetype=obj foo.s +bin/llvm-mc: error: this target does not support assembly parsing. +.... + +なお `createRV32KMCCodeEmitter` を実装しなくてもこの表示が出るが、それでいいのかはよくわからない。 +とりあえずパッチ<>にある分は全て実装する。 + +`createRV32KMCCodeEmitter` を実装する過程で、TableGenがどのように `InstrFormats` でのフィルード名と +`InstrInfo` での `ins` と `outs` の名前を対応付けるのか調べた。例えば次のように `BEQZ` 命令をTableGenにて宣言したとする。 +.... +class RVInstCB funct3, bits<2> op, dag outs, dag ins, string opcodestr, string argstr> +: RVInst { + bits<9> offset; + bits<3> rs1; + + let Inst{15-13} = funct3; + let Inst{12} = offset{8}; + let Inst{11-10} = offset{4-3}; + let Inst{9-7} = rs1; + let Inst{6-5} = offset{7-6}; + let Inst{4-3} = offset{2-1}; + let Inst{2} = offset{5}; + + let Opcode = op; +} +.... +.... +def BEQZ : RVInstCB<0b110, 0b01, (outs), (ins GPR:$rs1, simm9_lsb0:$imm), + "beqz", "$rs1, $imm">; +.... +このときこのTableGenソースコードは期待通りに**動かない**(多分:TODO)。 +なぜなら `class RVInstCB` で指定した `offset` と `rs1` が、 `def` で指定した `$rs1` と `$imm` に対応しないからである。 +TableGenの実装footnote:[`utils/TableGen/CodeEmitterGen.cpp` などを参照。]や +ドキュメント<>によると、これらを対応させる方法には2種類ある: + +. 両者で同じ名前を採用する。 +. 両者で宣言順序を揃えるfootnote:[関係するすべての `class` での宣言が対象になるようだが、それらがどの順に対応するのかは判然としない。]。 + +上の例では `RVInstCB` では `offset` が先に宣言されているにも関わらず、 `BEQZ` では `$rs1` を先に使用した。 +この結果 `$rs1` には `offset` と `rs1` の両方が結びつくことになるfootnote:[名前での対応により `rs1` が結びつき、順序による対応で `offset` が結びつく。]。 +なおこのような重複が発生してもTableGenは特に警告等を表示しないようだ。よくわからない +footnote:[アセンブルではエラーが出ないが、ディスアセンブルで「オペランドが無い」というエラーが(添字チェックに引っかかるという形で)出る場合がある。]。 + +`RV32KMCCodeEmitter::encodeInstruction` 内で使用している `support::endian::Writer(OS).write(Bits);` はそのままでは通らない。 +RISC Vを参考に `support::endian::write(OS, Bits, support::little);` としておく。 + +=== `RV32KAsmParser` を追加する + +アセンブリをパーズするための `RV32KAsmParser` を追加する。これによってアセンブリをオブジェクトファイルに直すことができるようになる。 +新しく `AsmParser` ディレクトリを作成し、その中に `RV32KAsmParser.cpp` 及びそれに付随するファイルを作成する。 +例によって、パッチ<>を参考に実装をすすめる。 + +`RV32KAsmParser.cpp` に記述するコード量がそれなりにあるため少々面食らうが、 +要するに `RV32KAsmParser` と `RV32KOperand` の2つのクラスを作成し、実装を行っている。 +パーズの本体は `RV32KAsmParser::ParseInstruction` である。これを起点に考えれば、それほど複雑な操作はしていない。 + +即値に関して `SImm6` を `SImm12` に直す必要がある。これらはTableGenが生成するコードから呼ばれるようだ。 + +ところでRV32Kの命令には `add` などレジスタを5bitで指定する命令と、 +`sub` などレジスタを3bitで指定する命令の2種類がある。エンコードに際して、これらの区別のための特別な処理は必要ない。 +というのも、3bitでレジスタを指定する場合その添字の下位3bit以外が無視されるため、 +結果的に正しいコードが出力されるfootnote:[LLVMのRISC V実装でもこの方式が採用されている。]。 +例えば `x8` を指定すると、これに `1000` という添字が振られ、4bit目を無視することで `000` となるため、 +3bitでのレジスタ指定方法として正しいものになる。 + +そうこうしていると簡易的なアセンブラが完成する: +.... +$ cat foo.s +li x9, 3 +mv x11, x1 +sub x9, x10 +add x8, x1 +nop + +$ bin/llvm-mc -arch=rv32k -filetype=obj foo.s | od -tx1z -Ax -v +000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< +000010 01 00 f5 00 01 00 00 00 00 00 00 00 00 00 00 00 >................< +000020 68 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 >h.......4.....(.< +000030 04 00 01 00 8d 44 86 85 89 8c 06 94 01 00 00 00 >.....D..........< +000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000050 00 2e 74 65 78 74 00 2e 73 74 72 74 61 62 00 2e >..text..strtab..< +000060 73 79 6d 74 61 62 00 00 00 00 00 00 00 00 00 00 >symtab..........< +000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +000090 07 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 >................< +0000a0 50 00 00 00 17 00 00 00 00 00 00 00 00 00 00 00 >P...............< +0000b0 01 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 >................< +0000c0 06 00 00 00 00 00 00 00 34 00 00 00 0a 00 00 00 >........4.......< +0000d0 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 >................< +0000e0 0f 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 >................< +0000f0 40 00 00 00 10 00 00 00 01 00 00 00 01 00 00 00 >@...............< +000100 04 00 00 00 10 00 00 00 >........< +000108 +.... +0x34から0x3dにある `8d 44 86 85 89 8c 06 94 01 00` が出力であり、 +正しく生成されていることが分かるfootnote:[白状すると、確認していない。次のステップでテストを書くため、そこで確認する。]。 +We made it! + +// ///// 以下は使用レジスタがx3-x10だと思っていたときの記述。ついでにコードも末尾に付けた。 +//そのため、3bitに収まらないレジスタを指定することはできないfootnote:[無理に指定すると4bit目が無視される。]。 +//これを仕様に従って訂正するfootnote:[なおRV32Cの場合、Integer Register Numberとして `x8` から `x15` が指定されている。これらは下位3bitをとればRVC Register Numberとして合法であるため、訂正の必要がない。]。 +//すなわち3bitでレジスタを指定する命令(現状では `SUB` のみ)では `x3` を0として順に添え字をふる。 +// +//これを行うためには、アセンブリをコードに変換する際に、そのレジスタ番号を補正すればよい。 +//このようなレジスタオペランドエンコードのフックを行う関数を指定する場所として `RegisterOperand` の `EncoderMethod` がある。 +//そこで `X3` から `X10` を `GPRC` という `RegisterClass` とした上で、これを `RegisterOperand` で包み `ShiftedGPRC` とするfootnote:[なお、これを使用した具体例を見つけられていないため、この使い方が正しいかどうかはよくわからない(TODO)。]。 +//これの `EncoderMethod` として `RV32KEncodeShiftedGPRCRegisterOperand` という関数を指定する。 +//これは `RV32KMCCodeEmitter` クラスのメンバ関数として定義する。これによって任意の処理をフックすることができる。 +//ちなみにこれは最近入った新しい機能である。https://reviews.llvm.org/rL303044 +// +//ビルドすると、次のような出力結果が得られる: +//.... +//$ cat foo.s +//li x3, 3 +//mv x3, x4 +//sub x9, x10 +//add x3, x4 +//nop +// +//$ bin/llvm-mc -arch=rv32k -filetype=obj foo.s | od -tx1z -Ax -v +//000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< +//000010 01 00 f5 00 01 00 00 00 00 00 00 00 00 00 00 00 >................< +//000020 68 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 >h.......4.....(.< +//000030 04 00 01 00 8d 41 92 81 1d 8f 92 91 01 00 00 00 >.....A..........< +//000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +//000050 00 2e 74 65 78 74 00 2e 73 74 72 74 61 62 00 2e >..text..strtab..< +//000060 73 79 6d 74 61 62 00 00 00 00 00 00 00 00 00 00 >symtab..........< +//000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +//000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< +//000090 07 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 >................< +//0000a0 50 00 00 00 17 00 00 00 00 00 00 00 00 00 00 00 >P...............< +//0000b0 01 00 00 00 00 00 00 00 01 00 00 00 01 00 00 00 >................< +//0000c0 06 00 00 00 00 00 00 00 34 00 00 00 0a 00 00 00 >........4.......< +//0000d0 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 >................< +//0000e0 0f 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 >................< +//0000f0 40 00 00 00 10 00 00 00 01 00 00 00 01 00 00 00 >@...............< +//000100 04 00 00 00 10 00 00 00 >........< +//000108 +//.... +//0x34から0x3cまでの `8d 41 92 81 1d 8f 92 91 01` が具体的なコードである。 +// ////// +// +// +//def GPRC : RegisterClass<"RV32K", [i32], 32, (add +// X3, X4, X5, X6, X7, X8, X9, X10 +// )>; +// +//def ShiftedGPRC : RegisterOperand { +// let EncoderMethod = "RV32KEncodeShiftedGPRCRegisterOperand"; +// //let DecoderMethod = "RV32KDecodeShiftedGPRCRegisterOperand"; +//} +// +// +// +//uint64_t +//RV32KEncodeShiftedGPRCRegisterOperand(const MCInst &MI, unsigned no, +// SmallVectorImpl &Fixups, +// const MCSubtargetInfo &STI) const; +// +//uint64_t RV32KMCCodeEmitter::RV32KEncodeShiftedGPRCRegisterOperand( +// const MCInst &MI, unsigned no, SmallVectorImpl &Fixups, +// const MCSubtargetInfo &STI) const { +// const MCOperand &MO = MI.getOperand(no); +// if (MO.isReg()) { +// uint64_t op = Ctx.getRegisterInfo()->getEncodingValue(MO.getReg()); +// assert(3 <= op && op <= 10 && "op should belong to GPRC."); +// return op - 3; +// } +// +// llvm_unreachable("Unhandled expression!"); +// return 0; +//} + +== 簡易アセンブラのテストを書く + +適当な入力に対してアセンブラが正しい出力を行うかを確認しつつ +新たなバグの発生を抑止するために、LLVMのフレームワークを用いてテストを書く。 + +=== `RV32KInstPrinter` を実装する + +テストを書くために、まずRV32Kのためのinstruction printerを作成する。 +これは内部表現である `MCInst` から文字列表現であるアセンブリに変換するための機構である。 +この後で作成するテストでは、アセンブリを `MCInst` に変換した上で、 +それをアセンブリに逆変換したものがもとのアセンブリと同じであるか否かでテストを行う。 + +まず例によって `InstPrinter` ディレクトリを作成した後、適切に `CMakeLists.txt` や +`LLVMBuild.txt` を作成・編集する。 +また `RV32KInstPrinter` を作成するための関数を `LLVMInitializeRV32KTargetMC` +にて登録する必要がある。 + +その後 `InstPrinter/RV32KInstPrinter.{cpp,h}` を作成する。 +いつものようにこれはパッチ<>を参考にして行う。 + +`RV32KInstPrinter::printRegName` の実装で用いる `getRegisterName` の第二引数に +何も渡さなければAltNameを出力に使用する。第二引数に `RV32K::NoRegAltName` を渡すことで、 +レジスタを `X0`, `X1`, ... と表示することができる。 + +上記をビルドした後、 +実際にアセンブリを `MCInst` に変換し、さらにそれをアセンブリに戻すには、 +`llvm-mc` を用いる: +.... +$ bin/llvm-mc -arch=rv32k -show-encoding foo.s + .text + li x9, 3 # encoding: [0x8d,0x44] + mv x11, x1 # encoding: [0x86,0x85] + sub x9, x10 # encoding: [0x89,0x8c] + add x8, x1 # encoding: [0x06,0x94] + nop # encoding: [0x01,0x00] +.... +`-show-encoding` を指定することよって当該アセンブリがどのような機械語に +翻訳されるか確認することができる。テストではこの機械語の正誤も確認する。 + +=== テストを書く + +簡易アセンブラが正しく動作していることを保証するためにテストを書く。 +ここでは「適当な入力を与えたときに正しく解釈し正しい機械語を生成するか」を確認する。 + +前節と同様にパッチ<>を参考に記述する。 +まず `test/MC/RISCV` ディレクトリを作成する。 +その中に `rv32k-valid.s` と `rv32k-invalid.s` を作成し、 +前者で正しいアセンブリが適切に処理されるか、 +後者で誤ったアセンブリに正しくエラーを出力するかを確認する。 + +記述後 `llvm-lit` を用いてテストを行う: +.... +$ bin/llvm-lit -as --filter 'RV32K' test +PASS: LLVM :: MC/RV32K/rv32k-valid.s (1 of 2) +Script: +-- +: 'RUN: at line 1'; /home/anqou/workspace/llvm-project/build/bin/llvm-mc /data/anqou/workspace/llvm-project/llvm/test/MC/RV32K/rv32k-valid.s -triple=rv32k -show-encoding | /home/anqou/workspace/llvm-project/build/bin/FileCheck -check-prefixes=CHECK,CHECK-INST /data/anqou/workspace/llvm-project/llvm/test/MC/RV32K/rv32k-valid.s +-- +Exit Code: 0 + + +******************** +PASS: LLVM :: MC/RV32K/rv32k-invalid.s (2 of 2) +Script: +-- +: 'RUN: at line 1'; not /home/anqou/workspace/llvm-project/build/bin/llvm-mc -triple rv32k < /data/anqou/workspace/llvm-project/llvm/test/MC/RV32K/rv32k-invalid.s 2>&1 | /home/anqou/workspace/llvm-project/build/bin/FileCheck /data/anqou/workspace/llvm-project/llvm/test/MC/RV32K/rv32k-invalid.s +-- +Exit Code: 0 + + +******************** +Testing Time: 0.11s + Expected Passes : 2 +.... +テストに通っていることが分かる。YATTA! + +== アセンブラに残りの命令を追加する + +簡易アセンブラに残りの命令を実装する。「簡易」を取り払うということである。 +参考にするRISC Vのパッチは<>である。 + +=== `lw` 命令を追加する + +`lw` 命令は7bit符号なし即値をとる。この即値の下位2ビットは0であることが要求される。 +この即値はアセンブリ中でもLLVM内部でも7bitのまま保持される。 + +`ParserMatchClass` は `AsmParser` がどのようにそのオペランドを読めばよいか指定する。 +TableGenで直接指定できない性質は `EncoderMethod` などを用いてフックし、C\++側から指定する<>。 + +`uimm8_lsb00` の `let EncoderMethod = "getImmOpValue";` は一見不思議に見えるが正常で、 +ここでえられた即値は `lw` の `imm` になり、そちらで整形される。 +[[RVInst16CJ-offset]]だが `simm12_lsb0` は `getImmOpValueAsr` が指定されるので `offset` は12bitではなく11bitになっている。 + +`uimm8_lsb00` をつくると `RV32KOperand::isUImm7Lsb00` が無いと言われるので追加する必要がある。 +この `UImm7Lsb00` という名前は `ImmAsmOperand` の `Name` に従って作られているようだ。 + +`generateImmOutOfRangeError` の実装に使われているおもしろデータ構造 `Twine` は +文字列の連結を二分木で持つことで高速に行うことができるらしい<>。 + +メモリ表記のアセンブリをパーズするためには `RV32KAsmParser` を変更する必要がある。 +この処理のエントリポイントは `parseOperand` で、ここから `parseMemOpBaseReg` を呼び出す。 + +`li x11, x12, x13` というアセンブリを流し込むと `invalid operand for instruction` を期待 +しているにもかかわらず `immediate must be an integer in the range [-32, 31]` と出てしまう。 +`LW` を追加する前はそのメッセージが出ていたのでいろいろ調査したが原因不明。 +`git stash` してもとのコードをテストしてみると、実際には +.... +$ bin/llvm-mc -arch=rv32k -show-encoding test.s + .text +test.s:1:14: error: invalid operand for instruction +li x11, x12, x13 + ^ +.... +となっている。すなわちエラーが発生している位置がずれている。もとからおかしかったのだ。 +おそらくこれは将来的に解決されるだろうという判断のもと保留(TODO)。 + +テストを忘れずに追加してコミット。 + +ところで `getImmOpValue` はオペランドにある即値をそのまま返す関数であり、 +即値の `EncoderMethod` にフックとして使用する。 +しかしこれは `simm6` の実装時には不要であり、 +実際 `let EncoderMethod = "getImmOpValue";` をコメントアウトしてもテストには通る。 +<>では `getImmOpValueAsr1` のみが実装されている。 +RISC Vの最終的なコードで `getImmOpValue` は、即値として使えるような複雑な命令を解釈している。 + +=== `sw` 命令を追加 + +`lw` 命令を追加する際に諸々を整えたので、やるだけ。 + +=== `lwsp` 命令を追加 + +tdファイルに `lwsp` を追加してビルドしようとすると次のようなエラーが出た。 +.... +error: Invalid bit range for value + let Inst{3-2} = imm{7-6}; + ^ +.... +どうやら `imm` が6bit即値として扱われているようだ。原因を探すと `RVInstCI` 内で `imm` を +6bitと定義していたことが原因だったようだ。RISC V側での実装に合わせ、これを10bitに変更する。 +同様に `RVInstCI` である `li` の定義も変更し、即値については各々の `def` で適切に処理するようにした。 + +`sp` を用いたテストを書くと、出力時にこれが `x2` に変換されてしまうためにエラーになってしまう。 +`RegisterInfo.td` の `RegAltNameIndices` の定義を変更しAltNameで出力をまかなえるようにした上で、 +`RV32KInstPrinter::printRegName` の実装を改変した。 + +=== `swsp` 命令を追加 + +tdファイルに `swsp` を追加するだけ。 + +=== `j` 命令を追加 + +RISC Vの仕様上 `c.j` 命令は12bitの即値フィールドを持つ。 +しかしLLVMのRISC Vバックエンド実装では `RVInst16CJ` は11bitの即値を持つ。 +これは末尾1bitが必ず0であるために( `lsb0` )この1bitを捨てることができるためである。 +しかしこの実装は `lw` などがとる即値をそのままのビット数でデータを持つことと一貫性がないように +見受けられる。そこでRV32K実装では `j` 命令などもすべてそのままのビット数でデータを持つことにする。 +したがって `simm12_lsb0` などに指定する `EncoderMethod` は `getImmOpValueAsr1` ではなく +`simm6_lsb00` と同様の `getImmOpValue` となる。 + +=== `jal` ・ `jr` ・ `jalr` ・ `beqz` ・ `benz` 命令を追加 + +やるだけ。 + +=== 属性footnote:[これを属性と呼んでいいかどうかよくわからないが、わかりやすいし呼びやすいのでとりあえずこれで。内部的にはrecordと書かれることが多いようだ。]を指定する + +`isBranch` やら `isTerminator` やら `hasSideEffects` やらを命令毎にちゃんと設定する。 +これはアセンブラでは意味をなさないが、コンパイラ部分を作り始めると重要になるのだろう。多分(TODO: ほんまか?)。 + +ところでどのような属性フィールドがあるのかはリファレンスを読んでも判然としない。 +TableGenのソースコードを読みに行くと `llvm/utils/TableGen/CodeGenInstruction.h` にて +属性のための大量のフラグが定義されているが、各々がどのような目的で使用されるかは書いていない。 +`llvm/include/llvm/CodeGen.h` に `mayLoad` や `isTerminator` ・ `isBarrier` などの一部分のフラグについて説明がある一方、 +`hasSideEffects` などのフラグについては説明がない。 + +`hasSideEffects` は `llvm/lib/Target/RISCV/RISCVInstrInfoC.td` の中で `C_UNIMP` と `C_EBREAK` でのみ `1` に設定されている。 +特殊な事象が起こらない限り `0` にしておいて良さそうだ。 + +`class` や `def` の中に `let field = value;` と書くのと、外に `let field = value in ...` ないし `let field = value in { ... }` +と書くのは同じ効果を持つが、外に書くと複数の `class` ・ `def` にまとめて効果を持つという点においてのみ異なる<>。 + +`class` の段階で `hasSideEffects` などについて列挙するのは、あまりよいスタイルと思えない。 +というのもRV32CなどのISAでは、エンコーディングフォーマットが同じでも全く違う意味をもつ命令を +意味することが往々にしてあるからだ。また我々の開発のように後々ISAを変更することが半ば確定している状況において、 +`class` と複数の `def` におけるフラグの整合を保ちつつ変更するのは骨が折れる。 +それなら `class` はビットパターンのみを扱うものとし、 +`def` にその意味的な部分(フラグの上げ下げ)を書くほうが、後々の拡張性を考えるとよいと思うfootnote:[ただしこれは命令数が少ない場合に限るのかもしれない。]。 + +=== レジスタ指定の `GPR` と `GPRC` を使い分ける + +`RV32KRegisterInfo.td` において `GPRC` は `X8` から `X15` までの汎用レジスタを指し、 +`GPR` は `X1` と `X2` を含むすべてのレジスタを指すように定義されている。 +RV32Kの命令のうち3bit幅でレジスタ番号を指定する命令には `GPRC` を用い、 +5bit幅でレジスタ番号を指定する命令には `GPR` を用いるようにすることで、 +LLVMに適切なレジスタを教えることができる。 + +この別はディスアセンブラを開発する段になって重要になる。というのも、バイナリ中にレジスタ番号として `1` と出てきた場合、 +このオペランドが `GPR` か `GPRC` かによって指すレジスタが `x1` または `x9` と異なるからである。 + +== ディスアセンブラを実装する + +アセンブラが一通り実装できたので、今度はディスアセンブラを実装する。 +これによって `llvm-objdump -d` を使用することができるようになる。 +このディスアセンブラのテストには、すでに記述した `rv32k-valid.s` などのテストを使用できる。 +すなわちアセンブラによってアセンブリを機械語に直し、さらにディスアセンブラによって機械語をアセンブリに直した結果が、 +元のアセンブリと一致するかどうかをみればよいfootnote:[<>にはfull round trip testingと表現されている。]。 + +参考にするRISC Vのパッチは<>である。 + +=== `RV32KDisassembler` を追加する + +`*_lsb0` や `*_lsb00` の取り回しがよくわからなくなったので整理footnote:[整理というより半分推測だが。]する。 +TableGenで指定するビットによって、即値のどの部分をどのように命令コード中に配置するかを決定することができる。 +これによって `RV32KCodeEmitter::getImmOpValueAsr1` などの `EncoderMethod` に頼る必要がなくなる。 +同様にディスアセンブラについても `RV32KDisassembler::decodeSImmOperandAndLsl1` などの `DecoderMethod` に +頼る必要がなくなる。言い換えれば、これらのフック関数が受け取る即値は、TableGenで指定したビット単位の +エンコード・デコードをすでに受けた値になるfootnote:[バイナリに直接触れるのはTableGenが出力するコードなので、当然といえば当然だが。]。 +`RV32KAsmParser::isUImm12` などが呼び出す `getConstantImm` が返す即値も同様である。 + +ナイーブに実装すると `lwsp` や `swsp` が入ったバイナリをディスアセンブルしようとしたときに +エラーがでる。これは例えば次のようにして確認することができる。 +.... +$ cat test.s +lwsp x11, 0(sp) + +$ bin/llvm-mc -filetype=obj -triple=rv32k < test.s | bin/llvm-objdump -d - +.... +原因は `lwsp` や `swsp` がアセンブリ上はspというオペランドをとるにも関わらず、 +バイナリにはその情報が埋め込まれないためである。このためディスアセンブル時に +オペランドが一つ足りない状態になり、配列の添字チェックに引っかかってしまう。 + +これを修正するためには `lwsp` や `swsp` に含まれる即値のDecoderが呼ばれたときをフックし、 +`sp` のオペランドが必要ならばこれを補えばよいfootnote:[この実装手法はRISC Vのそれによる。かなりad-hocだと感じるが、他の方法が分からないのでとりあえず真似る。]。 +この関数を `addImplySP` という名前で実装する。ここで即値をオペランドに追加するために呼ぶ +`Inst.addOperand` と `addImplySP` の呼び出しの順序に注意が必要である。 +すなわち `LWSP` を `RV32KInstrInfo.td` で定義したときのオペランドの順序で呼ばなければ +`lwsp x11, sp(0)` のようなおかしなアセンブリが生成されてしまう。 + +ちなみにエンコード方式にコンフリクトがある場合はビルド時に教えてくれる。 +.... +Decoding Conflict: + 111...........01 + 111............. + ................ + BNEZ 111___________01 + BNEZhoge 111___________01 +.... + +これを防ぐためには、もちろん異なるエンコード方式を指定すればよいのだが、 +他にディスアセンブル時に命令を無効化する方法としてTableGenファイルで +`isPseudo = 1` を指定して疑似命令にしたり +`isCodeGen = 1` を指定してコード生成時にのみ効力を持つ +命令にすることなどができる。 + +== relocationとfixupに対応する + +ワンパスでは決められない値についてあとから補うための機構であるfixupと、 +コンパイル時には決定できない値に対してリンカにその処理を任せるためのrelocationについて +対応する。参考にするパッチは<>。 + +必要な作業は大きく分けて次の通り。 +* Fixupの種類とその内容を定義する。 +* Fixupを適用する関数を定義する。 +* アセンブラがFixupを生成するように改変する。 +* Fixupが解決されないまま最後まで残る場合は、これをrelocationに変換する。 + +=== Fixupを定義する + +`RV32KFixupKinds.h` を新規に作成し `enum Fixups` を定義する。 +RV32Kv1では `beqz` ・ `bnez` がとる8bitの即値と `j` ・ `jal` がとる11bitの即値のために +必要なFixupを定義する。 +なお `enum` の最初のフィールドには `FirstTargetFixupKind` を設定し、 +最後のフィールドは `NumTargetFixupKinds` として、定義した `enum` のフィールドの個数を設定する。 + +Fixupの種類からその情報を返すのは `RV32KAsmBackend::getFixupKindInfo` が行う。 +ここでの `offset` の値は、Fixupのために得られた即値を何ビット左シフトするかを意味し、 +`bits` は TODO を意味している。そこで、即値のフィールドが命令中で2つに分かれている命令のためのFixup +である `fixup_rv32k_branch` では `offset` と `bits` を各々 `0` と `16` にしておく +footnote:[これはRISC Vの実装を真似ている。]。 + +=== Fixupを適用する関数を定義する + +要するに `RV32KAsmBackend::applyFixup` の実装である。補助関数として `adjustFixupValue` も実装する +footnote:[ところでRISC Vの `fixup_riscv_rvc_{jump,branch}` の実装では即値の幅のチェックを行っていない。 +なぜかはよくわからない。あと `fixup_riscv_jal` のコメントが間違っている気がする。] + +=== アセンブラにFixupを生成させる + +`AsmParser` と `CodeEmitter` を書き換え、必要なときにアセンブラにFixupを生成させるようにする。 + +さてRISC Vでは `%hi` や `%lo` などが使えるために、これらを評価するための機構として `RISCVMCExpr` を導入している。 +具体的には `RISCVAsmParser::parseImmediate` でトークンに `AsmToken::Percent` が現れた場合に +`RISCVAsmParser::parseOperandWithModifier` を呼び出し、この中でこれらをパーズして `RISCVMCExpr` を生成している。 + +しかしRV32Kではこれらの `%` から始まる特殊な即値footnote:[内部的にはoperand with modifiersと呼ばれているようだ。] +に対応せずfootnote:[とりあえずはね。]、あくまで分岐命令の即値におけるラベルのFixupのみが行えれば十分である。 +footnote:[なおRISC Vの `classifySymbolRef` を見るとシンボルの引き算ができるように見えるが、実際に試してみると `getImmOpValue` で +`Unhandled expression` が出て落ちてしまった。よくわからない。即値としては認識される( `isSImm12` などが `true` を返す)が、 +Fixupとして受領されない( `getImmOpValue` でエラーになる)気がする。TODO]。 + +そこでまず `isSImm9Lsb0` などにシンボルが来た場合には `true` を返すようにする。 +これはすなわち、即値を指定するべきところにラベル名が来た場合は `true` とするということである +footnote:[ここで `isImm` を呼び出す必要があることに注意が必要である。「即値」と聞くと整数値のみを受け取ると誤解しがちだが、 +実際には `RV32KOperand` の `ImmOp` は `MCExpr` をその値として持っている。したがって、シンボルなどを含めた +即値を求めるための演算そのものが対象になっている。すなわち `isImm` が `true` であることを確認することで、 +ラベル名が来るべきところにレジスタ名などが来ないことを担保しているのである。 +またより形式的には、これは `RV32KOperand` が持つ共用体の中身が `ImmOp` となっていることを保証している。]。 +その次に `getImmOpValue` を変更し、即値を書くべき場所にシンボルが来ている場合にはFixupを生成するようにする。 +このとき `fixup_rv32k_branch` と `fixup_rv32k_jump` のいずれを発行するかは、 +オペランドの種類で `switch-case` して判断している。 +これは良くないコーディングスタイルであり、実際<>ではこれを避けるために +種々のInstFormatに手を加えるとはっきり書いてあるのだが、ここでは作業の単純さを重視して +ハードコーディングすることにする。 + +それから `RV32KAsmParser::parseImmediate` を変更し `AsmToken::Identifier` が来たときには +`MCSymbolRefExpr` を生成するようにしておく。この変更は<>に含まれていたものだが、 +取り込み忘れていた。 + +=== Fixupからrelocationへの変換部を実装する + +`RV32KELFObjectWriter::getRelocType` を実装すればよいのだが、実のところRV32Kにおけるrelocationはほとんど想定おらず、 +仕様も決まっていない。そこでここでは実装を省略する。 + +=== Fixupのためのテストを書く + +`.space` を使って適当に間隔を開けながら命令を並べ、正しく動作しているかを確かめる。 + +これでアセンブラ部分は終了。We made it! + +== コンパイラのスケルトンを作成する + +空の関数とALUオペレーションをサポートできるような最小のバックエンドを作成する。 + +* `RV32KAsmPrinter` と `RV32KMCInstLower` +** `MachineInstr` を `MCInst` に変換する。 +** `llvm::LowerRV32KMachineInstrToMCInst` で `MCInst` のオペコード・オペランドを設定する +* `RV32KInstrInfo` +** TableGenによる自動生成。命令を表す `enum` の定義。 +* `RV32KRegisterInfo` +** ほとんどTableGenによる自動生成。 +** `getReservedRegs` で変数に割り付ける**べきでない**レジスタを指定する。 +** `getFrameRegister` はframe indicesに用いられるレジスタを指定する(?:TODO) +* `RV32KISelDAGToDAG` +** `SelectionDAG` ノードに対して適切なRV32K命令(多分 `MachineInstr` )を見繕う。 +** `RV32KDAGToDAGISel::Select` がエントリポイント +*** ただしこれはTableGenが生成した `SelectCode` 関数を呼んでいるだけ。 +* `RV32KISelLowering` +** LLVM IRを `SelectionDAG` に変換する。 +** この処理はほとんどターゲット非依存である。 +*** フックを設定して挙動を変化させる。 +* `RV32KInstrInfo.td` +** ここに「 `SelectionDAG` をどのようにRV32Kの命令セット(多分 `MachineInstr` )に変換するか」が書かれる。 +** `def : Pat<(hoge), (piyo)>;` という表記で「 `hoge` に `SelectionDAG` がパターンマッチしたとき `piyo` に変換する」を意味する。 +*** ここで `piyo` に記入するのは `ins` のオペランドのみである。 `outs` は「戻り値」となる。(TODO;詳細は?) +** どのような `SelectionDAG` にマッチできるかは `include/llvm/Target/TargetSelectionDAG.td` に情報がある。 +** マッチは命令( `add` のような)のみならず「オペランドが即値であるか」などにも適用できる。ただしこの場合その即値の定義が `ImmLeaf` を継承していることが必要。 +* 関数呼び出し規約やCallee-savedなレジスタについてもここで指定する。 +* `update_llc_test_checks.py` を使用することで、LLVM IRで書かれた関数から生成されるアセンブリのテストを自動的に生成することができる。 + +おおよそ次のような過程をたどるようだ<>。 + +.... +LLVM IR +| +| ISelLowering (関数呼び出し規約などSelectionDAGでは扱えない要素を消す) +v +SelectionDAG (仮想的/物理的なレジスタによる表現) +| +| ISelDAGToDAG +v +DAG (MachineInstrのリスト)(命令の順序を決める) +| +| +v +(ここでSSA-basedの最適化・レジスタ割り付け・プロローグエピローグ挿入を行う) +| +| MCInstLower +v +MCInst +.... + +=== `utils/UpdateTestChecks/asm.py` を変更する + +`update_llc_test_checks.py` を使用するために必要な変更らしい。RISCVのための記述を参考にしながら追記すれば良い。 + +=== `RV32KTargetLowering` を実装する + +とりあえずLLVM IRに近いところから実装していくことにする。 `RV32KTargetLowering` はLLVM IRを `SelectionDAG` に変換するためのクラスである。 +`RV32KISelLowering.{h,cpp}` で定義される。 + +まずこのクラスのコンストラクタで、ターゲットの仕様を指定する。 + +次いでメンバ関数の `LowerFormalArguments` で関数呼び出し時の引数の扱いについて記述する。 +ここでは引数はすべてレジスタ経由で渡されることにする。 +その場合、使用するレジスタクラスを指定して仮想レジスタを作成し、引数に割り付けて追加する。 + +最後に `LowerReturn` で、関数呼び出しから戻るときの戻り値の扱いについて記述する。 +戻り値を解析し、そのすべてをレジスタに詰め込み(複数個あることを前提?)、最後に `RET_FLAG` を出力している +footnote:[ここでの `RET_FLAG` は `RV32KDAGToDAGISel` にて `JR X1` に変換される疑似命令である。 +<>では直接 `jr` 命令を参照していたが、それよりも「関数からのリターンである」 +という意味的情報を付加できるというメリットがありそうだ(TODO;ほんまか?)]。 +ここの処理は全体的によくわからない(TODO;特に `Chain`, `Flag`, `RetOps` がどのような働きをしているのか判然としない)。 +`Chain` を通じて `SelectionDAG` がじゅじゅつなぎになっている? + +引数が渡ってくるレジスタを `CopyFromReg` でくるむことで、その(仮想的/物理的)レジスタがこの `SelectionDAG` の +外側で定義されたものであることを示している。 + +なおRV32Kは下位(sub)に位置づけられるべきターゲットが存在しないが、 +それでもターゲットの情報を保持するために `RV32KSubTarget` を定義することが必要である。 +`CPUName` は `RV32K.td` や `RV32KMCTargetDesc` と合わせて `generic-rv32k` としておく。 +Lanaiの実装を見ると `generic` だけで統一しても良いようだ(TODO)。 + +`setBooleanContents(ZeroOrOneBooleanContent);` はターゲットが1/0でtrue/falseを判定することを設定する。 + +さて上記のように関数呼び出し時の処理を記述するためには、 +そもそも関数呼び出し規約の定義を行う必要がある。これは `RV32KCallingConv.td` にて行う。 +関数呼び出し時の引数・戻り値を処理するためのレジスタを指定する。またcallee-savedなレジスタも指定する。 + +ここで戻り値を返すためのレジスタを複数指定することができるようだ。 +これはおそらくLLVM IRが複数の戻り値を扱うことができることの単純な反映であろうし(TODO;ほんまか?)、 +また例えばRISC VのCalling conventionではa0とa1が戻り値を返すためのレジスタとして指定されているためでも +あると思う。 + +なおここで `sp` である `x2` をcallee-savedとして指定する必要はない。 +というのもこの処理は関数プロローグ・エピローグで行うことに(将来的に)なるからだ +footnote:[<>のコミットメッセージを参考のこと。]。 + +=== `RV32KDAGToDAGISel` を実装する + +`SelectionDAG` をRV32Kコードに変換するためのクラスを実装する。 `RV32KISelDAGToDAG.cpp` で実装される。 +このクラスのエントリポイントは `Select` であるが、これ自体はTableGenが生成する関数である `SelectCode` を +呼び出すだけである。そこでこの処理の本体は `RV32KInstrInfo.td` に記述されることになる。 + +RISCVのCompressedの実装を見ると `Pat` を継承しない方法で処理をしていた。よくわからない(TODO)。 + +ALU操作のみにとりあえず対応するため、現状対応するのは `ADD` と `SUB` 、それから便宜上必要になる +`PseudoRET` の3つのみである。 + +=== `RV32KFrameLowering` を実装する + +関数のプロローグとエピローグを出力するためのクラスを実装する。 +これらではスタックフレームサイズの調整を行う。 +とはいいつつもこれらはまだ実装の必要がないため、ただのプレースホルダになっている。 + +スタックがアドレス負方向に伸びることを `TargetFrameLowering` のコンストラクタの第一引数に +`StackGrowDown` を渡すことで表現している。 + +=== `RegisterInfo` を実装する + +まず `RV32KRegisterInfo.td` を変更し、レジスタを割付優先度順に並び替える。 + +続いて `RV32KRegisterInfo` クラスを実装する。 +`getReservedRegs` で通常のレジスタ割り付けでは使用しないレジスタを指定する。 + +=== `RV32KAsmPrinter` と `RV32KMCInstLower` を実装する + +<>と同様の実装をすればよい。 +本体の処理は `LowerRV32KMachineInstrToMCInst` である。 +これは `MachineInstr` を `MCInst` に変換している。 + +=== その他 + +ビルドに必要なファイルなどを実装する。 + +`RV32KInstrInfo` に `let guessInstructionProperties = 0;` という文を追加している。 +命令の属性( `hasSideEffects` など)を推論するためのオプションのようだ(TODO)。 + +`RV32KGenInstrInfo.inc` を読み込むため `RV32KInstrInfo.{h,cpp}` を追加する。 + +`RV32KTarget>achine.{h,cpp}` を変更する。ここでは `RV32KSubtarget` のインスタンスを +得るための `getSubtargetImpl` を実装すると同時に、実行パスに +`RV32KDagToDAGISel` を挿入するために `RV32KPassConfig` を実装する +footnote:[他の変換は `RV32KSubtarget` 経由で呼び出される。この変換のみが異なるようだ。]。 + +=== テストを行う + +`CodeGen` のためのテストを作成する。これはLLVM IRを入力し、出力されるアセンブリが正しいかどうかを判定するものである。 + +テストの枠組みに頼らず、手でテストを行うためには次のようにする。 +.... +$ cat test.ll +define i32 @sub(i32 %a, i32 %b) nounwind { +; RV32K-LABEL: sub: +; RV32K: # %bb.0: +; RV32K-NEXT: sub x8, x9 +; RV32K-NEXT: jr x1 + %1 = sub i32 %a, %b + ret i32 %1 +} + +$ bin/llc -mtriple=rv32k -verify-machineinstrs < test.ll + .text + .file "" + .globl sub # -- Begin function sub + .p2align 3 + .type sub,@function +sub: # @sub +# %bb.0: + sub x8, x9 + jr x1 +.Lfunc_end0: + .size sub, .Lfunc_end0-sub + # -- End function + + .section ".note.GNU-stack","",@progbits +.... +ついに我々はLLVM IRからコード生成を行うその第一歩を踏み出せたようだ。Here we go! +footnote:[言い換えればここからが本番ということで、ここまでは長い長い前座だったのだ。 +それでも一つ終わったことは良いことだ。祝杯にコーヒーを入れよう。] + +=== `SelectionDAG` とはなにか + +<>を参考にまとめる。 + +* `SelectionDAG` は `SDNode` をノードとする有向非巡回グラフである。 +** `SDNode` は大抵opcodeとオペランドを持つ。 +** `include/llvm/CodeGen/ISDOpcodes.h` を見るとどのようなものがあるか分かる。 +* `SelectionDAG` は2つの値を持つ:データフローとコントロールフロー +** データフローは単に値がノードになっているだけ。 +** "`chain`" ノードによって副作用を持つ操作(load, store, call, returnなど)の順序が決まる。 +*** 副作用を持つ操作はすべてchainを入力として受け取り、新たなchainを出力しなければならない。 +**** 伝統的にchainの入力は0番目のオペランドになっており、出力は最後の値になっている。ただしinstruction selectionが起こった後はそうとも限らない。 +* "`legal`" なDAGは、そのターゲットがサポートしている操作・型のみで構成されている。 + +`SelectionDAG` を用いたinstruction selectionは `SelectionDAG` を最適化・正規化することで行われる +<>。 +この過程は `-view-dag-combine1-dags` などのオプションを `llc` に与えることでグラフとして見ることができる。 +.... +$ bin/llc -mtriple=rv32k -view-dag-combine1-dags < test.ll +# DOTファイルビューワがあればそれが起動する。ない場合は次のようにしてSVGに変換する。 +$ dot -Tsvg /tmp/dag.sub-86df29.dot -o dot.svg +.... +image::img/simple_sub_combine1.svg[] +それっぽい。 + +== 定数に対応する + +定数をレジスタに読み込むためのパターンを `RV32KInstrInfo.td` に追加する。 +すなわち `simm6` が来たら `li` を呼ぶようにすれば良い。ただしここで `li` のオペランドには、 +渡ってきた即値のみを渡せばよいことに注意が必要である。すなわちレジスタを指定する必要はない。 +これは `Pat` の右側に書くDAGは `(ins)` のオペランドのみを記載すればよいからである。 + +なお `simm6` を `Pat` でも使用できるように `ImmLeaf` を `simm6` の継承先に追加する必要がある。 + +== メモリ操作に対応する + +ロードとストアのための `Pat` を追加する。offsetが0の場合と非ゼロの場合で別の `def` が必要なことに注意。 +`lw` を `load` に対応付け `sw` を `store` に対応付ければ、最低限の実装が整う。 +RISC Vではさらに `sextloadi8` を `LB` に対応させるなどしているが、 +RV32Kv1ではこれらの命令が無い。 `lw` と他の命令を合わせれば実現できるかもしれないが、 +その実装方法がよくわからない。 `setOperationAction` で `Custom` 指定とかすればできそうだが詳細不明。(TODO) +footnote:[その後RV32Kv2以降でHW側で実装と合意。] + +[[implement-copyPhysReg]] +なお参考にするコミット<>のコミットメッセージには `copyPhysReg` を +実装する必要があると書いてあるが、実際に実装してみるとこれは必要ない。 +この関数はどうやらレジスタの中身を移動させる命令を生成するための関数のようで、 +使用するレジスタが多くなると使われるようだ。必要になるまで遅延することにする。 + +続くコミット<>はグローバル変数を扱うためのコミットのようだ。 +グローバル変数を `load` する場合、まずその読み込むべきアドレスの値をレジスタに作る必要があるが、 +即値ロード命令が限定されているため、即値のアドレスの上位ビットと下位ビットを分けて読み込む必要がある。 +そのためにコミットでは `%hi` と `%lo` を使用しているが、現状我々のバックエンドにはこれらが実装されていない。 +そこで一旦このコミットは飛ばすことにするfootnote:[そもそもRV32Kv1の枠組みでは32bit即値を読み込むことが +困難であるという事情もある。]。 + +== 条件分岐に対応する + +条件分岐に<>を参考にして対応する。 +ISDには条件分岐として `BRCOND` と `BR_CC` の2つがある。 +`BRCOND` は条件分岐のみを行うのに対し、 `BR_CC` は2ノード間の比較と条件分岐をともに行う。 +ここでは、よりパターンマッチが容易な `BRCOND` にのみ対応することにし、 +`BR_CC` は `setOPerationAction` を用いて `Expand` する +footnote:[おそらく `BRCOND` に書き換えるということ。どう書き換えるかがどこで定義されているかはよくわからない。TODO]。 + +=== `setlt` と `setgt` に対応する + +TableGenを用いて `brcond` に対するパターンマッチを記述する。 + +RV32Kv1では `BEQZ` と `BNEZ` のみが定義されているfootnote:[RV32Cではこれらは `BEQ` ・ `BNE` 命令から置換する形で使用される。]。 +そこで `setlt` と `setgt` が自然に定義できる。すなわち `$a < $b` という比較なら `SLT $a, $b` 後 `BNEZ $a, hoge` とする。 + +分岐命令のパターンマッチでは `bb` というクラスがよく登場する。( `bb:$imm9` )。これはおそらくbasic blockで、 +分岐の際に用いるプリミティブな型のようだ。パターンマッチの後半で正しい型を指定することで、実際の型を指定できる気がする。TODO +これと絡む即値の型は `Operand` の代わりに `Operand` を継承する必要があるようだ。 +なおbasic blockを処理するために `MachineOperand::MO_MachineBasicBlock` についての場合分けを +`LowerRV32KMachineOperandToMCOperand` に追加する必要がある。 + +LLVM IRをテキスト形式で書く際 `brcond` という命令は直接は存在せず `br` 命令を介してこれを使うことになる。 +その際 `false` 部の処理を行うため `br` 命令も発行されることに注意が必要である。 +すなわち `brcond` のためのパターンマッチのみならず `br` のためのパターンマッチも潜在的に不可欠である。 +これを解決するために + + def : Pat<(br bb:$imm12), (J simm12_lsb0:$imm12)>; + +としても良いように見えるし、実際動く。 +しかしRISC Vの実装では `PseudoBR` を定義して解消しているため、とりあえずこれを採用してみる +footnote:[要するにちゃんとした理由がないのだが、そのうち同様の実装を必要とする +Pseudo命令が出てくるだろうという予測はある。]。 + +このPseudo命令を定義するためには `RV32KAsmPrinter::lowerOperand` を +<>を参考にして実装する必要がある。 + +RV32Kv1の現在の定義では `SLT` のみが存在するため、ナイーブに定義できるのは `setlt` のみである。 +これを使用するようにSelectionDAGを構成するためには次のようなLLVM IRを入力する。 +.... +define i32 @foo(i32 %a, i32 *%b) { + %val3 = load volatile i32, i32* %b + %tst3 = icmp slt i32 %val3, %a + br i1 %tst3, label %end2, label %end +end: + ret i32 1 +end2: + ret i32 0 +} +.... +ここで +.... + br i1 %tst3, label %end2, label %end +.... +とすると途中で `setge` に変換されてしまうため注意が必要である。 + +image::img/brcond_with_br.svg[] + +引数を逆転させることで `setgt` についてもパターンマッチ可能である。 + +なお `setlt` などを挟まず `brcond` が単体で現れる場合についてもパターンマッチが可能だが、 +これについてLLVM IRでテストを書くと `and` のSelectionDAGが現れてしまうためRV32Kv1では +コンパイルできない。仕方がないのでテストはなしにしておく。 + +=== `seteq` と `setne` に対応する + +`sub` と `beqz` を組み合わせることで `seteq` を実現可能である。 +また `sub` と `bnez` を組み合わせることで `setne` を実現可能である。 +しかし `sub` は左辺を破壊する命令のため、前後の命令との兼ね合いによってはレジスタの値を別のレジスタに移したり、 +スタックに保存する必要があるfootnote:[実はこの時点ではこの需要は微妙なところだが、まあそのうち必要になることに疑いはない。]。そこで<>で示唆したように +<>を参考にして `RV32KInstrInfo::copyPhysReg` を実装し、 +さらに<>を参考にして `RV32KInstrInfo::storeRegToStackSlot` と +`RV32KInstrInfo::loadRegFromStackSlot` を実装する。 +またこれに必要な `RV32KRegisterInfo::eliminateFrameIndex` も実装する。 +この関数は `MachineInstr` にオペランドとして含まれる `FrameIndex` を +`FrameReg` と `Offset` に変換するための関数である。 +`storeRegToStackSlot` などではオペランドとして `FrameIndex` を指定し、 +即値は `0` にしておく。そのうえで `eliminateFrameIndex` を呼び出し、 +正しいオペランドに変換しているfootnote:[と思う。裏はあんまり取っていない。TODO]。 +ここでRV32Kv1の `lw` と `sw` が符号**なし**即値をとることが問題になる。 +すなわち `Offset` が負数になる場合に対処できない。 +この問題はどうすることもできないので、とりあえず放置する。 +また `FrameReg` はRV32Kv1でははっきりと決まっていいないがとりあえず `X15` にしておく。 + +ここまでで次のようなLLVM IRが +.... +define void @foo(i32 %a, i32 *%b) { + %val1 = load volatile i32, i32* %b + %tst1 = icmp slt i32 %val1, %a + br i1 %tst1, label %end, label %test2 + +test2: + %val2 = load volatile i32, i32* %b + %tst2 = icmp sgt i32 %val2, %a + br i1 %tst2, label %end, label %test3 + +test3: + %val3 = load volatile i32, i32* %b + %tst3 = icmp eq i32 %val3, %a + br i1 %tst3, label %end, label %test4 + +test4: + %val4 = load volatile i32, i32* %b + %tst4 = icmp ne i32 %val4, %a + br i1 %tst4, label %end, label %test5 + +test5: + %val5 = load volatile i32, i32* %b + br label %end + +end: + ret void +} +.... +次のようなRV32Kv1コードに変換される。 +.... + .globl foo # -- Begin function foo + .p2align 3 + .type foo,@function +foo: # @foo +# %bb.0: + lw x10, 0(x9) + slt x10, x8 + bnez x10, .LBB0_5 + j .LBB0_1 +.LBB0_1: # %test2 + lw x10, 0(x9) + mv x11, x8 + slt x11, x10 + bnez x11, .LBB0_5 + j .LBB0_2 +.LBB0_2: # %test3 + lw x10, 0(x9) + sub x10, x8 + beqz x10, .LBB0_5 + j .LBB0_3 +.LBB0_3: # %test4 + lw x10, 0(x9) + sub x10, x8 + bnez x10, .LBB0_5 + j .LBB0_4 +.LBB0_4: # %test5 + lw x8, 0(x9) +.LBB0_5: # %end + jr x1 +.Lfunc_end0: + .size foo, .Lfunc_end0-foo + # -- End function + + .section ".note.GNU-stack","",@progbits +.... + +== 関数呼び出しに対応する + +関数呼び出しをサポートするためにはi) `PseudoCALL` を実装しii) `RV32KTargetLowering::LowerCall` +を実装すれば良いようだ。前者は `JAL` に展開され、 +後者はLLVM IRから `SelectionDAG` への変換を担う。 + +LLVM IRの `call` は、まず `lui` と `addi` で関数のアドレスを取得した後、 +`jalr` でその場所で飛ぶようなアセンブリに変換される。ただLLVM 9の `clang` でアセンブリを出力 +させると `call` 命令を使うものが出力される。RISC Vのソースコードを見ると次のようにある。 +.... +// PseudoCALL is a pseudo instruction which will eventually expand to auipc +// and jalr while encoding. This is desirable, as an auipc+jalr pair with +// R_RISCV_CALL and R_RISCV_RELAX relocations can be be relaxed by the linker +// if the offset fits in a signed 21-bit immediate. +// Define AsmString to print "call" when compile with -S flag. +// Define isCodeGenOnly = 0 to support parsing assembly "call" instruction. +.... +アセンブリ出力時のみ `call` を使うようだ。 + +関数のアドレスはグローバルなので、そのアドレスを取得するコードを `lowerCall` 中に +記述する必要がある。パッチ<>では `lowerGlobalAddress` を読んでいるが、 +これは実装していない。そこでこの処理を応急処置的に書くことにする。 +現状のRV32Kv1ではすべての関数呼び出しのアドレスはFxiupで解決される(relocationに回らない) +と前提しているので、そのとおりに実装する。 + +`LowerCall` ではおおよそi) 引数を解析してii) `CALLSEQ_START` ノードを出力しiii) +引数を処理するためのDAG( `CopyToReg` )をはさみiii) `CALL` ノードを出力しiv) +`CALLSEQ_END` ノードを出力し v) 返ってきた値を `CopyFromReg` ノードを出力することで +取り出している、ようだ。他にも様々な処理が挟まっているが、よくわからない。TODO + +`CopyFromReg` は物理レジスタ(physical register)**から**仮想レジスタ(virtual register)に +値をコピーするようなDAGを生成し、逆に `CopyToReg` は物理レジスタ**へ**仮想レジスタの中の +値をコピーするようなDAGを生成する。一般のレジスタ割り付けが行われる以前であっても、 +例えば関数の引数・戻り値のように割り付けるべき物理レジスタが決まる場合がある。 +この区別をこのDAGは行っているようだ。 +したがって `LowerCall` では `CopyToReg` を使って引数を詰め込み、 +関数を呼び、それが終わったら `CopyFromReg` を使って戻り値を取り出しているということになる。 + +さて `PseudoCALL` はパッチ<>では `jalr` に展開される。 +一見 `jal` に展開すれば良いように思えるし、実際RISC Vの実装ではそうなっているのだが、 +そうするためには `PseudoCALL` の `ins` に指定するクラスを作成する必要がある。 +RISC Vでは `bare_symbol` が、Lanaiでは `CallTarget` がそれに当たる。 +複雑で良くわからないのでとりあえずここは `jalr` に展開することにする。 + +`let Defs = [X1]` を `PseudoCALL` にかぶせているのは、おそらく `X1` が `jalr` によって +書き換えられることを意味している。 `jalr` が `X1` を `outs` に含めていないのは、 +おそらく `jalr` が `X1` を出力するというわけではないからだと思うがわからない。TODO + +`getCallPreservedMask` を定義する必要がある。Callee-savedなレジスタに関する情報を +渡すための関数のようだ。(おそらく;TODO)TableGenが再生する `CSR_RegMask` をそのまま返せば良い。 + +`CallSeqStart` と `CallSeqEnd` は `ADJCALLSTACKDOWN` ・ `ADJCALLSTACKUP` に結びつける必要がある。 +これらが `let Defs = [X2], Uses = [X2]` で囲まれているのは(おそらく) `X2` を書き換えつつ、 +かつその(過去の?;TODO)値を使用するから。 + +すると次のようなエラーが出る。 + +.... +Can't store this register to stack slot +UNREACHABLE executed at /home/anqou/ano/secure_vm/llvm-project/llvm/lib/Target/RV32K/RV32KInstrInfo.cpp:55! +.... + +そこで `ADJCALLSTACKDOWN` らを `RV32KGenInstrInfo` のコンストラクタに渡すようにすると、 +また次のようなエラーが出る。 + +.... +Call Frame Pseudo Instructions do not exist on this target! +UNREACHABLE executed at /home/anqou/ano/secure_vm/llvm-project/llvm/include/llvm/CodeGen/TargetFrameLowering.h:299! +.... + +そこで `eliminateCallFramePseudoInstr` を実装する。 +この関数は関数呼び出しのための疑似命令を具体的な命令に置き換えるための関数のようだが、 +ここではその疑似命令を削除するに留める。 + +するとまた初めのエラーが再燃した。 + +.... +Can't store this register to stack slot +UNREACHABLE executed at /home/anqou/ano/secure_vm/llvm-project/llvm/lib/Target/RV32K/RV32KInstrInfo.cpp:56! +.... + +よくよく見てみると、当該ソースコードは次のようになっている。 + +.... +void RV32KInstrInfo::storeRegToStackSlot(MachineBasicBlock &MBB, + MachineBasicBlock::iterator I, + unsigned SrcReg, bool IsKill, int FI, + const TargetRegisterClass *RC, + const TargetRegisterInfo *TRI) const { + DebugLoc DL; + if (I != MBB.end()) + DL = I->getDebugLoc(); + + if (RV32K::GPRCRegClass.hasSubClassEq(RC)) + BuildMI(MBB, I, DL, get(RV32K::SW)) + .addReg(SrcReg, getKillRegState(IsKill)) + .addFrameIndex(FI) + .addImm(0); + else + llvm_unreachable("Can't store this register to stack slot"); +} +.... + +レジスタをスタックに対比するMIを生成するためのコードである。 +ここで `sw` 命令が `GPRC` レジスタのみをとることが災いし `x1` をスタックに積むことができない。 +したがって**RV32Kv1の枠組みでは関数呼び出しを行えないことが判明した**。 + +RV32Kv1のためのLLVMバックエンド作成はここで(突然)中止であるfootnote:[作りながら、このISAでは +限界がありそうだと感じていたが、しかしLLVMがハングするまでここで中止になることはわからなかった。] +footnote:[あとから気づいたが、直接スタックに積むのではなく一旦 `GPRC` のレジスタに `mv` してから +スタックに積めばこの問題を回避できたのかもしれない。ただここで「どのレジスタに一旦退避するか」を +どう求めればよいかは良くわからない。]。 + +[bibliography] +== 参照 + +- [[[github_riscv-llvm_docs_01,1]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/01-intro-and-building-llvm.mkd +- [[[llvm_getting-started,2]]] https://llvm.org/docs/GettingStarted.html +- [[[clang_gettings-started,3]]] https://clang.llvm.org/get_started.html +- [[[asciidoctor_user-manual,4]]] https://asciidoctor.org/docs/user-manual/ +- [[[riscv,5]]] https://riscv.org/ +- [[[riscv_specifications,6]]] https://riscv.org/specifications/ +- [[[fox-llvm,7]]] 『きつねさんでもわかるLLVM〜コンパイラを自作するためのガイドブック〜』(柏木 餅子・風薬・矢上 栄一、株式会社インプレス、2013年) +- [[[github_riscv-llvm_docs_02,8]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/02-starting-the-backend.mkd +- [[[github_riscv-llvm_patch_0002,9]]] https://github.com/lowRISC/riscv-llvm/blob/master/0002-RISCV-Recognise-riscv32-and-riscv64-in-triple-parsin.patch +- [[[github_riscv-llvm,10]]] https://github.com/lowRISC/riscv-llvm +- [[[youtube_llvm-backend-development-by-example,11]]] https://www.youtube.com/watch?v=AFaIP-dF-RA +- [[[msyksphinz_try-riscv64-llvm-backend,12]]] http://msyksphinz.hatenablog.com/entry/2019/01/02/040000_1 +- [[[github_riscv-llvm_patch_03,13]]] https://github.com/lowRISC/riscv-llvm/blob/master/0003-RISCV-Add-RISC-V-ELF-defines.patch +- [[[github_riscv-llvm_patch_04,14]]] https://github.com/lowRISC/riscv-llvm/blob/master/0004-RISCV-Add-stub-backend.patch +- [[[github_riscv-llvm_patch_06,15]]] https://github.com/lowRISC/riscv-llvm/blob/master/0006-RISCV-Add-bare-bones-RISC-V-MCTargetDesc.patch +- [[[github_riscv-llvm_patch_10,16]]] https://github.com/lowRISC/riscv-llvm/blob/master/0010-RISCV-Add-support-for-disassembly.patch +- [[[llvm-writing_backend-operand_mapping,17]]] https://llvm.org/docs/WritingAnLLVMBackend.html#instruction-operand-mapping +- [[[llvm-writing_backend,18]]] https://llvm.org/docs/WritingAnLLVMBackend.html +- [[[github_riscv-llvm_patch_07,19]]] https://github.com/lowRISC/riscv-llvm/blob/master/0007-RISCV-Add-basic-RISCVAsmParser.patch +- [[[github_riscv-llvm_patch_08,20]]] https://github.com/lowRISC/riscv-llvm/blob/master/0008-RISCV-Add-RISCVInstPrinter-and-basic-MC-assembler-te.patch +- [[[llvm-tablegen,21]]] https://llvm.org/docs/TableGen/index.html +- [[[github_riscv-llvm_patch_09,22]]] https://github.com/lowRISC/riscv-llvm/blob/master/0009-RISCV-Add-support-for-all-RV32I-instructions.patch +- [[[llvm_dev_ml-tablegen_definition_question,23]]] http://lists.llvm.org/pipermail/llvm-dev/2015-December/093310.html +- [[[llvm_doxygen-twine,24]]] https://llvm.org/doxygen/classllvm_1_1Twine.html +- [[[llvm-tablegen-langref,25]]] https://llvm.org/docs/TableGen/LangRef.html +- [[[github_riscv-llvm_docs_05,26]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/05-disassembly.mkd +- [[[github_riscv-llvm_patch_11,27]]] https://github.com/lowRISC/riscv-llvm/blob/master/0011-RISCV-Add-common-fixups-and-relocations.patch +- [[[github_riscv-llvm_docs_06,28]]] https://github.com/lowRISC/riscv-llvm/blob/master/docs/06-relocations-and-fixups.mkd +- [[[github_riscv-llvm_patch_13,29]]] https://github.com/lowRISC/riscv-llvm/blob/master/0013-RISCV-Initial-codegen-support-for-ALU-operations.patch +- [[[speakerdeck-llvm_backend_development,30]]] https://speakerdeck.com/asb/llvm-backend-development-by-example-risc-v +- [[[llvm-code_generator,31]]] https://llvm.org/docs/CodeGenerator.html +- [[[llvm-code_generator-target_independent_code_gen_alg,32]]] https://llvm.org/docs/CodeGenerator.html#target-independent-code-generation-algorithms +- [[[llvm-code_generator-selectiondag_instruction_selection,33]]] https://llvm.org/docs/CodeGenerator.html#selectiondag-instruction-selection-process +- [[[github_riscv-llvm_patch_15,34]]] https://github.com/lowRISC/riscv-llvm/blob/master/0015-RISCV-Codegen-support-for-memory-operations.patch +- [[[cpu0,35]]] https://jonathan2251.github.io/lbd/ +- [[[elvm-llvm_backend,36]]] https://github.com/shinh/llvm/tree/elvm +- [[[elvm-slide,37]]] http://shinh.skr.jp/slide/llel/000.html +- [[[github_riscv-llvm_patch_16,38]]] https://github.com/lowRISC/riscv-llvm/blob/master/0016-RISCV-Codegen-support-for-memory-operations-on-globa.patch +- [[[github_riscv-llvm_patch_17,39]]] https://github.com/lowRISC/riscv-llvm/blob/master/0017-RISCV-Codegen-for-conditional-branches.patch +- [[[todai_llvm_backend,40]]] https://github.com/cpu-experiment-2018-2/llvm/tree/master/lib/Target/ELMO +- [[[todai_llvm_backend-article,41]]] http://uenoku.hatenablog.com/entry/2018/12/25/044244 +- [[[github_riscv-llvm_patch_18,42]]] https://github.com/lowRISC/riscv-llvm/blob/master/0018-RISCV-Support-for-function-calls.patch +- [[[llvm-langref,43]]] http://llvm.org/docs/LangRef.html +- [[[fpga_develop_diary,44]]] http://msyksphinz.hatenablog.com/