編譯第一個C程序
#include <stdio.h> int main(void) { printf("hello world!\n"); return 0; }
使用gcc命令將hello.c編譯成可執行程序 a.out,並運行:
將源文件hello.c編譯爲一個指定名稱的可執行文件:hello,可以通過gcc -o參數來完成
GCC 編譯過程分析
以demo.c爲例:從一個C語言源文件,到生成最後的可執行文件,GCC編譯過程的基本流程如下:
- C 源文件: 編寫一個簡單的hello world程序
- 預處理:生成預處理後的C源文件 hello.i
- 編譯:將C源文件翻譯成彙編文件 hello.s
- 彙編:將彙編文件彙編成目標文件 hello.o
- 鏈接:將目標文件鏈接成可執行文件
gcc命令是GCC編譯器裏的一個前端程序,用來控制整個編譯過程:分別調用預處理器、編譯器和彙編器,完成編譯的每一個過程,最後調用鏈接器,生成可執行文件:a.out
默認情況下,gcc命令會自動完成上述的整個編譯過程。當然,gcc還提供了一系列參數,使用這個參數,可以讓用戶精準控制每一個編譯過程。
- -E :只做預處理,不編譯
- -S :只編譯,將C程序編譯爲彙編文件
- -c :只彙編,不鏈接。
- -o :指定輸出的文件名
GCC -E 參數
如果只對一段C語言程序做預處理操作,而不進行編譯,可以通過gcc -E 參數來完成。如下面的一段程序,在程序中分別使用#include包含頭文件,使用#define定義宏,使用#ifdef條件編譯。
#include <stdio.h> #define PI 3.14 int main(void) { printf("hello world!\n"); printf("PI = %f\n", PI); #ifdef DEBUG printf("debug mode\n"); #else printf("release mode\n"); #endif return 0; }
上面的C源程序使用gcc -E進行預處理,就可以生成原汁原味的C程序
通過預處理後的C程序,使用#include包含的的頭文件就地展開,我們可以看到stdio.h頭文件中printf函數的聲明。程序中使用#define定義的宏PI,也會在實際使用的地方展開爲實際的值。使用#ifdef定義的條件編譯,會根據條件判斷,選擇實際要編譯的代碼分支。
GCC -S 參數
如果只對C源程序做編譯處理,不彙編,可以使用gcc -S 參數:會gcc會將C源程序做預處理、編譯操作,生成對應的彙編文件,不再做進一步的彙編和鏈接操作。
也可以 利用上次的
gcc -S demo.i
GCC -c 參數
如果只想對一個C程序做彙編操作,不進行鏈接,可以使用gcc -c 來完成:
gcc只對源文件做預處理、編譯和彙編操作,不會做鏈接操作。在當前目錄下,我們可以看到demo.c經過彙編編譯,生成的對應的demo.o目標文件。
當然,gcc -c 選項,也可以對上幾節生成的 demo.i、demo.s文件直接彙編,生成對應的目標文件
默認情況下,gcc會將demo.c生成對應的demo.o目標文件。當然,我們也可以通過 -o 輸出選項,生成指定的目標文件:
GCC 靜態鏈接庫
我們也可以通過gcc命令,將自己實現的一些函數封裝成庫,提供給其他開發者使用。
製作靜態鏈接庫
假如現在有add.c和sub.c 源文件,分別實現了加法函數add()和減法函數sub():
// add.c int add(int a, int b) { return a + b; } // sub.c int sub(int a, int b) { return a - b; }
將它們編譯生成一個靜態庫libmath.a,供其他程序調用:
# gcc -c add.c # gcc -c sub.c # ls add.c add.o sub.c sub.o # ar rcs libmath.a add.o sub.o # ls add.c add.o libmath.a sub.c sub.o
生成的libmath.a就是一個靜態庫,裏面包含了我們實現的add()函數和sub()函數:
# ar t libmath.a add.o sub.o # nm libmath.a add.o: 0000000000000000 T add sub.o: 0000000000000000 T sub
使用靜態鏈接庫
接下來,我們就可以編寫一個main()函數,然後在main函數裏調用它們。
int add(int a, int b); int sub(int a, int b); int main(void) { add(1, 2); sub(4, 3); return 0; }
在編譯mainc源文件時,因爲調用了libmath.a庫中的add和sub函數,編譯時要使用gcc -l指定庫的名字,使用-L指定庫的路徑:
# ls libmath.a main.c # gcc main.c -L./ -lmath # ls a.out libmath.a main.c
GCC -I 參數
按照C語言的傳統,調用函數之前,要先聲明,然後才能使用。對add和sub函數的聲明,可以放到C源文件裏聲明,也可以單獨放到一個頭文件裏聲明,任何使用add和sub函數的源文件,直接包含這個頭文件就可以了。
# tree . ├── inc │ ├── add.h │ └── sub.h ├── libmath.a └── main.c # cat inc/add.h int add(int a, int b); # cat inc/sub.h int sub(int a, int b); # cat main.c #include "add.h" #include "sub.h" int main(void) { add(1, 2); sub(4, 3); return 0; }
因爲頭文件 add.h 和 sub.h 統一放到了inc目錄下,編譯器在預處理時,要告訴編譯器這個路徑,否則編譯器就會找不到這些頭文件報錯。通過 gcc -I參數可以告訴編譯器,這些頭文件的所在路徑:
# ls inc libmath.a main.c # gcc main.c -L./ -lmath -I inc/ # ls a.out inc libmath.a main.c
GCC 動態鏈接庫
靜態庫裏實現的函數,可能被多個應用程序調用,那麼在鏈接時,被調用的這個函數可能就會多次鏈接到不同的應用程序中。
比如C標準庫的printf函數,可能被一個應用程序調用多次,被不同的應用程序調用,當這些應用程序加載到內存運行時,內存中也就存在多個printf函數代碼的副本,太浪費了內存空間。而且,對於應用程序來說,每一個調用的庫函數都被鏈接進來,自身的文件體積也會大增。是可忍孰不可忍,動態鏈接此時就粉墨登場了。
動態鏈接跟靜態鏈接相比,具有以下優勢
- 庫的代碼不會鏈接到應用程序裏
- 同一份代碼(如printf代碼)可以被多個應用程序共享使用
- 大大節省了內存空間
但動態庫也有缺點,發佈軟件時,動態庫需要和應用程序一起發佈,否則你編譯的應用程序到了一個新的平臺可能就無法運行。
製作一個動態庫
gcc -shared -fPIC -o libmymath.dylib add.c sub.c
其中的參數說明:
- -shared :動態庫編譯,鏈接動態庫
- -fPIC(或fpic) :生成使用相對地址無關的目標代碼
- -Ldir :在動態庫的搜索路徑中增加dir目錄
- -lname :鏈接靜態庫(libname.a)或動態庫(libname.so)的庫
使用動態鏈接
將動態鏈接庫拷貝到main.c的同一目錄下
# tree
.
├── inc
│ ├── add.h
│ └── sub.h
├── libmymath.so
└── main.c
在編譯main.c,鏈接動態庫libmymath.so的時候,直接指定當前目錄下的libmymath.dylib文件:
# gcc main.c ./libmymath.dylib -I inc/ # ls a.out inc libmymath.dylib main.c
在當前目錄下運行生成的可執行文件a.out,可以正常運行。將a.out拷貝到其他目錄,比如/opt目錄下 會報錯
我們需要將對應的libmymath.dylib文件拷貝到a.out的同一目錄下,然後a.out才能正常運行。
Linux的動態鏈接庫一般都放在/lib官方默認目錄下。如果想讓a.out放在任何路徑下都可以運行,我們可以把libmymath.dylib動態庫拷貝到/lib下,然後在編譯應用程序時,通過-L參數指定動態鏈接庫的搜索路徑。編譯生成的a.out在運行時,就會到指定的/lib目錄下去加載動態庫libmyamth.dylib:
GCC -std標準
同樣一段C程序,使用GCC的不同標準去編譯,編譯的結果可能不相同。使用gcc -std參數可以指定GCC編譯時的標準,常用的標準如下:
- c89
- c99
- c11
- gnu89、gnu90、gnu99、gnu11
gnu89和c89標準的區別是:gnu89除了支持和兼容c89標準外,在c89的基礎上進行了語法擴展。
同樣一段C程序,使用GCC不同的C標準去編譯,結果可能就不一樣:
# cat hello.c #include <stdio.h> int main(void) { int a[10]; for(int i = 0; i < 10; i++) a[i] = i; return 0; }
如果根據提示,使用C99、C11、gnu99等標準編譯,就不會報錯,可以正常編譯:
GCC -Wall 參數
GCC編譯器的-Wall參數用於顯示所有的警告信息。大家在編寫程序時,不要以爲編譯通過,程序可以運行就萬事大吉了,任何一個隱藏的警告信息都可以對軟件的穩定運行帶來隱患。因此,我們不要放過任何一個警告信息,使用GCC編譯器的-Wall參數,可以開啓警告信息,顯示所有的警告信息。
GCC -g 參數
程序的編譯一般分爲兩種模式:debug模式和release模式。
軟件在開發階段,需要不斷地調試,甚至源碼級單步調試,此時建議使用debug模式編譯,生成的可執行文件中包含了大量的調試信息,可以方便我們使用gdb等調試器進行源碼級地調試:比如單步、在源碼中設置斷點等。
使用調試模式生成的可執行文件因爲包含大量的調試信息,所以文件體系會比release模式大。等開發結束,發佈軟件時,建議使用release模式,生成的可執行文件體積就比較小了。
使用gcc -g 選項,可以生成一個debug模式的可執行文件
使用debug模式生成的可執行文件裏,會有一些單獨的section保存各種符號及調試信息,這些信息包含了二進制代碼和源碼之間的一一對應關係。通過這種對應關係,我們就可以實現源碼級的單步調試和設置斷點。
對比一下,release模式的可執行文件,你會發現:debug模式的可執行文件裏有35個section,而release模式的可執行文件裏只有30個section: