GCC(GNU Compiler Collection,GNU 編譯器套件)是由 GNU 開發的編程語言編譯器,支持C、C++、Objective-C、Fortran、Java、Ada和Go語言等多種預言的前端,以及這些語言的庫(如libstdc++、libgcj等等),它是以 GLP 許可證所發行... ...
目錄
導語
GCC
(GNU Compiler Collection
,GNU 編譯器套件) 是由 GNU 開發的編程語言編譯器,支持C、C++、Objective-C、Fortran、Java、Ada和Go語言等多種預言的前端,以及這些語言的庫(如libstdc++、libgcj等等),它是以 GLP 許可證所發行的自由軟體,也是 GNU 計劃的關鍵部分。GCC 原本作為GNU操作系統的官方編譯器,現已被大多數類 Unix 操作系統(如Linux、BSD、Mac OS X 等)採納為標準的編譯器,GCC同樣適用於微軟的Windows 。
本文主要記錄 GCC 學習使用過程中的一些操作,便於後期使用時查閱,前期入門階段主要參考An Introduction to GCC
。
1 編譯C程式
1.1 單個源文件
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
編譯源代碼:
$ gcc helloworld.c
未指定編譯輸出文件名則預設輸出 a.out
。
$ gcc -Wall helloworld.c -o helloworld
-o: output 該選項用於指定存儲機器碼的輸出文件;
-Wall: Warning all 該選項用於打開所有最常用的編譯警告;
$ ./helloworld
Hello, world!
1.2 多個源文件
如果將源碼分為多個源文件,我們也可以使用以下命令進行編譯:
$ gcc -Wall hello.c world.c hello_world.c -o helloworld
對於包含多個源文件的工程,我們可以將編譯過程分為兩個階段:
第一階段:所有
.c
.h
源文件經過編譯,分別生成對應的.o
對象文件;
第二階段:所有.o
對象文件經過鏈接生成最終的可執行文件。
.o
對象文件包含機器碼,任何對其他文件中的函數或變數的記憶體地址的引用都留著沒有被解析,這些都留給鏈接器 GNU ld
生成可執行文件時再處理。
源文件生成對象文件
$ gcc -Wall -c hello.c world.c hello_world.c
註意:這裡不需要使用
-o
來指定輸出文件名,-c
選項會自動生成與源文件同名的對象文件對象文件生成可執行文件
$ gcc hello.o world.o hello_world.o -o hello_world
註意:這裡不需要使用者
-Wall
,因為源文件已經成功編譯成對象文件了,鏈接是一個要麼成功要麼失敗的過程。
通常,鏈接快於編譯,因此,對於大型項目,將不同功能在不同的源文件中進行實現,在修改功能時,可以只編譯被修改的文件,顯著節省時間。
1.3 靜態庫文件
編譯生成的 .o
對象文件可以通過歸檔器 GNU ar
打包成 .a
靜態庫文件,將某些功能提供給外部使用。在上述多個源文件的例子當中,我們可以將 hello.o
和 world.o
打包成靜態庫文件:
$ ar cr libhello.a hello.o world.o
這樣便生成了 .a
庫文件。在使用的時候,編譯器需要通過路徑找到對應的庫文件,而標準的系統庫,通常能在 /usr/lib
和 /lib
目錄下找到,自己工程中生成的庫文件的位置需要通過編譯選項告知給編譯器。
一種直接的方式是在編譯命令中通過絕對路徑或者相對路徑指定靜態庫的位置,例如:
$ gcc -Wall hello_world.c ./libhello.a -o helloworld
實際上,為了避免使用長路徑名,我們可以使用 -l
來鏈接庫文件,例如:
$ gcc -Wall -L./ hello_world.c -lhello -o helloworld
兩種方式的效果應當是一致的,這裡 -L
選項指示庫文件的位置位於 ./
,而 -lhello
選項會指示編譯器試圖鏈接 ./
目錄下的文件名為 libhello.a
的靜態庫文件。
2 編譯選項
2.1 庫文件
外部庫文件包含2種類型:靜態庫和共用庫。
靜態庫文件格式為
.a
,文件包含對象文件的機器碼,鏈接靜態庫文件時,鏈接器將程式用到的外部函數的機器碼從靜態庫文件中提取出來,並複製到最終的可執行文件當中。
共用庫文件格式為.so
,表示 shared object ,使用共用庫的可執行文件僅僅包它含用到的函數的表格,而不是外部函數所在對象文件的整個機器碼。共用庫在使用時需要先於可執行文件載入到記憶體,並支持同時被多個程式共用,這個過程稱為動態鏈接( dynamic linking )。
相比於靜態庫,共用庫具備如下優點:
- 減少可執行程式文件大小;
- 多個程式共用;
- 庫文件升級無需重新編譯可執行程式
2.2 搜索路徑
在編譯過程中,如果找不到頭文件會報錯,預設情況下,GCC會在下麵的目錄中搜索頭文件,這些路徑稱為 include路徑
:
/usr/local/include/
/usr/include/
同理,在鏈接過程中,如果找不到庫文件也會報錯,預設情況下,GCC在下麵的目錄中搜索庫文件,這些路徑稱為 鏈接路徑
:
/usr/local/lib/
/usr/lib/
如果需要檢索其他路徑下的頭文件或者庫文件,可以通過 -I
和 -L
的方式來分別擴展頭文件和庫文件的搜索路徑,這裡介紹2種搜索路徑的設置方法:
命令行選項
$ gcc -Wall [-static] -I<INC_PATH> -L<LIB_PATH> <INPUT_FILES> -l<INPUT_LIBS> -O <OUTPUT_FILES>
在前面生成的靜態庫文件的基礎上,我們可以進一步生成最終的可執行文件:
$ gcc -Wall [-static] -I. -L. hello_world.c -lhello -o helloworld
上述命令,
-I
將指定頭文件位於當前路徑.
,-L
將指定庫文件位於當前路徑.
,-lhello
指定參與編譯的自定義的庫文件。需要註意的是,gcc 編譯器在使用
-l
選項時會預設優先鏈接到共用庫文件,如果確認使用靜態庫,則可以使用-static
選項進行限制。環境變數
我們還可以在 shell 登錄文件(例如
.bashrc
)中,預先擴展可能用到的頭文件目錄和庫文件目錄,這樣,每次登錄shell時,將會自動設置他們。對於C頭文件路徑,我們有環境變數
C_INCLUDE_PATH
,對於C++頭文件路徑,我們有環境變數CPP_INCLUDE_PATH
。 例如:$ C_INCLUDE_PATH=./ $ export C_INCLUDE_PATH
對於靜態庫文件,我們有環境變數
LIBRARY_PATH
:$ LIBRARY_PATH=./ $ export LIBRARY_PATH
對於共用庫文件,我們有環境變數
LD_LIBRARY_PATH
:$ LD_LIBRARY_PATH=./ $ export LD_LIBRARY_PATH
上述目錄經過環境變數指定後,將在標準預設目錄之前搜索,後續編譯過程也無需在編譯命令中指定了。上面的編譯指令也可以進一步簡化:
$ gcc -Wall hello_world.c -lhello -o helloworld
對於多個搜索目錄,我們可以遵循標準Unix搜索路徑的規範,在環境變數中用冒號分割的列表進行表示:
DIR1:DIR2:DIR3: ...
DIR
可以用單個點.
指示當前目錄。舉個例子:$ C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include $ LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib
如果環境變數中已經包含路徑信息,則可以用以下語法進行擴展:
$ C_INCLUDE_PATH= NEWPATH:$C_INCLUDE_PATH $ LIBRARY_PATH= NEWPATH:$LIBRARY_PATH $ LD_LIBRARY_PATH= NEWPATH:$LD_LIBRARY_PATH
搜索順序
方式2和方式3本質上是同一種方法的不同表現方式。當環境變數和命令行選項被同時使用時,編譯器將按照下麵的順序搜索目錄:
- 從左到右搜索由命令行
-I
和-L
指定的目錄; - 由環境變數
C_INCLUDE_PATH
LIBRARY_PATH
指定的目錄; - 預設的系統目錄。
- 從左到右搜索由命令行
2.3 C語言標準
預設情況下, gcc 編譯程式時使用的是GNU C
語法規則,而非 ANSI/ISO C
標準語法規則,GNU C
在 ANSI/ISO C
標準語法基礎上增加了一些對C語言的擴展功能,因此標準 C 源碼在 GCC 下一般來說是無需修改即可編譯的。
同時,GCC也提供了對 C 語言標準的控制選項,用以解決不同語法規則之間的衝突問題,最常用的是 -ansi
、 -pedantic
和 -std
。
-ansi:禁止與ANSI/ISO標準衝突的GNU擴展特性,包括對GNU C標準庫
glibc
的支持;
-pedantic:禁止與ANSI/ISO標準不符的GNU擴展特性,更加嚴格。
-std:
-ansi:相容
ANSI/ISO
標準一個合法的
ANSI/ISO C
程式,可能無法相容GNU C
的擴展特性,可以通過-ansi
選項禁用那些與ANSI/ISO C
標準衝突的 GNU 擴展,即令 GCC 編譯器以相容ANSI/ISO C
標準的方式編譯程式。例如:#include <stdio.h> int main(void) { const char asm[] = "6502"; printf("the staring asm is '%s'\n", asm); return 0; }
這裡,變數名
asm
在ANSI/ISO
標準中是合法的,但asm
在GNU
擴展中是關鍵詞,用於指示 C 函數中混編的彙編指令,直接編譯會出現錯誤:$ gcc -Wall ansi.c -o ansi
使用
-ansi
選項後,即以ANSI/ISO C
標準編譯,可成功編譯。$ gcc -Wall -ansi ansi.c -o ansi
與
asm
類似的關鍵詞包括:inline
、typeof
、unix
、vax
等等,更多細節參考GCC參考手冊 “Using GCC”。-ansi
選項還會同時關閉 GNU C庫,對於 GNU C 庫中特有的變數、巨集定義、函數介面的調用都會出現未定義錯誤, GNU C 庫對外提供了一些功能特性的巨集開關,可以打開部分特性,例如,POSIX擴展(*_POSIX_C_SOURCE),BSD擴展(_BSD_SOURCE),SVID擴展(_SVID_SOURCE),XOPEN擴展(_XOPEN_SOURCE)和GNU擴展(_GNU_SOURCE*)。舉個例子,下麵的預定義
M_PI
是 GNU C庫math.h
的一部分,不在ANSI/ISO C
標準庫中。#include <math.h> #include <stdio.h> int main(void) { printf("the value of pi is %f\n",M_PI); return 0; }
如果強制使用
-ansi
編譯會出現未定義錯誤。$ gcc -Wall -ansi pi.c -o pi
如果一定需要使用GNU C庫巨集定義,可以單獨打開對GNU C庫的擴展。
$ gcc -Wall -ansi -D_GNU_SOURCE pi.c
這裡*_GNU_SOURCE* 巨集打開所有的擴展,而 POSIX 擴展在這裡如果與其他擴展有衝突,則優先於其他擴展。有關特征測試巨集進一步信息可以參見 GNU C 庫參考手冊。
-pedantic:嚴格的
ANSI/ISO
標準同時使用
-ansi
-pedantic
選項,編譯器將會以更加嚴格的標準檢查語法規則是否符合ANSI/ISO C
標準,同時拒絕所有GNU C
擴展語法規則。下麵是一個用到變長數組的程式,變長數組是
GNU C
擴展語法,但也不會妨礙合法的ANSI/ISO
程式的編譯。int main(int argc, char *argv[]) { int i, n = argc; double x[n]; for (i = 0; i < n; i++) x[i] = i; return 0; }
因此,使用
-ansi
不會出現相關編譯錯誤:$ gcc -Wall -ansi gnuarray.c -o gnuarray
但是,使用
-ansi -pedantic
編譯,會出現違反 ANSI/ISO 標準的警告。$ gcc -Wall -ansi -pedantic gnuarray.c -o gnuarray
-std:指定標準
可以通過
-std
選項來控制 GCC 編譯時採用的C語言標準。支持的可選項包括:-std=c89
-std=iso9899:1990
-std=iso9899:199409
-std=c99
-std=iso9899:1999
-std=gnu89
-std=gnu99
2.4 編譯警告
-Wall
-Wall 警告選項本身會打開很多常見錯誤的警告,這些錯誤通常總是有問題的代碼構造,或是很容易用明白無誤的方法改寫的錯,因此可以看作潛在嚴重問題的指示。這些錯誤主要包括:
-Wcomment:對嵌套註釋進行警告;
/* commented out double x = 1.23; /* x-position*/ */
- -Wformat:對格式化字元串與對應函數參數的類型一致性進行警告;
- -Wunused:對聲明但未使用的變數進行警告;
- -Wimplicit:對未聲明就被使用的變數進行警告;
-Wreturn-type:對函數聲明返回類型與實際返回類型的一致性進行警告;
int main(void) { printf("hello, world!\n"); return; }
-Wall
包含的警告選項都可以在GCC參考手冊Using GCC
中找到。
其他警告
GCC提供了很多可選的警告選項,它們沒有包含在
-Wall
中,但仍然很有參考價值。這些警告選項可能會對合法代碼也報警,所以編譯時通常不需要長期開啟,建議周期性的使用,檢查輸出結果,或在某些程式和文件中打開,更加合適。- -W:對常見的編程錯誤進行報警,類似
-Wall
,也常和-Wall
一起用。 - -Wconversion:對可能引起意外結果的隱式類型轉換進行報警。
- -Wshadow:對重覆定義同名變數進行報警。
- -Wcast-qual:對可能引起移除修飾符特性的操作進行報警。
- -Wwrite-strings:該選項隱含的使得所有字元串常量帶有
const
修飾符。 - -Wtraditional:對那些在 ANSI/ISO 編譯器下和在 ANSI 之前的“傳統”譯器下編譯方式不同的代碼進行警告。
- -Werror:將警告轉換為錯誤,一旦警告出現即停止編譯。
- -W:對常見的編程錯誤進行報警,類似
警告選項會產生診斷性的信息,但不會終止編譯過程,如果需要出現警告後停止編譯過程可以使用 -Werror
。
3 預處理選項
3.1 巨集定義
這裡主要介紹GNU C預處理器中巨集定義的常見用法。首先,看一個巨集定義的例子:
#include <stdio.h>
int main(void)
{
#ifdef TESTNUM
printf("TestMum is %d\n",TESTNUM);
#endif
#ifdef TESTMSG
printf("TestMsg:%s\n",TESTMSG);
#endif
printf("Runing...\n");
return 0;
}
如果在編譯命令中不加任何巨集定義選項,則編譯器會在預處理階段忽略 TESTNUM
巨集定義包裹的代碼:
$ gcc -Wall dtest.c -o dtest
$ ./dtest
Runing...
如果在編譯中增加 -D
選項,則編譯器會在預處理階段將 TESTNUM
巨集定義包裹的代碼進行編譯:
$ gcc -Wall -DTESTNUM dtest.c -o dtest
$ ./dtest
TestNum is 1
Runing...
如果對巨集定義進行巨集賦值,則編譯器會在預處理階段將賦值內容替換到 TESTNUM
巨集定義位置:
$ gcc -Wall -DTESTNUM=20 dtest.c -o dtest
$ ./dtest
TestNum is 20
Runing...
利用命令行上的雙引號,巨集可以被定義成字元串,字元串可以包含引號,需要用 \
進行轉義:
$ gcc -Wall -DTESTMSG="\"Hello,World!\"" dtest.c -o dtest
$ ./dtest
Hello,World!
Runing...
上述字元串也可以定義成空值,例如:-DTESTMSG=""
,這樣的巨集還是會被 #ifdef
看作已定義,但該巨集會被展開為空。
3.2 預處理輸出
使用 -E
選項,GCC 可以只允許運行預處理器,並直接顯示預處理器對源代碼的處理結果,並且不會進行後續的編譯處理流程:
$ gcc -DTESTMSG="\"Hello,World!\"" -E dtest.c
預處理器會對巨集文件進行直接替換,並對頭文件進行展開,預處理器還會增加一些以 #line-number "source-file"
形式記錄源文件和行數,便於調試和編譯器輸出診斷信息。
被預處理的系統頭文件通常產生許多輸出,它們可以被重定向到文件中,或者使用 -save-temps
選項進行保存:
$ gcc -c -save-temps dtest.c
運行該命令之後,預處理過的輸出文件將被存儲到 .i
文件中,同時還會保存 .s
彙編文件和 .o
對象文件。
3.3 調試信息
通常,編譯器輸出的可執行文件只是一份作為機器碼的指令序列,而不包含源程式中的任何引用信息,例如變數名或者行號等,因此如果程式出現問題,我們將無法確定問題在哪裡。
添加調試信息
GCC 提供
-g
選項,可以在編譯生成可執行文件時添加另外的調試信息,這些信息可以在追蹤錯誤時從特定的機器碼指令對應到源代碼文件中的行,調試器可以在程式運行時檢查變數的值。$ gcc -Wall -g helloworld.c -o helloworld
檢查core文件
程式異常退出時,操作系統將程式崩潰瞬間的記憶體狀態寫入到
core
文件,結合-g
選項生成的符號表中的信息,可以進一步確定程式崩潰時運行到的位置和此刻變數的值。但是,通常情況下操作系統配置在預設情況是下不寫
core文件
的,在開始之前,我們可以先查詢core文件
的最大限定值:$ ulimit -c
如果結果為0,則不會生成
core文件
,我們可以擴大core文件
上限,以便允許任何大小的core 文件
。$ ulimit -c unlimited
這裡,再準備一個包含非法記憶體錯誤的簡單程式,我們用它來生成
core
文件:int a(int *p) { int y = *p; return y; } int main(void) { int *p = 0; return a(p); }
編譯生成帶調試信息的可執行文件:
$ gcc -g null.c $ ./a.out Segmentation fault (core dumped)
根據可執行文件和
core文件
即可利用gdb
進行調試,定位錯誤位置:$ gdb a.out core
回溯堆棧
利用
gdb
的backtrace
命令可以方便的顯示當前執行點的函數調用及其參數,並且利用up
down
命令在堆棧的不同層級之間移動,檢查變數變化。gdb
相關操作可以參考“Debugging with GDB: The GNU Source-Level Debugger”
。
4 優化選項
編譯器的優化目標通常是 提高代碼的執行速度
或者 減少代碼體積
。
4.1 源碼級優化
公共子表達式消除
在優化功能打開之後,編譯器會自動對源代碼進行分析,使用臨時變數對多次重用的計算結果進行替代,減少重覆計算。例如:
x = cos(v)*(l+sin(u/2)) + sin(w)*(l-sin(u/2))
可以用臨時變數
t
替換sin(u/2)
:t=sin(u/2) x = cos(v)*(l+t) + sin(w)*(l-t)
函數內嵌
函數調用過程中,需要花費一定的額外時間來實施調用過程(壓棧、跳轉、返回執行點等),而函數內嵌優化會將計算過程簡單但是調用頻繁的函數調用直接用函數體進行替換,提升那些被頻繁調用函數的執行效率。例如:
double sq(double x) { return x * x; } int main(void) { double sum; for (int i = 0; i < 1000000; i++) { sum += sq(i + 5); } }
經過嵌入優化後,大致會得到:
int main(void) { double sum; for (int i = 0; i < 1000000; i++) { double t = (i + 5); sum += t * t; } }
GCC 會使用一些啟髮式的方法選擇哪些函數要內嵌,比如函數要適當小。另外,嵌入優化方式只在單個對象文件基礎上實施。關鍵字
inline
可以顯示要求某個指定函數在用到的地方儘量內嵌。
4.2 速度-空間優化
編譯器會根據指定的優化條件,對可執行文件的執行速度和空間進行折中優化,使得最終結果可能會犧牲一些執行速度來節省文件大小,也可能會犧牲文件的空間占用來提升運行速度,或是在兩者之間取得一定平衡。
迴圈展開 即是一種以常見的空間換時間的優化方式,例如:
for(i = 0; i < 4; i++)
{
y[i] = i;
}
直接將該迴圈展開後進行直接賦值,可以有效減少迴圈條件的判斷,減少運行時間:
y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;
對於支持並行處理的處理器,經過優化後的代碼可以使用並行運行,提高速度。對於未知邊界的迴圈,例如:
for(i = 0; i < n; i++)
{
y[i] = i;
}
可能會被編譯器優化成這樣:
for(i = 0; i < (n % 2); i++)
{
y[i] = i;
}
for(; i + 1 < n; i += 2)
{
y[i] = i;
y[i+1] = i+1;
}
上面第二個迴圈中的操作即可進行並行化處理。
4.3 指令調度優化
指令化調度是最底層的優化手段,由編譯器根據處理器特性決定各指令的最佳執行次序,以獲取最大的並行執行,指令調度沒有增加可執行文件大小,但改善了運行速度,對應的代價主要體現在編譯過程所需處理時間,以及編譯器占用的記憶體空間。
4.4 優化級別選項
GCC 編譯器為了生成滿足速度和空間要求的可執行文件,對優化級別使用 -O
選項進行定義。
-O0
或不指定-O
選項(預設)
不實施任何優化,源碼被儘量直接轉換到對應指令,編譯時間最少,適合調試使用。-O1
或-O
打開那些不需要任何速度-空間折衷的最常見形式的優化,對代碼大小和執行時間進行優化,生成的可執行文件更小、更快,編譯時間較少。-O2
在上一級優化的基礎上,增加指令調度優化,文件大小不會增加,編譯時間有所增加,它是各種 GNU 軟體發行包的預設優化級別。-O3
在上一級優化的基礎上,增加函數內嵌等深度優化,提升可執行文件的速度,但會增加它的大小,這一等級的優化可能會產生不利結果。-Os
該選項主要針對記憶體和磁碟空間受限的系統生成儘可能小的可執行文件。-funroll-loops
該選項獨立於上述優化選項,可以打開迴圈展開,增加可執行文件大小。
通常,開發調試過程可以使用
-O0
,開發部署時可以用-O2
,優化等級也不是越多越好、越高越好,需要儘量根據程式差異和使用平臺的差異經過測試數據確定。
4.5 優化和編譯警告
開啟優化後,作為優化過程的一部分,編譯器檢查所有變數的使用和他們的初始值,稱為 數據流分析 。數據流分析的一個作用是檢查是否使用了未初始化的變數,在開啟優化之後,-Wall
中的 -Wuninitialized
選項會對未初始化變數的讀操作產生警告。因此,開啟優化後,GCC 會輸出一些額外的警告信息,而這些信息在不開啟優化時是不會產生的。
5 編譯過程
這一部分主要介紹 GCC 怎麼把源文件轉變成可執行文件。編譯過程是一個多階段的過程,涉及到多個工具,包括 GNU 編譯器(gcc 或 g++ 前端),GNU彙編器 as
,GNU 鏈接器 ld
,編譯過程中用到的整套工具被稱為工具鏈。
5.1 預處理過程
預處理過程是利用預處理器 cpp
來擴展巨集定義和頭文件,GCC 執行下麵的命令來實施這個步驟:
$ cpp hello.c > hello.i
該命令可以輸出經過預處理器處理輸出的源文件 hello.i
。
5.2 編譯過程
編譯過程是編譯器把預處理的源代碼經過翻譯處理成特定處理器的彙編語言,命令行 -S
選項可以將預處理過的 .i
源文件轉變成 .s
彙編文件。
$ gcc -Wall -S hello.i
該命令可以輸出經過編譯器處理輸出的彙編文件 hello.s
。
5.3 彙編過程
彙編過程是彙編器 as
把編譯處理的彙編文件轉變成機器碼,並生成對象文件,如果彙編文件中包含外部函數的調用,彙編器會保留外部函數的地址處於未定義狀態,留給後面的鏈接器填寫。
$ as hello.s -o hello.o
這裡, -o
選項用來指定輸出 .o
文件。
5.4 鏈接過程
鏈接過程是鏈接器 ld
將各對象文件鏈接到一起,生成可執行文件。在鏈接過程中,鏈接器會將彙編輸出的 .o
文件和系統中的 C 運行庫中必要的外部函數鏈接到一起。
$ gcc hello.o
鏈接器主要調用 ld
命令,也可以直接把對象文件與C標準庫鏈接,生成可執行文件。
6 編譯工具
6.1 歸檔工具 ar
GNU 歸檔工具 ar
用於把多個對象文件組合成歸檔文件,也被稱為庫,歸檔文件是多個對象文件打包在一起發行的簡便方法。
在上面的多個源文件例子中,假設有 hello.c
world.c
hello_world.c
三個程式, 我們可以現將三者編譯成對象文件:
$ gcc -Wall -c hello.c
$ gcc -Wall -c world.c
$ gcc -Wall -c hello_world.c
生成 hello.o
world.o
hello_world.o
,我們將兩個子函數打包成靜態文件庫:
$ ar cr libhello.a hello.o world.o
選項 cr
不需要 -
,代表 creat and replace
,libhello.a
為目標文件,hello.o
world.o
表示輸入文件。
也可以通過 t
選項,查看庫文件中包含的文件:
$ ar t libhello.a
hello.o
world.o
再利用 libhello.a
和 hello_world.o
來鏈接生成可執行文件:
$ gcc -Wall hello_world.o libhello.a -o hello
$ ./hello
Hello, world
或者使用 -l
選項:
$ gcc -Wall -L. hello_world.o -lhello -o hello
$ ./hello
Hello, world
6.2 性能剖析器 gprof
GNU 性能剖析器 gprof
是衡量程式性能的有用工具,它可以記錄每個函數調用的次數和每個函數每次調用所花的時間。
這裡準備了一個數學上的 Collatz
猜想程式,我們用 gprof
來對其進行分析:
#include <stdio.h>
unsigned int step(unsigned int x)
{
if(x % 2 == 0)
{
return (x / 2);
}
else
{
return (3 * x + 1);
}
}
unsigned int nseq(unsigned int x0)
{
unsigned int i = 1, x;
if(x0 == 1 || x0 == 0)
return i;
x = step(x0);
while(x != 1 && x != 0)
{
x = step(x);
i++;
}
return i;
}
int main(void)
{
unsigned int i, m = 0, im = 0;
for(i = 1; i < 500000; i++)
{
unsigned int k = nseq(i);
if(k > m)
{
m = k;
im = i;
printf("sequence length = %u for %u\n", m, im);
}
}
return 0;
}
為了剖析性能,程式在編譯時需要用到 -pg
選項參與編譯鏈接:
$ gcc -Wall -c -pg collatz.c
$ gcc -Wall -pg collatz.o
這樣即可生成可分析的可執行文件,其包含有記錄每個函數所花時間的額外指令。
為了進行分析,需要先正常運行一次可執行文件:
$ ./a.out
運行結束後,會在本目錄下生成一個 gmon.out
文件。再以可執行文件名作為參數運行 gprof
就可以分析這些數據:
% | cumulative | self | self | total | ||
---|---|---|---|---|---|---|
time | seconds | seconds | calls | ns/call | ns/call | name |
50.00 | 0.13 | 0.13 | 499999 | 260.00 | 500.00 | nseq |
46.15 | 0.25 | 0.12 | 62135400 | 1.93 | 1.93 | step |
3.85 | 0.26 | 0.01 | frame_dummy |
剖析數據的第一列顯示的是該程式的所有子函數的運行時間。
6.3 代碼覆蓋測試工具 gcov
GNU 代碼覆蓋測試工具 gcov
可以用於分析程式運行期間每一行代碼執行的次數,因此可以用於查找沒有用到的代碼區域。
我們準備下麵這個小程式來展示 gcov
的功能。
#include <stdio.h>
int main(void)
{
int i;
for(i = 1; i < 10; i++)
{
if(i % 3 == 0)
printf("%d is divisible by 3\n",i);
if(i % 11 == 0)
printf("%d is divisible by 11\n",i);
}
return 0;
}
為了對該程式進行代碼覆蓋測試,編譯時必須攜帶 –fprofile-arcs
和 –ftest-coverage
選項:
$ gcc -Wall -fprofile-arcs -ftest-coverage cov.c
其中,–fprofile-arcs
用於添加計數被執行到的行的次數,而 –ftest-coverage
被用與合併程式中每條分支中的用於計數的代碼。可執行程式只有在運行後才能生成代碼覆蓋測試數據:
$ ./a.out
以 .c
源文件為參數調用 gov
命令,命令會生成一個原始源碼文件的帶註釋信息的版本,其尾碼名為 gcov
,包含執行到的每一行代碼的運行次數,沒有執行到的行數被用 ######
標記上,根據註釋信息就可以看到該源文件的覆蓋情況。
7 文件信息
7.1 辨識文件
對於一個可執行命令執行 file
命令可以查看該文件的編譯環境信息。
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=121d574fcb968c6a83624f4d982eb74495951841, not stripped
下麵是輸出信息的解釋:
ELF
:可執行文件的內部格式,ELF表示Executable and Linking Format
,另外的格式還有COFF(Common object File Format)
。32-bit
:表示字的位寬,另外的位寬還有64-bit
。LSB
:表示該文件的大小端方式。Intel 80386
:表示該文件適用的處理器。version 1 (SYSV)
:表示文件內部格式的版本dynamically linked
:表示文件會用到的共用庫,另外的還有statically linked
表示程式是靜態鏈接的,比如用到-static
選項 。not stripped
:表示可執行文件包含符號表。
7.2 符號映射表
符號映射表存儲了函數和命令變數的位置,用 nm
命令可以看到內容:
$ nm a.out
0804a01c B __bss_start
0804a01c b completed.7200
0804a014 D __data_start
0804a014 W data_start
……
0804840b T main
U puts@@GLIBC_2.0
08048380 t register_tm_clones
08048310 T _start
0804a01c D __TMC_END__
08048340 T __x86.get_pc_thunk.bx
其中,T
表示這是定義在對象文件中的函數,U
表示這是本對象文件中沒有定義的函數(在其他對象文件中找到了)。
nm
命令最常用的用法是通過查找 T
項對應的函數名,檢查某個庫是否包含特定函數的定義。
7.3 動態鏈接庫
當程式用到 .so
動態鏈接庫時,需要在運行期間動態載入這些庫。 ldd
命令可以列出所有可執行文件依賴的共用庫文件。
$ ldd a.out
linux-gate.so.1 => (0xb7749000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7578000)
/lib/ld-linux.so.2 (0x80017000)
ldd
命令也能夠用於檢查共用庫本身,可以跟蹤共用庫依賴鏈。
參考資料
- An Introduction to GCC
- Using GCC
- Debugging with GDB: The GNU Source-Level Debugger
- GNU Make: A Program for Directing Recompilation
- The GNU C Library Reference Manual
- The GNU Bash Reference Manual