原文:http://www.ibm.com/developerworks/cn/opensource/os-createcompilerllvm1/
LLVM(之前稱為低級虛擬機)是一種非常強大的編譯器基礎架構框架,專門為使用您喜愛的編程語言編寫的程序的編譯時、鏈接時和運行時優化而設計。LLVM 可運行於若干個不同的平台之上,它以能夠生成快速運行的代碼而著稱。
LLVM 框架是圍繞着代碼編寫良好的中間表示 (IR) 而構建的。本文(由兩部分組成的系列文章的第一部分)將深入講解 LLVM IR 的基礎知識以及它的一些微妙之處。在這里,您將構建一個可以自動為您生成 LLVM IR 的代碼生成器。擁有一個 LLVM IR 生成器意味着您所需要的是一個前端以供插入您所喜愛的編程語言,而且這還意味着您擁有一個完整的流程(前端解析器 + IR 生成器 + LLVM 后端)。創建一個自定義編譯器會變得更加簡單。
在開始之前,在您的開發計算器上必須已經擁有已編譯好的 LLVM(參閱 參考資料 獲取相關鏈接)。本文中的示例均基於 LLVM V3.0。對於 LLVM 代碼的后期生成和安裝,最重要的兩個工具是 llc
和 lli
。
因為 LLVM 是一個虛擬機,所以它可能應該擁有自己的中間字節代碼表示,不是嗎?最后,您需要將 LLVM 字節代碼編譯到特定於平台的匯編語言中。然后您才能通過本機匯編程序和鏈接器來運行匯編代碼,從而生成可執行的共享庫等。您可以使用 llc
將 LLVM 字節代碼轉換成特定於平台的匯編代碼(請參閱 參考資料,獲取關於此工具的更多信息的鏈接)。對於 LLVM 字節代碼的直接執行部分,不要等到在本機執行代碼崩潰后才發現您的程序中有一個或兩個 bug。這正是 lli
的用武之地,因為它可以直接執行字節代碼。lli
可以通過解釋器或使用高級選項中的即時 (JIT) 編譯器執行此工作。請參閱 參考資料,獲取關於 lli
的更多信息的鏈接。
llvm-gcc 是 GNU Compiler Collection (gcc) 的修改版本,可以在使用 -S -emit-llvm
選項運行時會生成 LLVM 字節代碼。然后您可以使用 lli
來執行這個已生成的字節代碼(也稱為 LLVM 匯編語言)。有關 llvm-gcc 的更多信息,請參閱 參考資料。如果您沒有在自己的系統中預先安裝 llvm-gcc,那么您應該能夠從源代碼構建它,請參閱 參考資料,獲取分步指南的鏈接。
要更好地理解 LLVM,您必須了解 LLVM IR 及其微妙之處。這個過程類似於學習另一種編程語言。但是,如果您熟悉 C
語言和 C++
語言以及它們的一些語法怪現象,那么在了解 LLVM IR 方面您應該沒有太大的障礙。清單 1 給出了您的第一個程序,該程序將在控制台輸出中打印 "Hello World"。要編譯此代碼,您可以使用 llvm-gcc。
#include <stdio.h> int main( ) { printf("Hello World!\n"); } |
要編譯此代碼,請輸入此命令:
Tintin.local# llvm-gcc helloworld.cpp -S -emit-llvm |
完成編譯后,llvm-gcc 會生成 helloworld.s 文件,您可以使用 lli
來執行該文件,將消息輸出到控制台。lli
的用法如下:
Tintin.local# lli helloworld.s Hello, World |
現在,先看一下 LLVM 匯編語言。清單 2 給出了該代碼。
清單 2. Hello World 程序的 LLVM 字節代碼
@.str = private constant [13 x i8] c"Hello World!\00", align 1 ; define i32 @main() ssp { entry: %retval = alloca i32 %0 = alloca i32 %"alloca point" = bitcast i32 0 to i32 %1 = call i32 @puts(i8* getelementptr inbounds ([13 x i8]* @.str, i64 0, i64 0)) store i32 0, i32* %0, align 4 %2 = load i32* %0, align 4 store i32 %2, i32* %retval, align 4 br label %return return: %retval1 = load i32* %retval ret i32 %retval1 } declare i32 @puts(i8*) |
LLVM 提供了一個詳細的匯編語言表示(參閱 參考資料 獲取相關的鏈接)。在開始編寫我們之前討論的自己的 Hello World 程序版本之前,有幾個需知事項:
- LLVM 匯編語言中的注解以分號 (
;
) 開始,並持續到行末。 - 全局標識符要以
@
字符開始。所有的函數名和全局變量都必須以@
開始。 - LLVM 中的局部標識符以百分號 (
%
) 開始。標識符典型的正則表達式是[%@][a-zA-Z$._][a-zA-Z$._0-9]*
。 - LLVM 擁有一個強大的類型系統,這也是它的一大特性。LLVM 將整數類型定義為
iN
,其中 N 是整數占用的字節數。您可以指定 1 到 223- 1 之間的任意位寬度。 - 您可以將矢量或陣列類型聲明為
[no. of elements X size of each element]
。對於字符串 "Hello World!",可以使用類型[13 x i8]
,假設每個字符占用 1 個字節,再加上為 NULL 字符提供的 1 個額外字節。 - 您可以對 hello-world 字符串的全局字符串常量進行如下聲明:
@hello = constant [13 x i8] c"Hello World!\00"
。使用關鍵字constant
來聲明后面緊跟類型和值的常量。我們已經討論過類型,所以現在讓我們來看一下值:您以c
開始,后面緊跟放在雙引號中的整個字符串(其中包括\0
並以0
結尾)。不幸的是,關於字符串的聲明為什么需要使用c
前綴,並在結尾處包含 NULL 字符和 0,LLVM 文檔未提供任何解釋。如果您有興趣研究更多有關 LLVM 的語法怪現象,請參閱 參考資料,獲取語法文件的鏈接。 - LLVM 允許您聲明和定義函數。而不是仔細查看 LLVM 函數的整個特性列表,我只需將精力集中在基本要點上即可。以關鍵字
define
開始,后面緊跟返回類型,然后是函數名。返回 32 字節整數的main
的簡單定義類似於:define i32 @main() { ; some LLVM assembly code that returns i32 }
。 - 函數聲明,顧名思義,有着重大的意義。這里提供了
puts
方法的最簡單聲明,它是printf
:declare i32 puts(i8*)
的 LLVM 等同物。該聲明以關鍵字declare
開始,后面緊跟着返回類型、函數名,以及該函數的可選參數列表。該聲明必須是全局范圍的。 - 每個函數均以返回語句結尾。有兩種形式的返回語句:
ret <type> <value>
或ret void
。對於您簡單的主例程,使用ret i32 0
就足夠了。 - 使用
call <function return type> <function name> <optional function arguments>
來調用函數。注意,每個函數參數都必須放在其類型的前面。返回一個 6 位的整數並接受一個 36 位的整數的函數測試的語法如下:call i6 @test( i36 %arg1 )
。
這只是一個開始。您還需要定義一個主例程、一個存儲字符串的常量,以及處理實際打印的 puts
方法的聲明。清單 3 顯示第一次嘗試創建的程序。
清單 3. 第一次嘗試創建手動編寫的 Hello World 程序
declare i32 @puts(i8*) @global_str = constant [13 x i8] c"Hello World!\00" define i32 @main { call i32 @puts( [13 x i8] @global_str ) ret i32 0 } |
這里提供了來自 lli
的日志:
lli: test.s:5:29: error: global variable reference must have pointer type call i32 @puts( [13 x i8] @global_str ) ^ |
程序並未按預期的運行。發生了什么?如之前所提及的,LLVM 擁有一個強大的類型系統。因為 puts
期望提供一個指向 i8
的指針,並且您能傳遞一個 i8
矢量,這樣 lli
才能快速指出錯誤。該問題的常用解決方法(來自 C
編程背景)是使用類型轉換。這將您引向了 LLVM 指令 getelementptr
。請注意,您必須將 清單 3 中的 puts
調用修改為與 call i32 @puts(i8* %t)
類似,其中%t
是類型 i8*
,並且是 [13 x i8] to i8*
的類型轉換結果。(請參閱 參考資料,獲取 getelementptr
的詳細描述的鏈接。)在進一步探討之前,清單 4 提供了可行的代碼。
清單 4. 使用 getelementptr 正確地將類型轉換為指針
declare i32 @puts (i8*) @global_str = constant [13 x i8] c"Hello World!\00" define i32 @main() { %temp = getelementptr [13 x i8]* @global_str, i64 0, i64 0 call i32 @puts(i8* %temp) ret i32 0 } |
getelementptr
的第一個參數是全局字符串變量的指針。要單步執行全局變量的指針,則需要使用第一個索引,即 i64 0
。因為getelementptr
指令的第一個參數必須始終是 pointer
類型的值,所以第一個索引會單步調試該指針。0 值表示從該指針起偏移 0 元素偏移量。我的開發計算機運行的是 64 位 Linux®,所以該指針是 8 字節。第二個索引 (i64 0
) 用於選擇字符串的第 0 個元素,該元素是作為 puts
的參數來提供的。
了解 LLVM IR 是件好事,但是您需要一個自動化的代碼生成系統,用它來轉儲 LLVM 匯編語言。謝天謝地,LLVM 提供了強大的應用程序編程接口 (API) 支持,讓您可以查看整個過程(請參閱 參考資料,獲取程序員手冊的鏈接)。在您的開發計算機上查找 LLVMContext.h 文件;如果該文件缺失,那么可能是您安裝 LLVM 的方式出錯。
現在,讓我們創建一個程序,為之前討論的 Hello World 程序生成 LLVM IR。該程序不會處理這里的整個 LLVM API,但是接下來的代碼樣例會證明,適量位數的 LLVM API 很直觀而且易於使用。
LLVM 提供了一款出色的工具,叫做 llvm-config
(參閱 參考資料)。運行 llvm-config –cxxflags
,獲取需要傳遞至 g++ 的編譯標志、鏈接器選項的 llvm-config –ldflags
以及 llvm-config –ldflags
,以便針對正確的 LLVM 庫進行鏈接。在 清單 5的樣例中,所有的選項均需要傳遞至 g++。
清單 5. 通過 LLVM API 使用 llvm-config 構建代碼
tintin# llvm-config --cxxflags --ldflags --libs \ -I/usr/include -DNDEBUG -D_GNU_SOURCE \ -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS \ -D__STDC_LIMIT_MACROS -O3 -fno-exceptions -fno-rtti -fno-common \ -Woverloaded-virtual -Wcast-qual \ -L/usr/lib -lpthread -lm \ -lLLVMXCoreCodeGen -lLLVMTableGen -lLLVMSystemZCodeGen \ -lLLVMSparcCodeGen -lLLVMPTXCodeGen \ -lLLVMPowerPCCodeGen -lLLVMMSP430CodeGen -lLLVMMipsCodeGen \ -lLLVMMCJIT -lLLVMRuntimeDyld \ -lLLVMObject -lLLVMMCDisassembler -lLLVMXCoreDesc -lLLVMXCoreInfo \ -lLLVMSystemZDesc -lLLVMSystemZInfo \ -lLLVMSparcDesc -lLLVMSparcInfo -lLLVMPowerPCDesc -lLLVMPowerPCInfo \ -lLLVMPowerPCAsmPrinter \ -lLLVMPTXDesc -lLLVMPTXInfo -lLLVMPTXAsmPrinter -lLLVMMipsDesc \ -lLLVMMipsInfo -lLLVMMipsAsmPrinter \ -lLLVMMSP430Desc -lLLVMMSP430Info -lLLVMMSP430AsmPrinter \ -lLLVMMBlazeDisassembler -lLLVMMBlazeAsmParser \ -lLLVMMBlazeCodeGen -lLLVMMBlazeDesc -lLLVMMBlazeAsmPrinter \ -lLLVMMBlazeInfo -lLLVMLinker -lLLVMipo \ -lLLVMInterpreter -lLLVMInstrumentation -lLLVMJIT -lLLVMExecutionEngine \ -lLLVMDebugInfo -lLLVMCppBackend \ -lLLVMCppBackendInfo -lLLVMCellSPUCodeGen -lLLVMCellSPUDesc \ -lLLVMCellSPUInfo -lLLVMCBackend \ -lLLVMCBackendInfo -lLLVMBlackfinCodeGen -lLLVMBlackfinDesc \ -lLLVMBlackfinInfo -lLLVMBitWriter \ -lLLVMX86Disassembler -lLLVMX86AsmParser -lLLVMX86CodeGen \ -lLLVMX86Desc -lLLVMX86AsmPrinter -lLLVMX86Utils \ -lLLVMX86Info -lLLVMAsmParser -lLLVMARMDisassembler -lLLVMARMAsmParser \ -lLLVMARMCodeGen -lLLVMARMDesc \ -lLLVMARMAsmPrinter -lLLVMARMInfo -lLLVMArchive -lLLVMBitReader \ -lLLVMAlphaCodeGen -lLLVMSelectionDAG \ -lLLVMAsmPrinter -lLLVMMCParser -lLLVMCodeGen -lLLVMScalarOpts \ -lLLVMInstCombine -lLLVMTransformUtils \ -lLLVMipa -lLLVMAnalysis -lLLVMTarget -lLLVMCore -lLLVMAlphaDesc \ -lLLVMAlphaInfo -lLLVMMC -lLLVMSupport |
LLVM 模塊類是其他所有 LLVM IR 對象的頂級容器。LLVM 模塊類能夠包含全局變量、函數、該模塊所依賴的其他模塊和符號表等對象的列表。這里將提供了 LLVM 模塊的構造函數:
explicit Module(StringRef ModuleID, LLVMContext& C); |
要構建您的程序,必須從創建 LLVM 模塊開始。第一個參數是該模塊的名稱,可以是任何虛擬的字符串。第二個參數稱為LLVMContext
。LLVMContext
類有些晦澀,但用戶足以了解它提供了一個用來創建變量等對象的上下文環境。該類在多線程的上下文環境中變得非常重要,您可能想為每個線程創建一個本地上下文環境,並且想讓每個線程完全獨立於其他上下文環境運行。目前,使用這個默認的全局上下文來處理 LLVM 所提供的代碼。這里給出了創建模塊的代碼:
llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module* module = new llvm::Module("top", context); |
您要了解的下一個重要類是能實際提供 API 來創建 LLVM 指令並將這些指令插入基礎塊的類:IRBuilder
類。IRBuilder
提供了許多華而不實的方法,但是我選擇了最簡單的可行方法來構建一個 LLVM 指令,即使用以下代碼來傳遞全局上下文:
llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module* module = new llvm::Module("top", context); llvm::IRBuilder<> builder(context); |
准備好 LLVM 對象模型后,就可以調用模塊的 dump
方法來轉儲其內容。清單 6 給出了該代碼。
#include "llvm/LLVMContext.h" #include "llvm/Module.h" #include "llvm/Support/IRBuilder.h" int main() { llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module* module = new llvm::Module("top", context); llvm::IRBuilder<> builder(context); module->dump( ); } |
運行 清單 6 中的代碼之后,控制台的輸出如下:
; ModuleID = 'top' |
然后,您需要創建 main
方法。LLVM 提供了 llvm::Function
類來創建一個函數,並提供了 llvm::FunctionType
將該函數與某個返回類型相關聯。此外,請記住,main
方法必須是該模塊的一部分。清單 7 給出了該代碼。
#include "llvm/LLVMContext.h" #include "llvm/Module.h" #include "llvm/Support/IRBuilder.h" int main() { llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module *module = new llvm::Module("top", context); llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getInt32Ty(), false); llvm::Function *mainFunc = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); module->dump( ); } |
請注意,您需要讓 main
返回 void
,這就是您調用 builder.getVoidTy()
的原因;如果 main
返回 i32
,那么該調用會是builder.getInt32Ty()
。在編譯並運行 清單 7 中的代碼后,出現的結果如下:
; ModuleID = 'top' declare void @main() |
您還尚未定義 main
要執行的指令集。為此,您必須定義一個基礎塊並將其與 main
方法關聯。基礎塊 是 LLVM IR 中的一個指令集合,擁有將標簽(類似於 C
標簽)定義為其構造函數的一部分的選項。builder.setInsertPoint
會告知 LLVM 引擎接下來將指令插入何處。清單 8 給出了該代碼。
#include "llvm/LLVMContext.h" #include "llvm/Module.h" #include "llvm/Support/IRBuilder.h" int main() { llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module *module = new llvm::Module("top", context); llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getInt32Ty(), false); llvm::Function *mainFunc = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc); builder.SetInsertPoint(entry); module->dump( ); } |
這里提供了 清單 8 的輸出。請注意,由於現在已經定義了 main
的基礎塊,所以 LLVM 轉儲將 main
看作為是一個方法定義,而不是一個聲明。非常酷!
; ModuleID = 'top' define void @main() { entrypoint: } |
現在,向代碼添加全局 hello-world 字符串。清單 9 給出了該代碼。
#include "llvm/LLVMContext.h" #include "llvm/Module.h" #include "llvm/Support/IRBuilder.h" int main() { llvm::LLVMContext& context = llvm::getGlobalContext(); llvm::Module *module = new llvm::Module("top", context); llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getVoidTy(), false); llvm::Function *mainFunc = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc); builder.SetInsertPoint(entry); llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n"); module->dump( ); } |
在 清單 9 的輸出中,注意 LLVM 引擎是如何轉儲字符串的:
; ModuleID = 'top' @0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00" define void @main() { entrypoint: } |
現在您需要做的就是聲明 puts
方法,並且調用它。要聲明 puts
方法,則必須創建合適的 FunctionType*
。從您的 Hello World 源始代碼中,您知道 puts
返回了 i32
並接受 i8*
作為輸入參數。清單 10 給出了創建 puts
的正確類型的代碼。
std::vector<llvm::Type *> putsArgs; putsArgs.push_back(builder.getInt8Ty()->getPointerTo()); llvm::ArrayRef<llvm::Type*> argsRef(putsArgs); llvm::FunctionType *putsType = llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false); llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType); |
FunctionType::get
的第一個參數是返回類型;第二個參數是一個 LLVM::ArrayRef
結構,並且最后的 false
指明了后面未跟可變數量的參數。ArrayRef
結構與矢量相似,只是它不包含任何基礎數據,並且主要用於包裝諸如陣列和矢量等數據塊。由於這個改變,輸出顯示將如 清單 11 所示。
; ModuleID = 'top' @0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00" define void @main() { entrypoint: } declare i32 @puts(i8*) |
剩下要做的是調用 main
中的 puts
方法,並從 main
中返回。LLVM API 非常關注轉換等操作:您需要做的是調用 puts
來調用builder.CreateCall
。最后,要創建返回語句,請調用 builder.CreateRetVoid
。清單 12 提供了完整的運行代碼。
#include "llvm/ADT/ArrayRef.h" #include "llvm/LLVMContext.h" #include "llvm/Module.h" #include "llvm/Function.h" #include "llvm/BasicBlock.h" #include "llvm/Support/IRBuilder.h" #include <vector> #include <string> int main() { llvm::LLVMContext & context = llvm::getGlobalContext(); llvm::Module *module = new llvm::Module("asdf", context); llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getVoidTy(), false); llvm::Function *mainFunc = llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc); builder.SetInsertPoint(entry); llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n"); std::vector<llvm::Type *> putsArgs; putsArgs.push_back(builder.getInt8Ty()->getPointerTo()); llvm::ArrayRef<llvm::Type*> argsRef(putsArgs); llvm::FunctionType *putsType = llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false); llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType); builder.CreateCall(putsFunc, helloWorld); builder.CreateRetVoid(); module->dump(); } |
在這篇初步了解 LLVM 的文章中,了解了諸如 lli
和 llvm-config
等 LLVM 工具,還深入研究了 LLVM 中間代碼,並使用 LLVM API 來為您自己生成中間代碼。本系列的第二部分(也是最后一部分)將探討可以使用 LLVM 完成的另一項任務,即毫不費力地添加額外的編譯傳遞。
使用 LLVM 框架創建一個工作編譯器,第 1 部分 探討了 LLVM 中間表示 (IR)。您手動創建了一個 “Hello World” 測試程序;了解了 LLVM 的一些細微差別(如類型轉換);並使用 LLVM 應用程序編程接口 (API) 創建了相同的程序。在這一過程中,您還了解到一些 LLVM 工具,如llc
和 lli
,並了解了如何使用 llvm-gcc 為您發出 LLVM IR。本文是系列文章的第二篇也是最后一篇,探討了可以與 LLVM 結合使用的其他一些炫酷功能。具體而言,本文將介紹代碼測試,即向生成的最終可執行的代碼添加信息。本文還簡單介紹了 clang,這是 LLVM 的前端,用於支持 C
、C++
和 Objective-C。您可以使用 clang API 對C/C++
代碼進行預處理並生成一個抽象語法樹 (AST)。
LLVM 以其提供的優化特性而著名。優化被實現為階段 (pass)(有關 LLVM 階段的高級功能的細節,請參見 參考資料)。這里需要注意的是 LLVM 為您提供了使用最少量的代碼創建實用階段 (utility pass) 的功能。例如,如果不希望使用 “hello” 作為函數名稱的開頭,那么可以使用一個實用階段來實現這個目的。
從 opt
的手冊頁中可以看到,“opt
命令是模塊化的 LLVM 優化器和分析器”。一旦您的代碼支持定制階段,您將使用 opt
把代碼編譯為一個共享庫並對其進行加載。如果您的 LLVM 安裝進展順利,那么 opt
應該已經位於您的系統中。opt
命令接受 LLVM IR(擴展名為 .ll)和 LLVM 位碼格式(擴展名為 .bc),可以生成 LLVM IR 或位碼格式的輸出。下面展示了如何使用 opt
加載您的定制共享庫:
tintin# opt –load=mycustom_pass.so –help –S |
還需注意,從命令行運行 opt –help
會生成一個 LLVM 將要執行的階段的細目清單。對 help
使用 load
選項將生成一條幫助消息,其中包括有關定制階段的信息。
您需要在 Pass.h 文件中聲明 LLVM 階段,該文件在我的系統中被安裝到 /usr/include/llvm 下。該文件將各個階段的接口定義為 Pass
類的一部分。各個階段的類型都從 Pass
中派生,也在該文件中進行了聲明。階段類型包括:
BasicBlockPass
類。用於實現本地優化,優化通常每次針對一個基本塊或指令運行FunctionPass
類。用於全局優化,每次執行一個功能ModulePass
類。用於執行任何非結構化的過程間優化
由於您打算創建一個階段,該階段拒絕任何以 “Hello ” 開頭的函數名,因此需要通過從 FunctionPass
派生來創建自己的階段。從 Pass.h 中復制 清單 1 中的代碼。
清單 1. 覆蓋 FunctionPass 中的 runOnFunction 類
Class FunctionPass : public Pass { /// explicit FunctionPass(char &pid) : Pass(PT_Function, pid) {} /// runOnFunction - Virtual method overridden by subclasses to do the /// per-function processing of the pass. /// virtual bool runOnFunction(Function &F) = 0; /// … }; |
同樣,BasicBlockPass
類聲明了一個 runOnBasicBlock
,而 ModulePass
類聲明了 runOnModule
純虛擬方法。子類需要為虛擬方法提供一個定義。
返回到 清單 1 中的 runOnFunction
方法,您將看到輸出為類型 Function
的對象。深入鑽研 /usr/include/llvm/Function.h 文件,就會很容易發現 LLVM 使用 Function
類封裝了一個 C/C++
函數的功能。而 Function
派生自 Value.h 中定義的 Value
類,並支持getName
方法。清單 2 顯示了代碼。
#include "llvm/Pass.h" #include "llvm/Function.h" class TestClass : public llvm::FunctionPass { public: virtual bool runOnFunction(llvm::Function &F) { if (F.getName().startswith("hello")) { std::cout << "Function name starts with hello\n"; } return false; } }; |
清單 2 中的代碼遺漏了兩個重要的細節:
FunctionPass
構造函數需要一個char
,用於在 LLVM 內部使用。LLVM 使用char
的地址,因此您可以使用任何內容對它進行初始化。- 您需要通過某種方式讓 LLVM 系統理解您所創建的類是一個新階段。這正是
RegisterPass
LLVM 模板發揮作用的地方。您在 PassSupport.h 頭文件中聲明了RegisterPass
模板;該文件包含在 Pass.h 中,因此無需額外的標頭。
清單 3 展示了完整的代碼。
class TestClass : public llvm::FunctionPass { public: TestClass() : llvm::FunctionPass(TestClass::ID) { } virtual bool runOnFunction(llvm::Function &F) { if (F.getName().startswith("hello")) { std::cout << "Function name starts with hello\n"; } return false; } static char ID; // could be a global too }; char TestClass::ID = 'a'; static llvm::RegisterPass<TestClass> global_("test_llvm", "test llvm", false, false); |
RegisterPass
模板中的參數 template
是將要在命令行中與 opt
一起使用的階段的名稱。也就是說,您現在所需做的就是在 清單 3 中的代碼之外創建一個共享庫,然后運行 opt
來加載該庫,之后是使用 RegisterPass
注冊的命令的名稱(在本例中為test_llvm
),最后是一個位碼文件,您的定制階段將在該文件中與其他階段一起運行。清單 4 中概述了這些步驟。
bash$ g++ -c pass.cpp -I/usr/local/include `llvm-config --cxxflags` bash$ g++ -shared -o pass.so pass.o -L/usr/local/lib `llvm-config --ldflags -libs` bash$ opt -load=./pass.so –test_llvm < test.bc |
現在讓我們了解另一個工具(LLVM 后端的前端):clang。
LLVM 擁有自己的前端:名為 clang 的一種工具(恰如其分)。Clang 是一種功能強大的 C/C++
/Objective-C 編譯器,其編譯速度可以媲美甚至超過 GNU Compiler Collection (GCC) 工具(參見 參考資料 中的鏈接,獲取更多信息)。更重要的是,clang 擁有一個可修改的代碼基,可以輕松實現定制擴展。與在 使用 LLVM 框架創建一個工作編譯器,第 1 部分 中對定制插件使用 LLVM 后端 API 的方式非常類似,本文將對 LLVM 前端使用該 API 並開發一些小的應用程序來實現預處理和解析功能。
您需要熟悉一些最常見的 clang 類:
CompilerInstance
Preprocessor
FileManager
SourceManager
DiagnosticsEngine
LangOptions
TargetInfo
ASTConsumer
Sema
ParseAST
也許是最重要的 clang 方法。
稍后將詳細介紹 ParseAST
方法。
要實現所有實用的用途,考慮使用適當的 CompilerInstance
編譯器。它提供了接口,管理對 AST 的訪問,對輸入源進行預處理,而且維護目標信息。典型的應用程序需要創建 CompilerInstance
對象來完成有用的功能。清單 5 展示了 CompilerInstance.h 頭文件的大致內容。
class CompilerInstance : public ModuleLoader { /// The options used in this compiler instance. llvm::IntrusiveRefCntPtr<CompilerInvocation> Invocation; /// The diagnostics engine instance. llvm::IntrusiveRefCntPtr<DiagnosticsEngine> Diagnostics; /// The target being compiled for. llvm::IntrusiveRefCntPtr<TargetInfo> Target; /// The file manager. llvm::IntrusiveRefCntPtr<FileManager> FileMgr; /// The source manager. llvm::IntrusiveRefCntPtr<SourceManager> SourceMgr; /// The preprocessor. llvm::IntrusiveRefCntPtr<Preprocessor> PP; /// The AST context. llvm::IntrusiveRefCntPtr<ASTContext> Context; /// The AST consumer. OwningPtr<ASTConsumer> Consumer; /// \brief The semantic analysis object. OwningPtr<Sema> TheSema; //… the list continues }; |
在 clang 中,至少可以使用兩種方法創建一個預處理器對象:
- 直接實例化一個
Preprocessor
對象 - 使用
CompilerInstance
類創建一個Preprocessor
對象
讓我們首先使用后一種方法。
單獨使用 Preprocessor
不會有太大的幫助:您需要 FileManager
和 SourceManager
類來讀取文件並跟蹤源位置,實現故障診斷。FileManager
類支持文件系統查找、文件系統緩存和目錄搜索。查看 FileEntry
類,它為一個源文件定義了 clang 抽象。清單 6 提供了 FileManager.h 頭文件的一個摘要。
class FileManager : public llvm::RefCountedBase<FileManager> { FileSystemOptions FileSystemOpts; /// \brief The virtual directories that we have allocated. For each /// virtual file (e.g. foo/bar/baz.cpp), we add all of its parent /// directories (foo/ and foo/bar/) here. SmallVector<DirectoryEntry*, 4> VirtualDirectoryEntries; /// \brief The virtual files that we have allocated. SmallVector<FileEntry*, 4> VirtualFileEntries; /// NextFileUID - Each FileEntry we create is assigned a unique ID #. unsigned NextFileUID; // Statistics. unsigned NumDirLookups, NumFileLookups; unsigned NumDirCacheMisses, NumFileCacheMisses; // … // Caching. OwningPtr<FileSystemStatCache> StatCache; |
SourceManager
類通常用來查詢 SourceLocation
對象。在 SourceManager.h 頭文件中,清單 7 提供了有關 SourceLocation
對象的信息。
/// There are three different types of locations in a file: a spelling /// location, an expansion location, and a presumed location. /// /// Given an example of: /// #define min(x, y) x < y ? x : y /// /// and then later on a use of min: /// #line 17 /// return min(a, b); /// /// The expansion location is the line in the source code where the macro /// was expanded (the return statement), the spelling location is the /// location in the source where the macro was originally defined, /// and the presumed location is where the line directive states that /// the line is 17, or any other line. |
很明顯,SourceManager
取決於底層的 FileManager
;事實上,SourceManager
類構造函數接受一個 FileManager
類作為輸入參數。最后,您需要跟蹤處理源代碼時可能出現的錯誤並進行報告。您可以使用 DiagnosticsEngine
類完成這項工作。和Preprocessor
一樣,您有兩個選擇:
- 獨立創建所有必需的對象
- 使用
CompilerInstance
完成所有工作
讓我們使用后一種方法。清單 8 顯示了 Preprocessor
的代碼;其他任何事情之前已經解釋過了。
using namespace clang; int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); // create DiagnosticsEngine ci.createFileManager(); // create FileManager ci.createSourceManager(ci.getFileManager()); // create SourceManager ci.createPreprocessor(); // create Preprocessor const FileEntry *pFile = ci.getFileManager().getFile("hello.c"); ci.getSourceManager().createMainFileID(pFile); ci.getPreprocessor().EnterMainSourceFile(); ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), &ci.getPreprocessor()); Token tok; do { ci.getPreprocessor().Lex(tok); if( ci.getDiagnostics().hasErrorOccurred()) break; ci.getPreprocessor().DumpToken(tok); std::cerr << std::endl; } while ( tok.isNot(clang::tok::eof)); ci.getDiagnosticClient().EndSourceFile(); } |
清單 8 使用 CompilerInstance
類依次創建 DiagnosticsEngine
(ci.createDiagnostics
方法調用)和FileManager
(ci.createFileManager
和 ci.CreateSourceManager
)。使用 FileEntry
完成文件關聯后,繼續處理源文件中的每個令牌,直到達到文件的末尾 (EOF)。預處理器的 DumpToken
方法將把令牌轉儲到屏幕中。
要編譯並運行 清單 8 中的代碼,使用 清單 9 中的 makefile(針對您的 clang 和 LLVM 安裝文件夾進行了相應調整)。主要想法是使用 llvm-config 工具提供任何必需的 LLVM(包含路徑和庫):您永遠不應嘗試將這些鏈接傳遞到 g++ 命令行。
CXX := g++ RTTIFLAG := -fno-rtti CXXFLAGS := $(shell llvm-config --cxxflags) $(RTTIFLAG) LLVMLDFLAGS := $(shell llvm-config --ldflags --libs) DDD := $(shell echo $(LLVMLDFLAGS)) SOURCES = main.cpp OBJECTS = $(SOURCES:.cpp=.o) EXES = $(OBJECTS:.o=) CLANGLIBS = \ -L /usr/local/lib \ -lclangFrontend \ -lclangParse \ -lclangSema \ -lclangAnalysis \ -lclangAST \ -lclangLex \ -lclangBasic \ -lclangDriver \ -lclangSerialization \ -lLLVMMC \ -lLLVMSupport \ all: $(OBJECTS) $(EXES) %: %.o $(CXX) -o $@ $< $(CLANGLIBS) $(LLVMLDFLAGS) |
編譯並運行以上代碼后,您應當獲得 清單 10 中的輸出。
Assertion failed: (Target && "Compiler instance has no target!"), function getTarget, file /Users/Arpan/llvm/tools/clang/lib/Frontend/../.. /include/clang/Frontend/CompilerInstance.h, line 294. Abort trap: 6 |
在這里,您遺漏了 CompilerInstance
設置的最后一部分:即編譯代碼所針對的目標平台。這里是 TargetInfo
和TargetOptions
類發揮作用的地方。根據 clang 標頭 TargetInfo.h,TargetInfo
類存儲有關代碼生成的目標系統的所需信息,並且必須在編譯或預處理之前創建。和預期的一樣,TargetInfo
包含有關整數和浮動寬度、對齊等信息。清單 11 提供了 TargetInfo.h 頭文件的摘要。
class TargetInfo : public llvm::RefCountedBase<TargetInfo> { llvm::Triple Triple; protected: bool BigEndian; unsigned char PointerWidth, PointerAlign; unsigned char IntWidth, IntAlign; unsigned char HalfWidth, HalfAlign; unsigned char FloatWidth, FloatAlign; unsigned char DoubleWidth, DoubleAlign; unsigned char LongDoubleWidth, LongDoubleAlign; // … |
TargetInfo
類使用兩個參數實現初始化:DiagnosticsEngine
和 TargetOptions
。在這兩個參數中,對於當前平台,后者必須將 Triple
字符串設置為相應的值。LLVM 此時將發揮作用。清單 12 顯示了對 清單 9 所附加的可以使預處理器工作的內容。
int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); // create TargetOptions TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); // create TargetInfo TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to); ci.setTarget(pti); // rest of the code same as in Listing 9… ci.createFileManager(); // … |
就這么簡單。運行代碼並觀察簡單的 hello.c 測試的輸出:
#include <stdio.h> int main() { printf("hello world!\n"); } |
清單 13 展示了部分預處理器輸出。
typedef 'typedef' struct 'struct' identifier '__va_list_tag' l_brace '{' unsigned 'unsigned' identifier 'gp_offset' semi ';' unsigned 'unsigned' identifier 'fp_offset' semi ';' void 'void' star '*' identifier 'overflow_arg_area' semi ';' void 'void' star '*' identifier 'reg_save_area' semi ';' r_brace '}' identifier '__va_list_tag' semi ';' identifier '__va_list_tag' identifier '__builtin_va_list' l_square '[' numeric_constant '1' r_square ']' semi ';' |
clang 庫的其中一個優點,就是您可以通過多種方法實現相同的效果。在本節中,您將創建一個 Preprocessor
對象,但是不需要直接向 CompilerInstance
發出請求。從 Preprocessor.h 頭文件中,清單 14 顯示了 Preprocessor
的構造函數。
Preprocessor(DiagnosticsEngine &diags, LangOptions &opts, const TargetInfo *target, SourceManager &SM, HeaderSearch &Headers, ModuleLoader &TheModuleLoader, IdentifierInfoLookup *IILookup = 0, bool OwnsHeaderSearch = false, bool DelayInitialization = false); |
查看該構造函數,顯然,要想讓這個預處理器工作,您還需要創建 6 個不同的對象。您已經了解了DiagnosticsEngine
、TargetInfo
和 SourceManager
。CompilerInstance
派生自 ModuleLoader
。因此您必須創建兩個新的對象,一個用於 LangOptions
,另一個用於 HeaderSearch
。LangOptions
類使您編譯一組 C/C++
方言,包括 C99
、C11
和C++0x
。參考 LangOptions.h 和 LangOptions.def 標頭,獲取更多信息。最后,HeaderSearch
類存儲目錄的 std::vector
,用於在其他對象中搜索功能。清單 15 顯示了 Preprocessor
的代碼。
using namespace clang; int main() { DiagnosticOptions diagnosticOptions; TextDiagnosticPrinter *printer = new TextDiagnosticPrinter(llvm::outs(), diagnosticOptions); llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagIDs; DiagnosticsEngine diagnostics(diagIDs, printer); LangOptions langOpts; clang::TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); TargetInfo *pti = TargetInfo::CreateTargetInfo(diagnostics, to); FileSystemOptions fsopts; FileManager fileManager(fsopts); SourceManager sourceManager(diagnostics, fileManager); HeaderSearch headerSearch(fileManager, diagnostics, langOpts, pti); CompilerInstance ci; Preprocessor preprocessor(diagnostics, langOpts, pti, sourceManager, headerSearch, ci); const FileEntry *pFile = fileManager.getFile("test.c"); sourceManager.createMainFileID(pFile); preprocessor.EnterMainSourceFile(); printer->BeginSourceFile(langOpts, &preprocessor); // … similar to Listing 8 here on } |
對於 清單 15 中的代碼,需要注意以下幾點:
- 您沒有初始化
HeaderSearch
並使它指向任何特定的目錄。但是您應當這樣做。 - clang API 要求在堆 (heap) 上分配
TextDiagnosticPrinter
。在棧 (stack) 上分配會引起崩潰。 - 您還不能處理掉
CompilerInstance
。總之是因為您正在使用CompilerInstance
,那么為什么還要費心去手動創建它而不是更舒適地使用 clang API 呢?
您目前為止一直使用的是 C
測試代碼:那么使用一些 C++
代碼如何?向 清單 15 中的代碼添加 langOpts.CPlusPlus = 1;
,然后嘗試使用 清單 16 中的測試代碼。
template <typename T, int n> struct s { T array[n]; }; int main() { s<int, 20> var; } |
清單 17 展示了程序的部分輸出。
identifier 'template' less '<' identifier 'typename' identifier 'T' comma ',' int 'int' identifier 'n' greater '>' struct 'struct' identifier 's' l_brace '{' identifier 'T' identifier 'array' l_square '[' identifier 'n' r_square ']' semi ';' r_brace '}' semi ';' int 'int' identifier 'main' l_paren '(' r_paren ')' |
clang/Parse/ParseAST.h 中定義的 ParseAST
方法是 clang 提供的重要方法之一。以下是從 ParseAST.h 復制的一個例程聲明:
void ParseAST(Preprocessor &pp, ASTConsumer *C, ASTContext &Ctx, bool PrintStats = false, TranslationUnitKind TUKind = TU_Complete, CodeCompleteConsumer *CompletionConsumer = 0); |
ASTConsumer
為您提供了一個抽象接口,可以從該接口進行派生。這樣做非常合適,因為不同的客戶端很可能通過不同的方式轉儲或處理 AST。您的客戶端代碼將派生自 ASTConsumer
。ASTContext
類存儲有關類型聲明的信息和其他信息。最簡單的嘗試就是使用 clang ASTConsumer API 在您的代碼中輸出一個全局變量列表。許多技術公司就全局變量在 C++
代碼中的使用有非常嚴格的要求,這應當作為創建定制 lint 工具的出發點。清單 18 中提供了定制 consumer 的代碼。
class CustomASTConsumer : public ASTConsumer { public: CustomASTConsumer () : ASTConsumer() { } virtual ~ CustomASTConsumer () { } virtual bool HandleTopLevelDecl(DeclGroupRef decls) { clang::DeclGroupRef::iterator it; for( it = decls.begin(); it != decls.end(); it++) { clang::VarDecl *vd = llvm::dyn_cast<clang::VarDecl>(*it); if(vd) std::cout << vd->getDeclName().getAsString() << std::endl;; } return true; } }; |
您將使用自己的版本覆蓋 HandleTopLevelDecl
方法(最初在 ASTConsumer
中提供)。Clang 將全局變量列表傳遞給您;您對該列表進行迭代並輸出變量名稱。清單 19 摘錄自 ASTConsumer.h,顯示了客戶端 consumer 代碼可以覆蓋的一些其他方法。
/// HandleInterestingDecl - Handle the specified interesting declaration. This /// is called by the AST reader when deserializing things that might interest /// the consumer. The default implementation forwards to HandleTopLevelDecl. virtual void HandleInterestingDecl(DeclGroupRef D); /// HandleTranslationUnit - This method is called when the ASTs for entire /// translation unit have been parsed. virtual void HandleTranslationUnit(ASTContext &Ctx) {} /// HandleTagDeclDefinition - This callback is invoked each time a TagDecl /// (e.g. struct, union, enum, class) is completed. This allows the client to /// hack on the type, which can occur at any point in the file (because these /// can be defined in declspecs). virtual void HandleTagDeclDefinition(TagDecl *D) {} /// Note that at this point it does not have a body, its body is /// instantiated at the end of the translation unit and passed to /// HandleTopLevelDecl. virtual void HandleCXXImplicitFunctionInstantiation(FunctionDecl *D) {} |
最后,清單 20 顯示了您開發的定制 AST consumer 類的實際客戶端代碼。
清單 20. 使用定制 AST consumer 的客戶端代碼
int main() { CompilerInstance ci; ci.createDiagnostics(0,NULL); TargetOptions to; to.Triple = llvm::sys::getDefaultTargetTriple(); TargetInfo *tin = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to); ci.setTarget(tin); ci.createFileManager(); ci.createSourceManager(ci.getFileManager()); ci.createPreprocessor(); ci.createASTContext(); CustomASTConsumer *astConsumer = new CustomASTConsumer (); ci.setASTConsumer(astConsumer); const FileEntry *file = ci.getFileManager().getFile("hello.c"); ci.getSourceManager().createMainFileID(file); ci.getDiagnosticClient().BeginSourceFile( ci.getLangOpts(), &ci.getPreprocessor()); clang::ParseAST(ci.getPreprocessor(), astConsumer, ci.getASTContext()); ci.getDiagnosticClient().EndSourceFile(); return 0; } |
這篇兩部分的系列文章涵蓋了大量內容:它探討了 LLVM IR,提供了通過手動創建和 LLVM API 生成 IR 的方法,展示了如何為 LLVM 后端創建一個定制插件,以及解釋了 LLVM 前端及其豐富的標頭集。您還了解了如何使用該前端進行預處理和使用 AST。在計算史上,創建一個編譯器並進行擴展,特別是針對 C++
等復雜的語言,看上去是個非常復雜的過程,但是有了 LLVM,一切都變得非常簡單。文檔工作是 LLVM 和 clang 需要繼續加強的部分,但是在此之前,我建議嘗試 VIM/doxygen 來瀏覽這些標頭。祝您使用愉快!
學習
- 在 使用 LLVM 框架創建一個工作編譯器,第 1 部分(Arpan Sen,developerWorks,2012 年 6 月)中了解 LLVM 的基礎知識。使用功能強大的 LLVM 編譯器基礎架構優化用任何語言編寫的應用程序。構建一個定制編譯器現在變得非常簡單!
- 了解有關 LLVM 階段 的更多信息。
- 獲取 clang 開發人員郵件列表。
- 閱讀 Getting Started: Building and Running Clang,獲取有關構建和安裝 clang 的詳細信息。
- 參加 官方 LLVM 教程,獲取有關 LLVM 的出色介紹。
- 深入鑽研 LLVM Programmer's Manual,這是了解 LLVM API 的必不可少的資源。
- 在 developerWorks 中國網站 Linux 技術專區,查找數百篇 how-to 文章和教程,以及下載、討論論壇和面向 Linux 開發人員和管理員的其他豐富資源。
- developerWorks 中國網站 Web 開發專區 提供了涵蓋各種基於 Web 的解決方案的文章。
- 查看 developerWorks 演示中心,包括面向初學者的產品安裝和設置演示,以及為經驗豐富的開發人員提供的高級功能。
- 在 Twitter 上關注 developerWorks。
- 隨時關注 developerWorks 技術活動和網絡廣播。
- 訪問 developerWorks Open source 專區獲得豐富的 how-to 信息、工具和項目更新以及最受歡迎的文章和教程,幫助您用開放源碼技術進行開發,並將它們與 IBM 產品結合使用。
獲得產品和技術
- 訪問 LLVM 項目站點 並下載 最新版本。
- 從 LLVM 站點查找有關 clang 的詳細信息。
- IBM 產品評估試用版軟件:下載產品試用版,在線試用產品,在雲環境中使用產品,或在 IBM SOA 人員沙箱 中花幾個小時了解如何高效實現面向服務的架構。
討論
- 加入 developerWorks 中文社區,developerWorks 社區是一個面向全球 IT 專業人員,可以提供博客、書簽、wiki、群組、聯系、共享和協作等社區功能的專業社交網絡社區。
- 加入 IBM 軟件下載與技術交流群組,參與在線交流。