[轉]使用 LLVM 框架創建一個工作編譯器


原文:http://www.ibm.com/developerworks/cn/opensource/os-createcompilerllvm1/

 

LLVM(之前稱為低級虛擬機)是一種非常強大的編譯器基礎架構框架,專門為使用您喜愛的編程語言編寫的程序的編譯時、鏈接時和運行時優化而設計。LLVM 可運行於若干個不同的平台之上,它以能夠生成快速運行的代碼而著稱。

LLVM 框架是圍繞着代碼編寫良好的中間表示 (IR) 而構建的。本文(由兩部分組成的系列文章的第一部分)將深入講解 LLVM IR 的基礎知識以及它的一些微妙之處。在這里,您將構建一個可以自動為您生成 LLVM IR 的代碼生成器。擁有一個 LLVM IR 生成器意味着您所需要的是一個前端以供插入您所喜愛的編程語言,而且這還意味着您擁有一個完整的流程(前端解析器 + IR 生成器 + LLVM 后端)。創建一個自定義編譯器會變得更加簡單。

開始使用 LLVM

在開始之前,在您的開發計算器上必須已經擁有已編譯好的 LLVM(參閱 參考資料 獲取相關鏈接)。本文中的示例均基於 LLVM V3.0。對於 LLVM 代碼的后期生成和安裝,最重要的兩個工具是 llc 和 lli

llc 和 lli

因為 LLVM 是一個虛擬機,所以它可能應該擁有自己的中間字節代碼表示,不是嗎?最后,您需要將 LLVM 字節代碼編譯到特定於平台的匯編語言中。然后您才能通過本機匯編程序和鏈接器來運行匯編代碼,從而生成可執行的共享庫等。您可以使用 llc 將 LLVM 字節代碼轉換成特定於平台的匯編代碼(請參閱 參考資料,獲取關於此工具的更多信息的鏈接)。對於 LLVM 字節代碼的直接執行部分,不要等到在本機執行代碼崩潰后才發現您的程序中有一個或兩個 bug。這正是 lli 的用武之地,因為它可以直接執行字節代碼。lli 可以通過解釋器或使用高級選項中的即時 (JIT) 編譯器執行此工作。請參閱 參考資料,獲取關於 lli 的更多信息的鏈接。

llvm-gcc

llvm-gcc 是 GNU Compiler Collection (gcc) 的修改版本,可以在使用 -S -emit-llvm 選項運行時會生成 LLVM 字節代碼。然后您可以使用 lli 來執行這個已生成的字節代碼(也稱為 LLVM 匯編語言)。有關 llvm-gcc 的更多信息,請參閱 參考資料。如果您沒有在自己的系統中預先安裝 llvm-gcc,那么您應該能夠從源代碼構建它,請參閱 參考資料,獲取分步指南的鏈接。

 

使用 LLVM 編寫 Hello World

要更好地理解 LLVM,您必須了解 LLVM IR 及其微妙之處。這個過程類似於學習另一種編程語言。但是,如果您熟悉 C 語言和 C++語言以及它們的一些語法怪現象,那么在了解 LLVM IR 方面您應該沒有太大的障礙。清單 1 給出了您的第一個程序,該程序將在控制台輸出中打印 "Hello World"。要編譯此代碼,您可以使用 llvm-gcc。


清單 1. 看起來非常熟悉的 Hello World 程序

				
#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 IR

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 方法的最簡單聲明,它是 printfdeclare 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 IR 是件好事,但是您需要一個自動化的代碼生成系統,用它來轉儲 LLVM 匯編語言。謝天謝地,LLVM 提供了強大的應用程序編程接口 (API) 支持,讓您可以查看整個過程(請參閱 參考資料,獲取程序員手冊的鏈接)。在您的開發計算機上查找 LLVMContext.h 文件;如果該文件缺失,那么可能是您安裝 LLVM 的方式出錯。

現在,讓我們創建一個程序,為之前討論的 Hello World 程序生成 LLVM IR。該程序不會處理這里的整個 LLVM API,但是接下來的代碼樣例會證明,適量位數的 LLVM API 很直觀而且易於使用。

針對 LLVM 代碼的鏈接

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 模塊類是其他所有 LLVM IR 對象的頂級容器。LLVM 模塊類能夠包含全局變量、函數、該模塊所依賴的其他模塊和符號表等對象的列表。這里將提供了 LLVM 模塊的構造函數:

explicit Module(StringRef ModuleID, LLVMContext& C);

 

