常見問題集 (FAQ)

授權

我可以修改 LLVM 原始碼並重新發布修改後的原始碼嗎?

是的。修改後的原始碼發布必須保留版權聲明,並遵循 Apache 授權條款 2.0 版(含 LLVM 例外條款) 中列出的條件。

我可以修改 LLVM 原始碼並重新發布基於它的二進制文件或其他工具,而不重新發布原始碼嗎?

是的。這就是為什麼我們在比 GPL 更寬鬆的授權條款下發布 LLVM,如上述第一個問題中所述。

原始碼

LLVM 是用什麼語言寫的?

所有 LLVM 工具和函式庫都是用 C++ 編寫的,並廣泛使用了 STL。

LLVM 原始碼的可移植性如何?

LLVM 原始碼應該可以移植到大多數現代類 Unix 作業系統。LLVM 在 Windows 系統上也有很好的支援。大部分程式碼都是用標準 C++ 編寫,作業系統服務被抽象化到一個支援庫中。用於建置和測試 LLVM 的工具已被移植到許多平台。

我應該使用哪個 API 將值儲存到 LLVM IR 的 SSA 表示形式的其中一個虛擬暫存器?

簡而言之:你不能。一旦你理解了發生的事情,這實際上是一個愚蠢的問題。基本上,在程式碼中,例如

%result = add i32 %foo, %bar

%result 只是賦予 add 指令之 Value 的名稱。換句話說,%result *就是* 加法指令。「賦值」並未明確地將任何內容「儲存」到任何「虛擬暫存器」;「=」更像是數學意義上的等式。

更長的解釋:為了產生 IR 的文字表示形式,必須為每個指令指定某種名稱,以便其他指令可以透過文字參考它。然而,你從 C++ 操作的同構記憶體表示沒有這樣的限制,因為指令可以簡單地保留指向它們引用的任何其他 Value 的指標。事實上,像 %1 這樣的虛擬編號暫存器的名稱在記憶體表示中根本沒有明確表示(請參閱 Value::getName())。

原始語言

支援哪些原始語言?

LLVM 目前透過 Clang 完全支援 C 和 C++ 原始語言。許多其他語言前端都是使用 LLVM 編寫的,不完整的清單可在 使用 LLVM 的專案 中找到。

我想寫一個自舉的 LLVM 編譯器。我應該如何與 LLVM 中間端優化器和後端程式碼產生器互動?

你的編譯器前端將透過以 LLVM 中間表示 (IR) 格式建立模組來與 LLVM 進行通訊。假設你想用語言本身(而不是 C++)編寫語言的編譯器,那麼有三種主要方法可以解決從前端產生 LLVM IR 的問題

  1. 使用你的語言的 FFI(外部函數介面)呼叫 LLVM 程式庫程式碼。

  • **優點:** 最能追蹤 LLVM IR、.ll 語法和 .bc 格式的變化

  • **優點:** 能夠在沒有發射/解析開銷的情況下執行 LLVM 優化遍

  • **優點:** 非常適合 JIT 環境

  • **缺點:** 需要編寫大量醜陋的膠水程式碼

  1. 從你的編譯器的原生語言發射 LLVM 組合語言。

  • **優點:** 非常容易上手

  • **缺點:** 與中間端互動時,.ll 解析器比位元組碼讀取器慢

  • **缺點:** 追蹤 IR 的變化可能會比較困難

  1. 從你的編譯器的原生語言發射 LLVM 位元組碼。

  • **優點:** 與中間端互動時,可以使用效率更高的位元組碼讀取器

  • 反對理由:您必須使用您的語言重新設計 LLVM IR 物件模型和位元碼編寫器

  • **缺點:** 追蹤 IR 的變化可能會比較困難

如果您選擇第一個選項,那麼 include/llvm-c 中的 C 語言繫結應該會很有幫助,因為大多數語言都強烈支援與 C 語言的介面。從託管程式碼呼叫 C 語言時,最常見的障礙是如何與垃圾收集器互動。C 語言介面的設計只需要很少的記憶體管理,因此在這方面非常簡單。

建構編譯器時,對於較高階的原始碼語言結構有哪些支援?

目前,支援不多。LLVM 支援一種中間表示法,這種表示法對於程式碼表示很有用,但不支援大多數編譯器所需的高階(抽象語法樹)表示法。它沒有提供詞彙分析或語義分析的功能。

我不懂 GetElementPtr 指令。救命!

請參閱 經常被誤解的 GEP 指令

使用 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... 是什麼東西?

如果您在 C++ 轉譯單元中 #include <iostream> 標頭,則該檔案可能會使用 std::cin/std::cout/… 全域物件。但是,C++ 不保證不同轉譯單元中的靜態物件之間的初始化順序,因此,例如,如果您的 .cpp 檔案中的靜態建構函式/解構函式使用了 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 會將呼叫慣例不符的函式呼叫轉換為「無法到達」?為什麼不讓驗證器拒絕它?

這是使用自訂呼叫慣例的前端作者經常遇到的問題:您需要確保在函式和每次呼叫函式時都設定正確的呼叫慣例。例如,這段程式碼

define fastcc void @foo() {
    ret void
}
define void @bar() {
    call void @foo()
    ret void
}

會使用「opt -instcombine -simplifycfg」最佳化為

define fastcc void @foo() {
    ret void
}
define void @bar() {
    unreachable
}

……結果是「所有程式碼都消失了」。間接呼叫需要在呼叫者和被呼叫者上設定呼叫慣例,所以人們經常會問為什麼不讓驗證器拒絕這種情況。

答案是這段程式碼的行為未定義,但它並不違法。如果我們將其視為違法,那麼每個可能產生這種情況的轉換都必須確保它不會發生,而且有些有效的程式碼可以產生這種結構(在無效程式碼中)。導致這種情況發生的原因相當做作,但我們仍然需要接受它們。以下是一個例子

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,這確保它以正確的呼叫慣例被動態呼叫(因此,程式碼定義良好)。如果您通過內聯器運行它,您將獲得以下結果(明確的「或」在那裡,以便內聯器不會刪除一堆東西的死碼)

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 才能使程式碼定義良好,但是任何數量的死碼消除都無法將損壞的呼叫作為不可達的程式碼刪除。但是,由於 instcombine/simplifycfg 將未定義的呼叫轉換為不可達,因此我們最終得到了一個條件分支,該分支轉到不可達:分支到不可達永遠不會發生,因此「-inline -instcombine -simplifycfg」能夠產生

define fastcc void @foo() {
   ret void
}
define void @test(i1 %X) {
F.i:
   call fastcc void @foo()
   ret void
}