FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(6. Cpu0 Architecture and LLVM Structure 続き)

LLVMにCpu0アーキテクチャを追加するチュートリアル。ちょっと飛ばしすぎたので一度立ち戻って、LLVMアーキテクチャについてもう一度勉強し直す。

LLVMにバックエンドを追加するチュートリアルCpu0 architecture and LLVM structure をもう一度読み直してまとめていく。

最初の方は一生懸命自分の言葉でまとめていたけど、途中からほぼ直訳みたいになってきた。纏めた分を掲載しておく。今回は以下の項目。 第1回のものはこちらから。

第2回のものはこちらから。


Cpu0 architecture and LLVM structure

Cpu0のバックエンドを作る

ここからは、Cpu0のバックエンドを1から、ステップバイステップで作っていく。読者がバックエンドの構造を簡単に理解できるようにするため、Cpu0のサンプルコードは章ごとに区切られており、章ごとに試すことができる。Cpu0のサンプルコードはlbdexに格納されており、本ウェブサイトの下のリンク先、もしくは http://jonathan2251.github.io/lbd/lbdex.tar.gz から入手することができる。

Cpu0バックエンドマシンIDとリロケーションレコード

新しいバックエンドを作るためには、\<>内のいくつかのファイルを変更する必要がある。新しいマシンのIDと名前、そしてリロケーションレコードを追加する。"ELF Support"の章では、リロケーションレコードについて説明がある。以下のファイルを修正して、Cpu0をバックエンドに加える。

  • 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.tdCpu0.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クラスから派生している。Cpu0RegRegisterクラスの既存のすべてのフィールドを継承している。"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 nZEROに設定される。

Cpu0名前空間上での生存しているレジスタとして、ZEROレジスタC++のバックエンドのコードでも、Cpu0::ZEROとして使用できる。

let表記の使い方について説明しておく: この表記により、すでに親クラスにより設定された値を上書きすることができる。例えば、Cpu0Regクラス内にてlet Namespace = "Cpu0"とすると、LLVMにビルトインされたRegisterClass内で設定されたデフォルトの名前空間を書き換えることができる。RegisterClassRegisterインスタンスの一部であり、従ってCpuRegsレジスタセットと説明できる。

Cpu0の命令セットのtdはCpu0InstrInfo.tdであり、以下のように記述されている。

  • lbdex/chapters/Chapter2/Cpu0InstrInfo.td

Cpu0InstrFormats.tdCpu0InstInfo.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”, “tra, 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 = “addiutra, 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はMCTargetDescTargetInfoディレクトリに共存している。これらの2つのディレクトリのCMakeLists.txtとLLVMBuild.txtは、Cpu0DescとCpu0Infoライブラリを生成するために使用される。ビルド後には、lib/ディレクトリ以下に libLLVMCpu0CodeGen.a, libLLVMCpu0Desc.a and libLLVMCpu0Info.a の3つのライブラリが生成される。より詳細については、“Building LLVM with CMake” [16] と “LLVMBuild Guide” [17]を参照のこと。