要構建您的程序,必須從創建 LLVM 模塊開始。第一個參數是該模塊的名稱,可以是任何虛擬的字符串。第二個參數稱為LLVMContextLLVMContext 類有些晦澀,但用戶足以了解它提供了一個用來創建變量等對象的上下文環境。該類在多線程的上下文環境中變得非常重要,您可能想為每個線程創建一個本地上下文環境,並且想讓每個線程完全獨立於其他上下文環境運行。目前,使用這個默認的全局上下文來處理 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 給出了該代碼。


清單 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 給出了該代碼。


清單 7. 將 main 方法添加至頂部模塊

				
#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 給出了該代碼。


清單 8. 向 main 添加一個基礎塊

				
#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 給出了該代碼。


清單 9. 向 LLVM 模塊添加全局字符串

				
#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 的正確類型的代碼。


清單 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 所示。


清單 11. 聲明 puts 方法

				
; 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 提供了完整的運行代碼。


清單 12. 輸出 Hello World 的完整代碼

				
#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 的前端,用於支持 CC++ 和 Objective-C。您可以使用 clang API 對C/C++ 代碼進行預處理並生成一個抽象語法樹 (AST)。

LLVM 階段

LLVM 以其提供的優化特性而著名。優化被實現為階段 (pass)(有關 LLVM 階段的高級功能的細節,請參見 參考資料)。這里需要注意的是 LLVM 為您提供了使用最少量的代碼創建實用階段 (utility pass) 的功能。例如,如果不希望使用 “hello” 作為函數名稱的開頭,那么可以使用一個實用階段來實現這個目的。

了解 LLVM opt 工具

從 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 選項將生成一條幫助消息,其中包括有關定制階段的信息。

創建定制的 LLVM 階段

您需要在 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 顯示了代碼。


清單 2. 創建一個定制 LLVM 階段

				
#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 展示了完整的代碼。


清單 3. 注冊 LLVM Function 階段

				
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 中概述了這些步驟。


清單 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。

 

clang 簡介

開始之前的注意事項

Clang 目前正在開發階段,和相同規模的任何項目一樣,項目文檔通常要在代碼基完成之后才能就緒。因此,最好的方法是查看開發人員郵件列表(參見 參考資料 中的鏈接)。您可能希望構建並安裝 clang 源,為此,您需要執行 clang 起步指南(參見 參考資料)中的說明。注意,要安裝到默認的系統文件夾,您需要在構建完成后發出make install 命令。本文后面的內容將假設 clang 標頭和庫分別位於類似於 /usr/local/include 和 /usr/local/lib 的系統文件夾中。

LLVM 擁有自己的前端:名為 clang 的一種工具(恰如其分)。Clang 是一種功能強大的 C/C++/Objective-C 編譯器,其編譯速度可以媲美甚至超過 GNU Compiler Collection (GCC) 工具(參見 參考資料 中的鏈接,獲取更多信息)。更重要的是,clang 擁有一個可修改的代碼基,可以輕松實現定制擴展。與在 使用 LLVM 框架創建一個工作編譯器,第 1 部分 中對定制插件使用 LLVM 后端 API 的方式非常類似,本文將對 LLVM 前端使用該 API 並開發一些小的應用程序來實現預處理和解析功能。

常見的 clang 類

您需要熟悉一些最常見的 clang 類:

  • CompilerInstance
  • Preprocessor
  • FileManager
  • SourceManager
  • DiagnosticsEngine
  • LangOptions
  • TargetInfo
  • ASTConsumer
  • Sema
  • ParseAST 也許是最重要的 clang 方法。

稍后將詳細介紹 ParseAST 方法。

要實現所有實用的用途,考慮使用適當的 CompilerInstance 編譯器。它提供了接口,管理對 AST 的訪問,對輸入源進行預處理,而且維護目標信息。典型的應用程序需要創建 CompilerInstance 對象來完成有用的功能。清單 5 展示了 CompilerInstance.h 頭文件的大致內容。


清單 5. CompilerInstance 類

				
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
};

 

 

預處理 C 文件

在 clang 中,至少可以使用兩種方法創建一個預處理器對象:

  • 直接實例化一個 Preprocessor 對象
  • 使用 CompilerInstance 類創建一個 Preprocessor 對象

讓我們首先使用后一種方法。

使用 Helper 和實用工具類實現預處理功能

單獨使用 Preprocessor 不會有太大的幫助:您需要 FileManager 和 SourceManager 類來讀取文件並跟蹤源位置,實現故障診斷。FileManager 類支持文件系統查找、文件系統緩存和目錄搜索。查看 FileEntry 類,它為一個源文件定義了 clang 抽象。清單 6 提供了 FileManager.h 頭文件的一個摘要。


