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.h
e_flagsの設定を行う。EF_CPU0_NOREORDER = 0x00000001, // 命令の並び替えを行わない。
EF_CPU0_PIC = 0x00000002, // PICを生成する
EF_CPU0_ARCH_32 = 0x50000000, // CPU032 instruction set per linux not elf.h
EF_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.cpp
Cpu0DisableUnreconginizedMessage
に基づいた設定を行う。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.cmake
src/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]を参照のこと。