天天看點

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

點選檢視第一章 點選檢視第二章

第3章 工具和設計

LLVM項目由一些庫和工具組成,它們一起構成一個大型的編譯器基礎架構。将所有這些零件連接配接在一起需要精心的設計,這是項目的關鍵。在整個過程中,LLVM都在強調“一切都是庫”的理念,隻有相當少量的代碼是不可重用的,并且不包括特定的工具。盡管如此,仍然有大量的工具允許使用者以多種方式從指令終端運作庫。在本章中,我們将介紹以下主題:

  • LLVM核心庫的概述和設計
  • 編譯器驅動程式的工作原理
  • 編譯器驅動程式進階:了解LLVM中間工具
  • 如何編寫你的第一個LLVM工具
  • 關于浏覽LLVM源代碼的正常建議

3.1 LLVM的基本設計原理及其曆史

LLVM是一個衆所周知的教學架構,這是因為它的幾個工具的組織化程度很高,進而使得感興趣的使用者可以觀察到編譯過程的許多步驟。其設計決策可以追溯到十多年前的第一個版本,當時這個專注于後端算法的項目隻是依靠GCC将C這樣的進階語言轉換成LLVM中間表示(intermediate representation,簡稱IR)。如今,LLVM的設計核心是它的IR。它使用的靜态單指派形式(SSA)具有兩個重要特征:

  • 代碼被組織為三位址指令
  • 它有數目不受限制的寄存器

但是,這并不意味着LLVM隻有一種表示程式的形式。在整個編譯過程中,其他中間資料結構都保持程式邏輯結構,并且有助于跨主要檢查點進行編譯。從技術上講,這些結構也是程式的中間表示形式。例如,LLVM在不同的編譯階段采用以下額外的資料結構:

  • 将C或C++轉換為LLVM IR時,Clang将使用抽象文法樹(AST)結構(Trans-lation-UnitDecl類)在記憶體中表示程式。
  • 在将LLVM IR轉換為特定于機器的彙編語言時,LLVM首先将程式轉換為有向無環圖(DAG)格式以便選擇指令(SelectionDAG類),然後将其轉換回三位址表示以便進行指令排程(MachineFunction類)。
  • 為了實作彙編器和連結器,LLVM使用第四種中間資料結構(MCModule類)在對象檔案的上下文中儲存程式表示。

相比于LLVM中其他形式的程式表示,LLVM IR是最重要的一個,它具有不僅是記憶體中的表示而且還能存儲在磁盤上的特性。LLVM IR因使用特定的編碼而能存在于外部世界的這一事實是在項目初期做出的另一個重要決策,反映了當時研究終身程式優化的學術興趣。

在這個理念中,編譯器的目标不隻是在編譯時進行優化,而且還要探索利用在安裝時、運作時和空閑時(程式未運作時)的優化機會。這樣,在整個程式的生命周期中都進行優化,這也解釋這個概念的名字。例如,當使用者沒有運作程式并且計算機空閑時,作業系統可以啟動編譯器守護程序來處理運作時收集的性能分析資料,以便針對該使用者的特定用例重新優化程式。

請注意,由于能夠存儲在磁盤上,LLVM IR(它是終身程式優化的關鍵)為對整個程式進行編碼提供了另一種方式。當整個程式以編譯器IR的形式存儲時,還可以執行新的一系列跨越單個編譯單元或C檔案邊界的非常有效的跨程式優化。是以,這也為進行強大的連結時優化提供了條件。

另一方面,如果終身程式優化成為現實,則程式分發需要在LLVM IR級别發生,這目前還沒有實作。這意味着LLVM将作為平台或虛拟機運作,并與Java展開競争,這也面臨着嚴峻的挑戰。例如,LLVM IR不是像Java那樣獨立于目标機器的。LLVM也沒有投資于在安裝後進行強大的基于回報的優化。如果有興趣進一步了解這些技術挑戰,請閱讀

http://lists.cs.uiuc.edu/pipermail/llvmdev/2011-October/043719.html

上的“LLVMdev”讨論主題。

随着項目逐漸成熟,維護編譯器IR在磁盤上表示的設計決策仍然是為了實作連結時優化,而較少關注終身程式優化的原始想法。最終,LLVM的核心庫通過放棄低級虛拟機(Low Level Virtual Machine)這個名稱,正式表明對成為一個平台不感興趣,而僅僅由于曆史原因使用了LLVM這個名稱,進而明确了LLVM項目立志成為強大和實用的C/C++ 編譯器,而不是Java平台的競争對手。

盡管如此,除了連結時優化之外,磁盤表示本身也有很好的應用前景,有些組織正在努力将其實作。例如,FreeBSD社群希望在可執行檔案中嵌入其LLVM程式表示,以允許進行安裝時或離線的微架構優化。在這種情況下,即使程式編譯為通用x86形式,當使用者安裝程式時(比如,在特定的Intel Haswell x86處理器上安裝程式時),LLVM基礎架構就可以使用二進制程式的LLVM表示形式,對程式進行特殊處理以使用Haswell支援的新指令。盡管這是一個正在評估的新想法,但它表明磁盤上的LLVM表示可應用于激進的新解決方案。我們能期望的優化主要針對微架構,因為Java中完全的平台無關性在LLVM中是不切實際的,目前僅在一些外部項目上探索這種可能性(參見PNaCl,Chromiu的Portable Native Client)。

