平常我們很少關注編譯和鏈結的過程,因為開發環境都集成開發的環境,比如Visual Studio、Eclipse,這樣的IDE一般都將編譯和鏈結的過程一步完成,因此我們必須深入了解這些被隱藏的過程。
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
給定一個普通的輸出 Hello World 的程式
通常我們可以使用GCC 進行編譯
$ gcc -o hello hello.c 會輸出 ELF 格式的執行檔
$ ./hello 即可執行
事實上,上述的過程可分解為四個步驟,分別是預處理、編譯、組譯和鏈結
C預處理器參照標頭檔stdio.h的內容,展開macro和驗證prototype,並輸出成一個.i文件。輸出的結果就不會再見到 "#"開頭字樣,預編譯的過程的命令用 -E 表示:
$ gcc -E hello.c -o hello.i
預編譯完成後,替換完標頭檔和macro之後的樣子如下
extern int printf (const char *__restrict __format, ...);
........
int main(void)
{
printf("Hello, world!\n");
return 0;
}
上面程式碼擷取了標頭檔文件中相關的部分,省略了stdio.h的其他部分,在這一步驟註釋也會被移除。
※不同的原碼文件,可能會引用一個標頭檔(比如stdio.h),編譯的時候,標頭檔也必須一起編譯,而編譯器會先編譯標頭檔,這是為了確保標頭檔只需編譯一次,不必每次用到的時候都重新編譯。
編譯過程就是把預處理完的文件進行一系列字彙分析、語法分析、語意分析、最佳化後生成對映的組合語言(hello.s),編譯過程的命令如下:
$gcc -S hello.i -o hello.s
組譯就是一個將組合語言轉換成機器可以執行的指令,組譯過程我們可以使用組譯器 as 來完成:
as hello.s -o hello.o 或 gcc -c hello.s -o hello.o
會發現其實出來的 hello.o 並無法執行,因為缺少連結的過程。
在編譯階段並不知道printf的位址,所以暫時會以printf符號名稱代替
main
LDR R1, [R2 + l2]
BAL printf
而在組譯器輸出的對應的組合語言,仍然不知道printf的位址,因此暫時不填入
main:
EC 00 00 12
F0 ?? ?? ??
printf 實作於libc.a(C語言標準含市庫的靜態版本),
其地址為0x1000 Linker重新配置(relocate)
0x2000 <main>:
EC 00 00 12
F0 00 10 00
連結通常是一個比較費解的過程,有靜態鏈結、和動態鏈結,下次會更詳細的分析此過程
From Source to Binary: How A Compiler Works: GNU Toolchain
程式設計師的自我修養