LLVMのバックエンドに焦点を当て、オリジナルのアーキテクチャターゲットをLLVMに追加する。 オリジナルのアーキテクチャといっても、一から作ると仕様の部分まで作る必要があるし、またそういう部分で本書の説明を策必要が生じてしまうため、RISC-Vをベースにしたオリジナルのアーキテクチャを作成する。
LLVMにはRISC-Vのバックエンド実装はすでに入っている。 しかしそれをパックてしまっては意味がないので、オリジナルのバックエンドを作成するために、RISC-Vと似たようなアーキテクチャとして"MYRISCVX"というターゲットを定義する。
LLVMは、このようなオリジナルのアーキテクチャを追加するための機構が備わっている。 まず、MYRISCVXを追加するにあたり、基本的な用語から押さえていくことにする。
TargetMachine
: ターゲットのアーキテクチャそのものを示する。LLVMにはTargetMachine
クラスがあり、ターゲットアーキテクチャのすべての情報を管理する役割の担いう。以下の図は、TargetMachineに関連するクラスの相関図を示している。TargetMachine
を親クラスとして、派生クラスに各バックエンドアーキテクチャのクラスが設計されていることが分かる。MYRISCVX
アーキテクチャをLLVMに新規追加する場合は、MYRISCVXTargetMachine
クラスを派生させることになる。lib/Target/MYRISCVX/MYRISCVXTargetMachine.h
... namespace llvm { class formatted_raw_ostream; class MYRISCVXRegisterInfo; // MYRISCVXターゲットマシンは、LLVMTargetMachine(ひいてはTargetMachine)クラスから派生しています。 class MYRISCVXTargetMachine : public LLVMTargetMachine { ...
Target Description File :
td
ファイル : その名の通り、LLVMバックエンドのターゲットマシンの情報を記述する。CPUには様々な情報が含まれています。レジスタのサイズ、名前、数、そして命令フォーマット、命令名、その命令がどのような意味を持つのか。その命令はどのような場合に使われるのか。。。これらのことをTarget Descriptionファイルに記述すると、LLVMはより詳細なバックエンドの実装を手助けするためのファイルを生成してくれる。MYRISCVXInstrInfo.td
,MYRISCVXRegisterInfo.td
というtdファイルをtblgen
が処理し、その結果サポートファイルが生成される。より詳細な命令の制御を行いたい場合、そのサポートファイルを使って実装を行う。LLVMでのバックエンドの追加は、tdファイルの記述とその詳細設計にほとんどの時間が費やされる。 このtdファイルに関してはネットを調べてもあまり情報が出て来ないので、サンプル(というか既存のアーキテクチャに書いてある実装)を見ながら進めていくことになる。
データレイアウト
ターゲットアーキテクチャには、データレイアウトというものが定義されている。データレイアウトとは、おおざっぱに言えばそのアーキテクチャがどのようなアドレス空間を想定し、どの大きさの型をサポートしているのか、アライメントはどうするのか、デフォルトの型は何を使うのか、などと言うものを指定する。
lib/Target/Mips/MipsTargetMachine.cpp
// ※ MIPSには、o32, n32, n64の3つのABIがあるのでちょっと複雑。 // o32 : 32-bit MIPSのABI。int型は32ビット、long型も32ビット、ポインタは32ビット // n64 : 64-bit MIPSのABI。int型は32ビット、long型は64ビット、ポインタは64ビット // n32 : 64-bit MIPSのABI。int型は32ビット、long型は32ビット、ポインタは32ビット。 static std::string computeDataLayout(const Triple &TT, StringRef CPU, const TargetOptions &Options, bool isLittle) { std::string Ret; MipsABIInfo ABI = MipsABIInfo::computeTargetABI(TT, CPU, Options.MCOptions); // MIPSにはビッグエンディアンとリトルエンディアンのモードがある if (isLittle) Ret += "e"; else Ret += "E"; // マングリングの設定。詳細は省略だが、アセンブリ内でのラベル等の表現方法の指定。 if (ABI.IsO32()) Ret += "-m:m"; else Ret += "-m:e"; // ポインタサイズの指定。64bitモードでない場合は32ビットサイズを使用する。 if (!ABI.IsN64()) Ret += "-p:32:32"; // 8bitと16bitの整数は、ABIとしては8ビット、16ビットアライメントである。 // しかし、どちらも32ビットアライメントに調整しようとする。 // 64ビットデータは、64ビットアライメントである。 Ret += "-i8:8:32-i16:16:32-i64:64"; // ネイティブな整数のサイズ。N64とN32では32ビット64ビットと、128ビットもサポートする。 // それ以外は、32ビットと64ビットをサポートする。 if (ABI.IsN64() || ABI.IsN32()) Ret += "-n32:64-S128"; else Ret += "-n32-S64"; return Ret; }
RISC-V の ABIにはどんなものがある?
ABIというのはApplication Binary Interfaceのことで、アプリケーションとシステム(OSやライブラリなど)の間で取り決められるインタフェースのこと。 例えば、関数呼び出しの際の引数の取り扱い。データ型、データのアライメント、システムコールの規約、実行ファイルのフォーマット、ライブラリのフォーマットなどを規定する。
このABIが同一のシステム間では、コンパイル済みのアプリケーションはリコンパイルすることなく動かすことができるものである。 したがって、CPUアーキテクチャにおいてABIの決定は非常に重要となる。
RISC-Vには、ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d
が定義されている。
ABI | ilp32 | ilp32f | ilp32d | lp64 | lp64f | lp64d |
---|---|---|---|---|---|---|
char型のサイズ | 8 | 8 | 8 | 8 | 8 | 8 |
short型のサイズ | 16 | 16 | 16 | 16 | 16 | 16 |
int型のサイズ | 32-bit | 32-bit | 32-bit | 32-bit | 32-bit | 32-bit |
long型のサイズ | 32-bit | 32-bit | 32-bit | 64-bit | 64-bit | 64-bit |
ポインタのサイズ | 32-bit | 32-bit | 32-bit | 64-bit | 64-bit | 64-bit |
浮動小数点命令の取り扱い | 浮動小数点の引数はレジスタ経由で渡されない | 32ビット以下の浮動小数点数の引数はレジスタ渡しが行われる。 | 64ビット以下の浮動小数点数の引数はレジスタ渡しが行われる。 | 浮動小数点の引数はレジスタ経由で渡されない | 32ビット以下の浮動小数点数の引数はレジスタ渡しが行われる。 | 64ビット以下の浮動小数点数の引数はレジスタ渡しが行われる。 |
ターゲットのTriple xxx
よく、GCCなどで`riscv64-unknown-elfなどという接頭語を見るが、これはどういう並びになっているのかというと、
ARCHITECTURE-VENDOR-OPERATING_SYSTEM ARCHITECTURE-VENDOR-OPERATING_SYSTEM-ENVIRONMENT
という並びになって、LLVMで定義されている。