作為編譯器IR,用于指導核心庫開發的兩個LLVM IR的基本原則如下:

  • SSA表示和允許快速優化的無限寄存器
  • 通過将整個程式存儲在磁盤IR表示中以實作便捷的連結時優化

3.2 了解目前的LLVM

目前,LLVM項目已經發展起來,并擁有數量巨大的編譯器相關工具。實際上,LLVM這個名稱可能是以下任意一項:

  • LLVM項目/基礎架構:這是對組成一個完整編譯器的如下幾個項目的總稱:前端、後端、優化器、彙編器、連接配接器、libc++、compiler-RT和JIT引擎。例如,在“LLVM由幾個項目組成”這句話中“LLVM”就是這個意思。
  • 基于LLVM的編譯器:這是一個部分或全部使用LLVM基礎架構所建構的編譯器。例如,編譯器可能使用LLVM作為前端和後端,但使用GCC和GNU系統庫來執行最終的連結。例如,在“我用LLVM将C程式編譯到MIPS平台”這句話中的“LLVM”就是這個意思。
  • LLVM庫:這是LLVM基礎架構的可重用代碼部分。例如,在“我的項目使用LLVM的即時編譯架構生成代碼”這句話中“LLVM”就是這個意思。
  • LLVM核心:在中間語言級别和後端算法上進行的優化形成了項目開始時的LLVM核心。“LLVM和Clang是兩個不同的項目”這句話中的“LLVM”就是這個意思。
  • LLVM IR:這是LLVM編譯器中間表示。在諸如“我建構了一個前端來将我自己的語言翻譯成LLVM”這樣的句子中,“LLVM”就有LLVM IR的意思。

要了解LLVM項目,需要知道基礎架構中最重要的部分:

  • 前端:這是将計算機程式語言(如C、C++和Objective-C)轉換為LLVM編譯器IR的編譯步驟。它包括詞法分析器、文法分析器、語義分析器和LLVM IR代碼生成器。Clang項目提供了一個插件接口和一個單獨的靜态分析工具用于進行深度分析,同時實作了所有與前端相關的步驟。更多詳細資訊,請參閱第4章、第9章和第10章。
  • IR:LLVM IR既有使用者可讀的表示形式,也有二進制編碼的表示形式。相應的工具和庫提供了IR建構、組裝和拆卸的接口。LLVM優化器還可以處理IR,以應用大多數優化。我們将在第5章詳細解釋IR。
  • 後端:這是負責生成代碼的步驟。它将LLVM IR轉換為特定于目标的彙編代碼或目标代碼二進制檔案。寄存器配置設定、循環轉換、窺視孔優化器以及特定于目标的優化/轉換屬于後端。我們在第6章對此進行深入分析。

圖3-1列出了這些元件,讓我們對在特定配置下使用的整個基礎架構有一個總體認識。請注意,我們可以重新組織這些元件,并根據不同的需求有選擇地使用它們,例如,如果我們不想探索連結時優化,則不使用LLVM IR連結器。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

每個編譯器元件之間的互動可以通過以下兩種方式進行:

  • 在記憶體中:該方式通過一個單獨的監督工具(如Clang)實作。該工具将每個LLVM元件作為一個庫,并依賴于記憶體中配置設定的資料結構将一個階段的輸出作為輸入提供給另一個階段。
  • 通過檔案:該方式通過使用者實作。使用者啟動較小的獨立工具,該工具将特定元件的結果寫入磁盤檔案,具體取決于使用者是否使用此檔案作為輸入來啟動下一個工具。

    是以,像Clang這樣的進階工具可以整合使用其他幾個更小的工具,具體做法是連結小工具的庫來實作這些工具的功能。該功能的可能性來自LLVM的設計十分重視以庫的形式進行大量代碼重用。此外,整合了少量庫的獨立工具非常有用,因為這樣的工具允許使用者通過指令行直接與特定的LLVM元件互動。

例如,請看圖3-2,該框圖中下面三項是工具的名稱,上面兩項是實作其功能的庫。在本例中,LLVM後端工具llc使用libLLVMCodeGen庫實作部分功能,而僅用于啟動LLVM IR級優化器的opt指令使用另一個庫libLLVMipa實作與目标無關的過程間優化。最後,我們看到一個更強大的工具clang,它使用兩個庫來代替llc和opt,并向使用者呈現更簡單的接口。是以,用這樣的進階工具執行的任何任務都可以分解成一系列低級任務,同時産生相同的結果。接下來的内容會繼續說明這個概念。實際上,Clang能夠執行整個編譯過程,而不僅僅是完成opt和llc的工作。這就解釋了為什麼在靜态建構中Clang二進制檔案通常是最大的,因為它連結并利用整個LLVM生态系統。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

