C語言程序從源代碼到二進制行程序都經歷了那些過程?本文以Linux下C語言的編譯過程為例,講解C語言程序的編譯過程。
編寫hello world C程序:
// hello.c
#include <stdio.h>
int main(){
printf("hello world!\n");
}
編譯過程只需:
$ gcc hello.c # 編譯
$ ./a.out # 執行
hello world!
這個過程如此熟悉,以至於大家覺得編譯事件很簡單的事。事實真的如此嗎?我們來細看一下C語言的編譯過程到底是怎樣的。
上述gcc命令其實依次執行了四步操作:1.預處理(Preprocessing), 2.編譯(Compilation), 3.匯編(Assemble), 4.鏈接(Linking)。
為了下面步驟講解的方便,我們需要一個稍微復雜一點的例子。假設我們自己定義了一個頭文件mymath.h
,實現一些自己的數學函數,並把具體實現放在mymath.c
當中。然後寫一個test.c
程序使用這些函數。程序目錄結構如下:
├── test.c
└── inc
├── mymath.h
└── mymath.c
程序代碼如下:
// test.c
#include <stdio.h>
#include "mymath.h"// 自定義頭文件
int main(){
int a = 2;
int b = 3;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}
頭文件定義:
// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif
頭文件實現:
// mymath.c
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
預處理用於將所有的#include頭文件以及宏定義替換成其真正的內容,預處理之後得到的仍然是文本文件,但文件體積會大很多。gcc
的預處理是預處理器cpp
來完成的,你可以通過如下命令對test.c
進行預處理:
gcc -E -I./inc test.c -o test.i
或者直接調用cpp
命令
$ cpp test.c -I./inc -o test.i
上述命令中-E
是讓編譯器在預處理之後就退出,不進行後續編譯過程;-I
指定頭文件目錄,這裡指定的是我們自定義的頭文件目錄;-o
指定輸出文件名。
經過預處理之後代碼體積會大很多:
預處理之後的程序還是文本,可以用文本編輯器打開。
這裡的編譯不是指程序從源文件到二進制程序的全部過程,而是指將經過預處理之後的程序轉換成特定匯編代碼(assembly code)的過程。編譯的指定如下:
$ gcc -S -I./inc test.c -o test.s
上述命令中-S
讓編譯器在編譯之後停止,不進行後續過程。編譯過程完成後,將生成程序的匯編代碼test.s
,這也是文本文件,內容如下:
// test.c匯編之後的結果test.s
.file "test.c"
.section .rodata
.LC0:
.string "a=%d, b=%d, a+b=%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.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 $32, %esp
movl $2, 20(%esp)
movl $3, 24(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl 20(%esp), %eax
movl %eax, (%esp)
call add
movl %eax, 28(%esp)
movl 28(%esp), %eax
movl %eax, 12(%esp)
movl 24(%esp), %eax
movl %eax, 8(%esp)
movl 20(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
請不要問我上述代碼是什麼意思!-_-
匯編過程將上一步的匯編代碼轉換成機器碼(machine code),這一步產生的文件叫做目標文件,是二進制格式。gcc
匯編過程通過as
命令完成:
$ as test.s -o test.o
等價於:
gcc -c test.s -o test.o
這一步會為每一個源文件產生一個目標文件。因此mymath.c
也需要產生一個mymath.o
文件
鏈接過程將多個目標文以及所需的庫文件(.so等)鏈接成最終的可執行文件(executable file)。
命令大致如下:
$ ld -o test.out test.o inc/mymath.o ...libraries...
經過以上分析,我們發現編譯過程並不像想象的那麼簡單,而是要經過預處理、編譯、匯編、鏈接。盡管我們平時使用gcc
命令的時候沒有關心中間結果,但每次程序的編譯都少不了這幾個步驟。也不用為上述繁瑣過程而煩惱,因為你仍然可以:
$ gcc hello.c # 編譯
$ ./a.out # 執行