Linux

沒有 GCC 優化,Linux 無法編譯;影響?

  • September 4, 2014

您可以在 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旨在清晰(而不是速度)的實現,我們會看到它會進行一些系統呼叫(例如mmapwith MAP_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(). 核心開發人員依靠類似的行為來使核心程式碼更容易讀/寫。

引用自:https://unix.stackexchange.com/questions/153788