前情提要:本系列文章皆使用 GCC 編譯器,在 Linux 作業系統可直接用套件管理工具安裝;使用 macOS 者可參考此連結安裝命令列工具取得;使用 Windows 作業系統者可參考此連結安裝 MinGW。
當我們寫一段 C 語言程式碼,要變成電腦上真正可以執行的程式,共要經過下列四大步驟:
# 開頭的指令。圖示如下:
(以下指令皆以 Windows 作業系統為例)
以 C 語言撰寫一檔名為 hello.c 的 Hello, World! 程式碼如下:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
這就是「原始程式(source program)」。
經過下列預處理指令:
gcc -E hello.c -o hello.i
會產生下列「預處理過的原始程式(modified source program)」,檔名為 hello.i:
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 省略超過四百行...
# 3 "hello.c"
int main() {
printf("Hello, World!\n");
return 0;
}
最下面的主程式 main() 還在,但原本的 #include <stdio.h> 被大幅展開,<stdio.h> 裡包含的其他原始碼也都被一層層展開,因此原始碼僅 6 行的程式碼大幅膨脹至超過 400 行。
預處理的主要功能是讓程式知道有 printf() 這個寫在 <stdio.h> 的函式可以用,等到最後連結時,再把 printf() 對應的機器語言一起組成執行檔。
這些預處理器被展開的原始碼再經過下列編譯指令:
gcc -S hello.i -o hello.s
產生如下的組合語言程式(assembly program),檔名為 hello.s:
.file "hello.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Hello, World!\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB10:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
movl $LC0, (%esp)
call _puts
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE10:
.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
.def _puts; .scl 2; .type 32; .endef
編譯之後產生的組合語言已經開始在寫機器的執行步驟,當中可看到如 %ebp、%esp 等暫存器調用相關指令。
組合語言仍是人類(經過訓練後)能看得懂的文字檔,但只要再用下列指令進行組譯:
gcc -c hello.s -o hello.o
組譯後產生的目標檔(relocatable object program)為由 0 跟 1 組成的機器碼,檔名為 hello.o,要查看其二進位內容可用如下指令:
xxd -b hello.o
可得:
00000006: 00000000 00000000 10111100 00000001 00000000 00000000 ......
0000000c: 00010010 00000000 00000000 00000000 00000000 00000000 ......
00000012: 00000100 00000001 00101110 01110100 01100101 01111000 ...tex
# 省略超過一百行...
0000032a: 01100001 01101101 01100101 00000000 ame.
最後再用以下指令:
gcc hello.o -o hello.exe
可得一名為 hello.exe 的執行檔(executable object program),與 hello.o 一樣可用以下指令檢視其二進位內容:
xxd -b hello.exe
內容如下:
00000000: 01001101 01011010 10010000 00000000 00000011 00000000 MZ....
00000006: 00000000 00000000 00000100 00000000 00000000 00000000 ......
# 省略超過六千七百行...
00009f36: 01110111 01110010 01101001 01110100 01100101 00000000 write.
除了主程式外,在預處理階段也已經展開 <stdio.h>,讓程式知道有已經寫在標準輸入輸出函式庫的 printf() 函式宣告可以用。
因此,hello.exe 不是只有 main() 主程式的目標檔機器碼,還有來自函式庫的 printf() 目標檔機器碼(可想成 printf.o),兩者結合才有辦法順利執行 printf("Hello, World!\n"); 的功能。
上述是從原始碼一步步變成執行檔的過程,但 GCC 指令也允許我們跳步驟產生不同檔案,例如:
從原始碼直接變成組合語言程式:
gcc -S hello.c -o hello.s
從原始碼直接變成目標檔:
gcc -c hello.c -o hello.o
從原始檔直接變成執行檔:
gcc hello.c -o hello.exe
最後一個指令相對常用,而若沒有提供 -o 參數,變成:
gcc hello.c
預設產生的執行檔名為 a.exe (或 a.out)。