清單 6. clang FileManager 類

				
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對象的信息。


清單 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 的代碼;其他任何事情之前已經解釋過了。


清單 8. 使用 clang API 創建一個預處理器

				
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 類依次創建 DiagnosticsEngineci.createDiagnostics 方法調用)和FileManagerci.createFileManager 和 ci.CreateSourceManager)。使用 FileEntry 完成文件關聯后,繼續處理源文件中的每個令牌,直到達到文件的末尾 (EOF)。預處理器的 DumpToken 方法將把令牌轉儲到屏幕中。

要編譯並運行 清單 8 中的代碼,使用 清單 9 中的 makefile(針對您的 clang 和 LLVM 安裝文件夾進行了相應調整)。主要想法是使用 llvm-config 工具提供任何必需的 LLVM(包含路徑和庫):您永遠不應嘗試將這些鏈接傳遞到 g++ 命令行。


清單 9. 用於構建預處理器代碼的 Makefile

				
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 中的輸出。


清單 10. 運行清單 7 中的代碼時發生崩潰

				
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 頭文件的摘要。


清單 11. Clang TargetInfo 類

				
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 所附加的可以使預處理器工作的內容。


清單 12. 為編譯器設置目標選項

				
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 展示了部分預處理器輸出。


清單 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 ';'

 

手動創建一個 Preprocessor 對象

clang 庫的其中一個優點,就是您可以通過多種方法實現相同的效果。在本節中,您將創建一個 Preprocessor 對象,但是不需要直接向 CompilerInstance 發出請求。從 Preprocessor.h 頭文件中,清單 14 顯示了 Preprocessor 的構造函數。


清單 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 個不同的對象。您已經了解了DiagnosticsEngineTargetInfo 和 SourceManagerCompilerInstance 派生自 ModuleLoader。因此您必須創建兩個新的對象,一個用於 LangOptions,另一個用於 HeaderSearchLangOptions 類使您編譯一組 C/C++ 方言,包括 C99C11 和C++0x。參考 LangOptions.h 和 LangOptions.def 標頭,獲取更多信息。最后,HeaderSearch 類存儲目錄的 std::vector,用於在其他對象中搜索功能。清單 15 顯示了 Preprocessor 的代碼。


清單 15. 手動創建的預處理器

				
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 測試代碼:那么使用一些 C++ 代碼如何?向 清單 15 中的代碼添加 langOpts.CPlusPlus = 1;,然后嘗試使用 清單 16 中的測試代碼。


清單 16. 對預處理器使用 C++ 測試代碼

				
template <typename T, int n>
struct s { 
  T array[n];
};
int main() {
  s<int, 20> var;
}

 

清單 17 展示了程序的部分輸出。


清單 17. 清單 16 中代碼的部分預處理器輸出

				
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。您的客戶端代碼將派生自 ASTConsumerASTContext 類存儲有關類型聲明的信息和其他信息。最簡單的嘗試就是使用 clang ASTConsumer API 在您的代碼中輸出一個全局變量列表。許多技術公司就全局變量在 C++ 代碼中的使用有非常嚴格的要求,這應當作為創建定制 lint 工具的出發點。清單 18 中提供了定制 consumer 的代碼。


清單 18. 定制 AST 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 代碼可以覆蓋的一些其他方法。


清單 19. 其他一些可以在客戶端代碼中覆蓋的方法

				
/// 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 框架創建有效的編譯器 系列中的其他文章。

這篇兩部分的系列文章涵蓋了大量內容:它探討了 LLVM IR,提供了通過手動創建和 LLVM API 生成 IR 的方法,展示了如何為 LLVM 后端創建一個定制插件,以及解釋了 LLVM 前端及其豐富的標頭集。您還了解了如何使用該前端進行預處理和使用 AST。在計算史上,創建一個編譯器並進行擴展,特別是針對 C++ 等復雜的語言,看上去是個非常復雜的過程,但是有了 LLVM,一切都變得非常簡單。文檔工作是 LLVM 和 clang 需要繼續加強的部分,但是在此之前,我建議嘗試 VIM/doxygen 來瀏覽這些標頭。祝您使用愉快!

 

參考資料

學習

獲得產品和技術

討論

關於作者

Arpan Sen 是致力於電子設計自動化行業的軟件開發首席工程師。他使用各種 UNIX 版本(包括 Solaris、SunOS、HP-UX 和 IRIX)以及 Linux 和 Microsoft Windows 已經多年。他熱衷於各種軟件性能優化技術、圖論和並行計算。Arpan 獲得了軟件系統的碩士學位。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM