LLVMのミドルエンドの実装が簡単とは言えませんが,バックエンドは覚えることが多く非常に難しいです.バックエンドは各モジュールが複雑に絡み合って実装されているため,各モジュールがLLVMのバックエンドコアからどのように使用されているかを理解して実装する必要があります.また,LLVMのバックエンドに関する情報はあまり多くありません.公式のドキュメントとしてWriting an LLVM Compiler Backend*4やThe LLVM Target-Independent Code Generator*5などがありますが,これらの情報だけでは全く十分とは言えず,かつ情報が古い部分もあります.ちなみに,LLVM内で命令がどのように変化していくかをまとめた素晴らしいブログエントリ*6がありますのでこちらも参考になるかと思います.公式以外のLLVMに関する情報は最近では多少増えてきましたが,コア部分に関する情報はあまり無いと言えるでしょう.バックエンドに限らず,LLVMについて理解するにはソースコードを読み続けていく必要があります.ソースコード自体は割と丁寧に書かれておりコメントも適度に入っているため,それほどわかりにくいという訳ではありませんが,上述の通り複数のモジュールが複雑に絡み合っているため全体が見えにくいときもあります.
今回この本を執筆するにあたり,試行錯誤しながらバックエンドの実装を行いました.実装したバックエンドは既存のバックエンド(MIPS)を参考にしてはいますが,スクラッチからの実装となっています.機能は最低限しか無く,不完全な部分も多くありますがご了承ください.全て一から実装したことにより,何が実装に必要なのか,どういう仕組みになっているのか,そしてまだ理解が不十分な部分などがよくわかりました.本章では私が理解したことをバックエンドの作成方法を紹介しながらお伝えしたいと思います.
私は一応GCCのバックエンドも少しだけ触ったことがあるため,LLVMとGCCのバックエンドについて比較してみます.実装の容易さのみであればLLVMもGCCもほとんどかわらないと個人的には思います.メインの記述をLLVMではTableGenで行い,GCCではRTLで行っており,それぞれ完全な記述が可能というわけでなく,C/C++での記述で補う必要があることに変わりはありません.しかし,LLVMの方がコード全体が整然としており,特にバックエンドで固有の最適化を行うときにはその実装は容易だと思っています.というわけで,これからコンパイラに関して何かやりたいという場合には断然LLVMをおすすめします.
これまではLLVMのフロントエンドでLLVM IRを生成し,ミドルエンドでLLVM IRの最適化を行って来ました.それではLLVMのバックエンドは何をするところなのでしょうか?実はLLVMのバックエンドの目的は次のように多岐に渡ります.
一般的にはコンパイラの出力はアセンブリになりますが,LLVMでは様々な出力を選択できるという事です.
特に注目すべきは実行オブジェクトを直接生成するというところです.LLVMでは一般的なコンパイラと異なり,アセンブリを経由せずに直接実行オブジェクトを生成することができるのです.
LLVM IRの実行というのは更にオブジェクト生成すら経由せずに直接LLVM IRのまま実行できるということです.その際にJIT(Just in Time)コンパイルしながら実行します.
コード生成は他のものよりは特殊で,LLVM IRを(C言語やJavaScriptのような)他の言語に翻訳して出力します.コード生成の目的としては,ある言語からある言語への翻訳することで再利用可能にすることや,逐次処理で記述されたC言語プログラムを並列処理で書かれたC言語プログラムに翻訳し,LLVMとは別のコンパイラに通す前の前処理として利用するなどがあります.
LLVMが流行している理由の1つとして,このJITとコード生成を活用することで,既存の言語や新規言語に対してLLVMの基盤を利用した高度な解析や最適化を容易に適用できることがあげられます.しかし,本書ではJITやトランスレータについては特に触れておらず,アセンブリ生成と実行オブジェクト生成を目的としています.
JITとトランスレータについては以降では紹介しませんのでここで簡単に説明します.
新しいバックエンドをJITに対応させたい場合も一からJITの処理を実装する必要は無く,バックエンド特有の処理を記述するクラスを実装するだけで良いです.JITのコア部分はLLVMのコア実装に含まれているためです.JIT処理自体に興味がある方はLLVMのソースのlib/ExecutionEngine以下を参照すると良いでしょう.
トランスレータについてはアセンブリ生成などと似ているようではありますが,処理の流れとしては全く異なります.最近ではコード生成を行うバックエンドもいくつか出てきたのでそれらの処理を参考にすると良いでしょう.LLVM本体にマージされている実装としてはCppBackend(lib/Target/CppBackend)やNVPTX(lib/Target/NVPTX)があります.NVPTXは非常に大きなプロジェクトで1万行近くあるため,最初に参考にするには難しいかもしれません.よく例として挙げられるのはCBackendですが,LLVM 3.1で削除されてしまいました.3.0までのソースを参考にするのも良いですが,最新版でも動作するようにメンテナンスされているバージョンもあるのでそちらを参考にするのが良さそうです.
LLVMのバックエンドはLLVM IRを入力として最終的にはアセンブリやオブジェクトを出力しますが,その過程でフォーマットが何度か変化します.フォーマット変化の流れを図7.1で示しています.フォーマットを変化させることをLoweringと言い,より低レベルの形式へ変化させていることを意味します.
図7.1: フォーマット変化の流れ
バックエンドでもミドルエンドと同様に全ての処理はパスで行われます.バックエンドでは特にMachineFunctionPassを使用します.図で示したSelectionDAGISelやAsmPrinterもMachineFunctionPassの1つです.SelectionDAGISelは(厳密には違いますが)最初に実行されるMachineFunctionPassであり,AsmPrinterが最後に実行されるMachineFunctionPassとなります.中央でMI LayerからMI Layerへ変換しているMachineFunctionPassは,ターゲット依存の最適化などを行うパスになります.
バックエンドではまず初めに,LLVM IRからSelectionDAGへフォーマットを変化させます*1.SelectionDAGはLLVM IRをグラフ形式で表したもので,各命令やデータ(レジスタなど)の依存関係を表します.DAGとは有向非巡回グラフ(Directed Acyclic Graph)を意味します.グラフのそれぞれのノードはSDNodeクラスのインスタンスとなります.SDNodeはOpcodeやOperandのリスト,そしてそのSDNodeを使用しているSDNodeのリストなどを持っています.
SelectionDAGへの変形はSelectionDAGISelというMachineFunctionPassの1つで行われます.SelectionDAGISelパスはMachineFunctionPassで最初に実行されるパスで,以降のMachineFunctionPassのために後述するMI(MachineInstr等)を出力します.つまり,SelectionDAGISelパスはLLVM IRを入力としていったんSelectionDAGへ変換した後,MIを出力するパスとなります.
SelectionDAGISelパスは大きく分けて次のフェーズから構成されます.
LowerはLLVM IRからSelectionDAGに変化させるフェーズです.このフェーズでは関数呼び出しなどを除いてLLVM IRから対応するSDNodeへほぼ1対1の変換を行います.この段階ではSelectionDAGが持つ命令やOperandなどはターゲットで利用できないものを含んでいるため,不正(illegal)な状態となります.
Combineはパターンマッチによる置き換えで最適化を行うフェーズで,主な目的は命令をより単純なものに置き換えることです.CombineはLegalizeの前後で何度か実行されます.
Legalizeはターゲットにサポートされていない命令やデータ形式を他のものに置き換えたり削除したりするフェーズです.Legalizeにより不正(illegal)な状態だったSDNodeは全て正当(legal)な状態になります.
SelectはSDNodeからMIの構築に必要な情報を含むSDNodeであるMachineSDNodeに変換するフェーズです.
Scheduleは構築したグラフの依存関係を元に命令をスケジューリングするフェーズです.
これらのフェーズが全て終わったあと,MIを出力します.
上述の通り全ての処理はSelectionDAGISelパスを通して行われますが,ターゲット固有の処理がいくつかあるため,各ターゲットではSelectionDAGISelを継承したクラスを作成し,一部のメソッドをオーバーライドして実装することでターゲット固有の処理が行えるようになります.SelectionDAGISelパスには様々なポイントでフックするためのメソッドがあるので,それらを実装することでより細かい動作を定義することも可能です.Lower, Legalize, Selectの処理を行うメソッドはターゲット毎に必ず実装しなければなりません.
MIはバックエンドでは最も一般的なフォーマットで,通常はこのフォーマットで最適化を行います.LLVM IRが機械語と比較すると非常に抽象的な表現になっているのに対し,MIは機械語に近い表現になっているため,ターゲット依存の最適化を行うのに適しています.MIとは後述するMachineInstrなどのクラスの総称であり,MI LayerとはデータのフォーマットとしてMIを扱うフェーズのことを指します*2.
MIはMachineFunction,MachineBasicBlock,MachineInstr,そしてMachineOperandから構成されます.この構成はLLVM IRとほとんど同じで,使い方も同じようになっています.LLVM IRではIRBuilderでInstructionを生成するように,MachineInstrおよびMachineOperandの生成はMachineInstrBuilderクラスを利用します.BuildMI関数でMachineInstrを引数に指定するとMachineInstrBuilderクラスのインスタンスが生成されるので,MachineInstrBuilderクラスのメソッドを利用してMachineOperandを追加していく形になります.
MIはSelectionDAGISelパスにより生成された直後はまだ,無限個のレジスタを仮定した仮想レジスタやPHI命令を含むSSA形式で表現されています.SSA形式でいくつかの最適化を行ったあと,アセンブリやオブジェクトを生成するAsmPrinterパスまでにレジスタ割り当てやPHI命令の削除を行って非SSA形式へ変換する必要があります.レジスタ割り当てはTargetPassConfig::addOptimizedRegAllocで追加される複数パスからなるようです.MI Layerでの最適化はTargetPassConfig::addMachinePassesで追加されるパスで行われるので興味がある場合にはこれらの処理を追ってみると良いでしょう.ここだけで30個以上のパスが実行されています.
MC Layerは機械語(バイナリ列)レベルの形式を表現したり扱うために利用されます.この段階ではラベル名,マシン命令,セクション,オブジェクトファイルと言った単位で扱います.このMC Layerが何のためにあるかというと,アセンブリ出力やオブジェクト出力,JITなどの処理を共通して扱うための抽象化レイヤーを用意して無駄な処理を無くすためです.
MC Layer(MC)で扱うクラスとしてはMCContext,MCSymbol,MCSection,MCInstなどがありますが,ターゲット実装時に実際に扱うことになるMCInstのみを紹介します*3.MCInstはMC Layerで命令を表現するための唯一のクラスです.MIとMCの違いは,MCInstにはFunctionのような構造が無く,オブジェクトファイルのように全ての命令がフラットに並ぶことです.
MCStreamer APIは最終的なファイル(アセンブリやオブジェクトファイルなど)を出力するための抽象APIです.ファイル毎にMCStreamerクラスを継承したクラスがあるため,それらを利用することで目的の結果を得られます.例えばアセンブリであればMCAsmStreamerクラスであったり,オブジェクトであればMCObjectStreamerクラスであったりします.オブジェクトの場合でもELFオブジェクトの場合は,更に継承したMCELFStreamerクラスになります.MCStreamerにはディレクティブ出力のためのメソッドや命令出力のためのメソッドなどがあります.
MCStreamer APIはMC Layerを入力とするMCStreamerクラスのただのメソッドに過ぎません.MIをMCに変換したり,MCStreamer APIを操作するのはAsmPrinterパスになります.例えば,AsmPrinterパスのアセンブリなどを出力するメソッドであるEmitInstructionでは命令毎にMI(MachineInstr)が引数として渡されるため,MC(MCInst)に変換してからMCStreamer APIに渡すという処理を実装する必要があります.AsmPrinterパスについての詳しい説明は後述します.
実際にターゲットの仕様を考える前に今回どこまで実装するかを考えましょう.本書ではバックエンド全体が見える程度の機能を実装したいと思います.目標としてはいくつかの命令をアセンブリ出力できて,オブジェクトファイル(ELF)を出力でき,その結果をディスアセンブルできるところまでいきます.本来ならオブジェクトファイルを実行する簡単な処理系を作るところですが,そこまでは時間が足りなかったため,ディスアセンブルにより正しくオブジェクトが生成されているか確かめる程度とします.
コンパイルの対象とするコードも決めました.リスト7.1およびリスト7.2のようなソースがコンパイル出来れば完成としましょう.リスト7.1はsample_addという32bitの整数2つを引数に取り,加算した後,32bit整数として返す関数です.これにより最も単純なコードがコンパイル可能かどうかを確認します.リスト7.2は関数呼び出しを含むコードです.こちらもコードとしてはシンプルですが,関数呼び出しがあることにより呼び出し規約や関数のアドレス解決などが必要になるため,難易度は高くなります.
リスト7.1: 目標のソースコード(sample_add.ll)
1: define i32 @sample_add(i32 %a, i32 %b) nounwind readnone {
2: entry:
3: %add = add nsw i32 %b, %a
4: ret i32 %add
5: }
リスト7.2: 目標のソースコード(sample_call.ll)
1: define i32 @add(i32 %a, i32 %b) nounwind readnone {
2: entry:
3: %add = add nsw i32 %b, %a
4: ret i32 %add
5: }
6:
7: define i32 @sample_call() nounwind readnone {
8: entry:
9: %call = tail call i32 @add(i32 1, i32 2)
10: ret i32 %call
11: }
さて,目標は決まりましたがターゲットの仕様が決まっていません.今回この本ではオリジナルのターゲットを作成して,その上で目標となるコードをコンパイルできるようにします.ターゲットの名前はSampleとしましょう.ターゲットの実装をする前に,どういうアーキテクチャなのかというのを決める必要があります.私が一番知ってるアーキテクチャがMIPSなのでMIPSに似た仕様で決めていきます.
まず何を決める必要があるかを考えましょう.大きく分けると命令と呼び出し規約(Calling Convention)があります.それぞれ更に細かく分けると以下のように分類できるでしょう.以降でそれぞれについて詳しく見ていきます.
命令を決める,と言った時に考えることはいろいろあります.どんな命令があるのか,各命令のオペランドは何をいくつ取るのか,機械語にするときのエンコーディング,レジスタって何があるの?などなど.x86は少し複雑で私自身あまり知らないので,MIPSを例に説明しましょう.MIPSでは命令の種類は6bitのopcode(命令コード)で定義します(細かく言えばfunctもありますが).オペランドのとり方には3種類あり,3つレジスタを取るRフォーマットと2レジスタと1つの即値を取るIフォーマット,そして1つの即値を取るJフォーマットがあります.これらの組み合わせを32bit固定で表現しており,各bitのどこからどこまでがopcodeかレジスタかなどが定義されてます.レジスタは計32個あり,一時レジスタ8個,保存レジスタ8個,引数レジスタ4個,戻り値レジスタ2個,あとはスタックレジスタ,フレームレジスタ,戻りアドレスレジスタなどがあります.
今回作成するターゲットでは32bitマシンを前提にして,32bitの固定命令長で考えましょう.命令はロード(load),ストア(store),即値ロード(move),関数呼び出し(call),呼び出し元ジャンプ(ret),加算(add),減算(sub)の7つのみとします.レジスタ数は4bitで表せる16個とします.
レジスタの一覧は表7.1で示しています.各レジスタの概要は後述する呼び出し規約との関連が大きいです.ZEROレジスタはどのような値を入れても値が0で固定されるレジスタです.Vレジスタは関数から戻るときに戻り値を入れるレジスタです.Aレジスタは関数呼び出し時に引数を入れるレジスタです.Tレジスタはなんでも使えるレジスタですが,関数呼び出しをすると戻った時に上書きされている可能性があるレジスタです.SレジスタはTレジスタと同様に何でも使えますが,関数呼び出し後でも値が変化しないことが保証されているレジスタです.SPレジスタはスタックの現在位置アドレスを格納するレジスタです.RAレジスタは関数から戻る先のアドレスを格納するレジスタです.
表7.1: レジスタ一覧
| レジスタ番号 | レジスタ名 | 概要 |
|---|---|---|
| 0 | ZERO | 常にゼロを格納 |
| 1 | V0 | 戻り値を格納 |
| 2 | A0 | 1つ目の引数を格納 |
| 3 | A1 | 2つ目の引数を格納 |
| 4 | A2 | 3つ目の引数を格納 |
| 5 | A3 | 4つ目の引数を格納 |
| 6 | T0 | 一時レジスタ |
| 7 | T1 | 一時レジスタ |
| 8 | T2 | 一時レジスタ |
| 9 | T3 | 一時レジスタ |
| 10 | S0 | 保存レジスタ |
| 11 | S1 | 保存レジスタ |
| 12 | S2 | 保存レジスタ |
| 13 | S3 | 保存レジスタ |
| 14 | SP | スタックレジスタ |
| 15 | RA | 戻りアドレスレジスタ |
これでレジスタと命令が決まったのでフォーマットを決めましょう.命令の種類はopcodeで表しMSB(Most Significant Bit)から8bit使うことにします.レジスタは4bitで表します.フォーマットの種類としては3種類有り,レジスタを使う数で分けられています.フォーマットを図7.2に示します.
図7.2: 命令フォーマット
まず,レジスタを使わない0レジスタフォーマットはcall命令で使われます.上位8bitがopcodeで残り24bitはアドレスに使われます.1レジスタフォーマットはレジスタ1つと1つの即値を取り,load命令,store命令,ret命令,move命令に使われます.opcodeとレジスタ1で上位12bitを使用し,残りの20bitを即値とします*8.最後の3レジスタフォーマットは3つのレジスタを取る形式で,add命令とsub命令で使われます.opcodeとレジスタ3つで20bitしか使わないため,残りの12bitがあまりますが未使用とします.
呼び出し規約はCalling Conventionと言います.関数を呼び出すときや関数から戻るときの決まりごとを定義しています.
まず関数呼び出し時に引数をどうやって渡すかを定義します.定義したレジスタにある通り引数専用のレジスタが4つあるため,引数4つまではA0から順に割り当てていくということにします.では,5つ以上あった場合はどうしましょうか.方法としてはレジスタに割り当てられない部分をスタックを利用して渡すということが可能です.しかし,今回は実装を簡単にするために引数が5つ以上ある場合はエラーとします.
戻り値の渡し方も同様にVレジスタを使います.Vレジスタは1つしかないため,2つ以上の戻り値がある場合は引数と同様にエラーとします.
スタックについての詳細は本書では説明しませんが,簡単に説明しておきます.関数呼び出し毎にスタック上に領域を確保します.確保した領域のことをフレームと呼びます.SPレジスタが現在の関数のフレームの先頭を示しています.関数呼び出し時には新しい関数フレームの先頭を示すようにSPレジスタを更新する必要があります.同様に関数から戻るときにも,呼び出し元の関数フレームの先頭を示すようにSPレジスタを戻す必要があります.呼び出し元のフレームの先頭位置を知る方法は2つあります.各フレームサイズが事前に計算可能,もしくは位置をどこかに保存しておくかになります.実装を簡単にするために,フレームサイズは必ず固定長として呼び出し元の関数フレームの先頭位置を現在の位置から単純に求められるようにします.
関数呼び出し時にレジスタをどう扱うかということも規約として決めておかなければいけません.引数のAレジスタ,戻り値のVレジスタ,スタックの位置を管理するSPレジスタについては既に述べました.それ以外のレジスタについても考えましょう.
まずは関数の呼び出し元アドレスを格納するRAレジスタについてです.RAレジスタには関数呼び出し時にアドレスを格納する必要がありますが,これはcall命令が実行された時にハードウェアが自動的に格納するとします*9.呼び出し元アドレスが自動的に保存されるからと言って安心してはいけません.呼び出し先の関数で再度関数呼び出しを行った場合にはRAレジスタが上書きされるため,最初の関数へ戻るためのアドレスが消滅してしまいます.ネストした関数呼び出しにも対応できるようにRAレジスタの値はスタックの最初に保存することにします.
次に一時レジスタであるTレジスタについてです.Tレジスタは関数の呼び出し先で変更される可能性があるレジスタです.関数呼び出しを跨いでTレジスタの値を使う場合には値をスタックに保存しておく必要があります.
保存レジスタであるSレジスタは,Tレジスタと似ていますが,Tレジスタとは逆に呼び出し先で変更してはいけないレジスタです.ただし呼び出し元の関数に戻る時に元の値に戻ってさえいれば良いです.関数内でSレジスタを使う場合には,最初に値をスタックに保存しておき最後に値を戻してあげる必要があります.
それぞれのレジスタは必要に応じてスタックに保存していくことになりますが,これは結局全てのレジスタがスタックに保存される可能性があるということです.今回の実装ではレジスタがどこに格納されているかという管理を簡単にするため,およびスタックフレームサイズを固定長にするために,スタック上に全てのレジスタ分の領域を確保します.
実装するターゲットの仕様が決まったところで実装をすすめたいですが,その前にターゲットの詳細を記述するためのDSL(Domain Specific Language)としてTableGenというものがあります.このTableGenの使い方について説明します*10.
TableGenでは命令やレジスタの定義など,大量に記述する必要があるものを簡単に,統一した方法で記述できます.TableGenで記述されたものはLLVMのソースコードがコンパイルされる前に変換が行われて,C++のソースコードになります.生成されたコードはターゲット個別のC++ソースコードからincludeして利用します.TableGenで定義すべきものを表7.2に示します.この他にも記述できるものはありますが最低限これだけで良いです.
表7.2: TableGenで定義すべきクラス一覧
| クラス名 | 詳細 |
|---|---|
| Register | レジスタを定義 |
| RegisterClass | レジスタの種類を定義 |
| FuncUnit | 機能ユニットを定義 |
| InstrItinClass | 命令の種類を定義 |
| ProcessorItineraries | IstrItinClassとFuncUnitの関係を定義して命令パイプラインを定義 |
| Operand | オペランドを定義 |
| Instruction | 命令を定義 |
| CallingConv | 呼び出し規約を定義 |
| CalleeSavedRegs | 呼び出し元保存レジスタを定義 |
| InstrInfo | 命令セットを定義 |
| Processor | ハードウェア定義.ProcessorItinerariesを持つ |
| Target | 命令セットやアセンブリパーサやライタの有無を定義 |
| Subtarget | サブターゲットの定義 |
| AsmWriter | アセンブリ出力の定義 |
TableGenでの記述には主にclassとdefという専用の構文を使います.既に述べた通りTableGenでは専用の文法を持っています.C++と非常に似た文法なのでわかりやすいでしょう.
classは新たに型を定義します.リスト7.3で示すようにclassにはクラス名と複数の値を持つことができます.クラスAでは値としてVという名前をもつbit型を定義してしており,その初期値は0です.クラスBはクラスAを継承しているため,クラスBも同様にbit型のVを持っています.クラスCはクラスAを継承し,更に値としてstrというstring型を定義しており,初期値は“hello”です.もちろんクラスCはクラスAの持っている値Vも持っています.クラスDはクラスAを継承していますが,継承時にAの持つ値Vの初期値を上書きするために,let式を使っています.既に定義された値を再定義する場合にはlet式を使う必要があります.
リスト7.3: TableGenのクラス定義
1: class A { bit V = 0; }
2: class B : A;
3: class C : A {
4: string str = "hello";
5: }
6: class D : A {
7: let V = 1;
8: }
classでクラス定義しただけでは実体化されていません.リスト7.4で示すように,定義したクラスをdefで実体化する必要があります.一番シンプルなdefの使い方としてはクラスをそのまま実体化させることです.リスト7.4の1行目ではクラスAをXとしてそのまま実体化しています.2-4行目では,クラスCをYとして実体化するときにstring型の値str2を定義しており,その値は“world”です.実体化するときにクラスで定義された値を変更する場合にはlet式を使う必要があります.5-6行目ではlet式を使って,クラスCをZとして実体化するときに値Vと値strを変更しています.let式は複数行に適用したり,ネストさせることもできます.7-11行目では値Vを3で上書きするlet式を複数行に適用させています.let式を複数行に適用させる場合には{}で囲む必要があります.9-10行目ではlet式の中で更にlet式を使っており,ZZZは値strが“Hello World!”で上書きされるだけでなく値Vも3で上書きされます.
リスト7.4: TableGenのクラスの実体化
1: def X : A;
2: def Y : C {
3: string str2 = "world";
4: }
5: let V = 2, str = "Hello World" in
6: def Z : C;
7: let V = 3 in {
8: def ZZ : C;
9: let str = "Hello World!" in
10: def ZZZ : C;
11: }
classやdefをまとめて定義する方法としてmulticlassとdefmがあります.これらはマクロのようなもので一定のパターンの定義を一度に実体化させることができます.使える場面も多くあると思いますが,classとdefだけでできないことではないためここでは紹介しません.
他にも制御構造としてforeachを使ったループや!ifを使った条件分岐などが記述可能です.これらの細かい説明はここでは行いませんので,公式のドキュメントやソースコードなどを参考にして下さい.利用できる式などは以降で列挙しています.
TableGenを記述するために専用の型や構文が用意されています.TableGenで利用できる型の一覧を表7.3に,値の一覧を表7.4に,構文を表7.5に示します.
表7.3: TableGenで利用できる型の種類
| 型 | 詳細 |
|---|---|
| bit | boolean型0か1を保持できる |
| int | 32bit整数型 |
| string | 文字列型 |
| bits<n> | 固定長の整数型 |
| list<ty> | 他の型のリスト |
| Class type | クラス型 |
| dag | 有向グラフ |
| code | ソースコード |
表7.4: TableGenで利用できる値の種類
| 値 | 詳細 |
|---|---|
| ? | 未初期化値 |
| 0b101 | 2進数値 |
| 0707 | 8進数値 |
| 190 | 10進数値 |
| 0x7f | 16進数値 |
| "foo" | 文字列 |
| [{ ... }] | ソースコード |
| [X,Y,Z]<type> | type型のリスト |
| {a, b, c} | bits<3>の初期化 |
| value | 値の参照 |
| value{17} | 値の特定bitにアクセス |
| value{15-17} | 値の複数bitにアクセス |
| DEF | 定義の参照 |
| CLASS<val list> | 引数付きの匿名クラスの参照 |
| X.Y | 値のサブフィールドの参照 |
| list[4-7,17,2-3] | リストスライス |
表7.5: TableGenで利用できる構文
| 構文 | 詳細 |
|---|---|
| foreach<var> = <list> in {<body>} | listの値をvarとしてbodyを実行する |
| foreach<var> = <list> in <def> | listの値をvarとしてdefを実行する |
| foreach<var> = 0-15 in ... | 整数値の範囲をvarとして...を実行 |
| foreach<var> = {0-15,32-37} in ... | 整数値の範囲をvarとして...を実行 |
| (DEF a,b) | DAG. 最初のDEFは定義したもの.残りは他の値. |
| !strconcat(a,b) | 文字列aとbの連結 |
| str1#str2 | str1とstr2を連結して文字列とする. |
| !cast<type>(a) | シンボルテーブルから文字列aをルックアップしてtype型のシンボルを取得 |
| !subst(a,b,c) | aとbが文字列ならcの中のaをbに置き換える |
| !foreach(a,b,c) | DAGもしくはlistであるaをbとして操作cを適用 |
| !head(a) | list aの先頭要素を取得 |
| !tail(a) | list aの先頭要素以外の残りの要素を取得 |
| !empty(a) | list aが空かどうかを0か1の整数で示す |
| !if(a,b,c) | 操作aの結果がゼロでなければbを返し,それ以外ならcを返す |
| !eq(a,b) | aとbが同じならbit1を返し,それ以外ならbit0を返す(文字列,数値,bitのみ) |
ここから本格的にターゲットを記述していきます.ターゲットに関する仕様はこれまで説明してきた通りなので,具体的にソースコードに落としていくところになります.
都合上全てのソースコードを載せていません.今回使用するソースコードは私のGitHub*11のllvm-sample-targetリポジトリにおいてありますので,そちらを参考にして下さい.
はじめにどれくらいのファイルを作成する必要があるのかというのを列挙し,各ファイルについての簡単な説明を付随しておきます.ここで列挙したファイルは全て作成しなければ動作しないと思って良いでしょう.実際には一部のクラス(サブターゲットなど)は無くても動きますが,中身が空っぽでも作っておいた方が良いです.
補足ですが,この定義はこのファイルでしなければいけないということはありません.他のターゲットではほとんどがこの構造で統一されています.他のターゲットの実装を調べるときに参考にしやすいという意味でも,従っておいた方が良いでしょう.
バックエンドで定義するクラスの一覧も見ておきましょう.本書ではクラス単位で説明していくため,こちらの方が重要です.図7.3ではバックエンドで定義するクラスとそれに関連するクラスを用途毎に分けて示しています.下線がついているものがターゲット独自に定義するクラスで,中でも斜体で表記しているもの(SampleGenXXX)はTableGenにより自動生成されるクラスです.実線で指しているものが親クラスで,点線がそのクラスのインスタンスを持っていることを示しています.17個もクラスを定義しなければなりませんが,ほとんど中身が空っぽのクラスもあったりします.図では親クラスまで示していますが,実装時には親クラスの方で扱うことが多いので親クラスがどうなっているか知っておくことは重要です.
図7.3: バックエンドのクラス一覧
用途毎にクラスを分けていますが,それぞれのグループについて簡単に説明しておきます.
既に説明した通り,TableGenで定義すべきものは決まっています.定義を始める前に必要なこととして,ベースとなる定義ファイルをインクルードすることです.ここに全ての定義が書かれているため,迷った時やベースのクラスについて知りたい時はこのファイルを調べましょう.
include "llvm/Target/Target.td"
TableGenの定義は後述するSelectionDAGISelパスやPEIパス(フレーム処理),AsmPrinterパスなどの様々な場所で利用されます.全ての定義をまとめて記述しているためその説明はかなり長くなりますが,重要なものですので一つ一つ確認していきましょう.
ではまず最初にレジスタを定義しましょう.レジスタはRegisterクラスで定義されており,これをdefで実体化するのみです.ただし,1つ1つdefで実体化すると面倒なので,ある程度共通部分をまとめてから実体化しましょう.
Registerクラスを継承したSampleRegクラスを定義します.NamespaceはTableGenからC++のコードを生成した時のnamespaceになります.ターゲット名と同じ"Sample"を指定します.
その後,SampleRegクラスを各レジスタ毎に実体化していきます(リスト7.5).DwarfRegNumクラスはおそらく必要無い(デバッグで使うDwarfだと思うので)ですが,一応定義しておきます.
リスト7.5: TableGenでのレジスタ定義
1: class SampleReg<bits<4> num, string n> : Register<n> {
2: field bits<4> num;
3: let Namespace = "Sample";
4: }
5:
6: // 汎用レジスタ
7: def ZERO : SampleReg< 0, "ZERO">, DwarfRegNum<[0]>;
8: def V0 : SampleReg< 1, "V0">, DwarfRegNum<[1]>;
9: def A0 : SampleReg< 2, "A0">, DwarfRegNum<[2]>;
10: def A1 : SampleReg< 3, "A1">, DwarfRegNum<[3]>;
11: def A2 : SampleReg< 4, "A2">, DwarfRegNum<[4]>;
12: def A3 : SampleReg< 5, "A3">, DwarfRegNum<[5]>;
13: def T0 : SampleReg< 6, "T0">, DwarfRegNum<[6]>;
14: def T1 : SampleReg< 7, "T1">, DwarfRegNum<[7]>;
15: def T2 : SampleReg< 8, "T2">, DwarfRegNum<[8]>;
16: def T3 : SampleReg< 9, "T3">, DwarfRegNum<[9]>;
17: def S0 : SampleReg< 10, "S0">, DwarfRegNum<[10]>;
18: def S1 : SampleReg< 11, "S1">, DwarfRegNum<[11]>;
19: def S2 : SampleReg< 12, "S2">, DwarfRegNum<[12]>;
20: def S3 : SampleReg< 13, "S3">, DwarfRegNum<[13]>;
21: def SP : SampleReg< 14, "SP">, DwarfRegNum<[14]>;
22: def RA : SampleReg< 15, "RA">, DwarfRegNum<[15]>;
Registerの定義が終わったのでRegisterClassを定義しましょう(リスト7.6).RegisterClassはRegisterの集合を定義します.サブターゲットで特殊なレジスタが存在するなど複数のレジスタの集合がある場合には,その数だけRegisterClassを定義する必要があります.今回のターゲットでは全てのレジスタを1つの集合としているので,1つのRegisterClassに全てのレジスタを登録します.RegisterClass名はCPURegsとしましょう.
リスト7.6: TableGenのレジスタクラス定義
1: def CPURegs : RegisterClass<"Sample", [i32], 32, (add 2: // 戻り値と引数用レジスタ (Return values and Arguments registers) 3: V0, A0, A1, A2, A3, 4: // 呼び出し元待避レジスタ (Caller saved registers) 5: T0, T1, T2, T3, 6: // 呼び出し先待避レジスタ (Callee saved registers) 7: S0, S1, S2, S3, 8: // 予約レジスタ (Reserved registers) 9: ZERO, SP, RA)>;
次に加算や減算などの機能単位での詳細を記述していきます(リスト7.7).ここで機能間での実行時の制約や命令パイプラインの定義,各機能の実行サイクル数など,細かいレベルの定義が可能です.今回は細かい定義はせずとりあえず使えるようにするため程度で定義しています.
リスト7.7: TableGenでの機能ユニット定義
1: // 機能ユニット 2: def ALU : FuncUnit; 3: 4: // 命令スケジュール(Instruction Itinerary) 5: def IICAlu : InstrItinClass; 6: def IICLoad : InstrItinClass; 7: def IICStore : InstrItinClass; 8: def IICBranch : InstrItinClass; 9: def IICPseudo : InstrItinClass; 10: 11: // Sampleターゲットのプロセッサスケジュール 12: def SampleGenericItineraries : ProcessorItineraries<[ALU], [], [ 13: InstrItinData<IICAlu , [InstrStage<1, [ALU]>]>, 14: InstrItinData<IICLoad , [InstrStage<1, [ALU]>]>, 15: InstrItinData<IICStore , [InstrStage<1, [ALU]>]>, 16: InstrItinData<IICBranch , [InstrStage<1, [ALU]>]>, 17: InstrItinData<IICPseudo , [InstrStage<1, [ALU]>]> 18: ]>;
InstrItinClassは命令を機能ごとにグルーピングしたものです.FuncUnitは複数の機能をまとめたもので,InstrItinClassをグルーピングしたものと考えてよいでしょう.最後にProcessorItinerariesでプロセッサがどんな機能を持っているかの詳細を定義します.InstrItinDataを使って各機能がどの機能ユニットを使って何サイクルで実行できるか定義しています.
全ての命令が1つの機能ユニットに属しており,命令の種類毎にInstrItinClassを定義しています.そして全ての機能が1サイクルで実行可能であるプロセッサを定義しています.
次に呼び出し規約について定義します(リスト7.8).呼び出し規約で定義するのは呼び出し時のレジスタの使い方,関数から戻るときのレジスタの使い方,呼び出し時に保存するレジスタの定義の3つです.
リスト7.8: TableGenでの呼び出し規約定義
1: // Sample Calling Convention 2: def CC_Sample : CallingConv<[ 3: // i8/i16型の引数はi32型に昇格する 4: CCIfType<[i8, i16], CCPromoteToType<i32>>, 5: 6: // 整数型はAレジスタに渡す 7: CCIfType<[i32], CCAssignToReg<[A0, A1, A2, A3]>>, 8: ]>; 9: 10: def RetCC_Sample : CallingConv<[ 11: // i32型はV0レジスタに渡す 12: CCIfType<[i32], CCAssignToReg<[V0]>> 13: ]>; 14: 15: // 呼び出し先待避レジスタ(Callee-saved register) 16: def CSR_SingleFloatOnly : CalleeSavedRegs<(add (sequence "S%u", 3, 0), RA)>;
CallingConvクラスを使って呼び出し時のレジスタの使い方を定義します.使い方としては,パターンマッチで引数がこの型ならこのレジスタに割り当てますという記述になります.整数型の8bit,16bitなら32bitに拡張し,32bitならA0, A1, A2, A3レジスタに割り当てるようにしています.
次に戻るときの定義ですが,これもCallingConvを使います.こちらは32bitの場合はV0レジスタを使うとだけ定義しています.
最後に関数呼び出し時に保存しなければいけないレジスタを定義します.ターゲットの仕様からS0, S1, S2, S3レジスタを保存する必要があるので, (add S0, S1, S2, S3, RA)と書けばよいですがsequenceを使って記述しています.
まだ命令定義を残していますが,先にターゲットの定義をします(リスト7.9).ターゲット定義ではProcessorクラスとTargetクラスの実体化が必要です.
リスト7.9: TableGenでのターゲット定義
1: def SampleInstrInfo : InstrInfo;
2:
3: def : Processor<"sample32", SampleGenericItineraries, []>;
4:
5: def SampleAsmWriter : AsmWriter {
6: string AsmWriterClassName = "InstPrinter";
7: bit isMCAsmWriter = 1;
8: }
9:
10: def Sample : Target {
11: let InstructionSet = SampleInstrInfo;
12: let AssemblyWriters = [SampleAsmWriter];
13: }
Processorクラスはターゲット持っている機能ユニットと特徴(サブターゲット)を引数で指定して実体化します.例えばターゲットの種類によっては浮動小数点数の機能があったりなかったりする場合には,特徴の異なるProcessorとして実体化します.今回は特にサブターゲットを定義していないので指定せずに実体化します.
Targetクラスでは命令セットとアセンブリのパーサやライタがあるかどうかなどを定義します.今回のターゲットではアセンブリを出力するためにアセンブリライタを定義しています.AsmWriterClassNameで指定した文字列の前にターゲット名を付けたもの,つまりSampleInstPrinterをアセンブリライタとして指定しています.SampleInstPrinterクラスについての詳細はAsmPrinterパスで説明します.
さて最後に命令を定義します.なぜ命令を最後にしたかというと命令の定義は非常に複雑だからです.即値を使うload/storeや関数呼び出しのcallは特に複雑になっています.
命令はInstructionクラスを実体化させるだけで良いのですが,必要なパラメータがいっぱいあるので1つ1つで実体化させるのは面倒です.まず命令のフォーマット毎にテンプレートを作成して実体化させます.
まずは共通のフォーマットを定義します.リスト7.10に定義を示します.命令長は32bitでOpcodeがMSBの8bitなどのターゲットの仕様はここで定義します.
リスト7.10: TableGenでの命令のフォーマットテンプレート定義
1: class Format<bits<3> val> {
2: bits<3> Value = val;
3: }
4:
5: def Pseudo : Format<0>;
6: def FormReg0 : Format<1>;
7: def FormReg1 : Format<2>;
8: def FormReg2 : Format<3>;
9: def FormReg3 : Format<4>;
10:
11: // 共通命令フォーマット
12: class SampleInst<dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClass itin, Format f>
13: : Instruction {
14: field bits<32> Inst;
15: Format Form = f;
16:
17: bits<8> Opcode = 0;
18:
19: let Namespace = "Sample";
20: let Size = 4;
21: let Inst{31-24} = Opcode;
22: let OutOperandList = outs;
23: let InOperandList = ins;
24: let AsmString = asmstr;
25: let Pattern = pattern;
26: let Itinerary = itin;
27:
28: bits<3> FormBits = Form.Value;
29:
30: let DecoderNamespace = "Sample";
31:
32: field bits<32> SoftFail = 0;
33: }
Instructionクラスで重要な値は,Inst,OutOperandList,InOperandList,AsmString,Pattern,Itineraryです.Instはオブジェクト出力等,バイナリで出力するときにこの値を使います.OutOperandListとInOperandListはその命令の依存関係を示すSDNodeのリストで,それぞれ命令の出力と命令の入力に対応します.AsmStringはこの命令をアセンブリ出力するときの文字列になります.PatternはSelectionDAGでパターンマッチする時のSDNodeです.Itineraryは命令がどのグループに属しているかを示すもので先程InstrItinClassで定義したものです.
次にフォーマット毎の定義を記述します.リスト7.11に各フォーマットの定義を示しています.ターゲットの仕様として決めた通り,フォーマットの種類としては0レジスタフォーマット,1レジスタフォーマット,2レジスタフォーマットの3種類あります.
リスト7.11: レジスタ数毎のフォーマット定義
1: // 0レジスタフォーマットの定義
2: class SampleInstFormReg0<bits<8> op, dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClass itin>
3: : SampleInst<outs, ins, asmstr, pattern, itin, FormReg0> {
4: bits<24> other;
5:
6: let Opcode = op;
7:
8: let Inst{23-0} = other;
9: }
10:
11: // 1レジスタフォーマットの定義
12: class SampleInstFormReg1<bits<8> op, dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClass itin>
13: : SampleInst<outs, ins, asmstr, pattern, itin, FormReg1> {
14: bits<4> rc;
15: bits<20> other;
16:
17: let Opcode = op;
18:
19: let Inst{23-20} = rc;
20: let Inst{19-0} = other;
21: }
22:
23: // 3レジスタフォーマットの定義
24: class SampleInstFormReg3<bits<8> op, dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClass itin>
25: : SampleInst<outs, ins, asmstr, pattern, itin, FormReg3> {
26: bits<4> rc;
27: bits<4> ra;
28: bits<4> rb;
29: bits<12> other;
30:
31: let Opcode = op;
32:
33: let Inst{23-20} = rc;
34: let Inst{19-16} = ra;
35: let Inst{15-12} = rb;
36: let Inst{11-0} = other;
37: }
38:
39: // 疑似命令の定義
40: class SamplePseudo<dag outs, dag ins, string asmstr, list<dag> pattern>:
41: SampleInst<outs, ins, asmstr, pattern, IICPseudo, Pseudo> {
42: let isCodeGenOnly = 1;
43: let isPseudo = 1;
44: }
図7.2の通りで特に難しい点は無いと思いますが,例としてSampleInstFormReg3で説明します.MSBから8bitはOpcodeとして使っているのでその次のbitから4bitで1レジスタを表すとして3レジスタ分確保します.rc = ra (op) rbとなるように,順番にrc, ra, rbの順に割り当てるとします.残りのbitはとりあえずotherとしておきます.
仕様で定義した命令フォーマットとは別に,実際の命令にはならない疑似命令を定義しておきます.疑似命令ではisCodeGenOnlyとisPseudoを1にしておきます.
add命令とsub命令を定義する前にSampleInstFormReg3を継承して算術演算用のテンプレートとしてArithLogicInstを定義します.リスト7.12に定義を示します.
リスト7.12: TableGenでのadd,sub命令定義
1: // 3つのオペランドを取る算術命令用のフォーマット定義
2: class ArithLogicInst<bits<8> op, string asmstr, SDNode OpNode, InstrItinClass itin, RegisterClass RC>
3: : SampleInstFormReg3<op, (outs RC:$rc), (ins RC:$ra, RC:$rb),
4: !strconcat(asmstr, "\t$rc, $ra, $rb"),
5: [(set RC:$rc, (OpNode RC:$ra, RC:$rb))], itin> {
6: }
7:
8: def ADD : ArithLogicInst<0x05, "add", add, IICAlu, CPURegs>;
9: def SUB : ArithLogicInst<0x06, "sub", sub, IICAlu, CPURegs>;
まずSampleInstFormgReg3に第4引数で実行している!strconcatは第1引数と第2引数の文字列をそのまま連結するものです.つまりasmstrが“add”だった場合“add $rc, $ra, $rb”が生成されます.次に第5引数ではマッチパターンのリストを渡しています.[]はリストで,()で囲まれたものはDAGのパターンを示しています.これは入力として受け取った$raと$rbに対してOpNodeを適用した結果を$rcにセットするというパターンになります.OpNodeは引数で指定されているSDNodeになりますが,今回の例ではaddやsubになります.RCは使用するレジスタの集合を示しています.
最後にArithLogicInstを使ってADDとSUBを定義します.ADDはopcodeが0x05でSUBは0x06としており,InstrItinClassはともにIICAluでRegisterClassはCPURegsです.既に述べたようにこの命令のSDNodeとしてaddとsubをそれぞれ指定しています.これはデフォルト定義されている命令のため,そのまま使うことができます.
次にmove命令を定義します.リスト7.13でmove命令の定義を示します.move命令はadd命令やsub命令よりも少し複雑になります.
リスト7.13: TableGenでのmove命令定義
1: // 符号付き20bit整数
2: def immSExt20 : PatLeaf<(imm), [{ return isInt<20>(N->getSExtValue()); }]>;
3:
4: // 即値ロード用のオペランド. 20bit
5: // 19-0: 符号付き20bit整数
6: // EncoderMethod: bit列から符号付き20bit整数を取得
7: def movetarget : Operand<i32> {
8: let EncoderMethod = "getMoveTargetOpValue";
9: }
10:
11: // 即値ロード命令(Load Immediate)
12: class LoadI<bits<8> op, string asmstr>:
13: SampleInstFormReg1<op, (outs CPURegs:$rc), (ins movetarget:$other),
14: !strconcat(asmstr, "\t$rc, $other"), [(set CPURegs:$rc, immSExt20:$other)], IICLoad> {
15: let DecoderMethod = "DecodeMoveTarget";
16: }
17:
18: def MOVE : LoadI<0x02, "move">;
move命令の即値には20bitの符号付き整数を利用するため,専用のSDNodeが必要になります.PatLeafを使って20bitの符号付き整数を定義しています.immは整数値を表すSDNodeで,その中でも20bitで表現可能な整数を符号拡張しています.
次にmove命令の即値用のOperandとしてmovetargetを定義します.move命令の即値は20bitしか使っていないため,32bitのオペランドの中の20bitしか使わないということを明示的に指定する必要があります.この処理を行う関数としてgetMoveTargetOpValueをEncoderMethodで指定します.EncoderMethodを設定するとSampleMCCodeEmitterクラスでオペランドをエンコードするために設定した関数が呼び出されるようになるため,その関数で任意の処理を実装することができます.
次に即値ロード命令用のフォーマットを定義しておきます.注目すべきところはSampleInstFormReg1の第三引数(入力のOperand)にmovetargetを指定しておくことと,第四引数にimmSExt20というPatternを指定しておくことです.DecoderMethodが指定されていますが,これはEncoderMethodとは逆にディスアセンブルするときに命令の何bit目から何bit目がこのOperandである,という処理を行う関数を指定できます.通常はEncoderMethodを指定した場合には対になるDecoderMethodも指定する必要があります.DecoderMethodで指定した関数はSampleDisassemblerで実装する必要があります.
これでmove命令を定義する準備ができたので,後はLoadIを使って実体化するだけです.move命令にはOpcodeとして0x02を割り当てます.
次にcall命令の定義をリスト7.14に示します.move命令よりもさらに複雑で,SDNodeなど定義する必要があるものが多いです.
リスト7.14: TableGenでのcall命令定義
1: // iPTR型のオペランドを1つ取るという制約条件
2: def SDT_SampleCall : SDTypeProfile<0, 1, [SDTCisVT<0, iPTR>]>;
3:
4: // Sampleターゲットの関数呼び出し命令のSDNode
5: def SampleCall : SDNode<"SampleISD::Call", SDT_SampleCall,
6: [SDNPHasChain, SDNPOutGlue, SDNPOptInGlue,
7: SDNPVariadic]>;
8:
9: def : Pat<(SampleCall (i32 tglobaladdr:$dst)),
10: (CALL tglobaladdr:$dst)>;
11: def : Pat<(SampleCall (i32 texternalsym:$dst)),
12: (CALL texternalsym:$dst)>;
13:
14: def calltarget : Operand<iPTR> {
15: let EncoderMethod = "getCallTargetOpValue";
16: }
17:
18: class Call<bits<8> op, string asmstr>:
19: SampleInstFormReg0<op, (outs), (ins calltarget:$other, variable_ops),
20: !strconcat(asmstr, "\t$other"), [(SampleCall imm:$other)],
21: IICBranch> {
22: let isCall=1;
23: let DecoderMethod = "DecodeCallTarget";
24: }
25:
26: def CALL : Call<0x03, "call">;
SDTypeProfileクラスはSDNodeの条件を定義します.第一引数は出力SDNode数で,第二引数がオペランドの数,第三引数がオペランドの制約条件になります.SDT_SampleCallは1つのオペランドを取り,ポインタ型であるという条件になります.
SDNodeクラスは新しいSDNodeを定義するもので,第一引数にopcode,第二引数に制約条件(SDTypeProfile),第三引数に特徴(SDNodeProperty)を指定します.特徴には例えば,オペランドが可換であるSDNPCommutativeや可変引数のSDNPVariadicなどがあります.SampleCallはSampleISD::Callをopcodeとして,先ほど定義したSD_SampleCallを条件としたSDNodeになります.
Patクラスは第一引数にマッチしたパターンを第二引数に変換するものです.SelectionDAGISelパスで関数呼び出しがSDNodeとしてSampleCallに変換されるため,それをCALLに対応付けしています.
calltargetは24bitのアドレスを取るためにmove命令と同様に独自のOperandを定義しています.オペランドとしてiPTR型を指定しているのが特徴です.EncoderMethodとしてgetCallTargetOpValueを指定し,この関数で24bitのアドレスを取得します.
関数呼び出し用のフォーマットとしてCallを定義します.isCallを1にしてDecoderMethodを設定します.variable_opsは可変長引数であることを示しています.
最後にCallを使ってcall命令を実体化して完了です.call命令のopcodeは0x03です.
ret命令の定義をリスト7.15に示します.call命令と非常に似ているので特に説明することは無いと思います.ret命令のopcodeは0x04です.
リスト7.15: TableGenでのret命令定義
1: def SDT_SampleRet : SDTypeProfile<0, 1, [SDTCisInt<0>]>;
2: def SampleRet : SDNode<"SampleISD::Ret", SDT_SampleRet,
3: [SDNPHasChain, SDNPOptInGlue, SDNPVariadic]>;
4:
5: class RetInst<bits<8> op, string asmstr>:
6: SampleInstFormReg1<op, (outs), (ins CPURegs:$rc),
7: !strconcat(asmstr, "\t$rc"), [(SampleRet CPURegs:$rc)], IICBranch> {
8: let isBranch=1;
9: let isTerminator=1;
10: let isBarrier=1;
11: let isReturn=1;
12: }
13:
14: def RET : RetInst<0x04, "ret">;
最後にload/store命令の定義をリスト7.16に示します.
リスト7.16: TableGenでの1レジスタ命令定義
1: // アドレスオペランド
2: def mem : Operand<i32> {
3: let PrintMethod = "printMemOperand";
4: let MIOperandInfo = (ops CPURegs, i16imm);
5: let EncoderMethod = "getMemEncoding";
6: }
7:
8: def addr : ComplexPattern<iPTR, 2, "SelectAddr", [], []>;
9:
10: // Load/Store命令共通フォーマット
11: class FMem<bits<8> op, dag outs, dag ins, string asmstr, list<dag> pattern, InstrItinClass itin>
12: : SampleInstFormReg1<op, outs, ins, asmstr, pattern, itin> {
13: bits<20> addr;
14: let Inst{19-0} = addr;
15: let DecoderMethod = "DecodeMem";
16: }
17:
18: // Load命令
19: let canFoldAsLoad = 1 in
20: class LoadM<bits<8> op, string asmstr, RegisterClass RC>:
21: FMem<op, (outs RC:$rc), (ins mem:$addr),
22: !strconcat(asmstr, "\t$rc, $addr"),
23: [(set RC:$rc, (load addr:$addr))], IICLoad>;
24:
25: // Store命令
26: class StoreM<bits<8> op, string asmstr, RegisterClass RC>:
27: FMem<op, (outs), (ins RC:$rc, mem:$addr),
28: !strconcat(asmstr, "\t$rc, $addr"),
29: [(store RC:$rc, addr:$addr)], IICStore>;
30:
31: def LOAD : LoadM<0x00, "load", CPURegs>;
32: def STORE : StoreM<0x01, "store", CPURegs>;
まずメモリのアドレスとして使うOperandを定義します.このOperandは少し特殊で1つのOperandでレジスタ1つと20bitのアドレスを取ります.このような特殊な形式となっているため,アセンブリなどで出力するためにPrintMethodで独自の出力関数を指定しています.PrintMethodで指定する関数はSampleInstPrinterで実装する必要があります.EncoderMethodも指定する必要があります.
ComplexPatternはプログラムで明示的にパターンマッチを記述する必要のある複雑なパターンを定義するために利用します.load/store命令で使うアドレスで利用します.第三引数で指定したSelectAddrをSampleDAGToDAGISelクラスで実装する必要があります.
LoadMとStoreMでアドレスを引数に取るパターンを定義し,実体化させてload/store命令の完成です.
ここまででTableGenでの定義は完了です.実際に利用するときにはC++のコードとして変換しなければいけません.C++への変換はllvm-tblgenコマンドを利用するのですが,LLVMをビルドするときにはMakefileに記述しておくだけで自動生成されます.
lib/Target/Sample/Makefileをリスト7.17のように記述すれば良いです.BUILT_SOURCESでTableGenで生成する種類を指定します.今回使っていないものも指定していますが,特に問題ないので良いでしょう.
リスト7.17: ターゲットのMakefile
1: LEVEL = ../../.. 2: LIBRARYNAME = LLVMSampleCodeGen 3: TARGET = Sample 4: 5: # Make sure that tblgen is run, first thing. 6: BUILT_SOURCES = SampleGenRegisterInfo.inc SampleGenInstrInfo.inc \ 7: SampleGenAsmWriter.inc SampleGenCodeEmitter.inc \ 8: SampleGenDAGISel.inc SampleGenCallingConv.inc \ 9: SampleGenSubtargetInfo.inc SampleGenMCCodeEmitter.inc \ 10: SampleGenEDInfo.inc SampleGenDisassemblerTables.inc 11: 12: DIRS = InstPrinter Disassembler TargetInfo MCTargetDesc 13: 14: include $(LEVEL)/Makefile.common
共通クラスは特定のパスで使われるという訳ではなく,全てのパスから使われる可能性があるクラスです.最も中心となるのがSampleTargetMachineクラスです.バックエンドで実行されるパスであるMachineFunctionPassはパスマネージャによりrunOnMachineFunctionが実行されますが,その際に引数としてMachineFunctionクラスが渡されます.MachineFunctionクラスからSampleTargetMachineクラスの親クラスであるTargetMachineクラスが得られます.そこからSubtargetクラス,MCInstrInfoクラス,MCRegisterInfoクラスなど全てが取得可能です.
各クラスでどのような関数を実装しなければいけないかは親クラスを見るしかありません.わかりやすい場合には純粋仮想関数になっていますが,必ず実行されるわけではない関数などはllvm_unreachableを呼び出して実行時エラーになるようになっています.必要に応じて子クラスでoverrideしましょう.
SampleTargetMachineクラスは最も中心となるクラスですが,それ自体では特に重要な実装はもっておらず,他のクラスのインスタンスを持っているくらいです.リスト7.18でクラス定義を示します.ほとんどのメソッドはメンバ変数を取得するだけです.
リスト7.18: SampleTargetMachineのクラス定義
1: class SampleTargetMachine : public LLVMTargetMachine {
2: const DataLayout DL;
3: SampleSubtarget Subtarget;
4: SampleInstrInfo InstrInfo;
5: SampleFrameLowering FrameLowering;
6: SampleTargetLowering TLInfo;
7: SampleSelectionDAGInfo TSInfo;
8:
9: public:
10: SampleTargetMachine(const Target &T, StringRef TT,
11: StringRef CPU, StringRef FS, const TargetOptions &Options,
12: Reloc::Model RM, CodeModel::Model CM,
13: CodeGenOpt::Level OL);
14:
15: virtual const SampleInstrInfo *getInstrInfo() const {
16: return &InstrInfo;
17: }
18: virtual const SampleSubtarget *getSubtargetImpl() const {
19: return &Subtarget;
20: }
21: virtual const SampleRegisterInfo *getRegisterInfo() const {
22: return &InstrInfo.getRegisterInfo();
23: }
24: virtual const DataLayout *getDataLayout() const {
25: return &DL;
26: }
27: virtual const SampleTargetLowering *getTargetLowering() const {
28: return &TLInfo;
29: }
30: virtual const SampleFrameLowering *getFrameLowering() const{
31: return &FrameLowering;
32: }
33: virtual const SampleSelectionDAGInfo* getSelectionDAGInfo() const {
34: return &TSInfo;
35: }
36:
37: virtual TargetPassConfig *createPassConfig(PassManagerBase &PM);
38: };
SampleTargetMachineクラスの中で唯一少し特殊なメソッドがcreatePassConfigです.リスト7.19に示している通り,後述するSamplePassConfigクラスを生成して返すだけとなっています.
リスト7.19: SampleTargetMachine::createPassConfig
1: TargetPassConfig *SampleTargetMachine::createPassConfig(PassManagerBase &PM) {
2: return new SamplePassConfig(this, PM);
3: }
SamplePassConfigクラスはSampleTargetMachineクラスでしか使われません.SamplePassConfigクラスの実装はリスト7.20で示しているのが全てです.このクラスで行うことはaddInstSelectorでこのターゲットで使うSelectionDAGISelパスを登録することです.createSampleISelDAGはSampleターゲットのSelectionDAGISelパスであるSampleDAGToDAGISelクラスを生成するファクトリ関数となります.
リスト7.20: SamplePassConfigクラス
1: class SamplePassConfig : public TargetPassConfig {
2: public:
3: SamplePassConfig(SampleTargetMachine *TM, PassManagerBase &PM)
4: : TargetPassConfig(TM, PM) {}
5:
6: SampleTargetMachine &getSampleTargetMachine() const {
7: return getTM<SampleTargetMachine>();
8: }
9:
10: virtual bool addInstSelector();
11: };
12:
13: bool SamplePassConfig::addInstSelector() {
14: addPass(createSampleISelDag(getSampleTargetMachine()));
15: return false;
16: }
SampleSubtargetクラスは全く利用していないので特に詳しい説明はしません.クラス定義はリスト7.21のようになっています.SampleSubtargetクラスはTableGenによって生成されたSampleGenSubtargetInfo.incをインクルードしており,そこで定義されたSampleGenSubtargetInfoクラスを継承したものです.また,自動生成されたParseSubtargetFeaturesメソッドを使用しています.
リスト7.21: SampleSubtargetのクラス定義
1: class SampleSubtarget : public SampleGenSubtargetInfo {
2: virtual void anchor() {};
3: bool ExtendedInsts;
4: public:
5:
6: SampleSubtarget(const std::string &TT, const std::string &CPU,
7: const std::string &FS);
8:
9: // TableGenにより自動生成
10: void ParseSubtargetFeatures(StringRef CPU, StringRef FS);
11: };
SampleInstrInfoクラスはTableGenで生成されたSampleGenInstrInfoクラスを継承していますが,更にその親クラスはTargetInstrInfoImplクラスです.様々な命令の動作を定義しています.その関数でどのような実装をすればよいかはTargetInstrInfoクラス(TargetInstrInfoImplの親クラス)のヘッダを見ると書いています.
リスト7.22: SampleInstrInfoのクラス定義
1: class SampleInstrInfo : public SampleGenInstrInfo {
2: SampleTargetMachine &TM;
3: const SampleRegisterInfo RI;
4: public:
5: explicit SampleInstrInfo(SampleTargetMachine &TM);
6:
7: virtual const SampleRegisterInfo &getRegisterInfo() const;
8:
9: virtual unsigned isLoadFromStackSlot(const MachineInstr *MI,
10: int &FrameIndex) const;
11:
12: virtual unsigned isStoreToStackSlot(const MachineInstr *MI,
13: int &FrameIndex) const;
14:
15: virtual void copyPhysReg(MachineBasicBlock &MBB,
16: MachineBasicBlock::iterator MI, DebugLoc DL,
17: unsigned DestReg, unsigned SrcReg,
18: bool KillSrc) const;
19: virtual void storeRegToStackSlot(MachineBasicBlock &MBB,
20: MachineBasicBlock::iterator MBBI,
21: unsigned SrcReg, bool isKill, int FrameIndex,
22: const TargetRegisterClass *RC,
23: const TargetRegisterInfo *TRI) const;
24:
25: virtual void loadRegFromStackSlot(MachineBasicBlock &MBB,
26: MachineBasicBlock::iterator MBBI,
27: unsigned DestReg, int FrameIndex,
28: const TargetRegisterClass *RC,
29: const TargetRegisterInfo *TRI) const;
30:
31: // 分岐解析
32: virtual bool AnalyzeBranch(MachineBasicBlock &MBB, MachineBasicBlock *&TBB,
33: MachineBasicBlock *&FBB,
34: SmallVectorImpl<MachineOperand> &Cond,
35: bool AllowModify) const;
36:
37: virtual unsigned RemoveBranch(MachineBasicBlock &MBB) const;
38:
39: virtual unsigned InsertBranch(MachineBasicBlock &MBB, MachineBasicBlock *TBB,
40: MachineBasicBlock *FBB,
41: const SmallVectorImpl<MachineOperand> &Cond,
42: DebugLoc DL) const;
43: };
load命令がスタックスロットから直接ロードする場合にレジスタ番号を返し,それ以外の命令なら0を返す関数を定義します.
store命令がスタックスロットに直接ストアする場合にレジスタ番号を返し,それ以外の命令なら0を返す関数を定義します.
レジスタをコピーするための命令を生成します.実装をリスト7.23に示します.add命令を使ってZEROレジスタとの加算で実装しています.BuildMI関数で命令とレジスタを追加していっています.
リスト7.23: SampleInstrInfo::copyPhysReg
1: void SampleInstrInfo::
2: copyPhysReg(MachineBasicBlock &MBB,
3: MachineBasicBlock::iterator I, DebugLoc DL,
4: unsigned DestReg, unsigned SrcReg,
5: bool KillSrc) const {
6: unsigned Opc = Sample::ADD;
7: unsigned ZeroReg = Sample::ZERO;
8: MachineInstrBuilder MIB = BuildMI(MBB, I, DL, get(Opc));
9:
10: if (DestReg)
11: MIB.addReg(DestReg, RegState::Define);
12:
13: if (ZeroReg)
14: MIB.addReg(ZeroReg);
15:
16: if (SrcReg)
17: MIB.addReg(SrcReg, getKillRegState(KillSrc));
18: }
レジスタを指定のフレームインデックスにstoreする関数です.実装をリスト7.24に示します.引数に保存するレジスタ,保存する位置などが与えられるので,それらから命令を生成します.BuildMI関数を使ってstore命令を生成します.
リスト7.24: SampleInstrInfo::storeRegToStackSlot
1: void SampleInstrInfo::
2: storeRegToStackSlot(MachineBasicBlock &MBB, MachineBasicBlock::iterator I,
3: unsigned SrcReg, bool isKill, int FI,
4: const TargetRegisterClass *RC,
5: const TargetRegisterInfo *TRI) const {
6: DebugLoc DL;
7: if (I != MBB.end()) DL = I->getDebugLoc();
8: MachineFunction &MF = *MBB.getParent();
9: MachineFrameInfo &MFI = *MF.getFrameInfo();
10:
11: MachineMemOperand *MMO =
12: MF.getMachineMemOperand(MachinePointerInfo::getFixedStack(FI),
13: MachineMemOperand::MOStore,
14: MFI.getObjectSize(FI),
15: MFI.getObjectAlignment(FI));
16:
17: BuildMI(MBB, I, DL, get(Sample::STORE))
18: .addReg(SrcReg, getKillRegState(isKill))
19: .addFrameIndex(FI).addImm(0).addMemOperand(MMO);
20: }
storeRegToStackSlotの逆で指定のフレームインデックスからレジスタにloadする関数です.実装をリスト7.25に示しています.BuildMI関数を使ってload命令を生成します.
リスト7.25: SampleInstrInfo::loadRegFromStackSlot
1: void SampleInstrInfo::
2: loadRegFromStackSlot(MachineBasicBlock &MBB,
3: MachineBasicBlock::iterator MI,
4: unsigned DestReg, int FI,
5: const TargetRegisterClass *RC,
6: const TargetRegisterInfo *TRI) const {
7: DebugLoc DL;
8: if (MI != MBB.end()) DL = MI->getDebugLoc();
9: MachineFunction &MF = *MBB.getParent();
10: MachineFrameInfo &MFI = *MF.getFrameInfo();
11:
12: MachineMemOperand *MMO =
13: MF.getMachineMemOperand(MachinePointerInfo::getFixedStack(FI),
14: MachineMemOperand::MOLoad,
15: MFI.getObjectSize(FI),
16: MFI.getObjectAlignment(FI));
17:
18: BuildMI(MBB, MI, DL, get(Sample::LOAD))
19: .addReg(DestReg).addFrameIndex(FI).addImm(0).addMemOperand(MMO);
20: }
今のところ分岐命令を使っていないため実装していません.
SampleRegisterInfoクラスはレジスタ関連の実装になります.クラス定義をリスト7.26に示します.TableGenで生成されたSampleGenRegisterInfoクラスを継承したクラスです.基本的にはそれほど複雑なものはないです.
リスト7.26: SampleRegisterInfoのクラス定義
1: struct SampleRegisterInfo : public SampleGenRegisterInfo {
2: const TargetInstrInfo &TII;
3:
4: SampleRegisterInfo(const TargetInstrInfo &tii);
5:
6: const uint16_t *getCalleeSavedRegs(const MachineFunction* MF = 0) const;
7: const uint32_t *getCallPreservedMask(CallingConv::ID) const;
8:
9: BitVector getReservedRegs(const MachineFunction &MF) const;
10:
11: void eliminateCallFramePseudoInstr(MachineFunction &MF,
12: MachineBasicBlock &MBB,
13: MachineBasicBlock::iterator I) const;
14:
15: void eliminateFrameIndex(MachineBasicBlock::iterator II,
16: int SPAdj, RegScavenger *RS = NULL) const;
17:
18: unsigned getFrameRegister(const MachineFunction &MF) const;
19: };
この関数は関数呼び出し時に保存しなければいけないレジスタのリストを返します.TableGenで生成されたSampleGenRegisterInfo.incに定義されているCSR_SingleFloatOnly_SaveListを返すだけで良いです.
関数呼び出し時に保存しなければいけないレジスタリストのマスクを返します.SampleGenRegisterInfo.incで定義されているCSR_SingleFloatOnly_RegMaskを返すだけで良いです.
この関数はコンパイラが一時的な値として使ってはいけないレジスタのセット(BitVector)を返す必要があります.今回の仕様ではZERO, SP, RAレジスタは特殊用途で使うため,これらを指定します.
この関数は擬似命令を削除するためのものです.単純に命令を削除するだけで良いです.
このメソッドではフレームインデックスが擬似的なものになっているのを本来のレジスタに置き換えます.実装をリスト7.27に示します.
リスト7.27: SampleRegisterInfo::eliminateFrameIndex
1: void SampleRegisterInfo::
2: eliminateFrameIndex(MachineBasicBlock::iterator II, int SPAdj,
3: RegScavenger *RS) const {
4: MachineInstr &MI = *II;
5: const MachineFunction &MF = *MI.getParent()->getParent();
6:
7: // フレームインデックスを探す
8: unsigned opIndex;
9: for (opIndex = 0; opIndex < MI.getNumOperands(); opIndex++) {
10: if (MI.getOperand(opIndex).isFI()) break;
11: }
12: assert(opIndex < MI.getNumOperands() && "Instr doesn't have FrameIndex operand!");
13:
14: int FrameIndex = MI.getOperand(opIndex).getIndex();
15: uint64_t stackSize = MF.getFrameInfo()->getStackSize();
16: int64_t spOffset = MF.getFrameInfo()->getObjectOffset(FrameIndex);
17: int64_t Offset = spOffset + stackSize + MI.getOperand(opIndex+1).getImm();
18: unsigned FrameReg = Sample::SP;
19:
20: // レジスタと即値を変更する
21: MI.getOperand(opIndex).ChangeToRegister(FrameReg, false);
22: MI.getOperand(opIndex+1).ChangeToImmediate(Offset);
23: }
実装としてはまず,与えられたMachineBasicBlockを走査してフレームインデックスがあるMachineInstrを探します.この時点でのMachineInstrをdumpすると以下のようになってます.
LOAD %RA, <fi#0>, 0;
これをChangeToRegisterを使ってオペランドを変えます.ついでに必要があれば即値を変えてもよいでしょう.変更した後は以下のようになるでしょう.この例ではスタックに保存したRAレジスタを取り出そうとしているので,相対位置が60となります.
LOAD %RA, %SP, 60;
フレームレジスタを返す関数です.仕様でSPレジスタを使うことにしているのでSPレジスタを返すだけです.
SampleMachineFunctionInfoクラスはMachineFunctionInfoを継承するクラスです.今の実装では特に使っていないため空っぽになっています.このクラスに任意の値を格納しておくことでMachineFunction単位でターゲット固有の情報を格納することができます.
リスト7.28: SampleMachineFunctionInfoのクラス定義
1: class SampleMachineFunctionInfo : public MachineFunctionInfo {
2: virtual void anchor();
3:
4: public:
5: SampleMachineFunctionInfo(MachineFunction& MF) {}
6: };
SelectionDAGISelパスはバックエンドで最も最初に実行されるパスです.このパスの最終的な目的はLLVM IRからMIへ変換することです.その間にSelectionDAGと呼ばれるグラフ構造に一度変換し,ターゲット非依存/依存両方の最適化を行います.SelectionDAGISelパスでは常にSelectionDAGを操作していきます.すでに述べたとおりSelectionDAGISelパスはLower,Combine,Legalize,Select,Scheduleの5つのフェーズから成ります.最後のSchedule以外はターゲット毎に実装すべきものがあります.
Lowerフェーズの具体的な処理はSelectionDAGBuilder::visitで行われ,基本的にはLLVM IRからSelectionDAGへの1対1への変換が行われます.関数呼び出しに関してはSampleTargetLoweringクラスのLowerCallとLowerReturnにより別途実装しなければいけません.
CombineフェーズではDAGCombinerクラスにより命令の置き換え処理が行われます.このフェーズで冗長な命令はよりシンプルな命令へと置き換えられます.ターゲット固有の処理を実装する場合にはSampleTargetLowering::PerformDAGCombineをoverrideすることで任意の処理を追加できます.
LegalizeフェーズではSampleTargetLoweringのコンストラクタで指定した内容に従ってターゲットで対応していない命令を対応している命令へと変換します.Legalizeは処理内容により担当するクラスが異なりDAGTypeLegalizerクラス,VectorLegalizerクラス,SelectionDAGLegalizeクラスなどがあります.
SelectフェーズではSelectionDAGの各命令とMIを対応付けするためにSampleDAGToDAGISel::Selectが呼び出されます.Selectフェーズの処理はSelectionDAGISel::DoInstructionSelectionで行われます.
SampleDAGToDAGISelクラスはSelectionDAGISelクラスを継承したもので,MachineFunctionPassを継承したものでもあります.クラス定義をリスト7.29に示します.特に重要なものはSelectとSelectAddrになります.これらについては個別に詳細を説明していきます.
リスト7.29: SampleDAGToDAGISelのクラス定義
1: class SampleDAGToDAGISel : public SelectionDAGISel {
2: SampleTargetMachine &TM;
3: const SampleSubtarget &Subtarget;
4:
5: public:
6: explicit SampleDAGToDAGISel(SampleTargetMachine &tm) :
7: SelectionDAGISel(tm),
8: TM(tm),
9: Subtarget(tm.getSubtarget<SampleSubtarget>()) {}
10:
11: // パス名
12: virtual const char *getPassName() const {
13: return "Sample DAG->DAG Pattern Instruction Selection";
14: }
15:
16: private:
17: // TableGenで自動生成されたメソッドをインクルード
18: #include "SampleGenDAGISel.inc"
19:
20: const SampleTargetMachine &getTargetMachine() {
21: return static_cast<const SampleTargetMachine &>(TM);
22: }
23:
24: const SampleInstrInfo *getInstrInfo() {
25: return getTargetMachine().getInstrInfo();
26: }
27:
28: SDNode *Select(SDNode *N);
29:
30: // Complex Pattern.
31: bool SelectAddr(SDValue N, SDValue &Base, SDValue &Offset);
32: };
Selectは必ず実装しなければならないメソッドです.実装をリスト7.30に示します.
リスト7.30: SampleDAGToDAGISel::Select
1: SDNode* SampleDAGToDAGISel::Select(SDNode *Node) {
2: // TableGenにより自動生成された関数を呼び出す
3: SDNode *ResNode = SelectCode(Node);
4: return ResNode;
5: }
メインの処理はTableGenで生成されたSampleGenDAGISel.incの中で共通処理であるSelectCode関数が定義されてます.Select関数ではこのSelectCode関数を使います.これは名前の通りSelectionDAGISelパスのSelectフェーズで実行されるものです.命令(SDNode)に対応するMIをTableGenにより自動生成されたテーブルから引いて,MIの情報を持ったSDNodeを返します.この対応関係はほとんど全てがTableGenにより生成されますが,ComplexPatternなど特殊なパターンマッチを必要とするものは対応する関数がコールバックされます.
これはTableGenで定義したComplexPatternでSelectAddrを指定しているため,必ず実装する必要があります.実装をリスト7.31に示します.Selectフェーズでload/store命令を扱うときにSelectCodeの内部から呼び出されます.
リスト7.31: SampleDAGToDAGISel::SelectAddr
1: bool SampleDAGToDAGISel::
2: SelectAddr(SDValue N, SDValue &Base, SDValue &Offset) {
3: EVT ValTy = N.getValueType();
4:
5: if (FrameIndexSDNode *FIN = dyn_cast<FrameIndexSDNode>(N)) {
6: Base = CurDAG->getTargetFrameIndex(FIN->getIndex(), ValTy);
7: Offset = CurDAG->getTargetConstant(0, ValTy);
8: return true;
9: }
10:
11: llvm_unreachable("Unknown pattern");
12: return true;
13: }
SelectAddrでは第一引数で受け取った値をレジスタ部分と即値部分に分けて返します.
SampleTargetLoweringクラスはSelectionDAGISelパスにおいて,LLVM IRからSelectionDAGに落とす(Lowering)ときにターゲット固有の処理を記述するクラスです.クラス定義をリスト7.32に示します.
リスト7.32: SampleTargetLoweringのクラス定義
1: class SampleTargetLowering : public TargetLowering {
2: const SampleSubtarget &Subtarget;
3:
4: public:
5: explicit SampleTargetLowering(SampleTargetMachine &TM);
6:
7: // Customで指定した操作のLowering
8: virtual SDValue LowerOperation(SDValue Op, SelectionDAG &DAG) const;
9:
10: // 引数のLowering
11: virtual SDValue
12: LowerFormalArguments(SDValue Chain, CallingConv::ID CallConv,
13: bool isVarArg,
14: const SmallVectorImpl<ISD::InputArg> &Ins,
15: DebugLoc dl, SelectionDAG &DAG,
16: SmallVectorImpl<SDValue> &InVals) const;
17:
18: // 関数呼び出しのLowering
19: virtual SDValue
20: LowerCall(CallLoweringInfo &CLI,
21: SmallVectorImpl<SDValue> &InVals) const;
22:
23: // 戻り値のLowering
24: virtual SDValue
25: LowerCallResult(SDValue Chain, SDValue InFlag,
26: CallingConv::ID CallConv, bool isVarArg,
27: const SmallVectorImpl<ISD::InputArg> &Ins,
28: DebugLoc dl, SelectionDAG &DAG,
29: SmallVectorImpl<SDValue> &InVals) const;
30:
31: // returnのLowering
32: virtual SDValue
33: LowerReturn(SDValue Chain,
34: CallingConv::ID CallConv, bool isVarArg,
35: const SmallVectorImpl<ISD::OutputArg> &Outs,
36: const SmallVectorImpl<SDValue> &OutVals,
37: DebugLoc dl, SelectionDAG &DAG) const;
38: };
SampleTargetLoweringクラスのメソッド名を見るとわかりますが,LowerOperationを除くと呼び出し規約に関わる部分を独自に実装しなければなりません.基本的にはSelectionDAGへのLoweringは1対1で行われるため特別な記述は必要ありませんが,呼び出し規約はターゲット毎に仕様が異なるため,Loweringする際に対応するメソッドがコールバックされる仕組みになっています.
コンストラクタではターゲットがサポートしている仕様について設定する必要があります.コンストラクタの実装をリスト7.33に示します.
リスト7.33: SampleTargetLoweringのコンストラクタ
1: SampleTargetLowering::
2: SampleTargetLowering(SampleTargetMachine &TM)
3: : TargetLowering(TM, new TargetLoweringObjectFileELF()),
4: Subtarget(*TM.getSubtargetImpl()) {
5:
6: // booleanをどう表すかを定義
7: setBooleanContents(ZeroOrOneBooleanContent);
8: setBooleanVectorContents(ZeroOrOneBooleanContent);
9:
10: // ターゲットで利用できるレジスタを登録
11: addRegisterClass(MVT::i32, Sample::CPURegsRegisterClass);
12:
13: // (符号)拡張ロード命令が対応していない型の操作方法を登録
14: setLoadExtAction(ISD::EXTLOAD, MVT::i1, Promote);
15: setLoadExtAction(ISD::EXTLOAD, MVT::i8, Promote);
16: setLoadExtAction(ISD::EXTLOAD, MVT::i16, Promote);
17: setLoadExtAction(ISD::ZEXTLOAD, MVT::i1, Promote);
18: setLoadExtAction(ISD::ZEXTLOAD, MVT::i8, Promote);
19: setLoadExtAction(ISD::ZEXTLOAD, MVT::i16, Promote);
20: setLoadExtAction(ISD::SEXTLOAD, MVT::i1, Promote);
21: setLoadExtAction(ISD::SEXTLOAD, MVT::i8, Promote);
22: setLoadExtAction(ISD::SEXTLOAD, MVT::i16, Promote);
23:
24: // 関数のアラインメント
25: setMinFunctionAlignment(2);
26:
27: // スタックポインタのレジスタを指定
28: setStackPointerRegisterToSaveRestore(Sample::SP);
29:
30: // レジスタの操作方法を計算
31: computeRegisterProperties();
32: }
まず,addRegisterClassでどの型(32bit整数型とか)がどのレジスタクラスで対応しているかを対応付けします.
次に,ターゲットでサポートしてない命令(SelectionDAG)を処理するためにコールバックに登録します.コールバックの種類はPromote, Expand, Custom, Legalの4つがあります.
Promoteはターゲットでサポートされてない型をより大きな型へ拡張して対応させるものです.例えば,i1型に対する操作をターゲットで対応していなくても,i8型なら対応している場合はPromoteすれば良いです.Expandは他の命令(もしくは命令郡)で対応させるようにするものです.Customは特別な操作が必要な命令のために独自でその操作を実装するためのものです.例えば,特定のレジスタが必要であったりスタック操作が必要であったりする命令などです.Customで指定した命令はLowerOperationでその命令に対する操作を実装する必要があります.Legalはその命令がターゲットで対応されていることを示すものです.Legalはデフォルトで指定されているため改めて使用することはほとんどないですが,一旦Expandで指定したものを(サブターゲットで)後からLegalにする場合などで使います.
とりあえず今回の実装では整数型のPromoteのみを一通りしています.最後にcomputeRegisterPropertiesを呼び出して設定完了です.
LowerOperationはsetOperationActionでCustomに指定したDAGノードに対する操作を実装する関数です.第一引数のOpのgetOpcodeメソッドで操作すべきopcodeが得られるので,switchで分岐して個別の操作を実装します.
Customに指定しているノードがない場合には特に実装する必要はありません.
LowerFormalArgumentsでは関数呼び出し時の引数の処理をします.実装をリスト7.34に示します.
リスト7.34: SampleTargetLowering::LowerFormalArguments
1: SDValue SampleTargetLowering::
2: LowerFormalArguments(SDValue Chain, CallingConv::ID CallConv,
3: bool isVarArg,
4: const SmallVectorImpl<ISD::InputArg> &Ins,
5: DebugLoc dl, SelectionDAG &DAG,
6: SmallVectorImpl<SDValue> &InVals) const {
7: MachineFunction &MF = DAG.getMachineFunction();
8: MachineFrameInfo *MFI = MF.getFrameInfo();
9: MachineRegisterInfo &RegInfo = MF.getRegInfo();
10:
11: // 引数を格納する
12: SmallVector<CCValAssign, 16> ArgLocs;
13: CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(),
14: getTargetMachine(), ArgLocs, *DAG.getContext());
15: CCInfo.AnalyzeFormalArguments(Ins, CC_Sample);
16:
17: for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
18: CCValAssign &VA = ArgLocs[i];
19: if (VA.isRegLoc()) {
20: // 引数がレジスタ経由で渡された場合
21: EVT RegVT = VA.getLocVT();
22: // 使用するレジスタクラスを指定
23: const TargetRegisterClass *RC = Sample::CPURegsRegisterClass;
24:
25: if (VA.getLocInfo() != CCValAssign::Full) {
26: llvm_unreachable("not supported yet");
27: }
28:
29: // 仮想レジスタを作成
30: unsigned VReg = RegInfo.createVirtualRegister(RC);
31: RegInfo.addLiveIn(VA.getLocReg(), VReg);
32: SDValue ArgValue = DAG.getCopyFromReg(Chain, dl, VReg, RegVT);
33: InVals.push_back(ArgValue);
34: } else { // VA.isRegLoc()
35: // 引数がスタック経由で渡された場合
36: assert(VA.isMemLoc());
37:
38: // フレームインデックスを作成する
39: unsigned ObjSize = VA.getLocVT().getSizeInBits()/8;
40: int FI = MFI->CreateFixedObject(ObjSize, VA.getLocMemOffset(), true);
41:
42: // スタックから引数を取得するためにloadノードを作成する
43: SDValue FIN = DAG.getFrameIndex(FI, MVT::i32);
44: InVals.push_back(DAG.getLoad(VA.getLocVT(), dl, Chain, FIN,
45: MachinePointerInfo::getFixedStack(FI),
46: false, false, false, 0));
47: }
48: }
49:
50: return Chain;
51: }
全ての引数を走査して引数がレジスタで渡される場合とスタックで渡される場合で処理を分けます.実際には今回の仕様では引数はレジスタ経由でのみ渡すことにしているため,スタック経由の方は実行されることはありません.レジスタで渡される場合は使用するレジスタクラスを指定して仮想レジスタを作成します.処理が終わったらそれぞれをInValsに追加して終わりです.
LowerCallでは関数呼び出し時の処理を行います.実装をリスト7.35に示します.
リスト7.35: SampleTargetLowering::LowerCall
1: SDValue SampleTargetLowering::
2: LowerCall(CallLoweringInfo &CLI,
3: SmallVectorImpl<SDValue> &InVals) const {
4: SelectionDAG &DAG = CLI.DAG;
5: DebugLoc &dl = CLI.DL;
6: SmallVector<ISD::OutputArg, 32> &Outs = CLI.Outs;
7: SmallVector<SDValue, 32> &OutVals = CLI.OutVals;
8: SmallVector<ISD::InputArg, 32> &Ins = CLI.Ins;
9: SDValue InChain = CLI.Chain;
10: SDValue Callee = CLI.Callee;
11: bool &isTailCall = CLI.IsTailCall;
12: CallingConv::ID CallConv = CLI.CallConv;
13: bool isVarArg = CLI.IsVarArg;
14:
15: // 末尾呼び出しは未対応
16: isTailCall = false;
17:
18: // 関数のオペランドを解析してオペランドをレジスタに割り当てる
19: SmallVector<CCValAssign, 16> ArgLocs;
20: CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(),
21: getTargetMachine(), ArgLocs, *DAG.getContext());
22:
23: CCInfo.AnalyzeCallOperands(Outs, CC_Sample);
24:
25: // スタックを何Byte使っているか取得
26: unsigned NumBytes = CCInfo.getNextStackOffset();
27:
28: // 関数呼び出し開始のNode
29: InChain = DAG.getCALLSEQ_START(InChain,
30: DAG.getConstant(NumBytes, getPointerTy(), true));
31:
32: SmallVector<std::pair<unsigned, SDValue>, 4> RegsToPass;
33: SDValue StackPtr;
34:
35: // 引数をRegsToPassに追加していく
36: for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
37: SDValue Arg = OutVals[i];
38: CCValAssign &VA = ArgLocs[i];
39: ISD::ArgFlagsTy Flags = Outs[i].Flags;
40:
41: // 引数が数値
42: if (Flags.isByVal()) {
43: assert(Flags.getByValSize() &&
44: "ByVal args of size 0 should have been ignored by front-end.");
45: llvm_unreachable("ByVal arguments are not supported");
46: continue;
47: }
48:
49: // 必要に応じてPromoteする
50: switch (VA.getLocInfo()) {
51: default: llvm_unreachable("Unknown loc info!");
52: case CCValAssign::Full: break;
53: case CCValAssign::SExt:
54: Arg = DAG.getNode(ISD::SIGN_EXTEND, dl, VA.getLocVT(), Arg);
55: break;
56: case CCValAssign::ZExt:
57: Arg = DAG.getNode(ISD::ZERO_EXTEND, dl, VA.getLocVT(), Arg);
58: break;
59: case CCValAssign::AExt:
60: Arg = DAG.getNode(ISD::ANY_EXTEND, dl, VA.getLocVT(), Arg);
61: break;
62: }
63:
64: // レジスタ経由の引数はRegsToPassに追加
65: if (VA.isRegLoc()) {
66: RegsToPass.push_back(std::make_pair(VA.getLocReg(), Arg));
67: } else {
68: assert(VA.isMemLoc());
69: llvm_unreachable("MemLoc arguments are not supported");
70: }
71: }
72:
73: // レジスタをコピーするノードを作成
74: SDValue InFlag;
75: for (unsigned i = 0, e = RegsToPass.size(); i != e; ++i) {
76: InChain = DAG.getCopyToReg(InChain, dl, RegsToPass[i].first,
77: RegsToPass[i].second, InFlag);
78: InFlag = InChain.getValue(1);
79: }
80:
81: if (GlobalAddressSDNode *G = dyn_cast<GlobalAddressSDNode>(Callee)) {
82: Callee = DAG.getTargetGlobalAddress(G->getGlobal(), dl, MVT::i32);
83: } else if (ExternalSymbolSDNode *E = dyn_cast<ExternalSymbolSDNode>(Callee)) {
84: Callee = DAG.getTargetExternalSymbol(E->getSymbol(), MVT::i32);
85: }
86:
87: SDVTList NodeTys = DAG.getVTList(MVT::Other, MVT::Glue);
88: SmallVector<SDValue, 8> Ops;
89: Ops.push_back(InChain);
90: Ops.push_back(Callee);
91:
92: // 引数のレジスタをリストに追加
93: for (unsigned i = 0, e = RegsToPass.size(); i != e; ++i) {
94: Ops.push_back(DAG.getRegister(RegsToPass[i].first,
95: RegsToPass[i].second.getValueType()));
96: }
97:
98: if (InFlag.getNode())
99: Ops.push_back(InFlag);
100:
101: InChain = DAG.getNode(SampleISD::Call, dl, NodeTys, &Ops[0], Ops.size());
102: InFlag = InChain.getValue(1);
103:
104: // 関数呼び出し終了のNode
105: InChain = DAG.getCALLSEQ_END(InChain,
106: DAG.getConstant(NumBytes, getPointerTy(), true),
107: DAG.getConstant(0, getPointerTy(), true),
108: InFlag);
109: InFlag = InChain.getValue(1);
110:
111: // 戻り値の処理
112: return LowerCallResult(InChain, InFlag, CallConv, isVarArg,
113: Ins, dl, DAG, InVals);
114: }
このメソッドでもSelectionDAGの操作が行われるため処理が複雑になっています.おおまかな処理の流れとしては以下のようになります.
まず最初に引数が呼び出し規約通りになっているか確認するための解析を行います.この処理はTableGenによって生成されたCC_Sample関数で行われます.その後,引数のリストを配列として格納します. 今回はレジスタでしか引数を渡さないため単純に配列に追加していくだけになっています.
次にDAGを構築していきます.まず引数の配列をそれぞれ仮想レジスタにコピーするためのノードを作成します.そして,それらを引数に取る関数呼び出しのノードを作成します.関数呼び出しの前後にCALLSEQ_STARTとCALLSEQ_ENDというノードを作成していますが,これは関数呼び出しの開始と終了を意味する擬似命令で,後のパスで削除されます.
ここまでで関数呼び出し処理は終わりになりますが,そのまま戻り値の処理をするためのLowerCallResultを呼び出します.
LowerCallResultは関数呼び出し後に戻り値をどう扱うかの処理を行います.実装をリスト7.36に示します.
リスト7.36: SampleTargetLowering::LowerCallResult
1: SDValue SampleTargetLowering::
2: LowerCallResult(SDValue Chain, SDValue InFlag,
3: CallingConv::ID CallConv, bool isVarArg,
4: const SmallVectorImpl<ISD::InputArg> &Ins,
5: DebugLoc dl, SelectionDAG &DAG,
6: SmallVectorImpl<SDValue> &InVals) const {
7: SmallVector<CCValAssign, 16> RVLocs;
8: CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(),
9: getTargetMachine(), RVLocs, *DAG.getContext());
10:
11: CCInfo.AnalyzeCallResult(Ins, RetCC_Sample);
12:
13: // 結果レジスタをコピー
14: for (unsigned i = 0; i != RVLocs.size(); ++i) {
15: Chain = DAG.getCopyFromReg(Chain, dl, RVLocs[i].getLocReg(),
16: RVLocs[i].getValVT(), InFlag).getValue(1);
17: InFlag = Chain.getValue(2);
18: InVals.push_back(Chain.getValue(0));
19: }
20:
21: return Chain;
22: }
処理としては他のターゲットと似たような処理になるようです.AnalyzeCallResult関数を呼び出して,その結果を引数で与えられた結果を格納する変数に入れていくだけです.
LowerReturnはLowerFormalArgumentsの逆で関数呼び出しから戻るときの戻り値の処理をします.実装をリスト7.37に示します.
リスト7.37: SampleTargetLowering::LowerReturn
1: SDValue SampleTargetLowering::
2: LowerReturn(SDValue Chain,
3: CallingConv::ID CallConv, bool isVarArg,
4: const SmallVectorImpl<ISD::OutputArg> &Outs,
5: const SmallVectorImpl<SDValue> &OutVals,
6: DebugLoc dl, SelectionDAG &DAG) const {
7:
8: SmallVector<CCValAssign, 16> RVLocs;
9: CCState CCInfo(CallConv, isVarArg, DAG.getMachineFunction(),
10: getTargetMachine(), RVLocs, *DAG.getContext());
11:
12: // 戻り値を解析する
13: CCInfo.AnalyzeReturn(Outs, RetCC_Sample);
14:
15: // この関数で最初の戻り値の場合はレジスタをliveoutに追加
16: if (DAG.getMachineFunction().getRegInfo().liveout_empty()) {
17: for (unsigned i = 0; i != RVLocs.size(); ++i)
18: if (RVLocs[i].isRegLoc())
19: DAG.getMachineFunction().getRegInfo().addLiveOut(RVLocs[i].getLocReg());
20: }
21:
22: SDValue Flag;
23:
24: // 戻り値をレジスタにコピーするノードを作成
25: for (unsigned i = 0; i != RVLocs.size(); ++i) {
26: CCValAssign &VA = RVLocs[i];
27: assert(VA.isRegLoc() && "Can only return in registers!");
28:
29: Chain = DAG.getCopyToReg(Chain, dl, VA.getLocReg(),
30: OutVals[i], Flag);
31:
32: Flag = Chain.getValue(1);
33: }
34:
35: // 常に "ret $ra" を生成
36: if (Flag.getNode())
37: return DAG.getNode(SampleISD::Ret, dl, MVT::Other,
38: Chain, DAG.getRegister(Sample::RA, MVT::i32), Flag);
39: else // Return Void
40: return DAG.getNode(SampleISD::Ret, dl, MVT::Other,
41: Chain, DAG.getRegister(Sample::RA, MVT::i32));
42: }
戻り値が呼び出し規約とあっているかどうかを解析します.戻り値をレジスタにコピーするノードを作成し,呼び出し元に戻るためのノードとしてret命令を作成します.
SampleSelectionDAGInfoクラスはTargetSelectionDAGInfoクラスを継承したものです.SampleMachineFunctionInfoクラスと同様にSelectionDAGISelパスでターゲット固有の情報を格納するために使用します.今回は特に使っていないので空っぽのクラスを作成したのみです.クラス定義をリスト7.38を示します.
リスト7.38: SampleSelectionDAGInfoのクラス定義
1: class SampleSelectionDAGInfo : public TargetSelectionDAGInfo {
2: public:
3: explicit SampleSelectionDAGInfo(const SampleTargetMachine &TM);
4: ~SampleSelectionDAGInfo();
5: };
SelectionDAGISelが終わった後はMIの状態で様々なパスにより最適化が行われます.その中の一つがフレーム処理を行うPEI(PrologEpilogInserter)パスです.PEI以外のパスは新規ターゲットを作成時にターゲット固有の処理を実装する必要はありませんが,PEIパスはターゲット毎に実装しなければいけません.
PEIパスで行うことは関数呼び出し時またはリターン時のスタックフレームサイズの処理です.なぜこの処理をSelectionDAGISelパスなどではなくこのタイミングで行う必要があるかというと,SelectionDAGISelパスやSelectionDAGISelパスが終了直後のMIがまだ仮想レジスタを使っているため,必要になるスタックサイズがわからないからです.PEIパスの前にレジスタ割り当てを行うパスが実行されているため,この段階で必要となるスタックサイズが判明します.
PEIパスではTargetFrameLoweringクラスのemitPrologueとemitEpilogueを呼び出し,関数(MachineFunction)の最初と最後に行う処理を実行します.ターゲット固有の処理を実装するために,TargetFrameLoweringクラスを継承したSampleFrameLoweringクラスを記述していきます.
SampleFrameLoweringクラスはTargetFrameLoweringクラスを継承したクラスです.クラス定義をリスト7.39に示します.重要なメソッドはemitPrologueとemitEpilogueで,これらがPEIパスから呼び出されます.
リスト7.39: SampleFrameLoweringのクラス定義
1: class SampleFrameLowering : public TargetFrameLowering {
2: protected:
3: const SampleSubtarget &STI;
4:
5: public:
6: explicit SampleFrameLowering(const SampleSubtarget &sti)
7: : TargetFrameLowering(StackGrowsDown, 8, 0), STI(sti) {
8: }
9:
10: void emitPrologue(MachineFunction &MF) const;
11: void emitEpilogue(MachineFunction &MF, MachineBasicBlock &MBB) const;
12: bool hasFP(const MachineFunction &MF) const;
13: };
emitPrologueは名前通りMachineFunction毎に最初に処理すべきものを定義する関数です.MachineFunctionで必要なスタックサイズを計算し,そのサイズ分のスタックを確保する必要があります.
emitPrologueの実装をリスト7.40に示します.
リスト7.40: SampleFrameLowering::emitPrologue
1: void SampleFrameLowering::
2: emitPrologue(MachineFunction &MF) const {
3: MachineBasicBlock &MBB = MF.front();
4: MachineFrameInfo *MFI = MF.getFrameInfo();
5:
6: const SampleInstrInfo &TII =
7: *static_cast<const SampleInstrInfo*>(MF.getTarget().getInstrInfo());
8:
9: MachineBasicBlock::iterator MBBI = MBB.begin();
10: DebugLoc dl = MBBI != MBB.end() ? MBBI->getDebugLoc() : DebugLoc();
11:
12: // 実装を簡単にするため固定サイズのスタック割り当て(4byte x レジスタ数)
13: uint64_t StackSize = 4 * 16;
14:
15: // スタックサイズを保存
16: MFI->setStackSize(StackSize);
17:
18: // スタックを伸ばす命令を生成
19: BuildMI(MBB, MBBI, dl, TII.get(Sample::MOVE), Sample::T0)
20: .addImm(-StackSize);
21: BuildMI(MBB, MBBI, dl, TII.get(Sample::ADD), Sample::SP)
22: .addReg(Sample::SP)
23: .addReg(Sample::T0);
24: }
今回の実装では実装を簡単にするために,固定サイズのスタック割り当てにしています.本来であれば必要最小限のサイズを計算して割り当てる必要があります.
スタックサイズが決まれば,最後に命令を生成して完了です.即値のロードはmove命令を,レジスタの加算にはadd命令を生成しています.
emitEpilogueはMachineFunctionの最後に処理すべきものを定義します.主にemitPrologueで行なったことの逆のことをすると思います.emitEpilogueの実装をリスト7.41に示します.
リスト7.41: SampleFrameLowering::emitEpilogue
1: void SampleFrameLowering::
2: emitEpilogue(MachineFunction &MF, MachineBasicBlock &MBB) const {
3: MachineBasicBlock::iterator MBBI = MBB.getLastNonDebugInstr();
4: MachineFrameInfo *MFI = MF.getFrameInfo();
5: const SampleInstrInfo &TII =
6: *static_cast<const SampleInstrInfo*>(MF.getTarget().getInstrInfo());
7: DebugLoc dl = MBBI->getDebugLoc();
8:
9: // FrameInfoからスタックサイズを取得
10: uint64_t StackSize = MFI->getStackSize();
11:
12: // スタックを戻す命令を生成
13: BuildMI(MBB, MBBI, dl, TII.get(Sample::MOVE), Sample::T0)
14: .addImm(StackSize);
15: BuildMI(MBB, MBBI, dl, TII.get(Sample::ADD), Sample::SP)
16: .addReg(Sample::SP)
17: .addReg(Sample::T0);
18: }
emitPrologueで設定したスタックサイズを取得して,SPレジスタを戻すだけとなります.
ターゲットがフレームポインタを持っているかどうかをbooleanで返します.今回のターゲットの仕様ではフレームレジスタを定義しなかったのでfalseを返しています.
AsmPrinterパスはクラス構成がSelectionDAGISelパスに比べると複雑になっています.AsmPrinterパスでMI LayerからMC Layerへの変換を通して最終的なコード生成,つまりアセンブリやオブジェクトの出力を行います.その過程でどのようなクラスを利用しているかを知っておくとわかりやすいと思います.
AsmPrinterパス(クラス)に関連するクラスを図7.4に示します.AsmPrinterクラスはMachineFunctionPassを継承したパスとなります.SampleAsmPrinterクラスはAsmPrinterクラスを継承して,必要に応じてメソッドをoverrideして実装することによりターゲット固有の処理を実現しています.
図7.4: AsmPrinterのクラス図
AsmPrinterクラスの内部ではMCStreamerクラスを持っており,アセンブリ出力やオブジェクト出力はMCStreamerクラスが担当します.MCStreamerが扱うのはMC Layerとなるため,AsmPrinter側でMI LayerからMC LayerへのLoweringをする必要があります.Loweringの処理はSampleMCInstLowerクラスで行います.
MCStreamerの種類(子クラス)として2種類あり,アセンブリ生成の場合はMCAsmStreamerクラス,オブジェクト生成の場合はMCObjectStreamerクラスがあります.オブジェクト生成の場合でもELFファイル生成の場合はMCObjectStreamerの子クラスであるMCELFStreamerクラスを使います.
MCAsmStreamerクラスはMCInstPrinterクラスを操作してアセンブリ生成を行います.また,MCCodeEmitterクラスとMCAsmBackendクラスが実装されている場合は,これらのクラスを利用してアセンブリ生成時に機械語も出力します.
MCAssemblerクラスはMCObjectWriterクラスを操作してオブジェクト生成を行います.実際にはELFファイルを生成する場合にはその子クラスであるMCELFObjectTargetWriterクラスとなります.オブジェクト生成の場合にはMCCodeEmitterとMCAsmBackendの実装は必須となります.これらのクラスを組み合わせてオブジェクト生成します.
SampleAsmPrinterクラスはAsmPrinterクラスを継承したクラスでMachineFunctionPassとなります.SampleAsmPrinterクラスのクラス定義をリスト7.42に示します.
リスト7.42: SampleAsmPrinterのクラス定義
1: class SampleAsmPrinter : public AsmPrinter {
2: public:
3: SampleAsmPrinter(TargetMachine &TM, MCStreamer &Streamer)
4: : AsmPrinter(TM, Streamer) {}
5:
6: virtual const char *getPassName() const {
7: return "Sample Assembly Printer";
8: }
9:
10: void EmitInstruction(const MachineInstr *MI);
11: };
AsmPrinterパスの本体となる重要なクラスですが,ターゲットで個別に実装する必要があるメソッドはEmitInstructionしかありません.実際には親クラスであるAsmPrinterクラスにほとんどの機能があり,そこから様々なクラスを操作しています.
EmitInstructionの実装をリスト7.43に示します.
リスト7.43: SampleAsmPrinter::EmitInstruction
1: void SampleAsmPrinter::
2: EmitInstruction(const MachineInstr *MI) {
3: SampleMCInstLower MCInstLowering(OutContext, *Mang, *this);
4: MCInst TmpInst;
5: MCInstLowering.Lower(MI, TmpInst);
6: OutStreamer.EmitInstruction(TmpInst);
7: }
このメソッドでは引数で与えられたMachineInstr(MI)をMCInst(MC)に変換する必要があります.実際の変換処理はSampleMCInstrLowerクラスに委譲します.MCInstに変換したあとどのように処理されるかはOutStreamer(MCStreamer)によって抽象化されています.アセンブリを出力する場合でもオブジェクトを出力する場合でもMCStreamerにMCInstを渡すだけで良いです.
SampleMCInstLowerクラスは特に何かを継承しているわけではなく,SampleAsmPrinterからのみ使われるクラスになります.このクラスの目的はMachineInstrをMCInstにLoweringすることです.クラス定義をリスト7.44に示します.
リスト7.44: SampleMCInstLowerのクラス定義
1: class SampleMCInstLower {
2: typedef MachineOperand::MachineOperandType MachineOperandType;
3: MCContext &Ctx;
4: Mangler &Mang;
5: AsmPrinter &Printer;
6:
7: public:
8: SampleMCInstLower(MCContext &ctx, Mangler &mang, AsmPrinter &printer)
9: : Ctx(ctx), Mang(mang), Printer(printer) {}
10: void Lower(const MachineInstr *MI, MCInst &OutMI) const;
11:
12: private:
13: MCOperand LowerOperand(const MachineOperand& MO, unsigned offset = 0) const;
14: MCOperand LowerSymbolOperand(const MachineOperand &MO,
15: MachineOperandType MOTy, unsigned Offset) const;
16: };
他のターゲットの実装を見ると,基本的にはMachineInstrからMCInstに変換するLower関数を外部から利用し,Lower関数の内部ではOperandやシンボルを変換するための個別の関数を利用するという実装になっています.
LowerはMachineInstrからMCInstに変換するための基本関数になります.SampleAsmPrinterのEmitInstructionから利用されます.実装をリスト7.45に示します.
リスト7.45: SampleMCInstLower::Lower
1: void SampleMCInstLower::
2: Lower(const MachineInstr *MI, MCInst &OutMI) const {
3: OutMI.setOpcode(MI->getOpcode());
4:
5: for (unsigned i = 0, e = MI->getNumOperands(); i != e; ++i) {
6: const MachineOperand &MO = MI->getOperand(i);
7: MCOperand MCOp = LowerOperand(MO);
8:
9: if (MCOp.isValid())
10: OutMI.addOperand(MCOp);
11: }
12: }
第一引数で渡されたMachineInstrをMCInstに変換し第二引数で返します.やることは単純でOpcodeとOperandをセットするだけです.Operandをセットする際にはLowerOperandを使っています.
LowerOperandの実装をリスト7.46に示します.
リスト7.46: SampleMCInstLower::LowerOperand
1: MCOperand SampleMCInstLower::
2: LowerOperand(const MachineOperand& MO) const {
3: MachineOperandType MOTy = MO.getType();
4: switch (MOTy) {
5: case MachineOperand::MO_Register:
6: if (MO.isImplicit()) break;
7: return MCOperand::CreateReg(MO.getReg());
8: case MachineOperand::MO_Immediate:
9: return MCOperand::CreateImm(MO.getImm());
10: case MachineOperand::MO_MachineBasicBlock:
11: case MachineOperand::MO_GlobalAddress:
12: case MachineOperand::MO_ExternalSymbol:
13: case MachineOperand::MO_JumpTableIndex:
14: case MachineOperand::MO_ConstantPoolIndex:
15: case MachineOperand::MO_BlockAddress:
16: return LowerSymbolOperand(MO, MOTy);
17: case MachineOperand::MO_RegisterMask:
18: break;
19: default:
20: llvm_unreachable("unknown operand type");
21: }
22:
23: return MCOperand();
24: }
Lowerから呼び出されるのみの関数になります.MachineOperandを受け取ってMCOperandを返します.Operandの種類によって処理を分岐させており,レジスタや即値の場合はその場で対応するMCOperandを生成し,それ以外はLowerSymbolOperandに処理を委譲しています.
LowerSymbolOperandの実装をリスト7.47に示します.
リスト7.47: SampleMCInstLower::LowerSymbolOperand
1: MCOperand SampleMCInstLower::
2: LowerSymbolOperand(const MachineOperand &MO,
3: MachineOperandType MOTy) const {
4: switch(MO.getTargetFlags()) {
5: default: llvm_unreachable("Invalid target flag!");
6: case 0: break;
7: }
8:
9: const MCSymbol *Symbol;
10: unsigned Offset = 0;
11: switch (MOTy) {
12: case MachineOperand::MO_MachineBasicBlock:
13: Symbol = MO.getMBB()->getSymbol();
14: break;
15: case MachineOperand::MO_GlobalAddress:
16: Symbol = Mang.getSymbol(MO.getGlobal());
17: Offset = MO.getOffset();
18: break;
19: case MachineOperand::MO_BlockAddress:
20: Symbol = Printer.GetBlockAddressSymbol(MO.getBlockAddress());
21: break;
22: case MachineOperand::MO_ExternalSymbol:
23: Symbol = Printer.GetExternalSymbolSymbol(MO.getSymbolName());
24: break;
25: case MachineOperand::MO_JumpTableIndex:
26: Symbol = Printer.GetJTISymbol(MO.getIndex());
27: break;
28: case MachineOperand::MO_ConstantPoolIndex:
29: Symbol = Printer.GetCPISymbol(MO.getIndex());
30: Offset = MO.getOffset();
31: break;
32: default:
33: llvm_unreachable("<unknown operand type>");
34: }
35:
36: const MCExpr *Expr = MCSymbolRefExpr::Create(Symbol, Ctx);
37:
38: if (Offset) {
39: const MCConstantExpr *OffsetExpr = MCConstantExpr::Create(Offset, Ctx);
40: Expr = MCBinaryExpr::CreateAdd(Expr, OffsetExpr, Ctx);
41: }
42:
43: return MCOperand::CreateExpr(Expr);
44: }
LowerOperandから呼び出され,シンボルの処理を行う関数です.ターゲット固有のシンボルなどを定義していないためこれらの処理は他のターゲットで実装されているものと同じになります.
SampleInstPrinterクラスはMCInstPrinterクラスを継承したクラスです.クラス定義をリスト7.48に示します.
リスト7.48: SampleInstPrinterのクラス定義
1: class SampleInstPrinter : public MCInstPrinter {
2: public:
3: SampleInstPrinter(const MCAsmInfo &MAI, const MCInstrInfo &MII,
4: const MCRegisterInfo &MRI)
5: : MCInstPrinter(MAI, MII, MRI) {}
6:
7: // MCStreamerから呼び出される
8: virtual void printRegName(raw_ostream &OS, unsigned RegNo) const;
9: virtual void printInst(const MCInst *MI, raw_ostream &O, StringRef Annot);
10:
11: private:
12: // TableGenにより自動生成
13: void printInstruction(const MCInst *MI, raw_ostream &O);
14: static const char *getRegisterName(unsigned RegNo);
15:
16: // printInstructionからコールバックされる
17: void printOperand(const MCInst *MI, unsigned OpNo, raw_ostream &O);
18: void printMemOperand(const MCInst *MI, int opNum, raw_ostream &O);
19: };
MCInstPrinterという名前から想像できるようにMCInstを表示,つまりアセンブリを出力するためのクラスになります.アセンブリを出力するMCStreamerであるMCAsmStreamerクラス,もしくはディスアセンブラのllvm-objdumpなどから利用されます.
命令を表示するための関数です.実装をリスト7.49に示します.
リスト7.49: SampleInstPrinter::printInst
1: void SampleInstPrinter::
2: printInst(const MCInst *MI, raw_ostream &O, StringRef Annot) {
3: printInstruction(MI, O);
4: printAnnotation(O, Annot);
5: }
実際には命令を表示する関数はprintInstructionで定義されているため,単に呼び出すだけで問題ないです.printAnnotationはコメントを表示する関数です.
レジスタを表示するための関数です.getRegisterNameでレジスタ番号に対応するレジスタ名を取得できるので,それを利用して表示するだけです.
この関数はTableGenから生成されたSampleGenAsmWriter.incに実装があるため,別途実装する必要はありません.命令(Opcode)の表示に関してはこの関数の中に全て実装されていますが,Operandの表示は実装されていないためOperandの表示に対応する関数であるprintOperandがコールバックされます.
この関数も同様にTableGenから生成されたSampleGenAsmWriter.incに実装があるため,別途実装する必要はありません.レジスタ番号を引数に渡すとレジスタ名を文字列で返してくれる関数です.
デフォルトのOperandを表示するための関数です.実装をリスト7.50に示します.
リスト7.50: SampleInstPrinter::printOperand
1: void SampleInstPrinter::
2: printOperand(const MCInst *MI, unsigned OpNo, raw_ostream &O) {
3: const MCOperand &Op = MI->getOperand(OpNo);
4: if (Op.isReg()) {
5: printRegName(O, Op.getReg());
6: } else if (Op.isImm()) {
7: O << Op.getImm();
8: } else {
9: assert(Op.isExpr() && "unknown operand kind in printOperand");
10: O << *Op.getExpr();
11: }
12: }
printInstructionからコールバックで呼び出されます.Operandによってはコールバックで呼び出す関数を指定できますが,特に指定していない場合はprintOperandが呼び出されます.
printOperandと同様にOperandを表示するための関数です.実装をリスト7.51に示します.
リスト7.51: SampleInstPrinter::printMemOperand
1: void SampleInstPrinter::
2: printMemOperand(const MCInst *MI, int opNum, raw_ostream &O) {
3: printOperand(MI, opNum+1, O);
4: O << "(";
5: printOperand(MI, opNum, O);
6: O << ")";
7: }
TableGenでmemというOperandを定義したときにprintMethodとしてprintMemOperandを指定したため,memのOperandを表示するためにこの関数がprintInstructionからコールバックで呼び出されます.
SampleAsmBackendクラスは名前と異なりアセンブリというよりはオブジェクトの処理に関わるクラスです.クラス定義をリスト7.52に示します.
リスト7.52: SampleAsmBackendのクラス定義
1: class SampleAsmBackend : public MCAsmBackend {
2: Triple::OSType OSType;
3:
4: public:
5: SampleAsmBackend(const Target &T, Triple::OSType _OSType)
6: :MCAsmBackend(), OSType(_OSType) {}
7:
8: MCObjectWriter *createObjectWriter(raw_ostream &OS) const {
9: return createSampleELFObjectWriter(OS, OSType);
10: }
11:
12: void applyFixup(const MCFixup &Fixup, char *Data, unsigned DataSize,
13: uint64_t Value) const;
14:
15: unsigned getNumFixupKinds() const { return Sample::NumTargetFixupKinds; }
16:
17: const MCFixupKindInfo &getFixupKindInfo(MCFixupKind Kind) const;
18:
19: bool mayNeedRelaxation(const MCInst &Inst) const {
20: return false;
21: }
22:
23: bool fixupNeedsRelaxation(const MCFixup &Fixup,
24: uint64_t Value,
25: const MCInstFragment *DF,
26: const MCAsmLayout &Layout) const {
27: return false;
28: }
29:
30: void relaxInstruction(const MCInst &Inst, MCInst &Res) const {
31: }
32:
33: bool writeNopData(uint64_t Count, MCObjectWriter *OW) const {
34: return true;
35: }
36: };
fixupは現時点では具体的な値を決定できないため,後に値を修正しなければいけないものです.関数のアドレスやグローバル変数のアドレスなど,リンク時にアドレスが決定するものなどが該当します.今回の実装でfixupが関わる部分としてはcall命令のOperandである関数のアドレスです.
createObjectWriterでオブジェクト生成時に使用するObjectWriterクラスのインスタンスを生成します.ObjectWriterクラスのインスタンスを生成するための関数としてcreateSampleELFObjectWriterを作成しているので,それをそのまま呼び出すだけで良いです.createSampleELFObjectWriterはSampleELFObjectWriterクラスのインスタンスを生成する関数です.
fixupが必要な値を修正する関数です.実装をリスト7.53に示します.
リスト7.53: SampleAsmBackend::applyFixup
1: void SampleAsmBackend::
2: applyFixup(const MCFixup &Fixup, char *Data, unsigned DataSize,
3: uint64_t Value) const {
4: // 修正後の値を求める
5: MCFixupKind Kind = Fixup.getKind();
6: Value = adjustFixupValue((unsigned)Kind, Value);
7:
8: if (!Value)
9: return; // Doesn't change encoding.
10:
11: // オブジェクトのどこから修正するか
12: unsigned Offset = Fixup.getOffset();
13: // 何バイト修正するか
14: unsigned NumBytes = (getFixupKindInfo(Kind).TargetSize + 7) / 8;
15:
16: // 現在の値を取得
17: uint64_t CurVal = 0;
18:
19: for (unsigned i = 0; i != NumBytes; ++i) {
20: unsigned Idx = i;
21: CurVal |= (uint64_t)((uint8_t)Data[Offset + Idx]) << (i*8);
22: }
23:
24: uint64_t Mask = ((uint64_t)(-1) >> (64 - getFixupKindInfo(Kind).TargetSize));
25: CurVal |= Value & Mask;
26:
27: // 修正後の値を書きだす
28: for (unsigned i = 0; i != NumBytes; ++i) {
29: unsigned Idx = i;
30: Data[Offset + Idx] = (uint8_t)((CurVal >> (i*8)) & 0xff);
31: }
32: }
adjustFixupValueが実際に値を決定する関数となります.この時点で値を決定できない場合は0が返ってくるため処理が終了します.値が決定できた場合は,Dataから現在の値を取得し,修正後の値で上書きします.
fixupの数を返します.SampleFixupKinds.hでターゲットで使用するfixupを定義しているので,ここではSample::NumTargetFixupKindsを返すだけで良いです.SampleFixupKinds.hについては本書では省略していますので,別途ソースコードを参照して下さい.
fixupの種類を返します.現在定義しているfixupの種類としてはcall命令のOperandで使うfixup_Sample_24だけです.実装をリスト7.54に示します.
リスト7.54: SampleAsmBackend::getFixupKindInfo
1: const MCFixupKindInfo &SampleAsmBackend::
2: getFixupKindInfo(MCFixupKind Kind) const {
3: const static MCFixupKindInfo Infos[Sample::NumTargetFixupKinds] = {
4: // name offset bits flags
5: {"fixup_Sample_24", 0, 24, 0}
6: };
7:
8: if (Kind < FirstTargetFixupKind)
9: return MCAsmBackend::getFixupKindInfo(Kind);
10:
11: assert(unsigned(Kind - FirstTargetFixupKind) < getNumFixupKinds() &&
12: "Invalid kind!");
13: return Infos[Kind - FirstTargetFixupKind];
14: }
Relaxationというのは命令を(効率の良い)より長い命令で置き換えることを意味します.今回は特に使用していないため未実装となっています.
NOP(何もしない命令)を挿入するためのメソッドです.今回のターゲットではNOPを定義してないので使っていません.
SampleMCCodeEmitterクラスはMCInstをエンコードしてバイナリに変換する機能を提供します.クラス定義をリスト7.55に示します.外部から利用されるのはEncodeInstrcutionメソッドだけで,このメソッドから他のメソッドが利用されています.
リスト7.55: SampleMCCodeEmitterのクラス定義
1: class SampleMCCodeEmitter : public MCCodeEmitter {
2: SampleMCCodeEmitter(const SampleMCCodeEmitter &); // DO NOT IMPLEMENT
3: void operator=(const SampleMCCodeEmitter &); // DO NOT IMPLEMENT
4: const MCInstrInfo &MCII;
5: const MCSubtargetInfo &STI;
6: MCContext &Ctx;
7:
8: public:
9: SampleMCCodeEmitter(const MCInstrInfo &mcii, const MCSubtargetInfo &sti,
10: MCContext &ctx)
11: : MCII(mcii), STI(sti) , Ctx(ctx) {}
12: ~SampleMCCodeEmitter() {}
13:
14: // 命令をバイナリにして出力する
15: void EncodeInstruction(const MCInst &MI, raw_ostream &OS,
16: SmallVectorImpl<MCFixup> &Fixups) const;
17:
18: private:
19: // 命令のバイナリエンコーディングを取得
20: uint64_t getBinaryCodeForInstr(const MCInst &MI,
21: SmallVectorImpl<MCFixup> &Fixups) const;
22:
23: // オペランドのバイナリエンコーディングを取得
24: unsigned getMachineOpValue(const MCInst &MI,const MCOperand &MO,
25: SmallVectorImpl<MCFixup> &Fixups) const;
26:
27: // load/storeのオペランドのバイナリエンコーディングを取得
28: unsigned getMemEncoding(const MCInst &MI, unsigned OpNo,
29: SmallVectorImpl<MCFixup> &Fixups) const;
30:
31: // move命令のオペランドのバイナリエンコーディングを取得
32: unsigned getMoveTargetOpValue(const MCInst &MI, unsigned OpNo,
33: SmallVectorImpl<MCFixup> &Fixups) const;
34:
35: // call命令のオペランドのバイナリエンコーディングを取得
36: unsigned getCallTargetOpValue(const MCInst &MI, unsigned OpNo,
37: SmallVectorImpl<MCFixup> &Fixups) const;
38: };
EncodeInstructionは与えられた命令(MCInst)を出力するための関数です.実装をリスト7.56に示します.
リスト7.56: SampleMCCodeEmitter::EncodeInstruction
1: void SampleMCCodeEmitter::
2: EncodeInstruction(const MCInst &MI, raw_ostream &OS,
3: SmallVectorImpl<MCFixup> &Fixups) const {
4: uint32_t Binary = getBinaryCodeForInstr(MI, Fixups);
5:
6: // 全ての命令は4バイト
7: int Size = 4;
8:
9: for (int i = Size - 1; i >= 0; --i) {
10: unsigned Shift = i * 8;
11: OS << char((Binary >> Shift) & 0xff);
12: }
13: }
実装としては単純で,getBinaryCodeForInstrにMCInstを渡すと命令をエンコードした結果が返ってくるのでそれを表示するだけです.
getBinaryCodeForInstrはTableGenによって自動生成される関数です.命令をエンコードする際にOpcodeは自動的にエンコードされますが,Operandは自動的にエンコードされずエンコードするための専用の関数がコールバックされます.例えば汎用レジスタのエンコードにはgetMachineOpValueが呼び出されます.コールバック関数は別途実装する必要があります.
OperandをデコードするためにそれぞれのOperandに対応する関数がgetBinaryCodeForInstrからコールバックされます.それぞれの実装をリスト7.57に示します.
リスト7.57: SampleMCCodeEmitterのエンコード関数
1: unsigned SampleMCCodeEmitter::
2: getMachineOpValue(const MCInst &MI, const MCOperand &MO,
3: SmallVectorImpl<MCFixup> &Fixups) const {
4: if (MO.isReg()) {
5: unsigned Reg = MO.getReg();
6: unsigned RegNo = getSampleRegisterNumbering(Reg);
7: return RegNo;
8: } else if (MO.isImm()) {
9: return static_cast<unsigned>(MO.getImm());
10: } else if (MO.isFPImm()) {
11: return static_cast<unsigned>(APFloat(MO.getFPImm())
12: .bitcastToAPInt().getHiBits(32).getLimitedValue());
13: }
14:
15: // Expr以外はエラー.
16: assert(MO.isExpr());
17: llvm_unreachable("not implemented");
18: return 0;
19: }
20:
21: unsigned SampleMCCodeEmitter::
22: getMemEncoding(const MCInst &MI, unsigned OpNo,
23: SmallVectorImpl<MCFixup> &Fixups) const {
24: // レジスタが19-16bit目,即値が15-0bit目
25: assert(MI.getOperand(OpNo).isReg());
26: unsigned RegBits = getMachineOpValue(MI, MI.getOperand(OpNo),Fixups) << 16;
27: unsigned OffBits = getMachineOpValue(MI, MI.getOperand(OpNo+1), Fixups);
28:
29: return (OffBits & 0xFFFF) | RegBits;
30: }
31:
32: unsigned SampleMCCodeEmitter::
33: getMoveTargetOpValue(const MCInst &MI, unsigned OpNo,
34: SmallVectorImpl<MCFixup> &Fixups) const {
35: // 即値が19-0bit目
36: assert(MI.getOperand(OpNo).isImm());
37: unsigned value = getMachineOpValue(MI, MI.getOperand(OpNo),Fixups);
38: return value & 0xFFFFF;
39: }
40:
41: unsigned SampleMCCodeEmitter::
42: getCallTargetOpValue(const MCInst &MI, unsigned OpNo,
43: SmallVectorImpl<MCFixup> &Fixups) const {
44:
45: const MCOperand &MO = MI.getOperand(OpNo);
46: assert(MO.isExpr() && "getCallTargetOpValue expects only expressions");
47:
48: const MCExpr *Expr = MO.getExpr();
49: Fixups.push_back(MCFixup::Create(0, Expr,
50: MCFixupKind(Sample::fixup_Sample_24)));
51: return 0;
52: }
getMachineOpValueはデフォルトのOperandをエンコードするために呼び出されます.レジスタならレジスタ番号,即値ならその値を返します.Exprの場合は未実装のためエラーになります.
getMemEncodingはload/store命令で使っているmem Operandをエンコードするために呼び出されます.このOperandは1つのレジスタと1つの即値から構成されています.レジスタ番号を19-16bit目に,即値の値を15-0bit目に格納します.
getMoveTargetOpValueはmove命令で使っているmovetarget Operandをエンコードするために呼び出されます.即値を19-0bit目に格納します.
getCallTargetOpValueはcall命令で使っているcalltarget Operandをエンコードするために呼び出されます.call命令のアドレスはこの時点では値が決まらないため,fixupに登録しています.
SampleMCAsmInfoはMCAsmInfoクラスを継承したクラスで,アセンブリの特徴を定義するためのクラスです.クラス定義をリスト7.58に示します.
リスト7.58: SampleMCAsmInfoのクラス定義
1: class SampleMCAsmInfo : public MCAsmInfo {
2: virtual void anchor() {};
3: public:
4: explicit SampleMCAsmInfo(const Target &T, StringRef TT);
5: };
6:
7: SampleMCAsmInfo::SampleMCAsmInfo(const Target &T, StringRef TT) {
8: PointerSize = 4;
9:
10: PrivateGlobalPrefix = ".L";
11: PCSymbol=".";
12: CommentString = ";";
13:
14: AlignmentIsInBytes = false;
15: AllowNameToStartWithDigit = true;
16: UsesELFSectionDirectiveForBSS = true;
17: }
コンストラクタでターゲットで使うアセンブリの特徴を定義する必要があります.多くの設定可能な項目がありますが,今回は出力するアセンブリの仕様が特に決まっているわけではないので適当に記述しています.既存のターゲット向けのアセンブリを出力する場合には他にも設定が必要となるでしょう.
Disassemblerはコンパイルして生成したオブジェクトファイルをディスアセンブルして内容を確認したりする際に必要になります.コンパイルするだけなら特に必要ありませんが,コンパイル結果を確認するためにも作成しておいた方が良いでしょう.Disassemblerの実装自体はそれほど複雑ではありません.
Disassemblerは他のクラスと異なり,何らかのパス上から利用されるわけではありません.実際にDisassemblerを利用するプログラムはllvm-objdumpとllvm-mcだけのようです.どちらの場合もプログラム内で直接MCDisassemblerクラスをインスタンス化して操作しています.ディスアセンブルしたい場合には,そのインスタンスのgetInstructionメソッドにバイト列を渡すとMCInst(MC Layer)で返ってくるという実装になっています.llvm-objdumpの実装は割と単純なものなので自分で同等のものを作るのもそれほど難しくないでしょう.
SampleDisassemblerクラスはMCDisassemblerクラスを継承したクラスとなります.クラス定義をリスト7.59に示します.
リスト7.59: SampleDisassemblerのクラス定義
1: class SampleDisassembler : public MCDisassembler {
2: public:
3: SampleDisassembler(const MCSubtargetInfo &STI)
4: : MCDisassembler(STI) {}
5:
6: ~SampleDisassembler() {}
7:
8: // 命令のディスアセンブルの結果を返す
9: DecodeStatus getInstruction(MCInst &instr,
10: uint64_t &size,
11: const MemoryObject ®ion,
12: uint64_t address,
13: raw_ostream &vStream,
14: raw_ostream &cStream) const;
15:
16: const EDInstInfo *getEDInfo() const;
17:
18: private:
19: DecodeStatus readInstruction32(const MemoryObject ®ion,
20: uint64_t address,
21: uint64_t &size,
22: uint32_t &insn) const;
23: };
メインで使われるgetInstructionメソッドとgetEDInfoメソッドだけが必ず必要なメソッドとなります.TableGenで生成されたSampleGenEDInfo.incとSampleGenDisassemblerTables.incをincludeする必要があります.
getInstructionは最もメインとなるメソッドで,バイト列から命令を取り出してMCInst(MC Layer)を返します.実装をリスト7.60に示します.
リスト7.60: SampleDisassembler::getInstruction
1: DecodeStatus SampleDisassembler::
2: getInstruction(MCInst &instr,
3: uint64_t &Size,
4: const MemoryObject &Region,
5: uint64_t Address,
6: raw_ostream &vStream,
7: raw_ostream &cStream) const {
8: uint32_t Insn;
9:
10: DecodeStatus Result = readInstruction32(Region, Address, Size, Insn);
11: if (Result == MCDisassembler::Fail)
12: return MCDisassembler::Fail;
13:
14: // TableGenによって生成された関数を呼び出す
15: Result = decodeInstruction(DecoderTableSample32,
16: instr, Insn, Address, this, STI);
17: if (Result != MCDisassembler::Fail) {
18: Size = 4;
19: return Result;
20: }
21:
22: return MCDisassembler::Fail;
23: }
まず,readInstrution32を呼び出して命令を取り出します.この時点ではまだディスアセンブルはしておらずバイナリのままです.次に,TableGenによって生成されたdecodeInstructionを呼び出します.これでMCInstがセットされて返ってくるため,ディスアセンブル完了となります.
なお,decodeInstructionでは,Opcodeに関しては自動的にデコード(バイナリからOpcodeやOperandに変換)されるものの,Operandに関しては自動的にはデコードされずにデコード用の関数がコールバックされます.例えば汎用レジスタのデコードにはDecodeCPURegsRegisterClassが呼ばれます.ここで呼ばれる関数はTableGenでDecoderMethodに設定したものになります.コールバックされる関数は別途実装する必要があります.
getEDInfoはTableGenで定義されたinstInfoSampleを返すだけのメソッドです.これが内部でどのように使われているかは不明ですが,ターゲット毎に特に実装することもないため問題ないです.
readInstruction32はバイト列から命令部分を取り出すメソッドです.実装をリスト7.61に示します.
リスト7.61: SampleDisassembler::readInstruction
1: DecodeStatus SampleDisassembler::
2: readInstruction32(const MemoryObject ®ion,
3: uint64_t address,
4: uint64_t &size,
5: uint32_t &insn) const {
6: uint8_t Bytes[4];
7:
8: // バイト列から4バイト読み込む
9: if (region.readBytes(address, 4, (uint8_t*)Bytes, NULL) == -1) {
10: size = 0;
11: return MCDisassembler::Fail;
12: }
13:
14: // リトルエンディアンで命令をエンコーディング
15: insn = (Bytes[3] << 0) |
16: (Bytes[2] << 8) |
17: (Bytes[1] << 16) |
18: (Bytes[0] << 24);
19:
20: return MCDisassembler::Success;
21: }
regionでバイト列,addressで現在位置が渡されるので,読み込みたいバイト数を指定して命令部分を読み込みます.その後,エンディアンを意識してバイト列を詰め込みなおします.
既に述べたようにOperandのデコードは専用の関数がdecodeInstructionからコールバックされます.今回の実装では4種類のデコード関数があります.それぞれの実装をリスト7.62に示します.
リスト7.62: Operandのデコーダー
1: // tablegenで作成したCPURegs(RegisterClass)を表示する
2: static DecodeStatus DecodeCPURegsRegisterClass(MCInst &Inst,
3: unsigned RegNo,
4: uint64_t Address,
5: const void *Decoder) {
6: if (RegNo > 31)
7: return MCDisassembler::Fail;
8:
9: Inst.addOperand(MCOperand::CreateReg(CPURegsTable[RegNo]));
10: return MCDisassembler::Success;
11: }
12:
13: // load/store命令のoperandをデコード
14: static DecodeStatus DecodeMem(MCInst &Inst,
15: unsigned Insn,
16: uint64_t Address,
17: const void *Decoder) {
18: int Offset = SignExtend32<16>(Insn & 0xffff);
19: int Reg = (int)fieldFromInstruction(Insn, 16, 4);
20: int Base = (int)fieldFromInstruction(Insn, 20, 4);
21:
22: Inst.addOperand(MCOperand::CreateReg(CPURegsTable[Reg]));
23: Inst.addOperand(MCOperand::CreateReg(CPURegsTable[Base]));
24: Inst.addOperand(MCOperand::CreateImm(Offset));
25:
26: return MCDisassembler::Success;
27: }
28:
29: // move命令のoperandをデコード
30: static DecodeStatus DecodeMoveTarget(MCInst &Inst,
31: unsigned Insn,
32: uint64_t Address,
33: const void *Decoder) {
34: int Offset = SignExtend32<20>(Insn & 0xfffff);
35: int Reg = (int)fieldFromInstruction(Insn, 20, 4);
36:
37: Inst.addOperand(MCOperand::CreateReg(CPURegsTable[Reg]));
38: Inst.addOperand(MCOperand::CreateImm(Offset));
39:
40: return MCDisassembler::Success;
41: }
42:
43: // call命令のoperandをデコード
44: static DecodeStatus DecodeCallTarget(MCInst &Inst,
45: unsigned Insn,
46: uint64_t Address,
47: const void *Decoder) {
48: unsigned CallOffset = fieldFromInstruction(Insn, 0, 24) << 2;
49: Inst.addOperand(MCOperand::CreateImm(CallOffset));
50: return MCDisassembler::Success;
51: }
DecodeCPURegsRegisterClassは汎用レジスタをデコードするための関数です.引数で渡されたレジスタ番号に対応するレジスタをMCOperand::CreateRegで生成してMCInstに追加します.
DecodeMemはload/store命令のOperandをデコードするための関数です.fieldFromInstructionを使って命令から任意の位置のbit列を取り出して,レジスタ2つ即値1つからなるOperandをMCInstに追加します.fieldFromInstructionはTableGenで自動生成された関数です.
DecodeMoveTargetはmove命令のOperandをデコードするための関数です.レジスタ1つと20bitの即値1つを取り出してMCInstに追加します.
DecodeCallTargetはcall命令のOperandをデコードするための関数です.24bitの即値を取り出してMCInstに追加します.
他のプログラムからターゲットを利用する場合,以下の初期化関数を呼ぶ必要があります.
InitializeAllTargetsを呼ぶと内部でInitializeAllTargetInfosも呼ばれます.例えばコンパイルを行うllcではTargets,TargetMCs,AsmPrinters,AsmParsersを呼び出しています.これらの関数を呼び出すと,Target毎の初期化関数が個別に呼び出されます.InitializeAllTargetsであればInitializeXXXTarget,つまり今回の実装であればInitializeSampleTargetが呼び出されることになります.
この関数ではTarget自体の情報を登録します.実装をリスト7.63に示します.TargetクラスのインスタンスTheSampleTargetにTriple(Triple::sample)を対応付けたりします.
リスト7.63: LLVMInitializeSampleTargetInfo
1: extern "C" void LLVMInitializeSampleTargetInfo() {
2: RegisterTarget<Triple::sample, /*HasJIT=*/false>
3: X(TheSampleTarget, "sample", "Sample");
4: }
実装をリスト7.64に示します.TheSampleTargetにSampleTargetMachineクラスを対応付けします.
リスト7.64: LLVMInitializeSampleTarget
1: extern "C" void LLVMInitializeSampleTarget() {
2: RegisterTargetMachine<SampleTargetMachine> X(TheSampleTarget);
3: }
実装をリスト7.65に示します.
リスト7.65: LLVMInitializeSampleTargetMC
1: extern "C" void LLVMInitializeSampleTargetMC() {
2: // MC asm infoの登録
3: RegisterMCAsmInfoFn X(TheSampleTarget, createSampleMCAsmInfo);
4: // MC codegen infoの登録
5: TargetRegistry::RegisterMCCodeGenInfo(TheSampleTarget,
6: createSampleMCCodeGenInfo);
7: // MC instruction infoの登録
8: TargetRegistry::RegisterMCInstrInfo(TheSampleTarget, createSampleMCInstrInfo);
9: // MC register infoの登録
10: TargetRegistry::RegisterMCRegInfo(TheSampleTarget, createSampleMCRegisterInfo);
11: // MC Code Emitterの登録
12: TargetRegistry::RegisterMCCodeEmitter(TheSampleTarget,
13: createSampleMCCodeEmitter);
14: // object streamerの登録
15: TargetRegistry::RegisterMCObjectStreamer(TheSampleTarget, createMCStreamer);
16: // asm backendの登録
17: TargetRegistry::RegisterMCAsmBackend(TheSampleTarget,
18: createSampleAsmBackend);
19: // MC subtarget infoの登録
20: TargetRegistry::RegisterMCSubtargetInfo(TheSampleTarget,
21: createSampleMCSubtargetInfo);
22: // MCInstPrinterの登録
23: TargetRegistry::RegisterMCInstPrinter(TheSampleTarget,
24: createSampleMCInstPrinter);
25: }
この関数ではMIの操作に関連するクラスを登録する関数を実行する必要があります.実行可能な関数として以下のようなものがあります.
全ての関数で引数はTargetクラスのインスタンス(TheSampleTarget)と各クラスのインスタンスを生成する関数ポインタになります.実行するとTargetクラスに生成関数が登録されるため,以後はcreateXXX関数を呼び出すだけでインスタンスを生成できるようになります.上記以外にもアセンブリパーサを登録するためのRegisterMCAsmLexerなどもあります.
実装をリスト7.66に示します.TheSampleTargetにSampleAsmPrinterクラスの生成関数を登録します.createAsmPrinterでインスタンスが生成できるようになります.
リスト7.66: LLVMInitializeSampleAsmPrinter
1: extern "C" void LLVMInitializeSampleAsmPrinter() {
2: RegisterAsmPrinter<SampleAsmPrinter> X(TheSampleTarget);
3: }
実装をリスト7.67に示します.TheSampleTargetにSampleDisassemblerクラスの生成関数を登録します.createMCDisassemblerでインスタンスが生成できるようになります.
リスト7.67: LLVMInitializeSampleDisassembler
1: extern "C" void LLVMInitializeSampleDisassembler() {
2: TargetRegistry::RegisterMCDisassembler(TheSampleTarget,
3: createSampleDisassembler);
4: }
ターゲットとしてLLVMに登録するにはTripleのArchTypeにターゲット名を追加する必要があります.この定義はinclude/llvm/ADT/Triple.hにあります.また,プログラムからターゲットの情報を取得できるようにlib/Support/Triple.cppのgetArchTypeName,parseArch,getArchPointerBitWidth,get32BitArchVariant,get64BitArchVariantあたりを修正する必要があります.
LLVMのTopディレクトリでmakeした際にSampleターゲットが自動的にビルド対象になるようにするにはconfigureの修正なども必要になります.configureのTARGETS_TO_BUILDに追加されるように修正しましょう.
このあたり(lib/Target/Sample以外の部分)の修正はソースコードではパッチ形式で提供しています.後述するビルド方法でパッチの当て方を含むビルド方法を説明しています.
ターゲットの記述をしたディレクトリ毎にMakefileとLLVMBuild.txtを設置しなければいけません.おそらくLLVMBuild.txtの情報をもとにライブラリ間の依存関係をチェックしています.ディレクトリ毎に記述内容は異なりますが,ターゲットのTopディレクトリのMakefile(リスト7.68)とLLVMBuild.txt(リスト7.69)だけ載せておきます.
リスト7.68: Sample ターゲットのMakefile(lib/Target/Sample/Makefile)
1: LEVEL = ../../.. 2: LIBRARYNAME = LLVMSampleCodeGen 3: TARGET = Sample 4: 5: # Make sure that tblgen is run, first thing. 6: BUILT_SOURCES = SampleGenRegisterInfo.inc SampleGenInstrInfo.inc \ 7: SampleGenAsmWriter.inc SampleGenCodeEmitter.inc \ 8: SampleGenDAGISel.inc SampleGenCallingConv.inc \ 9: SampleGenSubtargetInfo.inc SampleGenMCCodeEmitter.inc \ 10: SampleGenEDInfo.inc SampleGenDisassemblerTables.inc 11: 12: DIRS = InstPrinter Disassembler TargetInfo MCTargetDesc 13: 14: include $(LEVEL)/Makefile.common
リスト7.69: SampleターゲットのLLVMBuild.txt(lib/Target/Sample/LLVMBuild.txt)
1: [ common ] 2: subdirectories = InstPrinter Disassembler MCTargetDesc TargetInfo 3: 4: [component_0] 5: type = TargetGroup 6: name = Sample 7: parent = Target 8: ;has_asmparser = 1 9: has_asmprinter = 1 10: has_disassembler = 1 11: ;has_jit = 1 12: 13: [component_1] 14: type = Library 15: name = SampleCodeGen 16: parent = Sample 17: required_libraries = AsmPrinter CodeGen Core MC SampleAsmPrinter SampleDesc SampleInfo SelectionDAG Support Target 18: add_to_library_groups = Sample
ソースコードの準備はできました.ちなみにコンパイルというのはLLVM自体のコンパイルのことではなくLLVM IRからアセンブリやオブジェクトに変換することを意味しています.まだLLVMの準備ができてない方にもその手順から紹介します.その後,今回作成したターゲットを用いてLLVM IRからアセンブリとオブジェクトを生成し,ディスアセンブルして結果が正しいかを確認します.
コンパイルを始める前にもう一度,目標のソースコードを確認しましょう.リスト7.70とリスト7.71が今回目標としてきたソースコードになり,これからコンパイルするものになります.
リスト7.70: 目標のソースコード(sample_add.ll)
1: define i32 @sample_add(i32 %a, i32 %b) nounwind readnone {
2: entry:
3: %add = add nsw i32 %b, %a
4: ret i32 %add
5: }
リスト7.71: 目標のソースコード(sample_call.ll)
1: define i32 @add(i32 %a, i32 %b) nounwind readnone {
2: entry:
3: %add = add nsw i32 %b, %a
4: ret i32 %add
5: }
6:
7: define i32 @sample_call() nounwind readnone {
8: entry:
9: %call = tail call i32 @add(i32 1, i32 2)
10: ret i32 %call
11: }
まだLLVM自体をコンパイルしていないなら以下の手順に従ってビルドしましょう.今回作成したターゲットのソースコードは私のGitHub*13にあります.本書作成時のバージョンにはllvm-3.2というtagをつけていますのでそれをcheckoutしましょう.ターゲット固有のファイル以外は修正点をパッチで提供しています.パッチを当てたらコンパイルの準備はOK.configureでいっぱいオプションをつけてますが,これはデバッグモードで最大限動かすようにするためです.makeしたあとmake installでインストールしてもいいですが毎回installするのも面倒です.コンパイルしたLLVMのバイナリはconfigureしたディレクトリのDebug+Asserts/binの中にあります.これをこのまま実行しても問題ないです.なお,本書ではLLVM 3.2を対象としているため,LLVM 3.2以外の環境ではコンパイル及び実行が正しくできるかどうかの保証はできません.
## LLVMのダウンロード $ wget http://llvm.org/releases/3.2/llvm-3.2.src.tar.gz $ tar zxf llvm-3.2.src.tar.gz ## GitHubからターゲットのcheckout $ cd llvm-3.2.src/lib/Target $ git clone git://github.com/sabottenda/llvm-sample-target.git Sample $ cd - && cd llvm-3.2.src/lib/Target/Sample $ git checkout llvm-3.2 ## パッチを当てる(configureの更新) $ cd - && cd llvm-3.2.src $ patch -p1 < ./lib/Target/Sample/config.patch ## コンパイルとインストール $ cd - && mkdir build && cd build $ ../llvm-3.2.src/configure --prefix=/opt/llvm \ --enable-debug-runtime --enable-assertions \ --enable-debug-symbols --disable-optimized --enable-debug-runtime $ make -j4 $ sudo make install
さて,本題のLLVM IRからアセンブリへのコンパイルをしましょう.コンパイルのコマンドは次のようになります.
$ llc -march=sample -print-machineinstrs -debug sample_add.ll
-marchで今回作成したターゲットの名前を指定します.-print-machineinstrsはマシン命令を表示してくれます.-debugを指定するとデバッグ用のメッセージも表示されるようになります.コンパイルする対象はもちろん序盤で紹介したコードです.再度sample_add.ll(list[target-code])を確認しましょう.このソースコードをコンパイルするとsample_add.sというファイルができており,内容はリスト7.72のようになるはずです.
リスト7.72: sample_add.s
1: .file "sample_add.ll" 2: .text 3: .globl sample_add 4: .align 2 5: .type sample_add,@function 6: sample_add: ; @sample_add 7: ; BB#0: ; %entry 8: move $t0, -64 9: add $sp, $sp, $t0 10: store $ra, 60($sp) ; 4-byte Folded Spill 11: add $a0, $a1, $a0 12: add $v0, $zero, $a0 13: load $ra, 60($sp) ; 4-byte Folded Reload 14: move $t0, 64 15: add $sp, $sp, $t0 16: ret $ra 17: .Ltmp0: 18: .size sample_add, .Ltmp0-sample_add
更に次のコマンドのようにコンパイル時に-show-mc-encodingオプションを付けるとエンコーディングが16進数で出力され,-show-mc-instオプションをつけるとMCInstおよびMCOperandのダンプが見られます.リスト7.73に結果を示します.
$ llc -march=sample -show-mc-encoding -show-mc-inst -debug sample_add.ll
リスト7.73: sample_add.s(ダンプ付き)
1: .file "sample_add.ll" 2: .text 3: .globl sample_add 4: .align 2 5: .type sample_add,@function 6: sample_add: ; @sample_add 7: ; BB#0: ; %entry 8: move $t0, -64 ; encoding: [0x02,0x6f,0xff,0xc0] 9: ; <MCInst #22 MOVE 10: ; <MCOperand Reg:11> 11: ; <MCOperand Imm:-64>> 12: add $sp, $sp, $t0 ; encoding: [0x05,0xee,0x60,0x0e] 13: ; <MCInst #17 ADD 14: ; <MCOperand Reg:10> 15: ; <MCOperand Reg:10> 16: ; <MCOperand Reg:11>> 17: store $ra, 60($sp) ; 4-byte Folded Spill 18: ; encoding: [0x01,0xfe,0x00,0x3c] 19: ; <MCInst #24 STORE 20: ; <MCOperand Reg:5> 21: ; <MCOperand Reg:10> 22: ; <MCOperand Imm:60>> 23: add $a0, $a1, $a0 ; encoding: [0x05,0x23,0x20,0x02] 24: ; <MCInst #17 ADD 25: ; <MCOperand Reg:1> 26: ; <MCOperand Reg:2> 27: ; <MCOperand Reg:1>> 28: add $v0, $zero, $a0 ; encoding: [0x05,0x10,0x20,0x01] 29: ; <MCInst #17 ADD 30: ; <MCOperand Reg:15> 31: ; <MCOperand Reg:16> 32: ; <MCOperand Reg:1>> 33: load $ra, 60($sp) ; 4-byte Folded Reload 34: ; encoding: [0x00,0xfe,0x00,0x3c] 35: ; <MCInst #21 LOAD 36: ; <MCOperand Reg:5> 37: ; <MCOperand Reg:10> 38: ; <MCOperand Imm:60>> 39: move $t0, 64 ; encoding: [0x02,0x60,0x00,0x40] 40: ; <MCInst #22 MOVE 41: ; <MCOperand Reg:11> 42: ; <MCOperand Imm:64>> 43: add $sp, $sp, $t0 ; encoding: [0x05,0xee,0x60,0x0e] 44: ; <MCInst #17 ADD 45: ; <MCOperand Reg:10> 46: ; <MCOperand Reg:10> 47: ; <MCOperand Reg:11>> 48: ret $ra ; encoding: [0x04,0xf0,0x00,0x0f] 49: ; <MCInst #23 RET 50: ; <MCOperand Reg:5>> 51: .Ltmp0: 52: .size sample_add, .Ltmp0-sample_add
もう一つのsample_call.ll(リスト7.2)もコンパイルしましょう.リスト7.74に結果を示します.無事call命令も生成されているようです.
$ llc -march=sample -print-machineinstrs -debug sample_call.ll
リスト7.74: sample_call.s
1: .file "sample_call.ll" 2: .text 3: .globl add 4: .align 2 5: .type add,@function 6: add: ; @add 7: ; BB#0: ; %entry 8: move $t0, -64 9: add $sp, $sp, $t0 10: store $ra, 60($sp) ; 4-byte Folded Spill 11: add $a0, $a1, $a0 12: add $v0, $zero, $a0 13: load $ra, 60($sp) ; 4-byte Folded Reload 14: move $t0, 64 15: add $sp, $sp, $t0 16: ret $ra 17: .Ltmp0: 18: .size add, .Ltmp0-add 19: 20: .globl sample_call 21: .align 2 22: .type sample_call,@function 23: sample_call: ; @sample_call 24: ; BB#0: ; %entry 25: move $t0, -64 26: add $sp, $sp, $t0 27: store $ra, 60($sp) ; 4-byte Folded Spill 28: move $a0, 1 29: move $a1, 2 30: call add 31: load $ra, 60($sp) ; 4-byte Folded Reload 32: move $t0, 64 33: add $sp, $sp, $t0 34: ret $ra 35: .Ltmp1: 36: .size sample_call, .Ltmp1-sample_call
さて,次にオブジェクトの生成をしましょう.llcでオブジェクトを生成する場合は-filetype=objをオプションに追加します.
$ llc -march=sample -filetype=obj sample_add.ll $ llc -march=sample -filetype=obj sample_call.ll
コンパイルが完了するとsample_add.oもしくはsample_call.oができるはずです.これはバイナリなのでそのままでは正しく生成できたか確認する方法はありません.またオリジナルのターゲットであるため,GNU Binutilsのobjdumpは使えません.llvm-objdumpを使うことでディスアセンブルが可能です.llvm-objdumpは以下のように実行します.
$ llvm-objdump -d -arch=sample sample_add.o $ llvm-objdump -d -arch=sample sample_call.o
sample_add.oとsample_call.oのディスアセンブルの結果をそれぞれリスト7.75とリスト7.76に示します.最初の8bit(16進数で2つ)がOpcodeでレジスタを使う場合は4bit(16進数1つ)ずつなので見やすいと思います.call命令のアドレスが0になっているのはアドレス解決がリンク時に解決されるためです.結果が関数の区切りもなくフラットに表示されているのはllvm-objdumpの仕様です.
リスト7.75: sample_add.oのディスアセンブル
1: sample_add.o: file format ELF32-unknown 2: 3: Disassembly of section .text: 4: .text: 5: 0: 02 6f ff c0 move $t0, -64 6: 4: 05 ee 60 0e add $sp, $sp, $t0 7: 8: 01 fe 00 3c store $sp, 60($ra) 8: c: 05 23 20 02 add $a0, $a1, $a0 9: 10: 05 10 20 01 add $v0, $zero, $a0 10: 14: 00 fe 00 3c load $sp, 60($ra) 11: 18: 02 60 00 40 move $t0, 64 12: 1c: 05 ee 60 0e add $sp, $sp, $t0 13: 20: 04 f0 00 0f ret $ra
リスト7.76: sample_call.oのディスアセンブル
1: sample_call.o: file format ELF32-unknown 2: 3: Disassembly of section .text: 4: .text: 5: 0: 02 6f ff c0 move $t0, -64 6: 4: 05 ee 60 0e add $sp, $sp, $t0 7: 8: 01 fe 00 3c store $sp, 60($ra) 8: c: 05 23 20 02 add $a0, $a1, $a0 9: 10: 05 10 20 01 add $v0, $zero, $a0 10: 14: 00 fe 00 3c load $sp, 60($ra) 11: 18: 02 60 00 40 move $t0, 64 12: 1c: 05 ee 60 0e add $sp, $sp, $t0 13: 20: 04 f0 00 0f ret $ra 14: 24: 02 6f ff c0 move $t0, -64 15: 28: 05 ee 60 0e add $sp, $sp, $t0 16: 2c: 01 fe 00 3c store $sp, 60($ra) 17: 30: 02 20 00 01 move $a0, 1 18: 34: 02 30 00 02 move $a1, 2 19: 38: 03 00 00 00 call 0 20: 3c: 00 fe 00 3c load $sp, 60($ra) 21: 40: 02 60 00 40 move $t0, 64 22: 44: 05 ee 60 0e add $sp, $sp, $t0 23: 48: 04 f0 00 0f ret $ra
これで完成です.今までいろんなクラスを作成してきた割にできることは少ないです.最初の実装はかなり大変ですが,これをベースに少しずつ拡張していけばよいでしょう.ここまで作成してきた知識を合わせれば実装しやすいと思います.
バックエンドに興味があるということはおそらくx86やARMなどのアーキテクチャに興味があるか,オリジナルの命令セットを作りたいという野望があるでしょう.LLVMのx86バックエンドは実装が進んでいますが,ARMはまだしも他のバックエンドは未成熟です.オブジェクト生成が出来なかったり,命令が使えなかったりということもあります.ぜひ様々なことに挑戦してLLVMへコミットしていきましょう.
[*1] 実際にはSelectionDAGへフォーマットを変化させる前にLLVM IR上でターゲット依存の最適化と前処理が行われます.
[*2] 電子書籍版v1.0.0以前及び商業誌版ではMachineCodeと記述していましたがMIに変更しました.MIやMI Layerも依然として正式な名称ではないようですが,開発者間では表現するときにこの名称が使われることがあるため採用しています.
[*3] 単にMCと言った場合、MIとの対比としてMCInstなどのクラスの総称を意味することもありますが,機械語(Machie Code)やMC Layerを指すこともあります.
[*4] http://llvm.org/docs/WritingAnLLVMBackend.html
[*5] http://llvm.org/docs/CodeGenerator.html
[*6] http://eli.thegreenplace.net/2012/11/24/life-of-an-instruction-in-llvm/
[*7] この分類や呼称が正しくない可能性がありますが,このように分類するとわかりやすくなると思います.
[*8] 実際にはload命令とstore命令では特殊なフォーマットを利用しているためこの形式通りではありません.詳しくはプログラム上で定義するときに説明します.
[*9] ちなみにMIPSがこのような仕様です.
[*10] LLVMの公式ページにTableGen Fundamentalsというドキュメントもあります.
[*11] https://github.com/sabottenda/
[*12] ターゲット名のヘッダである必要性がよくわかっていません.
[*13] https://github.com/sabottenda/llvm-sample-target