組合語言與 CPU

一行 C 如何對應到指令

人很容易以為一行 C 會變成一條機器指令。它幾乎從不如此。一個看似無害的述句通常展開成好幾條指令,而整個迴圈或運算式可能被大幅重排。理解這個對應——一行 C 實際編譯成什麼——正是把整個本領域繫回你所寫之語言的關鍵。

走一個小例子。述句 sum += arr[i];(arr 是 int 陣列)大致編譯成:把 i 乘以比例算出元素位址(用 mov/lea 形成 arr + i*4)、把該元素載入暫存器(mov eax, [rax])、加進裝著 sum 的暫存器(add),再把結果存回。於是一行變成了四五條指令。常見樣式反覆出現:區域變數住在像 [rbp-4] 這樣的固定堆疊偏移、對陣列做索引變成帶比例索引的記憶體運算元、函式呼叫把引數編排進慣例的暫存器並發出 call、if 變成一條 cmp 接條件分支。對某個給定的編譯器與最佳化等級而言,這個對應是確定的,卻不是固定的:開了 -O2,編譯器可能把變數留在暫存器、把迴圈展開、向量化,或刪掉它能證明不必要的程式碼。

這就是關鍵的誠實之處:並沒有簡單、穩定的一對一對應,而在較高的最佳化等級下,發出的指令可能完全不像你原始碼逐行的翻譯。編譯器可以自由重排與變換任何東西,只要可觀察的行為符合 C 抽象機器——而且它獲准假設你的程式沒有未定義行為,這正是邊界檢查或空指標檢查能合法地從輸出中消失的原因。要確切知道一行編譯成什麼,唯一的辦法是去讀反組譯。

函式中的 return a + b;(在 x86-64 System V 下)大致編譯成 lea eax, [rdi + rsi] / ret——兩個引數抵達於 rdi 與 rsi,它們的總和經 eax 進入 rax,ret 把它回傳。

一行 C 通常變成好幾條指令,而最佳化能把它重塑到難以辨認。

別假設指令會逐行對應你的原始碼,也別假設未最佳化(-O0)的輸出有代表性——正式建構用 -O2,看起來大不相同。程式碼可以合法地消失:因為編譯器可以假設沒有未定義行為,一個防範未定義行為的檢查可能被整個最佳化掉。

又称
C to assemblysource-to-machine-code mappingC 對組合語言