常見問題 (FAQ)¶
授權條款¶
我可以修改 LLVM 原始碼並重新發布修改後的原始碼嗎?¶
是的。修改後的原始碼發布必須保留版權聲明,並遵守 Apache License v2.0 with LLVM Exceptions 中列出的條件。
我可以修改 LLVM 原始碼並在不重新發布原始碼的情況下,重新發布基於它的二進制文件或其他工具嗎?¶
是的。這就是為什麼我們在比 GPL 限制更少的授權下發布 LLVM 的原因,如第一個問題所述。
我可以使用 AI 編碼工具,例如 GitHub Copilot,來撰寫 LLVM 修補程式嗎?¶
是的,只要最終作品可以根據專案授權條款授權,如 LLVM 開發者政策 中所述。使用 AI 工具重製受版權保護的作品並不會消除其版權,也不會授予您重新授權的權利。
原始碼¶
LLVM 是用什麼語言寫成的?¶
所有 LLVM 工具和函式庫都是用 C++ 編寫的,並大量使用了 STL。
LLVM 原始碼的可移植性如何?¶
LLVM 原始碼應可移植到大多數現代類 Unix 作業系統。LLVM 在 Windows 系統上也具有出色的支援。大多數程式碼是用標準 C++ 編寫的,作業系統服務被抽象化到一個支援函式庫。構建和測試 LLVM 所需的工具已被移植到許多平台。
我該使用哪個 API 將值儲存到 LLVM IR 的 SSA 表示法中的虛擬暫存器之一?¶
簡而言之:你不能。一旦你理解了正在發生的事情,這實際上是一個有點愚蠢的問題。基本上,在像這樣的程式碼中:
%result = add i32 %foo, %bar
, %result
只是賦予 add
指令的 Value
的名稱。換句話說,%result
*就是* add 指令。「賦值」並不明確地將任何東西「儲存」到任何「虛擬暫存器」;「=
」更像是數學意義上的相等。
更長的解釋:為了產生 IR 的文字表示形式,必須為每個指令指定某種名稱,以便其他指令可以透過文字引用它。然而,您從 C++ 操作的同構記憶體表示形式沒有這樣的限制,因為指令可以簡單地保留指向它們引用的任何其他 Value
的指標。事實上,像 %1
這樣的虛擬編號臨時變數的名稱根本沒有在記憶體表示形式中明確表示(請參閱 Value::getName()
)。
原始語言¶
支援哪些原始語言?¶
LLVM 目前透過 Clang 完全支援 C 和 C++ 原始語言。許多其他語言前端已使用 LLVM 編寫,不完整的列表可在 projects with LLVM 中找到。
我想編寫一個自舉的 LLVM 編譯器。我應該如何與 LLVM 中間層優化器和後端程式碼產生器介接?¶
您的編譯器前端將透過在 LLVM 中間表示法 (IR) 格式中建立模組來與 LLVM 通訊。假設您想用該語言本身(而不是 C++)編寫語言的編譯器,則有 3 種主要方法可以從前端產生 LLVM IR
使用您語言的 FFI(外部函數介面)呼叫 LLVM 函式庫程式碼。
優點: 最佳追蹤 LLVM IR、.ll 語法和 .bc 格式的變更
優點: 能夠在沒有 emit/parse 額外負擔的情況下執行 LLVM 優化通道
優點: 很好地適應 JIT 環境
缺點: 很多醜陋的膠水程式碼要寫
從您編譯器的原生語言發出 LLVM 組語。
優點: 非常容易上手
缺點: 當與中間層介接時,.ll 解析器比位元碼讀取器慢
缺點: 可能更難追蹤 IR 的變更
從您編譯器的原生語言發出 LLVM 位元碼。
優點: 當與中間層介接時,可以使用更有效率的位元碼讀取器
缺點: 您必須在您的語言中重新設計 LLVM IR 物件模型和位元碼寫入器
缺點: 可能更難追蹤 IR 的變更
如果您選擇第一個選項,include/llvm-c 中的 C 綁定應該會有很大幫助,因為大多數語言都強烈支援與 C 介接。從託管程式碼呼叫 C 最常見的障礙是與垃圾收集器介接。C 介面旨在需要非常少的記憶體管理,因此在這方面很簡單。
對於建構編譯器的高階原始語言結構,有什麼支援?¶
目前,沒有太多。LLVM 支援一種中間表示法,它對於程式碼表示很有用,但不會支援大多數編譯器所需的高階(抽象語法樹)表示法。沒有用於詞彙或語義分析的工具。
我不理解 GetElementPtr
指令。請協助!¶
使用 C 和 C++ 前端¶
我可以將 C 或 C++ 程式碼編譯為平台獨立的 LLVM 位元碼嗎?¶
不行。C 和 C++ 本質上是平台相關的語言。最明顯的例子是前處理器。使 C 程式碼具有可移植性的一種非常常見的方法是使用前處理器來包含平台特定的程式碼。實際上,在預處理之後,關於其他平台的信息會丟失,因此結果本質上取決於預處理所針對的平台。
另一個例子是 sizeof
。 sizeof(long)
在不同平台之間變化是很常見的。在大多數 C 前端中,sizeof
會立即展開為常數,從而硬編碼平台特定的細節。
此外,由於許多平台根據 C 定義其 ABI,並且由於 LLVM 比 C 低階,因此前端目前必須發出平台特定的 IR,以使結果符合平台 ABI。
關於示範頁面產生的程式碼的問題¶
當我 #include <iostream>
時,出現的 llvm.global_ctors
和 _GLOBAL__I_a...
是什麼?¶
如果您將 <iostream>
標頭檔包含到 C++ 翻譯單元中,該檔案可能會使用 std::cin
/std::cout
/… 全域物件。但是,C++ 不保證不同翻譯單元中靜態物件之間的初始化順序,因此,例如,如果您的 .cpp 檔案中的靜態 ctor/dtor 使用了 std::cout
,則該物件不一定會在您使用之前自動初始化。
為了使 std::cout
及其友元在這些情境中正確運作,我們使用的 STL 宣告了一個靜態物件,該物件在每個包含 <iostream>
的翻譯單元中建立。此物件具有靜態建構子和解構子,它們在全域 iostream 物件可能在檔案中使用之前初始化和銷毀它們。您在 .ll
檔案中看到的程式碼對應於建構子和解構子註冊程式碼。
如果您想更輕鬆地*理解*示範頁面中編譯器產生的 LLVM 程式碼,請考慮使用 printf()
而不是 iostream
來列印值。
我的程式碼都跑到哪裡去了?¶
如果您正在使用 LLVM 示範頁面,您可能經常想知道您輸入的所有程式碼發生了什麼事。請記住,示範腳本正在透過 LLVM 優化器執行程式碼,因此如果您的程式碼實際上沒有做任何有用的事情,它可能會全部被刪除。
為了防止這種情況發生,請確保程式碼實際上是需要的。例如,如果您正在計算某些表達式,請從函數傳回值,而不是將其留在區域變數中。如果您真的想限制優化器,您可以讀取和賦值給 volatile
全域變數。
出現在我的程式碼中的 “undef
” 是什麼?¶
undef
是 LLVM 表示未定義值的方式。如果您在使用變數之前沒有初始化它,您可能會得到這些。例如,C 函數
int X() { int i; return i; }
會被編譯為 “ret i32 undef
”,因為從未為 “i
” 指定值。
為什麼 instcombine + simplifycfg 會將呼叫調用慣例不符的函數變成 “unreachable”?為什麼不讓驗證器拒絕它?¶
這是使用自訂調用慣例的前端作者經常遇到的問題:您需要確保在函數和對函數的每次呼叫中都設定正確的調用慣例。例如,這段程式碼
define fastcc void @foo() {
ret void
}
define void @bar() {
call void @foo()
ret void
}
會被優化為
define fastcc void @foo() {
ret void
}
define void @bar() {
unreachable
}
… 使用 “opt -instcombine -simplifycfg
”。這經常困擾人們,因為「他們的所有程式碼都消失了」。為了使間接呼叫能夠工作,需要在呼叫者和被呼叫者上設定調用慣例,因此人們經常問為什麼不讓驗證器拒絕這類事情。
答案是,這段程式碼具有未定義的行為,但它並非非法。如果我們使其非法,那麼每個可能產生這種情況的轉換都必須確保它不會發生,並且存在可以產生這種結構的有效程式碼(在無效程式碼中)。可能導致這種情況發生的事情相當牽強,但我們仍然需要接受它們。這是一個例子
define fastcc void @foo() {
ret void
}
define internal void @bar(void()* %FP, i1 %cond) {
br i1 %cond, label %T, label %F
T:
call void %FP()
ret void
F:
call fastcc void %FP()
ret void
}
define void @test() {
%X = or i1 false, false
call void @bar(void()* @foo, i1 %X)
ret void
}
在這個例子中,“test” 總是將 @foo
/false
傳遞到 bar
中,這確保了它以正確的調用慣例進行動態呼叫(因此,程式碼是完全定義的)。如果您透過內聯器執行此操作,您會得到這個結果(顯式的 “or” 在那裡是為了讓內聯器不會消除一堆無效程式碼)
define fastcc void @foo() {
ret void
}
define void @test() {
%X = or i1 false, false
br i1 %X, label %T.i, label %F.i
T.i:
call void @foo()
br label %bar.exit
F.i:
call fastcc void @foo()
br label %bar.exit
bar.exit:
ret void
}
在這裡您可以看到,內聯通道對 @foo
進行了調用慣例錯誤的未定義呼叫。我們真的不希望讓內聯器知道這類事情,因此它需要是有效程式碼。在這種情況下,無效程式碼消除可以輕鬆地刪除未定義的程式碼。但是,如果 %X
是 @test
的輸入參數,則內聯器會產生這個結果
define fastcc void @foo() {
ret void
}
define void @test(i1 %X) {
br i1 %X, label %T.i, label %F.i
T.i:
call void @foo()
br label %bar.exit
F.i:
call fastcc void @foo()
br label %bar.exit
bar.exit:
ret void
}
關於這一點有趣的是,%X
*必須*為 false 程式碼才能被良好定義,但再多的無效程式碼消除也無法將損壞的呼叫作為 unreachable 刪除。然而,由於 instcombine
/simplifycfg
將未定義的呼叫變成 unreachable,我們最終得到一個分支,該分支基於一個條件跳轉到 unreachable:永遠不會發生跳轉到 unreachable 的分支,因此 “-inline -instcombine -simplifycfg
” 能夠產生
define fastcc void @foo() {
ret void
}
define void @test(i1 %X) {
F.i:
call fastcc void @foo()
ret void
}