沒有 GCC 優化,Linux 無法編譯;影響?
您可以在 Internet 上找到幾個執行緒,例如:
http://www.gossamer-threads.com/lists/linux/kernel/972619
人們抱怨他們無法使用 -O0 建構 Linux,並被告知不支持;Linux 依靠 GCC 優化來自動內聯函式、刪除死程式碼以及執行建構成功所必需的其他操作。
我自己已經為至少一些 3.x 核心驗證了這一點。如果使用 -O0 編譯,我嘗試在建構時間幾秒鐘後退出的那些。
這通常被認為是可接受的編碼實踐嗎?編譯器優化(例如自動內聯)是否足夠可預測以供依賴;至少在只處理一個編譯器時?未來版本的 GCC 有多大可能會破壞具有預設優化(即 -O2 或 -Os)的目前 Linux 核心的建構?
更迂腐的說法是:既然 3.x 核心不經過優化就無法編譯,那麼它們是否應該被視為技術上不正確的 C 程式碼?
您已經將幾個不同(但相關)的問題組合在一起。其中一些並不是真正的主題(例如,編碼標準),所以我將忽略這些。
我將從核心是否是“技術上不正確的 C 程式碼”開始。我從這裡開始是因為答案解釋了核心佔據的特殊位置,這對於理解其餘部分至關重要。
核心在技術上是不正確的 C 程式碼嗎?
答案肯定是它的“不正確”。
有幾種方法可以說 C 程序是不正確的。讓我們先解決一些簡單的問題:
- 不遵循 C 語法(即有語法錯誤)的程序是不正確的。核心對 C 語法使用各種 GNU 擴展。就 C 標準而言,這些是語法錯誤。(當然,對於 GCC,它們不是。嘗試使用
-std=c99 -pedantic
或類似的編譯…)- 一個不做它設計做的事情的程序是不正確的。核心是一個巨大的程序,即使快速檢查其變更日誌也會證明,肯定不是。或者,正如我們通常所說的,它有錯誤。
優化在 C 中的含義
$$ NOTE: This section contains a very lose restatement of the actual rules; for details, see the standard and search Stack Overflow. $$ 現在對於需要更多解釋的那個。C 標准說某些程式碼必須產生某些行為。它還說某些在語法上有效的 C 具有“未定義的行為”;一個(不幸的是,很常見!)範例是超出數組末尾的訪問(例如,緩衝區溢出)。
未定義的行為非常強大。如果一個程序包含它,哪怕是一點點,C 標準就不再關心程序表現出的行為或編譯器在面對它時產生的輸出。
但是即使程序只包含已定義的行為,C 仍然允許編譯器有很大的餘地。作為一個簡單的例子(注意:對於我的例子,為簡潔起見,我省略了
#include
行等):void f() { int *i = malloc(sizeof(int)); *i = 3; *i += 2; printf("%i\n", *i); free(i); }
當然,這應該列印 5 後跟換行符。這就是 C 標準所要求的。
如果您編譯該程序並反彙編輸出,您會期望呼叫 malloc 以獲取一些記憶體,返回的指針儲存在某處(可能是寄存器),值 3 儲存到該記憶體,然後將 2 添加到該記憶體(也許甚至需要載入、添加和儲存),然後將記憶體複製到堆棧中,並將一個點字元串
"%i\n"
放入堆棧,然後printf
呼叫該函式。相當多的工作。但相反,您可能會看到好像您寫過:/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */ void f() { printf("%i\n", 5) }
事情是這樣的:C 標准允許這樣做。C 標準只關心結果,而不關心實現的方式。
這就是 C 語言中的優化。編譯器提出了一種更智能(通常更小或更快,取決於標誌)的方法來實現 C 標準所需的結果。有一些例外,例如 GCC 的
-ffast-math
選項,但除此之外,優化級別不會改變技術上正確程序的行為(即,僅包含已定義行為的程序)。你能寫一個只使用定義行為的核心嗎?
讓我們繼續檢查我們的範常式序。我們編寫的版本,而不是編譯器將其轉換為的版本。我們要做的第一件事是呼叫
malloc
以獲取一些記憶體。C 標準告訴我們做什麼malloc
,但不告訴我們它是如何做的。如果我們查看一個
malloc
旨在清晰(而不是速度)的實現,我們會看到它會進行一些系統呼叫(例如mmap
withMAP_ANONYMOUS
)來獲取大量記憶體。它在內部保留一些資料結構,告訴它該塊的哪些部分已使用與空閒。它找到一個至少和你要求的一樣大的空閒塊,切出你要求的數量,並返回一個指向它的指針。它也完全用 C 語言編寫,並且只包含已定義的行為。如果它是執行緒安全的,它可能包含一些 pthread 呼叫。現在,最後,如果我們看看什麼
mmap
確實,我們看到了各種有趣的東西。首先,它會檢查系統是否有足夠的空閒 RAM 和/或交換用於映射。接下來,它會找到一些空閒的地址空間來放入塊。然後它編輯一個稱為頁表的資料結構,並且可能會在此過程中進行一堆內聯彙編呼叫。它實際上可能會找到一些物理記憶體的空閒頁面(即實際 DRAM 模組中的實際位)——一個可能需要強制其他記憶體交換的過程——以及。如果它不對整個請求的塊執行此操作,它將改為設置一些內容,以便在首次訪問所述記憶體時發生這種情況。其中大部分是通過一些內聯彙編、寫入各種魔術地址等來完成的。還要注意,它還使用了核心的大部分,尤其是在需要交換的情況下。內聯彙編、寫入魔術地址等都在 C 規範之外。這並不奇怪。C 執行在許多不同的機器架構中——包括在 1970 年代早期 C 被發明時幾乎無法想像的一堆。隱藏特定於機器的程式碼是核心(在某種程度上是 C 庫)的核心部分。
當然,如果你回到範常式序,就會發現
printf
一定是相似的。很清楚如何在標準 C 中進行所有格式化等;但實際上把它放在顯示器上?還是通過管道傳輸到另一個程序?再一次,核心(可能還有 X11 或 Wayland)完成了很多魔法。如果你想想核心做的其他事情,很多都在 C 之外。例如,核心從磁碟(C 不知道磁碟、PCIe 匯流排或 SATA)中讀取數據到物理記憶體(C 只知道 malloc,不是 DIMM、MMU 等),使其可執行(C 對處理器執行位一無所知),然後將其作為函式呼叫(不僅在 C 之外,非常不允許)。
核心與其編譯器之間的關係
如果你還記得,如果一個程序包含未定義的行為,就 C 標準而言,所有的賭注都沒有了。但是核心確實必須包含未定義的行為。所以核心和它的編譯器之間必須有某種關係,至少核心開發人員可以確信核心在違反 C 標準的情況下仍然可以工作。至少在 Linux 的情況下,這包括核心對 GCC 如何在內部工作有一些了解。
破的可能性有多大?
未來的 GCC 版本可能會破壞核心。我可以很自信地這麼說,因為它以前發生過好幾次。當然,諸如 GCC 中嚴格的別名優化之類的東西也破壞了核心之外的很多東西。
另請注意,Linux 核心所依賴的內聯不是自動內聯,而是核心開發人員手動指定的內聯。有很多人用 -O0 編譯核心並報告它在修復了一些小問題後基本可以工作。(一個甚至在您連結到的執行緒中)。大多數情況下,核心開發人員認為沒有理由使用 編譯
-O0
,並且需要優化作為副作用使得一些技巧起作用,並且沒有人測試使用-O0
,所以它不受支持。例如,這編譯和連結使用
-O1
或更高版本,但不使用-O0
:void f(); int main() { int x = 0, *y; y = &x; if (*y) f(); return 0; }
通過優化,gcc 可以找出
f()
永遠不會被呼叫的,並忽略它。如果沒有優化,gcc 會留下呼叫,並且連結器會失敗,因為沒有f()
. 核心開發人員依靠類似的行為來使核心程式碼更容易讀/寫。