一、簡介:
gcc 最初是 "GNU C Compiler" 的簡稱,只是當作一個 C 語言的編譯器,現在已經變成了 "GNU Compiler Collection",可以編譯多種語言。
二、編譯的四個階段:
在使用 gcc 編譯程序時,編譯過程可以被細分為 4 個階段:
◆ 預處理(Pre-Processing)
◆ 編譯(Compiling)
◆ 匯編(Assembling)
◆ 鏈接(Linking)
下面以一份簡單的 C 代碼來說明:
#include <stdio.h> int main() { printf("Hello,World!\n"); }
在使用GCC編譯時, 可以直接使用 gcc test.c -o test 命令一次性完成編譯(注:如果使用 gcc 命令(未做g++的軟鏈接)來編譯 C++ 庫,需要鏈接 -lstdc++ 庫。),也可以分成 4 個步驟分步編譯:
gcc -E test.c -o test.i //需要使用 -o 將預處理后的結果輸出到 test.i (仍然是 C 代碼),否則只將預處理后結果輸出到屏幕。 gcc -S test.i //將預處理后的代碼進行反匯編,生成匯編代碼 gcc -c test.s //將匯編代碼編譯成目標文件,即二進制代碼。-c 可以直接把 C/C++ 代碼編譯成機器代碼。 gcc test.o -o test //將中間文件鏈接成可執行文件
本人使用的 gcc 安裝在 CentOS 6.4 64bit 上的 /usr/local/gcc-4.8.1 目錄,故以此為例說明:
1、預處理階段:調用 cpp 命令
cpp test.c -o test.i
2、編譯階段:調用 cc1 命令(如果是 C++ 代碼則使用 cc1plus 命令)
/usr/local/gcc-4.8.1/libexec/gcc/x86_64-unknown-linux-gnu/4.8.1/cc1 test.i
3、匯編階段:調用 as 命令
as test.s -o test.o
4、連接階段:調用 ld 命令(如果是 C++ 代碼還需要鏈接 -lstdc++)
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o test test.o /usr/lib64/crt1.o /usr/lib64/crti.o -lc /usr/local/gcc-4.8.1/lib/gcc/x86_64-unknown-linux-gnu/4.8.1/crtbegin.o /usr/local/gcc-4.8.1/lib/gcc/x86_64-unknown-linux-gnu/4.8.1/crtend.o /usr/lib64/crtn.o
注:可以通過 -v 參數打印出編譯器內部編譯各過程的命令行信息和編譯器的版本,如 gcc -v test.c 2>&1 | grep cc1 ,另外關於 ld 鏈接 C/C++ ,可參考:
http://stackoverflow.com/questions/14163208/how-to-link-c-object-files-with-ld
三、頭文件的處理
GCC 的 -I 參數可以用來指定頭文件目錄,當前目錄和缺省目錄(/usr/include && /usr/local/include)不用指定。
另外,-include 用來包含某個頭文件,但一般情況下因為已經在源碼里用 #include 指令實現了,所以這個很少用。
環境變量 C_INCLUDE_PATH 和 CPLUS_INCLUDE_PATH ,分別用來指定 C 與 C++ 的默認的頭文件目錄。那么,該方法與使用 -I 來包含頭文件有什么區別呢?區別是,在使用 GCC -MM 生成依賴關系時,前者會忽略生成指定路徑的頭文件的依賴關系,即編譯時不依賴於環境變量中指定的頭文件,這在我們使用穩定的第三方庫頭文件時,非常有利於編譯檢查的時間優化,如 BOOST 庫等。-M 選項生成文件關聯的信息,包含目標文件所依賴的所有源;而 -MM 選項會忽略由 #include <...> 造成的依賴關系。-MD 選項與 -M 相同,只是將依賴關系寫到 .d 文件中去,同樣的 -MMD 也是如此。
在代碼中使用 #include <stdio.h> 與使用 #include "stdio.h" 一般都可以使代碼正常編譯,但對於預處理程序 cpp 的工作效率是有影響的,前者會優先搜索系統默認頭文件目錄,而后者會優先搜索當前目錄,如果沒有搜到就到 -I 指定的目錄去搜索。
四、鏈接庫的處理
下面先來看看靜態庫和動態庫是如何生成的:
◆ 靜態鏈接庫其實就可以看成是 *.o 文件的簡單打包:
ar -cr libtest.a test.o 或: ar -cr libtest.a test.cpp
-r 選項是必須的,表示插入到備份文件,並且有則替換 -c 表示創建備份文件
(一段小歷史:最早的 ar 命令只是用來打包文件的,就類似 tar 一樣,並不處理 .o 文件里的符號表,所以出現一個名為 ranlib 的工具來做這件事,后來被廠商集成到 ar 命令中,相當於 ar -s,現在只要使用 ar 命令,已經默認使用了 ranlib 的功能。)
◆ 動態鏈接庫優於靜態鏈接庫,無論是在內存使用和磁盤使用上(多進程時),既可以使用源碼文件直接生成,也可以使用 *.o 文件生成(*.o 文件的生成必須使用 -fPIC 參數,而從*.o 文件生成動態鏈接庫則不用再重復此參數):
gcc -c -fPIC test.cpp && gcc test.o -o libtest.so -shared
或: gcc test.cpp -o libtest.so -shared -fPIC
-shared 該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號),不用該標志外部程序無法連接。相當於一個可執行文件。
-fPIC 表示編譯為位置獨立的代碼,不用此選項的話編譯后的代碼是位置相關的,所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。
(小知識:可以通過 ldd 命令來查看可執行文件需要哪些動態鏈接庫)
再來看看 gcc 如何使用靜態庫和動態庫:
編譯時可以直接帶上完整路徑的鏈接庫進行編譯,如 g++ main.cpp lib/libmyfun.so -o main ,但更多的是使用下面的方法。
gcc 使用 -l 參數來指定鏈接庫,如鏈接 libtest.a 或 libtest.so,使用 -ltest。使用 -l 鏈接時去除 lib 前綴和后綴即可。
編譯時,默認的鏈接庫目錄為 /usr/lib && /usr/local/lib ,如果是64位系統,還有 /usr/lib64;否則就需要通過 -L 參數來指定自定義鏈接庫目錄。(注:有的LINUX發行版好像不支持 /usr/local/lib 和 /usr/local/lib64,所以最好是使用 /usr/lib)
靜態庫鏈接后,是將代碼直接嵌入到可執行文件中,所以運行時,不需要再指定靜態庫;而動態庫則不然。如果動態庫位於系統默認的庫目錄,則可直接運行,如果是自定義目錄,有以下三種方法可以配置:
1、直接執行:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/project/lib
可臨時生效,(重啟后失效)
2、 ~/.bashrc 或者 ~/.bash_profile 里添加上面的命令:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/project/lib
重啟終端生效。
3、編輯 /etc/ld.so.conf 文件(或者在 /etc/ld.so.conf.d 目錄下新建一個 project.conf 文件),添加自定義動態庫路徑,然后使用 ldconfig 命令更新,使其生效。
當靜態庫和動態庫同時存在時,程序會優先使用動態庫,如果需要強制只使用靜態庫,可使用 -static 選項,此時不會鏈接任何動態庫,生成的目標文件會比較大。
此外,還有另外一種方式,在鏈接時加上如 -L../lib -Wl,-rpath=../lib 選項,則可以指定編譯好的程序在運行時動態庫的優先搜索目錄,這種方法會將動態庫路徑寫入到elf文件中去,即使找不到,也可以再去環境變量指定路徑去尋找。該方式便於部署,可把動態庫跟配置文件一樣放在單獨文件夾里,與可執行文件一起打包發給運維即可運行。總結一下:-rpath制定的搜索路徑,優先於LD_LIBRARY_PATH 和 /etc/ld.so.conf ,另外它是被寫入到可執行文件中的。可以使用 ldd 命令查看可執行文件中使用的動態鏈接庫和相應路徑。
(小知識:我們可以給鏈接庫帶一個版本號,如 libmyfun.so.6,然后用 ln -s libmyfun.so.6 libmyfun.so,項目中鏈接 libmyfun.so 即可。gcc庫文件的 libstdc++.so.6 就是如此)
運行時,Linux動態鏈接庫的搜索路徑按優先級排序為:
1、編譯目標代碼時 ”-Wl,-rpath,” 指定的動態庫搜索路徑(當指定多個動態庫搜索路徑時,路徑之間用冒號”:”分隔。);
2、環境變量 LD_LIBRARY_PATH 指定的動態庫搜索路徑;
3、配置文件 /etc/ld.so.conf 中指定的動態庫搜索路徑;
4、默認的動態庫搜索路徑 /lib,如果是64位系統還包括 /lib64;
5、默認的動態庫搜索路徑 /usr/lib,如果是64位系統還包括 /usr/lib64;
注意: /usr/local/lib 和 /usr/local/lib64 居然不在標准路徑之列。
五、靜態檢查
-pedantic //檢查代碼是否符合 ANSI/ISO 標准: -Wall //輸出警告信息 -Wextra //輸出更多的警告信息 -Weffc++ //檢查是否符合 《Effective C++》這本書中提到的標准 -w //關閉所有警告
六、調試與優化
-g //在編譯時生成原生格式的調試符號信息,可以使用 gdb 或 ddx 等調試器調試。-g 分為三個級別,默認為 -g2,其中 -g3 除包含 -g2 中的所有調試信息外,還包含源代碼中定義的宏。 -ggdb //在編譯時生成 gdb 專用格式的調試符號信息,信息更為豐富,但只能使用 gdb 調試,而不能使用其它調試器。級別設定與 -g 基本相同。 -gdwarf //如果升級 GCC 之后,發現 GDB 不能用了,那么在編譯的時候加上 -gdwarf 選項試一下,如果還不行,再使用 -gdwarf-2 試試。 -p //將剖析(Profiling)信息加入到最終生成的二進制代碼中,生成可以被通用剖析工具(Prof)能夠識別的統計信息,它對於找出程序瓶頸很有幫助。生成后的可執行文件要運行一次,才能生成剖析文件,默認文件名是 gmon.out。 -pg //生成只有 GNU 剖析工具(Gprof)才能識別的統計信息。 -save-temps //保存編譯過程中生成的一系列中間文件。 -On //n 可以為 0~3,默認為 1,數字越大優化越高,一般使用 -O2 可以在優化長度、編譯時間和代碼大小之間取得較好平衡,開發調試建議使用 -O0 -Os //相當於 -O2.5,使用了所有 -O2 的優化選項,但卻沒有使用增加大小。也就是說,相對於 -O2,能減少一點可執行文件的大小。 -s //從執行文件中刪除符號表與重定向信息,以減少執行文件的大小。跟直接編譯之后,使用 strip 命令減小文件大小效果一致。
七、編譯器擴展
#include <iostream> #include <string> class T { public: void fun(std::string s,double d) { std::cout<<__FUNCTION__<<std::endl; //輸出函數名 std::cout<<__PRETTY_FUNCTION__<<std::endl; //輸出函數完整原型 } }; int main() { T().fun("Hello",3.1); }
輸出結果如下:
八、GCC 版本升級
通過源碼安裝新版本的GCC,要保證已經安裝了GCC編譯器:
1、下載源碼包
下載高版本GCC源碼包
ftp://ftp.gnu.org/gnu/gmp 下載最新版
http://ftp.gnu.org/gnu/mpfr/ 下載 mpfr 最新版
http://www.multiprecision.org/mpc 下載 mpc 最新版
2、依次安裝 gmp 、mpfr、mpc
./configure --prefix=/usr/local/gmp-5.1.1&& make -j4 && make install ./configure --prefix=/usr/local/mpfr-3.1.2 --with-gmp=/usr/local/gmp-5.1.1&& make -j4 && make install ./configure --prefix=/usr/local/mpc-1.0 --with-gmp=/usr/local/gmp-5.1.1 --with-mpfr=/usr/local/mpfr-3.1.2&& make -j4 && make install
3、安裝GCC
//export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/mpc-1.0/lib:/usr/local/gmp-5.1.1/lib:/usr/local/mpfr-3.1.2/lib ./configure --prefix=/usr/local/gcc-4.8 --enable-threads=posix --disable-checking --disable-multilib --enable-languages=c,c++ --with-gmp=/usr/local/gmp-5.1.1 --with-mpfr=/usr/local/mpfr-3.1.2 --with-mpc=/usr/local/mpc-1.0 --with-gmp-lib=/usr/local/gmp-5.1.1/lib --with-mpfr-lib=/usr/local/mpfr-3.1.2/lib --with-mpc-lib=/usr/local/mpc-1.0/lib make -j4 && make install
4、做軟鏈接
ln -s /usr/local/gcc-4.8/bin/gcc /usr/bin/gcc ln -s /usr/local/gcc-4.8/bin/g++ /usr/bin/g++ cp /usr/local/mpc-1.0/lib/libmpc.so /usr/local/gmp-5.1.1/lib/libgmp.so /usr/local/mpfr-3.1.2/lib/libmpfr.so /lib64/
這時候用gcc來編譯C++程序會報缺少一些C++標准庫,這時候可以使用 gcc -lstdc++ 或直接使用 g++ 命令。
5、解決 GDB 的問題
升級 gcc 之后,編譯代碼加上 -g 選項,在使用 gdb 調試時,可能會出現 Missing separate debuginfos, use: debuginfo-install libgcc-4.4.7-3.el6.x86_64 這樣的錯誤,是因為只加 -g 選項,此時已經無法生成符號表了。解決方案有兩種:
◆ 編譯時加上 -gdwarf 選項試一下,如果還不行,再使用 -gdwarf-2 試試。
◆ 升級 gdb 版本,可升級至 7.6 ,正常的 ./configure && make && make install 之后,就可以正常使用了。如果 make 過程中出現 configure: error: no termcap library found 的錯誤,可以 yum install ncurses-devel 來解決。
預編譯頭:
gcc -x c++-header -c stdc++.h -o stdc++.h.gch -std=c++0x
6、當執行一個可執行文件時,如果報類似 `GLIBCXX_3.4.15' not found 的錯誤,是因為編譯這個可執行文件的GCC版本和當前環境中的GCC版本不同,可以選擇升級GCC版本,如下網頁:
http://blog.csdn.net/davidwang9527/article/details/19197511、
7、如果只是將GCC4.8.2編譯的可執行文件配置到一台LINUX機器上運行,並不需要安裝GCC4.8.2,只需要將原機器上的 /usr/local/gcc-4.8.2/lib64/libstdc++.so.6 文件復制到新機器的 /usr/lib64/libstdc++.so.6 即可(注意備份原文件)