3.3 與編譯器驅動程式互動

一個編譯器驅動程式與漢堡店的售貨員相似,售貨員會與你互動,識别你的訂單,将你的訂單傳到後端做出漢堡,然後把它和可樂或番茄醬小包一起端到你面前,進而完成你的訂單。驅動程式負責整合所有必要的庫和工具,以便為使用者提供更友好的體驗,使使用者不必單獨應付編譯器的各個階段,比如前端、後端、彙編器和連結器等。一旦你将程式源代碼提供給編譯器驅動程式,它就可以生成可執行檔案。在LLVM和Clang中,編譯器驅動程式就是clang工具。

假設有一個簡單的C程式hello.c:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

要為此簡單程式生成可執行檔案,請使用以下指令:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

請按第1章中的說明擷取LLVM的直接可使用版本。

對于熟悉GCC的人,請注意上述指令與GCC非常相似。實際上,Clang編譯器驅動程式被設計成與GCC标志和指令結構相相容,進而允許在許多項目中用LLVM替代GCC。對于Windows,Clang也有一個名為clang-cl.exe的版本,可模拟Visual Studio C++編譯器指令行界面。Clang編譯器驅動程式隐式地從前端到連結器調用所有其他工具。

如果想檢視驅動程式為了完成你的指令而調用的所有其他工具,請使用-###指令參數:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

Clang驅動程式調用的第一個工具是帶有-cc1參數的clang自身,以便在啟用編譯器模式時禁用編譯器驅動程式模式。它還使用了衆多參數來調整C/C++選項。由于LLVM元件是庫,是以clang -cc1會與IR生成器、目标機器的代碼生成器以及彙編器庫進行連結。是以,在解析之後,clang -cc1本身能夠調用其他庫,并監視記憶體中的編譯過程,直到目标檔案完成。之後,Clang驅動程式(與編譯器clang -cc1不同)調用作為外部工具的連結程式來生成可執行檔案,如上述輸出行所示。它使用系統連結器完成編譯,因為LLVM連結器lld仍在開發中。

注意,使用記憶體要比使用磁盤快得多,這使得中間編譯檔案很少被用到。這就解釋了為什麼Clang(LLVM前端,也就是第一個與輸入互動的工具)負責在記憶體中執行剩餘的編譯工作,而不會産生要被其他工具讀取的中間輸出檔案。

3.4 使用獨立工具

我們也可以通過使用LLVM獨立工具來練習之前描述的編譯工作流程,這會将一個工具的輸對外連結接到另一個工具的輸出。雖然将中間檔案寫入磁盤會導緻編譯速度減慢,但是觀察編譯流水過程是一個有趣的教學練習。這個過程也讓你有機會微調中間工具的參數,其中一些工具如下:

  • opt:這是一個旨在IR級對程式進行優化的工具。輸入必須是LLVM位碼檔案(編碼的LLVM IR),并且生成的輸出檔案必須具有相同的類型。
  • llc:這是一個通過特定後端将LLVM位碼轉換成目标機器彙編語言檔案或目标檔案的工具。你可以通過傳遞參數來選擇優化級别、打開調試選項以及啟用或禁用特定于目标的優化。
  • llvm-mc:這個工具能夠彙編指令并生成諸如ELF、MachO和PE等對象格式的目标檔案。它也可以反彙編相同的對象,進而轉儲這些指令的相應的彙編資訊和内部LLVM機器指令資料結構。
  • lli:這個工具是LLVM IP的解釋器和JIT編譯器。
  • llvm-link:這個工具将幾個LLVM位碼連結在一起,以産生一個包含所有輸入的LLVM位碼。
  • llvm-as:該工具将人工可讀的LLVM IR檔案(稱為LLVM彙編碼)轉換為LLVM位碼。
  • llvm-dis:這個工具将LLVM位碼解碼成LLVM彙編碼。

我們來看一個由分散在多個源檔案中的函數組成的簡單的C程式。第一個源檔案是main.c,它的内容如下:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

第二個檔案是sum.c,它的内容如下:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

我們可以用下面的指令編譯這個C程式:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

但是,我們使用獨立工具也可以獲得相同的結果。首先,我們改變clang指令以便為每個C源檔案生成LLVM位碼檔案,然後停下來,而不是繼續完成編譯:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

-emit-llvm标志告訴clang根據是否存在-c或-S标志來生成LLVM位碼或LLVM彙編碼檔案。在前面的示例中,-emit-llvm和-c标志一起使用,将告訴clang以LLVM位碼格式生成一個目标檔案。使用-flto -c标志組合可以得到相同的結果。如果你打算生成人工可讀的LLVM彙編碼,請使用下述兩個指令:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

.bc和.ll分别是LLVM位碼和彙編檔案的檔案擴充名。為了繼續完成編譯,後續步驟可以采取以下兩種方式:

  • 從每個LLVM位碼檔案生成特定于目标的目标檔案,并通過将其連結到系統連結器來建構可執行程式(圖3-3的A部分):
帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計
  • 首先,将兩個LLVM位碼檔案連接配接成最終的LLVM位碼檔案。然後,從最終的位碼檔案建構特定于目标的目标檔案,并通過調用系統連結程式來生成可執行程式(圖3-3的B部分):
帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

-filetype=obj參數指定輸出一個目标檔案,而不是目标彙編檔案。我們使用Clang驅動程式clang來調用連結器,但是,如果知道系統連結器與系統庫連結所需要的所有參數,則可以直接使用系統連結器。

通過在後端調用(llc)之前連結IR檔案,将允許最終生成的IR能夠被opt工具提供的連結時優化機制進一步優化(請參閱第5章)。另外,llc工具可以生成彙編輸出,可以使用llvm-mc對該輸出進行進一步彙編。我們将在第6章介紹這個接口的更多細節。

3.5 深入LLVM内部設計

為了将編譯器解耦成多個工具,LLVM設計通常強制元件在高度抽象層次上發生互動。它将不同的元件分隔成不同的庫,而且它是使用面向對象的範例用C++編寫的,可以提供可插入的通道接口,進而允許在整個編譯過程中友善地內建轉換和優化步驟。

3.5.1 了解LLVM的基本庫

LLVM和Clang的工作邏輯被精心組織到以下庫中:

  • libLLVMCore:該庫包含與LLVM IR相關的所有邏輯:IR構造(資料布局、指令、基本塊和函數)以及IR校驗器。它還負責編管理譯器中各種編譯流程。
  • libLLVMAnalysis:該庫包含幾個IR分析過程,如别名分析、依賴分析、常量折疊、循環資訊、記憶體依賴分析和指令簡化。
  • libLLVMCodeGen:該庫實作與目标無關的代碼生成和機器級别(LLVM IR的更低級版本)的分析和轉換。
  • libLLVMTarget:該庫通過通用目标抽象來提供對目标機器資訊的通路接口。這些進階抽象為在libLLVMCodeGen中實作的通用後端算法與為下一個庫保留的特定于目标的邏輯之間進行通信提供網關。
  • libLLVMX86CodeGen:該庫具有特定于x86目标的代碼生成資訊、轉換和分析過程,它們組成x86後端。請注意,每個目标機器都有一個不同的庫,比如分别實作ARM和MIPS後端的LLVMARMCodeGen和LLVMMipsCodeGen庫。
  • libLLVMSupport:該庫包括一個通用工具集合。錯誤、整數和浮點處理、指令行解析、調試、檔案支援和字元串處理都是在這個庫中實作的算法示例,它們在LLVM各元件中通用。
  • libclang:該庫實作了一個C接口(而不是C++接口),它是LLVM代碼的預設實作語言,可以通路Clang的大部分前端功能:診斷報告、AST周遊、代碼完成、遊标映射和源代碼。由于它使用C語言,使用更簡單的接口,是以它允許以其他語言(如Python)編寫的項目更容易地使用Clang功能,當然C接口設計得更為穩定,并允許外部項目依賴它。該庫僅涵蓋内部LLVM元件所使用的C++接口的一個子集。
  • libclangDriver:該庫包含編譯器驅動程式工具使用的一組類,用于了解類似于GCC的指令行參數,以便為外部工具完成編譯的不同步驟準備作業群組織足夠的參數。它可以根據目标平台管理不同的編譯政策。
  • libclangAnalysis:該庫是由Clang提供的一組前端級分析器。它具有CFG和調用圖結構、代碼可達性、格式字元串安全性等。

對于如何使用這些庫來建構LLVM工具,我們舉了一個例子,圖3-4顯示llc工具對libLLVMCodeGen、libLLVMTarget等庫的依賴關系,以及這些庫對其他庫的依賴關系。不過請注意,前面的清單并不完整。

我們将把在上面省略的其他庫留給後面的章節去介紹。對于版本3.0,LLVM團隊編寫了一個很好的文檔,來介紹所有LLVM庫之間的依賴關系。盡管檔案已經過時,但它仍然提供了關于庫的組織關系的有趣概述,可以通過

http://llvm.org/releases/3.0/docs/UsingLibraries.html

通路該文檔。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

3.5.2 介紹LLVM的C++慣例

LLVM庫和工具都是用C++編寫的,以利用面向對象的程式設計範例,并增強其各元件之間的互操作性。另外,為了盡可能避免代碼中的低效性,要求強制執行良好的C++程式設計慣例。

3.5.2.1 在慣例中看到多态性

繼承和多态性通過将通用的代碼生成算法留給基類來抽象後端的公共任務。在這個方案中,每個特定的後端都可以通過編寫更少的必要方法來重寫超類泛型操作,進而專注于實作其特殊性。LibLLVMCodeGen包含公共算法,而LibLLVMTarget包含用于抽象單個機器的接口。以下代碼片段(來自llvm/lib/Target/Mips/MipsTargetMachine.h)展示了如何将MIPS目标機器的描述類聲明為LLVMTargetMachine類的子類,并說明了這個概念。這段代碼是LLVMMipsCodeGen庫的一部分:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

為了進一步闡述這個設計選擇,我們将展示另一個後端示例,在該例中,與目标無關的寄存器配置設定器(它對所有後端都十分常見)需要知道哪些寄存器是保留的,不能用于配置設定。這個資訊取決于具體的目标,并且不能被編碼成通用的超類。我們通過使用MachineRegisterInfo::getReservedRegs()來執行此任務,這是一個必須由每個目标重寫的通用方法。以下代碼片段(來自llvm/lib/Target/Sparc/SparcRegisterInfo.cpp)顯示SPARC目标如何重寫此方法:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

在此代碼中,SPARC後端通過建構位向量來逐一選擇哪些寄存器不能參加通用寄存器配置設定。

3.5.2.2 介紹LLVM中的C++模闆

雖然LLVM經常使用C++模闆,但要特别小心控制C++項目的編譯時間,因為C++項目中典型的模闆濫用問題會造成較長編譯時間。一旦有可能,LLVM就會使用模闆特化來允許實作快速和經常使用的任務。作為LLVM代碼中的一個模闆示例,我們來介紹一個函數,該函數檢查作為參數傳遞的整數是否适合給定的位寬(模闆參數)(代碼來自llvm/include/llvm/Support/MathExtras.h):

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

在這段代碼中,請注意模闆中的代碼是如何處理所有位寬值N的。它有一個較早的比較,隻要位寬大于64位就傳回true,相反則建立兩個表達式,它們是這個位寬的下限和上限,以檢查x是否在這兩個邊界以内。将此代碼與下面的模闆特化相比較,該模闆用于擷取位寬為8這一常見情況下更快的代碼:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

該代碼将比較的數量從三個減少到一個,進而證明了特化的合理性。

3.5.2.3 在LLVM中執行C++最佳慣例

程式設計時無意中引入錯誤的現象是很常見的,問題在于如何管理錯誤。LLVM理念建議盡可能使用在libLLVMSupport中實作的斷點機制。注意,調試編譯器可能特别困難,因為編譯的産物是另一個程式。是以,如果能夠更早檢測出錯誤的行為,就不需要為了确定程式是否正确而編寫一個并不重要的複雜輸出,這樣,就可以節省大量的時間。例如,讓我們來看一段ARM後端代碼,它改變常量池的布局,進而以跨函數的幾個較小常量孤池重新配置設定它們。這個政策通常用在ARM程式中,以便用一個有限(相對于PC)的尋址機制來加載大型常量,因為一個較大的獨立池可能被放置在距離使用它的指令很遠的地方。該代碼來自llvm/lib/Target/ARM/ARMConstantIslandPass.cpp,我們在下面展示它的一部分:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

在這個片段中,代碼周遊一個代表ARM常量池的資料結構,程式員期望這個對象的每個字段都遵守特定的限制條件。請注意程式員如何使用assert調用來保持對資料語義的控制。如果程式員在編寫這段代碼的時候發現有什麼内容和自己的想法有差别時,程式将立即退出執行,并列印失敗的斷言調用。程式員在布爾表達式字尾&&"error cause!"的習慣用法不會影響assert的布爾表達式的計算,但如果失敗則會在列印此表達式時給出關于斷言失敗的簡短文本解釋。一旦LLVM項目執行釋出版本編譯,斷言對性能的影響就會被完全删除,因為它會禁用斷言。

你将在LLVM代碼中頻繁看到的另一種常見做法是使用智能指針。一旦符号超出範圍,智能指針将自動釋放記憶體,它們在LLVM代碼庫中用于(例如)保持目标資訊和子產品。過去,LLVM提供了一個叫作OwningPtr的特殊智能指針類,它在llvm/include/llvm/ADT/OwningPtr.h中定義。從LLVM 3.5開始,這個類已被棄用,而被std::unique_ptr()替代,這是在C++ 11标準中引入的。

如果你對LLVM項目中采用的C++最佳慣例的完整清單感興趣,請通路

http://llvm.org/docs/CodingStandards.html

。每位C++程式員都應該讀一下。

3.5.2.4 在LLVM中使用輕量級字元串引用

LLVM項目有一個支援常見算法的資料結構擴充庫,在該LLVM庫中字元串有特殊的地位。它們屬于C++中的一個類,并引發了熱烈的讨論:我們應該在什麼時候使用一個簡單的char而不是C++标準庫的string類?要在LLVM的上下文中讨論這個問題,可以考慮在整個LLVM庫中密集使用字元串調用,來引用LLVM子產品、函數和值等的名稱。在某些情況下,LLVM處理的字元串可以包含空字元,但是,因為空字元會終止C風格的字元串,是以将常量字元串引用作為const char指針進行傳遞的方法是不可能的。另一方面,頻繁使用const std::string&會引入額外的堆配置設定,因為string類需要擁有字元緩沖區。我們可以從下面的例子中看到這一點:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計
帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

請注意,每次我們嘗試在自己的緩沖區中建立字元串時,都會花費額外的堆配置設定來将此字元串複制到string對象的内部緩沖區,而該對象必須擁有自己的緩沖區。在第一種情況下,我們有一個堆棧配置設定的字元串,而在第二種情況下,字元串被當作一個全局常量。對于這種情況,C++中缺少一個簡單的類,能夠在我們隻需要引用字元串時,避免不必要的配置設定。即使我們嚴格使用string對象,以避免不必要的堆配置設定,但對字元串對象的引用也會産生兩個間接引用。由于string類已經使用一個内部指針來持有其資料,是以當我們通路實際資料時,傳遞字元串對象的指針會造成兩次引用的開銷。

我們可以利用一個LLVM類來更加高效地處理字元串引用:StringRef。這是一個輕量級類,它可以像const char*那樣進行值傳遞,但是它也存儲字元串的大小,進而允許空字元的存在。然而,與string對象不同,它并不擁有自己的緩沖區,是以永遠不會配置設定堆空間,而隻是引用其外部的字元串。在其他C++項目中也涉及這個概念,例如,Chromium使用StringPiece類來實作相同的目的。

LLVM還引入了另一個字元串操作類。為了通過幾個連接配接建構一個新的字元串,LLVM提供了Twine類。該類隻存儲用來構成最終結果的字元串的引用,通過這種方式來推遲實際的連接配接。這是在C++ 11之前建立的技術,那時字元串連接配接的開銷較高。

如果你有興趣了解LLVM為程式員提供的其他通用類,那麼應該儲存在書簽中的一個非常重要的文檔是LLVM程式員手冊,該手冊讨論可能對任何代碼都有用的LLVM通用資料結構。該手冊位于

http://llvm.org/docs/ProgrammersManual.html

3.5.3 示範可插拔的流程接口

一個流程(pass)是指一次轉換分析或優化。LLVM API允許你輕松地在程式編譯生命周期的不同部分注冊任何流程,這是LLVM設計中值得稱道的亮點。流程管理器用于注冊、排程和聲明流程之間的依賴關系。是以,PassManager類的執行個體在不同的編譯器階段都是可用的。

例如,目标可以在代碼生成期間的多個點自由地應用自定義優化,例如,在寄存器配置設定之前和之後,或者在彙編碼生成之前。為了說明這一點,我們展示一個例子,其中X86目标在彙編碼生成之前有條件地注冊一對自定義流程(來自lib/Target/X86/X86TargetMachine.cpp):

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計
帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

請注意後端如何使用特定目标資訊來判斷是否應該添加流程。在添加第一個流程之前,X86目标會檢查它是否支援SSE2多媒體擴充。對于第二個流程,它會檢查是否有特别的填充請求。

在圖3-5中,A部分是一個示例,它展示了如何在opt工具中插入優化流程,B部分說明代碼生成中可以插入自定義目标優化的幾個目标鈎子。請注意,插入點分散在不同的代碼生成階段。當你編寫第一個流程并需要決定在何處運作時,此圖表會非常有用。第5章會較長的描述PassManager接口。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

3.6 編寫你的第一個LLVM項目

在本節中,我們将展示如何使用LLVM庫的編寫你的第一個項目。在前面的章節中,我們介紹了如何使用LLVM工具來生成與程式相對應的中間語言檔案,即位碼檔案。現在我們将建立一個程式,該程式能夠讀取此位碼檔案并列印其中定義的函數名稱,以及它們的基本塊數量,進而顯示LLVM庫的易用性。

3.6.1 編寫Makefile

連結LLVM庫需要使用長指令行,如果沒有建構系統的幫助,想寫出這些指令行是不切實際的。在下面的代碼中,我們展示了一個Makefile檔案(基于在DragonEgg中使用的代碼)來完成這個任務,同時解釋所提到的每個部分。如果複制并粘貼此代碼,将會丢失制表符。請記住,Makefile依賴于制表符來指定定義規則的指令,是以,應該手動插入制表符:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

第一部分定義将用作編譯器标志的第一個Makefile變量。第一個變量決定llvm-config程式的位置,在這裡,它需要在你的路徑中。llvm-config工具是一個LLVM程式,它可以列印建構需要與LLVM庫連結的外部項目的各種有用資訊。

例如,定義在C++編譯器中使用的标志集時,請注意,我們要求Make啟動llvm-config --cxxflags shell指令行,該指令行将列印用于編譯LLVM項目的C++标志集。這樣,我們就使得項目源碼的編譯與LLVM源碼相容。最後一個變量定義要傳遞給編譯器預處理器的标志集。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

在第二個片段中,我們定義了Makefile規則。第一個規則總是預設的,我們用它建構hello-world可執行檔案。第二個是通用規則,它将所有C++檔案編譯成目标檔案,我們将預處理器标志和C++編譯器标志傳遞給它。我們還使用$(QUIET)變量來省略螢幕上出現的完整指令行,但是如果你想要一個詳細的建構日志,則可以在運作GNU Make時定義VERBOSE。

最後一個規則連結所有目标檔案(在這裡隻有一個)來建構與LLVM庫連結的項目可執行檔案。這部分工作是由連結器完成的,但是一些C++标志也可能會生效。是以,我們将C++和連結器标志都傳遞給指令行。我們用'command'結構來完成此操作,它訓示shell用'command'的輸出替換這部分内容。在我們的例子中,指令是llvm-config --libs bitreader core support。--libs标志向llvm-config請求提供用于連結到所請求的LLVM庫的連結器标志清單。這裡,我們請求libLLVMBitReader、libLLVMCore和libLLVMSupport。

由llvm-config傳回的标志清單是一系列-l連結器參數,如-lLLVMCore -lLLVMSupport。但請注意,傳遞給連結器的參數順序很重要,并且要求你将依賴于其他庫的參數放在前面。例如,由于libLLVMCore使用libLLVMSupport提供的通用功能,是以正确的順序是-lLLVMCore -lLLVMSupport。

順序很重要,因為一個庫就是一個目标檔案的集合,在将項目與庫連結時,連結器隻選擇到目前為止已知的目标檔案來解析所見到的未定義符号。是以,如果它正在處理指令行參數中的最後一個庫,并且該庫恰好使用了已經處理過的庫中的符号,則大多數連結器(包括GNU ld)将不會傳回去包括有可能缺失的目标檔案,進而導緻建構失敗。

如果你想避免這個問題,并強制連結器疊代通路每個庫,直到所有必要的目标檔案都被解析,則必須在庫清單的開始和結束處使用--start-group和--end-group标志,但這可能會減慢連結器速度。在建構完整的依賴關系圖時,為了避免因為要弄清楚連結器參數的順序而頭疼,可以簡單使用llvm-config --libs,讓它為你做這些工作,就像我們之前做的那樣。

Makefile檔案的最後一部分定義了一條清理規則以删除編譯器生成的所有檔案,使我們可以從頭開始重新啟動建構。清理規則的格式如下:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

3.6.2 編寫代碼

下面展示這個流程的完整代碼。它相對較短,因為它建立在LLVM流程基礎設施上,後者替我們完成了大部分工作。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計
帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

我們的程式使用cl命名空間中的LLVM工具(cl代表指令行)來實作我們的指令行接口。我們隻需調用ParseCommandLineOptions函數并聲明cl::opt 類型的全局變量,以顯示我們的程式接收單個參數,并且該參數是包含位碼檔案名的string類型。

之後,我們執行個體化一個LLVMContext對象,以存放與LLVM編譯相關的所有資料,進而使LLVM是線程安全的。MemoryBuffer類為記憶體塊定義一個隻讀接口,ParseBitcodeFile函數将使用這個對象來讀取我們的輸入檔案的内容,并解析檔案中LLVM IR的内容。在檢查完錯誤并確定一切正常後,我們周遊該檔案中子產品的所有函數。LLVM子產品的概念類似于翻譯單元,其中包含所有編碼到位碼檔案中的内容,也是LLVM層次結構中的最高實體,在它後面是函數,然後是基本塊,最後是指令。如果函數隻是一個聲明,則丢棄它,因為我們想查找函數定義。當我們找到這些函數定義時,将列印它們的名稱和它包含的基本塊的數量。

如果編譯此程式,并使用-help運作,可以檢視已為你的程式準備好的LLVM指令行功能。之後,查找要轉換為LLVM IR的C或C++檔案,然後将其轉換并使用程式進行分析:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

如果要進一步了解可從函數中提取的内容,請參閱LLVM Doxygen文檔中關于llvm::Function類的内容,網址為

http://llvm.org/docs/doxygen/html/classllvm_1_1Function.html

。作為一個練習,請嘗試擴充這個例子,以列印每個函數的參數清單。

3.7 關于LLVM源代碼的一般建議

在進一步學習LLVM實作之前,注意還有一些值得了解的要點,主要是針對開源軟體領域的新程式員。如果你在公司内部的一個封閉源代碼的項目中工作,那麼你可能會從項目中比你年長的程式員那裡得到很多幫助,并對許多起初聽起來可能很晦澀的設計決定有更深入的了解。如果遇到問題,元件的作者可能願意口頭向你解釋。其好處是,在解釋的時候,他甚至可以讀懂你的面部表情,弄清楚你什麼時候不了解某個特定的關鍵點,并調整他的話語來為你提供一個更合适的解釋。

但是,由于在大多數社群項目中人們都是遠端工作的,是以通常無法進行面對面的溝通。是以,開源社群有更大的動機采用更強的文檔機制。另一方面,即使是在英文書寫的文檔中明确指出所有的設計決定,文檔本身也可能并不是最讓人期待的東西。大部分文檔中重要的部分是代碼本身,從這個意義上說,編寫清晰的代碼是有壓力的,因為你還需要幫助其他人在沒有英文文檔的情況了解代碼。

3.7.1 将代碼了解為文檔

盡管LLVM中最重要的部分都有相應的英文文檔,并且我們在本書中也引用了這些文檔,但我們的最終目标是讓你準備好直接閱讀代碼,因為這是深入了解LLVM基礎結構的先決條件。我們将為你提供必要的基本概念,以幫助你了解LLVM的工作原理,并且讓你從了解LLVM代碼中享受到樂趣,能夠在即使沒有閱讀英文文檔或缺乏英文文檔的情況下讀懂大部分代碼。即使這樣做可能是有挑戰性的,但當你開始這樣做的時候,你就會更加深入地了解這個項目,并且越來越有信心自己去做一些改變。這樣,你将成為一名了解LLVM内部知識的程式員,并且可以幫助郵件清單中的其他人。

