Creating an LLVM Backend for the Cpu0 Architecture
Backend structure
- TargetMachine structure
- Add AsmPrinter
- Add Cpu0DAGToDAGISel class
- Handle return register $lr
- Add Prologue/Epilogue functions
- Data operands DAGs
- Summary of this Chapter
Fig. 14 Cpu0 backend class access link
圖 14 Cpu0 后端類訪問鏈接
添加了大多數 Cpu0 后端類,代碼可以概括為圖14。類 Cpu0Subtarget 提供接口 getInstrInfo(),getFrameLowering(),...,獲取其它 Cpu0 類。大多數類(如 Cpu0InstrInfo,Cpu0RegisterInfo 等),都有 Subtarget 引用成員,允許通過 Cpu0Subtarget 接口,訪問其它類。如果后端模塊沒有 Subtarget 引用,這些類仍然可以通過 static_cast<Cpu0TargetMachine &>(TM).getSubtargetImpl(),通過 Cpu0TargetMachine(通常使用 TM 作為符號)訪問 Subtarget 類。一旦獲取到 Subtarget 類,后端代碼就可以訪問其它類。對於 Cpu0SExx 類的名稱,表示標准32 位類。遵循 llvm 3.5 Mips 后端風格。Mips 后端使用 Mips16,MipsSE 和 Mips64 文件/類名稱,分別為 16,32 和 64 位架構定義類。
圖15顯示了 Cpu0 TableGen 的繼承關系。后端類可以包含 TableGen 生成的類並從中繼承。Cpu0后端的所有TableGen生成的類,都在build/lib/Target/Cpu0/*.inc中。通過 C++ 繼承機制,TableGen 為后端程序員,提供了一種靈活的方式,使用生成的代碼。如果需要,程序員有機會覆蓋此功能。
圖 15繼承自 TableGen 生成文件的 Cpu0 類
Fig. 15 Cpu0 classes inherited from TableGen generated files
由於llvm有很深的繼承樹,這里就不深挖了。受益於繼承樹結構,不需要在指令,幀/堆棧和選擇 DAG 類中,實現太多代碼,很多代碼是由父類實現的。llvm-tblgen 根據Cpu0InstrInfo.td 的信息,生成 Cpu0GenInstrInfo.inc。Cpu0InstrInfo.h 通過定義“#define GET_INSTRINFO_HEADER”,從 Cpu0GenInstrInfo.inc 中,提取需要的代碼。使用TabelGen,通過編譯器開發的模式匹配理論,減少了后端的代碼量。這在 “DAG”和“指令選擇”中,都有解釋。
To make the registration clearly, summary as the following diagram, Fig. 16.
圖 16 Tblgen 為 Cpu0 后端生成文件
Fig. 16 Tblgen generate files for Cpu0 backend
createCpu0MCAsmInfo() 為目標 TheCpu0Target 和 TheCpu0elTarget,注冊了類 Cpu0MCAsmInfo 的對象。TheCpu0Target 用於大端,TheCpu0elTarget 用於小端。Cpu0MCAsmInfo 派生自 MCAsmInfo,一個 llvm 內置類。大多數代碼在父級中實現,后端通過繼承重用這些代碼。
createCpu0MCInstrInfo() 實例化 MCInstrInfo 對象X,通過 InitCpu0MCInstrInfo(X) ,進行初始化。由於 InitCpu0MCInstrInfo(X) 是在 Cpu0GenInstrInfo.inc 中定義的,所以這個函數會添加指定的 Cpu0InstrInfo.td 中的信息。
createCpu0MCInstPrinter() 實例化 Cpu0InstPrinter,支持打印功能的說明。
createCpu0MCRegisterInfo()類似於“MC指令信息的注冊函數”,初始化了Cpu0RegisterInfo.td中,指定的寄存器信息。共享來自指令/寄存器 td 描述的一些值,如果與 td 描述文件一致,無需在 Initialize 例程中,再次指定。
createCpu0MCSubtargetInfo() 實例化 MCSubtargetInfo 對象,使用 Cpu0.td 信息,進行初始化。
根據“目標注冊部分” ,可以通過動態注冊機制,在 LLVMInitializeCpu0TargetMC() 按需注冊 Cpu0 后端類,如上述函數 LLVMInitializeCpu0TargetMC()。
現在,可以使用 AsmPrinter,如下所示,
Summary above translation into Table: Chapter 3 .bc IR instructions.
下層:初始選擇 DAG(Cpu0ISelLowering.cpp,LowerReturn(…))
- ISel:指令選擇
- RVR:重寫虛擬寄存器,刪除 CopyToReg
- AsmP:Cpu0 Asm 打印
- Post-RA:Post-RA 偽指令擴展pass
從上面的llc
-print-before-all
-print-after-all顯示,ret在stage Optimized legalized selection DAG中,翻譯成 Cpu0ISD::Ret,最后翻譯成Cpu0指令ret。由於 ret 使用常量 0(在此示例中為ret i32 0),因此常量 0通過Cpu0InstrInfo.td 中定義的以下模式,轉換為“addiu $2, $zero, 0”。
Cpu0ISelLowering.cpp 的函數LowerReturn() 正確處理返回變量。Chapter3_4/Cpu0ISelLowering.cpp在LowerReturn()中創建Cpu0ISD::Ret節點,當llvm系統遇到C的return關鍵字時調用。創建 DAG(Cpu0ISD::Ret (CopyToReg %X, %V0, %Y), %V0, Flag)。由於 V0 寄存器,在 CopyToReg 中分配,Cpu0ISD::Ret 使用 V0,帶有 V0 寄存器的 CopyToReg,繼續存在,不會在任何后續優化步驟中刪除。如果使用“return DAG.getNode(Cpu0ISD::Ret, DL, MVT::Other, Chain, DAG.getRegister(Cpu0::LR, MVT::i32));”,不是“返回 DAG.getNode (Cpu0ISD::Ret, DL, MVT::Other, &RetOps[0], RetOps.size());”,V0 寄存器將不會生效,DAG(CopyToReg %X, %V0, %Y)將在以后的優化步驟中刪除。
概念
以下來自 tricore_llvm.pdf 部分“4.4.2 非靜態寄存器信息”。
對於某些目標架構,目標架構的寄存器集的某些方面,取決於可變因素,必須在運行時確定。不能從 TableGen,描述靜態生成——盡管在 TriCore 后端,大部分是可能的。有以下幾點:
- 調用者保存的寄存器。通常,ABI 指定一組寄存器,如果內容在執行期間可能被修改,函數必須在進入時,保存這些寄存器,在返回時,恢復這些寄存器。
- 保留寄存器。盡管 TableGen 文件中,已經定義了一組不可用的寄存器,TriCoreRegisterInfo 包含一個方法,用於在位向量中,標記所有不可分配的寄存器編號。
實現了以下方法:
- emitPrologue() 在函數的開頭,插入序言代碼。由於 TriCore 的上下文模型,這是一項微不足道的任務,不需要手動保存任何寄存器。唯一需要做的,通過遞減堆棧指針,為函數的堆棧幀保留空間。如果函數需要一個幀指針,幀寄存器 %a14 被預先設置為堆棧指針的舊值。
- emitEpilogue() 旨在發出指令,在從函數返回之前,銷毀堆棧幀,恢復所有先前保存的寄存器。由於 %a10(堆棧指針),%a11(返回地址)和 %a14(幀指針,如果有),都是上層上下文的一部分,根本不需要結尾代碼。所有清理操作,都由 ret 指令隱式執行。
- 對於引用堆棧槽中,一個數據字的每條指令,都調用消除幀索引()。代碼生成器之前的所有過程,都通過抽象幀索引和立即偏移量,尋址堆棧槽。此函數的目的,將這樣的引用轉換為寄存器-偏移對。根據包含指令的機器函數,是否具有固定或可變堆棧幀,使用堆棧指針 %a10,或幀指針 %a14,作為基址寄存器。相應計算偏移量。圖 17展示了兩種情況下,堆棧槽的尋址方式。
如果受影響指令的尋址模式,由於偏移量太大,無法處理該地址(偏移字段對於 BO 尋址模式,有 10 位,對於 BOL 模式,有 16 位),發出一系列指令,顯式計算有效地址。臨時結果,放入一個未使用的地址寄存器。如果沒有可用的,清除已占用的地址寄存器。LLVM 的框架提供了一個名為 RegScavenger 的類,負責處理所有細節。
able 11 Handle return register lr
表 11處理返回寄存器 lr
圖 17位於堆棧上的變量 a 的尋址。如果堆棧幀具有可變大小,必須相對於幀指針尋址槽
Fig. 17 Addressing of a variable a located on the stack. If the stack frame has a variable size, slot must be addressed relative to the frame pointer
Table 12 Backend functions called in PrologEpilogInserter.cpp
表 12 PrologEpilogInserter.cpp 中調用的后端函數
File PrologEpilogInserter.cpp includes the calling of backend functions spillCalleeSavedRegisters(), emitProlog(), emitEpilog() and eliminateFrameIndex() as follows,
文件 PrologEpilogInserter.cpp,包括調用后端函數,spillCalleeSavedRegisters(), emitProlog(), emitEpilog() ,eliminateFrameIndex()。
Table 13 Cpu0 stack adjustment instructions before replace addiu and shl with lui instruction |
Cpu0AnalyzeImmediate.cpp遞歸方式編寫,邏輯上有點復雜。不過前端編譯,用到了遞歸技巧。不跟蹤代碼,列出“表:用lui指令替換addiu和shl之前的Cpu0堆棧,調整指令”和“表:用lui指令替換addiu和shl之后的Cpu0堆棧,調整指令”中的堆棧大小和指令。
Table 14 Cpu0 stack adjustment instructions after replace addiu and shl with lui instruction
由於 Cpu0 堆棧是 8 字節對齊,從 0x7ff9 到 0x7fff 的地址,不可能存在的。
假設 sp = 0xa0008000,stack size = 0x90008000, (0xa0008000 - 0x90008000) => 0x10000000。使用 Cpu0 Prologue 說明,進行驗證,如下所示,
- “addiu $1, $zero, -9” => ($1 = 0 + 0xfffffff7) => $1 = 0xfffffff7.
- “shl $1, $1, 28;” => $1 = 0x70000000.
- “addiu $1, $1, -32768” => $1 = (0x70000000 + 0xffff8000) => $1 = 0x6fff8000.
- “addu $sp, $sp, $1” => $sp = (0xa0008000 + 0x6fff8000) => $sp = 0x10000000.
使用 sp = 0x10000000,堆棧大小stack size = 0x90008000 的 Cpu0 Epilogue 指令,進行驗證。
- “addiu $1, $zero, -28671” => ($1 = 0 + 0xffff9001) => $1 = 0xffff9001.
- “shl $1, $1, 16;” => $1 = 0x90010000.
- “addiu $1, $1, -32768” => $1 = (0x90010000 + 0xffff8000) => $1 = 0x90008000.
- “addu $sp, $sp, $1” => $sp = (0x10000000 + 0x90008000) => $sp = 0xa0008000.
Cpu0AnalyzeImmediate::GetShortestSeq() ,將調用 Cpu0AnalyzeImmediate:: ReplaceADDiuSHLWithLUi() ,僅用單個指令 lui,替換 addiu 和 shl。
假設 sp = 0xa0008000 和堆棧大小 = 0x90008000,那么 (0xa0008000 - 0x90008000) => 0x10000000。使用 Cpu0 Prologue 說明進行驗證,如下所示,
- “lui $1, 28671” => $1 = 0x6fff0000。
- “ori $1, $1, 32768” => $1 = (0x6fff0000 + 0x00008000) => $1 = 0x6fff8000。
- “addu $sp, $sp, $1” => $sp = (0xa0008000 + 0x6fff8000) => $sp = 0x10000000。
使用 sp = 0x10000000 和堆棧大小 = 0x90008000 的 Cpu0 Epilogue 指令進行驗證,如下所示,
- “lui $1, 36865” => $1 = 0x90010000。
- “addiu $1, $1, -32768” => $1 = (0x90010000 + 0xffff8000) => $1 = 0x90008000。
- “addu $sp, $sp, $1” => $sp = (0x10000000 + 0x90008000) => $sp = 0xa0008000。
表 15 llvm 后端階段的函數
able 15 Functions for llvm backend stages
在“添加 Cpu0DAGToDAGISel 類”部分的指令,添加了一個pass。可以將代碼嵌入到其它類似的pass中。有關信息,請查看 CodeGen/Passes.h。根據llc
-debug-pass=Structure 指示的功能單元,調用pass。
已經完成了一個簡單的 cpu0 編譯器,只支持ld, st,addiu,ori,lui,addu,shl和ret 8 條指令。
可能會想“在編寫了這么多代碼之后,只需得到這 8 條指令!”。重點是已經為 Cpu0 目標機,創建了一個框架(llvm 后端結構類繼承樹)。有超過 3000 行帶有注釋的源代碼,包括文件 *.cpp,*.h,*.td 和 CMakeLists.txt。可以通過命令計數wc
`find
dir
-name
*.cpp`對於文件 *.cpp,*.h,*.td,*.txt。LLVM 前端,總共有 700 行源代碼,沒有注釋。實際上,編寫后端是啟動緩慢,但運行很快。Clang 在 clang/lib 目錄中,有超過 500,000 行,帶有注釋的源代碼,包括 C++ 和 Obj C 支持。llvm 3.1 的 Mips 后端,只有 15,000 行,帶有注釋。即使是復雜的X86 CPU,外有CISC,內有RISC(微指令),在llvm 3.1中,也只有45000行注釋。
Fig. 20 Code generation and execution flow
圖20的上半部分,生成和執行計算機程序,工作流程和軟件包。IR代表中間表示。中間部分是工作流程。除了clang,其它塊都需要擴展,進行新的后端開發(許多后端也擴展clang,Cpu0后端沒有這個需求)。實現了黃框部分。該圖的綠色部分,用於 Cpu0 后端的 lld 和 elf2hex,可以在http://jonathan2251.github.io/lbt/index.html上找到 。十六進制是 ascii 文件格式,使用“0”到“9”和“a”到“f”,表示十六進制值,因為 Verilog 語言機器,用作輸入文件。
參考鏈接:
http://jonathan2251.github.io/lbd/ctrlflow.html