LLVMにCpu0アーキテクチャを追加するチュートリアル。ちょっと飛ばしすぎたので一度立ち戻って、LLVMのアーキテクチャについてもう一度勉強し直す。
LLVMにバックエンドを追加するチュートリアル の Cpu0 architecture and LLVM structure をもう一度読み直してまとめていく。
最初の方は一生懸命自分の言葉でまとめていたけど、途中からほぼ直訳みたいになってきた。纏めた分を掲載しておく。今回は以下の項目。 第1回のものはこちらから。
Cpu0 architecture and LLVM structure
Cpu0のバックエンドを作る
ここからは、Cpu0のバックエンドを1から、ステップバイステップで作っていく。読者がバックエンドの構造を簡単に理解できるようにするため、Cpu0のサンプルコードは章ごとに区切られており、章ごとに試すことができる。Cpu0のサンプルコードはlbdexに格納されており、本ウェブサイトの下のリンク先、もしくは http://jonathan2251.github.io/lbd/lbdex.tar.gz から入手することができる。
Cpu0バックエンドマシンIDとリロケーションレコード
新しいバックエンドを作るためには、\<
lbdex/src/modify/src/config-ix.cmake:LLVM_NATIVE_ARCHにCpu0を加える。lbdex/src/modify/src/CMakeLists.txt:LLVM_ALL_TARGETSにCpu0を加える。lbdex/src/modify/src/include/llvm/ADT/Triple.h:ArchTypeにcpu0とcpu0elを加える。lbdex/src/modify/src/include/llvm/Object/ELFObjectFile.h: ELFのタイプにELF::EM_CPU0を加える。LittleEndianではTriple::cpu0el, BigEndianではTriple::cpu0を選択する。lbdex/src/modify/src/include/llvm/Support/ELF.he_flagsの設定を行う。EF_CPU0_NOREORDER = 0x00000001, // 命令の並び替えを行わない。EF_CPU0_PIC = 0x00000002, // PICを生成するEF_CPU0_ARCH_32 = 0x50000000, // CPU032 instruction set per linux not elf.hEF_CPU0_ARCH = 0xf0000000 // Mask for applying EF_CPU0_ARCH_ variant
lbdex/src/modify/src/lib/MC/MCSubtargetInfo.cpp:InitMCProcessorInfoにて、Cpu0DisableUnreconginizedMessageの設定を行う。これの意味は良く分からない。lbdex/src/modify/src/lib/MC/SubtargetFeature.cppCpu0DisableUnreconginizedMessageに基づいた設定を行う。lib/object/ELF.cpp:getELFRelocationTypeName()に、Cpu0の設定を加える。Cpu0.defをincludeする。include/llvm/Support/ELFRelocs/Cpu0.def: リロケーションについて記述してある。lbdex/src/modify/src/lib/Support/Triple.cpp:Triple::cpu0,Triple::cpu0elに基づいた設定を行う。Tripleの定義自体は良く分からない。
最初のCpu0用.tdファイルの作成
前章で議論したように、LLVMはターゲットアーキテクチャのバックエンドの様々なコンポーネントを記述するためにターゲット記述ファイル(拡張子.tdのファイルである)を使用する。例えば、これらの.tdファイrうはターゲットのレジスタセット、命令セット、命令のスケジューリング情報、そして関数の呼び出し規約などを記述する。バックエンドがコンパイルされると、LLVMから提供されているtablegenツールがこれらの.tdファイルを.inc拡張子で記述されるC++のソースコードに変換する。tablegenの使い方についてより詳細な情報は[21]を参照すること。
全てのバックエンドは、ターゲットの情報を定義するための.tdファイルを保持している。これらのファイルの文法はC++の文法と似ている。Cpu0では、ターゲット記述ファイルはCpu0Other.tdである。
lbdex/chapters/Chapter2/Cpu0Other.td
include "llvm/Target/Target.td" include "Cpu0RegisterInfo.td" include "Cpu0RegisterInfoGPROutForOther.td" // except AsmParser include "Cpu0.td"
Cpu0Other.tdとCpu0.tdは他のいくつかの.tdファイルを含んでいる。以下に示すCpu0RegisterInfo.tdはCpu0のレジスタセットの情報を記述している。このファイルを見ると、各レジスタには名前が付けられていることが分かる。例えば、"def PC"ではレジスタ名がPCであることが分かる。レジスタの情報に加えて、レジスタクラスの定義も含まれている。複数のレジスタクラスを持つことができ、例えばCPURegs, SR, C0Regs, GPROutなどである。GPROutはCpu0RegisterInfoGPROutForOther.tdに定義されており、SWレジスタ以外のCPURegsである。したがって、SWはレジスタ割り当てのステージに置いて、SWにはレジスタ割り当てが行われない。
lbdex/chapters/Chapter2/Cpu0RegisterInfo.td
// We have banks of 16 registers each.
class Cpu0Reg<bits<16> Enc, string n> : Register<n> {
// For tablegen(... -gen-emitter) in CMakeLists.txt
let HWEncoding = Enc;
let Namespace = "Cpu0";
}
// Cpu0 CPUレジスタ
class Cpu0GPRReg<bits<16> Enc, string n> : Cpu0Reg<Enc, n>;
// コプロセッサ0のレジスタ
class Cpu0C0Reg<bits<16> Enc, string n> : Cpu0Reg<Enc, n>;
// The register string, such as "9" or "gp" will show on "llvm-objdump -d"
//@ All registers definition
let Namespace = "Cpu0" in {
//@ General Purpose Registers
def ZERO : Cpu0GPRReg<0, "zero">, DwarfRegNum<[0]>;
def AT : Cpu0GPRReg<1, "1">, DwarfRegNum<[1]>;
def V0 : Cpu0GPRReg<2, "2">, DwarfRegNum<[2]>;
def V1 : Cpu0GPRReg<3, "3">, DwarfRegNum<[3]>;
def A0 : Cpu0GPRReg<4, "4">, DwarfRegNum<[4]>;
def A1 : Cpu0GPRReg<5, "5">, DwarfRegNum<[5]>;
def T9 : Cpu0GPRReg<6, "t9">, DwarfRegNum<[6]>;
def T0 : Cpu0GPRReg<7, "7">, DwarfRegNum<[7]>;
def T1 : Cpu0GPRReg<8, "8">, DwarfRegNum<[8]>;
def S0 : Cpu0GPRReg<9, "9">, DwarfRegNum<[9]>;
def S1 : Cpu0GPRReg<10, "10">, DwarfRegNum<[10]>;
def GP : Cpu0GPRReg<11, "gp">, DwarfRegNum<[11]>;
def FP : Cpu0GPRReg<12, "fp">, DwarfRegNum<[12]>;
def SP : Cpu0GPRReg<13, "sp">, DwarfRegNum<[13]>;
def LR : Cpu0GPRReg<14, "lr">, DwarfRegNum<[14]>;
def SW : Cpu0GPRReg<15, "sw">, DwarfRegNum<[15]>;
// def MAR : Register< 16, "mar">, DwarfRegNum<[16]>;
// def MDR : Register< 17, "mdr">, DwarfRegNum<[17]>;
def PC : Cpu0C0Reg<0, "pc">, DwarfRegNum<[20]>;
def EPC : Cpu0C0Reg<1, "epc">, DwarfRegNum<[21]>;
}
//===----------------------------------------------------------------------===//
//@Register Classes
//===----------------------------------------------------------------------===//
def CPURegs : RegisterClass<"Cpu0", [i32], 32, (add
// Reserved
ZERO, AT,
// Return Values and Arguments
V0, V1, A0, A1,
// Not preserved across procedure calls
T9, T0, T1,
// Callee save
S0, S1,
// Reserved
GP, FP,
SP, LR, SW)>;
//@Status Registers class
def SR : RegisterClass<"Cpu0", [i32], 32, (add SW)>;
//@Co-processor 0 Registers class
def C0Regs : RegisterClass<"Cpu0", [i32], 32, (add PC, EPC)>;
lbdex/chapters/Chapter2/Cpu0RegisterInfoGPROutForOther.td
//===----------------------------------------------------------------------===// // Register Classes //===----------------------------------------------------------------------===// def GPROut : RegisterClass<"Cpu0", [i32], 32, (add (sub CPURegs, SW))>;
C++では、クラスにはいくつかのデータや関数の構造が含まれている。この定義はクラスのインスタンスを作成する際に使用される。例えば、
class Date { // declare Date int year, month, day; }; Date birthday; // define birthday, an instance of Date
クラスDateには、メンバ変数としてyear, month, dayが含まれているが、この状態では実際のオブジェクトに結びついているわけではない。Dateクラスのbirthdayという名前でインスタンスすると、特定のオブジェクトのためにメモリ輪がりつけられ、クラス内のyear, month, dayがインスタンスされる。
.tdファイルでは、クラスは、特定のインスタンスの動きの定義とともに、データがどのように並べられるかという構造が記述されている。Cpu0RegisterInfo.tdファイルに立ち戻ってみると、Cpu0Regと呼ばれるクラスが定義されており、これはLLVMで提供されているRegisterクラスから派生している。Cpu0RegはRegisterクラスの既存のすべてのフィールドを継承している。"let HWEncoding = Enc"はパラメータEncからHWEncodingフィールドを割り当てることを意味している。Cpu0は命令フォーマット中で16個のレジスタを指定するために4ビットを使用するため、割り当てられるれ値の範囲は0から15である。0から15がHWEncodingに割り当てられると、バックエンドレジスタ番号はTableGenが自動的にレジスタ番号をセットするとLLVMのレジスタクラスから入手できるようになる。
defキーワードは、クラスのインスタンスを作成する場合に使用される。以下のコードは、Cpu0GPRRegクラスのメンバとして、ZEROレジスタを定義するものである。
def ZERO : Cpu0GPRReg< 0, "ZERO">, DwarfRegNum<[0]>;
def ZEROはこのレジスタの名前を示している。<0, "ZERO">はCpu0GPRRegクラスの特定のインスタンスを作成する場合に使用されるパラメータであり、Encは0に設定され、string nはZEROに設定される。
Cpu0の名前空間上での生存しているレジスタとして、ZEROレジスタはC++のバックエンドのコードでも、Cpu0::ZEROとして使用できる。
let表記の使い方について説明しておく: この表記により、すでに親クラスにより設定された値を上書きすることができる。例えば、Cpu0Regクラス内にてlet Namespace = "Cpu0"とすると、LLVMにビルトインされたRegisterClass内で設定されたデフォルトの名前空間を書き換えることができる。RegisterClassはRegisterインスタンスの一部であり、従ってCpuRegsはレジスタセットと説明できる。
Cpu0の命令セットのtdはCpu0InstrInfo.tdであり、以下のように記述されている。
lbdex/chapters/Chapter2/Cpu0InstrInfo.td
Cpu0InstrFormats.tdはCpu0InstInfo.tdに含まれている。
lbdex/chapters/Chapter2/Cpu0InstrFormats.td
ADDiu命令は、FLのArithLogicIクラスから継承されており、以下のようにして拡張し、メンバ値を取得することができる:
def ADDiu : ArithLogicI<0x09, "addiu", add, simm16, immSExt16, CPURegs>;
/// Arithmetic and logical instructions with 2 register operands.
class ArithLogicI<bits<8> op, string instr_asm, SDNode OpNode,
Operand Od, PatLeaf imm_type, RegisterClass RC> :
FL<op, (outs GPROut:$ra), (ins RC:$rb, Od:$imm16),
!strconcat(instr_asm, "\t$ra, $rb, $imm16"),
[(set GPROut:$ra, (OpNode RC:$rb, imm_type:$imm16))], IIAlu> {
let isReMaterializable = 1;
}
従って、
op = 0x09 instr_asm = “addiu” OpNode = add Od = simm16 imm_type = immSExt16 RC = CPURegs
tdを拡張すると、いくつかの基本的なところが見えてくる:
let: 親クラスの既存のフィールドをオーバライドする。例えば、
let isRematerializable = 1;という記述はTarget.td内のisReMaterializableという変数をオーバライドする。declaration: そのクラスでの新しいフィールドを宣言する。例えば、
bits<4> raはクラスFLで新しいraフィールドを宣言する。
拡張の詳細は、以下の表にまとめた:
| ADDiu | ArithLogicI | FL |
|---|---|---|
| 0x09 | op = 0x09 | Opcode = 0x09; |
| addiu | instr_asm = “addiu” | (outs GPROut:ra); !strconcat(“addiu”, “tra, rb, $imm16”); |
| add | OpNode = add | [(set GPROut:$ra, (add CPURegs:$rb, immSExt16:$imm16))] |
| simm16 | Od = simm16 | (ins CPURegs:$rb, simm16:$imm16); |
| immSExt16 | imm_type = immSExt16 | Inst{15-0} = imm16; |
| CPURegs | RC = CPURegs isReMaterializable=1; | Inst{23-20} = ra; Inst{19-16} = rb; |
| Cpu0Inst | instruction |
|---|---|
| Namespace = “Cpu0” | Uses = []; ... |
| Inst{31-24} = 0x09; | Size = 0; ... |
| OutOperandList = GPROut:$ra; | |
| InOperandList = CPURegs:rb,simm16:imm16; | |
| AsmString = “addiutra, rb, $imm16” | |
| pattern = [(set GPROut:ra, (add RC:rb, immSExt16:$imm16))] | |
| Itinerary = IIAlu | |
| TSFlags{3-0} = FrmL.value | |
| DecoderNamespace = “Cpu0” |
tdの拡張はお粗末な処理だ。同様に、LDとST命令の定義も同じ方法で拡張できる。Pattern = [(set GPROut:$ra, (add RC:$rb, immSExt16:$imm16))]には、addというキーワードが含まれていることに注意しよう。"add"を使用したADDiuは過去のセクションで命令選択でも使用した。
Cpu0Schedule.tdファイルには、機能ユニットとパイプラインステージの情報が入っている。
lbdex/chapters/Chapter2/Cpu0Schedule.td
... //===----------------------------------------------------------------------===// // Cpu0 Generic instruction itineraries. //===----------------------------------------------------------------------===// //@ http://llvm.org/docs/doxygen/html/structllvm_1_1InstrStage.html def Cpu0GenericItineraries : ProcessorItineraries<[ALU, IMULDIV], [], [ //@2 InstrItinData<IIAlu , [InstrStage<1, [ALU]>]>, InstrItinData<II_CLO , [InstrStage<1, [ALU]>]>, InstrItinData<II_CLZ , [InstrStage<1, [ALU]>]>, InstrItinData<IILoad , [InstrStage<3, [ALU]>]>, InstrItinData<IIStore , [InstrStage<1, [ALU]>]>, InstrItinData<IIBranch , [InstrStage<1, [ALU]>]> ]>;
cmakeファイルを書く
Target/Cpu0ディレクトリには、以下のCMakeLists.txtとLLVMBuild.txtが含まれている:
lbdex/chapters/Chapter2/CMakeLists.txt
set(LLVM_TARGET_DEFINITIONS Cpu0Other.td) # ハンドコードC++ファイルにインクルードされる Cpu0GenRegisterInfo.inc, Cpu0GenInstrInfo.inc を生成する。 # Cpu0GenRegisterInfo.incはCpu0RegisterInfo.tdから生成される。Cpu0GenInstrInfo.incはCpu0InstrInfo.tdから生成される。 tablegen(LLVM Cpu0GenRegisterInfo.inc -gen-register-info) tablegen(LLVM Cpu0GenInstrInfo.inc -gen-instr-info) tablegen(LLVM Cpu0GenSubtargetInfo.inc -gen-subtarget) tablegen(LLVM Cpu0GenMCPseudoLowering.inc -gen-pseudo-lowering) # Cpu0CommondTableGenを定義なければならない。 add_public_tablegen_target(Cpu0CommonTableGen) # Cpu0CodeGenはLLVMBuild.txtCpu0のCodeGenとマッチしなければならない。 add_llvm_target(Cpu0CodeGen Cpu0TargetMachine.cpp ) # LLVMBuild.txtの"subdirectories = MCTargetDesc TargetInfo"とマッチするべきである。 add_subdirectory(TargetInfo) add_subdirectory(MCTargetDesc)
lbdex/chapters/Chapter2/LLVMBuild.txt
CMakeLists.txtはcmakeのためのMake情報であり、#はコメントである。LLVMBuild.txtはシンプルなINIファイル形式で記述されている。どちらのファイルもコメントは#で記述されている。コメント欄でそれぞれのファイルについて説明しているので、ぜひ読んでください。CMakeLists.txtの"tablegen"はcmake/modules/TableGen.cmakeで定義されている。
src/cmake/modules/TableGen.cmakesrc/utils/TableGen/CMakeLists.txt
CMakeLists.txtに含まれるadd_tablegenにより、Cpu0のCMakeLists.txtに含まれる"tablegen()"
が生成される。
src/cmake/modules/TableGen.cmake
llvm_tblgenファイルはLLVMバックエンドソースコードコンパイルされる前に生成されるため、TableGenリクエストがなされるときには、llvm-tblgenは用意されている状態になっている。
本書はバックエンドのコードの関数について、章毎に紹介する。章の中で出てくるすべてのコードを理解しようとしない方がよい。コンピュータの概念を理解するためには、ソースコードを無視してよいが、既存のオープンソフトウェアに基づいて実装するのは不可能だ。プログラミングでは、ドキュメントはソースコードをすべてカバーすることはできない。ソースコードを読むことは、オープンソース開発での大きな機会である。
CMakeLists.txtとLLVMBuild.txtはMCTargetDescとTargetInfoディレクトリに共存している。これらの2つのディレクトリのCMakeLists.txtとLLVMBuild.txtは、Cpu0DescとCpu0Infoライブラリを生成するために使用される。ビルド後には、lib/ディレクトリ以下に libLLVMCpu0CodeGen.a, libLLVMCpu0Desc.a and libLLVMCpu0Info.a の3つのライブラリが生成される。より詳細については、“Building LLVM with CMake” [16] と “LLVMBuild Guide” [17]を参照のこと。