3.7.2 請求社群的幫助

電子郵件清單的存在提醒你,你并不是一個人在戰鬥。它們是Clang前端的cfe-dev清單和LLVM核心的llvmdev清單。請花點時間從以下位址訂閱兩個清單:

項目中有很多人在努力實作你也感興趣的事情,是以很有可能你會針對别人已經做過的事情提問。

在尋求幫助之前,請先自己動腦思考,并嘗試在沒有幫助的情況下了解代碼,看看自己能飛得多高,并盡力拓展你的知識。如果遇到一些令你感到困惑的事情,可以向清單發出一封電子郵件,清楚說明你已經探索過這個問題但沒有結果,然後再尋求幫助。通過遵循這些準則,你将有更好的機會擷取問題的最佳答案。

3.7.3 應對更新:使用SVN日志作為文檔

LLVM項目在不斷變化,實際上,你可能會發現一個非常常見的情況是,你經常需要更新LLVM版本,并發現充當與LLVM庫接口的軟體部分出現問題。在嘗試再次讀取代碼以檢視其更改情況之前,請使用合适的代碼修訂版本。

為了實際看看這麼做如何有效,讓我們練習将前端Clang從3.4更新到3.5。假設你為執行個體化BugType對象的靜态分析器編寫了一段代碼:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

這個對象用來生成你自己的檢查器(更多細節請參閱第9章),用于報告特定種類的錯誤。現在,讓我們将整個LLVM和Clang代碼庫更新到3.5版本,并編譯這些代碼行。我們将得到以下輸出:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

發生此錯誤是因為BugType構造函數從一個版本更改為另一個版本。如果你很難确定如何使你的代碼适應新版本,則需要通路更改日志,這是一個重要的文檔,它會記錄特定時期的代碼更改情況。幸運的是,對于使用代碼修訂系統的每個開源項目,我們都可以通過查詢代碼修訂伺服器來擷取影響特定檔案的送出消息,進而輕松獲得更改日志。在LLVM的情況下,甚至可以使用浏覽器通

http://llvm.org/viewvc

通路ViewVC 來這樣做。

在這裡,我們需要檢視定義這個構造方法的頭檔案中有什麼變化。通過檢視LLVM源代碼樹,可以在include/clang/StaticAnalyzer/Core/BugReporter/BugType.h找到該檔案。

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

要檢視影響該特定頭檔案的送出郵件,可以通路

http://llvm.org/viewvc/llvm-project/cfe/trunk/include/clang/StaticAnalyzer/Core/BugReporter/BugType.h?view=log

,然後将在浏覽器中看到日志。現在,我們看到了在編寫本書時三個月前發生的特定修訂,當時LLVM正在更新到v3.5:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

這個送出郵件是非常全面的,解釋了BugType構造函數改變的所有原因:以前,用兩個字元串執行個體化這個對象并不足以知道哪個檢查器發現了一個特定的錯誤。是以,現在必須通過傳遞你的檢查器對象的執行個體來執行個體化對象,該對象将存儲在BugType對象中,并且可以很容易發現每個錯誤是由哪個檢查器産生的。

現在,我們更改我們的代碼以符合以下經過更新的接口。我們假設這個代碼是作為Checker類的函數成員的一部分運作的,通常在實作靜态分析器檢查器的情況下均是如此。是以,this關鍵字應該傳回一個Checker對象:

帶你讀《LLVM編譯器實戰教程》之三:工具和設計第3章 工具和設計

3.7.4 結束語

當你聽說LLVM項目有很好的文檔資源時,不要指望能找到一個精确描述所有代碼細節的英文頁面。這意味着,當你依賴于閱讀代碼、接口、注釋和送出郵件時,你将能夠了解LLVM項目,并跟進最新的變化。不要忘記通過練習修改源代碼去了解原理,這意味着你需要準備好你的CTAGS去開始探索!

3.8 總結

在本章中,我們從曆史的視角向你介紹了LLVM項目中使用的設計決策,并概述了其中最重要的項目。我們還展示了如何以兩種不同的方式使用LLVM元件:首先,使用編譯器驅動程式,這是一個進階工具,可以在單個指令中執行整個編譯;其次,使用單獨的LLVM獨立工具。除了在磁盤上存儲中間結果(這會減慢編譯速度)之外,這些工具還允許我們通過指令行與LLVM庫的特定片段進行互動,進而更好地控制編譯過程,它們是了解LLVM如何工作的絕佳方式。我們還展示了LLVM中使用的幾種C++編碼風格,并解釋了應該如何對待LLVM代碼文檔,以及如何通過社群尋求幫助。

在下一章中,我們将詳細介紹Clang前端的實作及其庫檔案。

繼續閱讀