GCC除錯基礎知識

mybwu_com發表於2014-02-19
The History of GCC

  --------------------------------------------------------------------------------

  1984年,Richard Stallman發起了自由軟體運動,GNU (Gnu's Not Unix)專案應運而生,3年後,最初版的GCC橫空出世,成為第一款可移植、可優化、支援ANSI C的開源C編譯器。

  GCC最初的全名是GNU C Compiler,之後,隨著GCC支援的語言越來越多,它的名稱變成了GNU Compiler Collection。

  這裡介紹的gcc是GCC的前端,C編譯器.

  警告資訊

  --------------------------------------------------------------------------------

  -Wall : 顯示所有常用的編譯警告資訊。

  -W : 顯示更多的常用編譯警告,如:變數未使用、一些邏輯錯誤。

  -Wconversion : 警告隱式型別轉換。

  -Wshadow : 警告影子變數(在程式碼塊中再次宣告已宣告的變數)

  -Wcast-qual :警告指標修改了變數的修飾符。如:指標修改const變數。

  -Wwrite-strings : 警告修改const字串。

  -Wtraditional : 警告ANSI編譯器與傳統C編譯器有不同的解釋。

  -Werror : 即使只有警告資訊,也不編譯。(gcc預設:若只有警告資訊,則進行編譯,若有錯誤資訊,則不編譯)

  C語言標準

  --------------------------------------------------------------------------------

  你可以在gcc的命令列中通過指定選項來選擇相應的C語言標準: 從傳統c到最新的GNU擴充套件C. 預設情況下, gcc使用最新的GNU C擴充套件.

  -ansi : 關閉GNU擴充套件中與ANSI C相牴觸的部分。

  -pedantic : 關閉所有的GNU擴充套件。

  -std=c89 : 遵循C89標準

  -std=c99 : 遵循C99標準

  -std=traditional : 使用原始C

  注意:後4個選項可以與-ansi結合使用,也可以單獨使用。

  可在gcc中使用大量GNU C擴充套件.

  生成特定格式的檔案

  --------------------------------------------------------------------------------

  以hello.c為例子,可以設定選項生成hello.i, hello.s, hello.o以及最終的hello檔案:

  hello.c : 最初的原始碼檔案;

  hello.i : 經過編譯預處理的原始碼;

  hello.s : 彙編處理後的彙編程式碼;

  hello.o : 編譯後的目標檔案,即含有最終編譯出的機器碼,但它裡面所引用的其他檔案中函式的記憶體位置尚未定義。

  hello / a.out : 最終的可執行檔案

  (還有.a(靜態庫檔案), .so(動態庫檔案), .s(彙編原始檔)留待以後討論)

  如果你不通過-o指定生成可執行檔名,那麼會預設生成a.out. 不指定生成檔名肯能覆蓋你上次生成的a.out.

  e.g.

  $ gcc hello.c

  在不給gcc傳遞任何引數的情況下, gcc執行預設的操作: 將原始檔編譯為目標檔案--> 將目標檔案連線為可執行檔案(名為a.out) --> 刪除目標檔案.

  -c生成.o檔案時,預設生成與原始碼的主幹同名的.o檔案。比如對應hello.c生成hello.o. 但也可在生成目標檔案時指定目標檔名(注意同時要給出.o字尾): $ gcc -c -o demo.o demo.c

  $ gcc -Wall -c hello.c : 生成hello.o

  $ gcc -Wall -c -save-temps hello.c : 生成hello.i, hello.s, hello.o

  注意-Wall 選項的使用場合:僅在涉及到編譯(即會生成.o檔案時,用-Wall)

  多檔案編譯、連線

  --------------------------------------------------------------------------------

  如果原檔案分佈於多個檔案中:file1.c, file2,c

  $ gcc -Wall file1.c file2.c -o name

  若對其中一個檔案作了修改,則可只重新編譯該檔案,再連線所有檔案:

  $ gcc -Wall -c file2.c

  $ gcc file1.c file2.o -c name

  注意:若編譯器在命令列中從左向右順序讀取.o檔案,則它們的出現順序有限制:含有某函式定義的檔案必須出現在含有呼叫該函式的檔案之後。好在GCC無此限制。

  編譯預處理

  --------------------------------------------------------------------------------

  以上述的hello.c為例, 要對它進行編譯預備處理, 有兩種方法: 在gcc中指定-E選項,或直接呼叫cpp.gcc的編譯預處理命令程式為cpp,比較新版本的gcc已經將cpp整合了,但仍提供了cpp命令. 可以直接呼叫cpp命令,也可以在gcc中指定-E選項指定它只進行編譯預處理.

  $ gcc -E hello.c == $ cpp hello.c

  上述命令馬上將預處理結果顯示出來. 不利於觀看. 可採用-c將預處理結果儲存:

  $ gcc -E -c hello.i hello.c == $ cpp -o hello.i hello.c

  注意, -c指定名稱要給出".i"字尾.

  另外, gcc針對編譯預處理提供了一些選項:

  (1) 除了直接在原始碼中用 #define NAME來定義巨集外,gcc可在命令列中定義巨集:-DNAME(其中NAME為巨集名),也可對巨集賦值: -DNAME=value 注意等號兩邊不能有空格!由於巨集擴充套件只是一個替換過程,也可以將value換成表示式,但要在兩邊加上雙括號: -DNAME="statement"

  e.g. $ gcc -Wall -DVALUE="2+2" tmp.c -o tmp

  如果不顯示地賦值,如上例子,只給出:-DVALUE,gcc將使用預設值:1.

  (2) 除了使用者定義的巨集外, 有一些巨集是編譯器自動定義的,它們以__開頭,執行: $ cpp -dM /dev/null, 可以看到這些巨集. 注意, 其中含有不以__開頭的非ANSI巨集,它們可以通過-ansi選項被禁止。

  檢視巨集擴充套件

  1, 執行 $ gcc -E test.c ,gcc對test.c進行編譯預處理,並立馬顯示結果. (不執行編譯) 2, 執行 $gcc -c -save-temps test.c ,不光產生test.o,還產生test.i, test.s,前者是編譯預處理結果,後者是彙編結果.

  利用Emacs檢視編譯預處理結果

  針對含有編譯預處理命令的程式碼,可以利用emacs方便地檢視預處理結果,而不需執行編譯,更為方便的是,可以只選取一段程式碼,而非整個檔案:

  1,選擇想要檢視的程式碼

  2,C-c C-e (M-x c-macro-expand)

  這樣,就自動在一個名為"Macroexpansion"的buffer中顯示pre-processed結果.

  生成彙編程式碼

  --------------------------------------------------------------------------------

  使用"-S"選項指定gcc生成以".s"為字尾的彙編程式碼:

  $ gcc -S hello.c

  $ gcc -S -o hello.s hello.c

  生成組合語言的格式取決於目標平臺. 另外, 如果是多個.c檔案, 那麼針對每一個.c檔案生成一個.s檔案.

包含標頭檔案在程式中包含與連線庫對應的標頭檔案是很重要的方面,要使用庫,就一定要能正確地引用標頭檔案。一般在程式碼中通過#include引入標頭檔案,如果標頭檔案位於系統預設的包含路徑(/usr/includes), 則只需在#include中給出標頭檔案的名字, 不需指定完整路徑.但若要包含的標頭檔案位於系統預設包含路徑之外, 則有其它的工作要做: 可以(在原始檔中)同時指定標頭檔案的全路徑.但考慮到可移植性,最好通過-I在呼叫gcc的編譯命令中指定。

  下面看這個求立方的小程式(陰影語句表示剛開始不存在):

  #include <stdio.h>

  #include <math.h>

  int main(int argc, char *argv[])

  {

  double x = pow (2.0, 3.0);

  printf("The cube of 2.0 is %f\n", x);

  return 0;

  }

  使用gcc-2.95來編譯它(-lm選項在後面的連線選項中有介紹, 這裡只討論標頭檔案的包含問題):

  $ gcc-2.95 -Wall pow.c -lm -o pow_2.95

  pow.c: In function `main':

  pow.c:5: warning: implicit declaration of function `pow'

  程式編譯成功,但gcc給出警告: pow函式隱式宣告。

  $ ./pow_2.95

  The cube of 2.0 is 1.000000

  明顯執行結果是錯誤的,在源程式中引入標頭檔案(#include <math.h>),消除了錯誤。

  不要忽略Warning資訊!它可能預示著,程式雖然編譯成功,但執行結果可能有錯。故,起碼加上"-Wall"編譯選項!並儘量修正Warning警告。

  搜尋路徑

  首先要理解 #include<file.h>和#include"file.h"的區別:

  #include<file.h>只在預設的系統包含路徑搜尋標頭檔案

  #include"file.h"首先在當前目錄搜尋標頭檔案, 若標頭檔案不位於當前目錄, 則到系統預設的包含路徑搜尋標頭檔案.

  UNIX類系統預設的系統路徑為:

  標頭檔案,包含路徑: /usr/local/include/ or /usr/include/

  庫檔案,連線路徑: /usr/local/lib/ or /usr/lib/

  對於標準c庫(glibc或其它c庫)的標頭檔案, 我們可以直接在原始檔中使用#include <file.h>來引入標頭檔案.

  如果要在原始檔中引入自己的標頭檔案, 就需要考慮下面的問題:

  1, 如果使用非系統標頭檔案, 標頭檔案和原始檔位於同一個目錄, 如何引用標頭檔案呢?

  ——我們可以簡單地在原始檔中使用 #include "file.h", gcc將當前目錄的file.h引入到原始檔. 如果你很固執,仍想使用#include <file.h>語句, 可以在呼叫gcc時新增"-I."來將當前目錄新增到系統包含路徑.細心的朋友可能會想到: 這樣對引用其它標頭檔案會不會有影響? 比如,#include<file.h>之後緊接著一個#include<math.h>, 它能正確引入math.h嗎?答案是: 沒有影響. 仍然能正確引用math.h. 我的理解是: "-I."將當前目錄作為包含路徑的第一選擇, 若在當前目錄找不到標頭檔案,則在預設路徑搜尋標頭檔案. 這實際上和#include"file.h"是一個意思.

  2, 對於比較大型的工程, 會有許多使用者自定義的標頭檔案, 並且標頭檔案和.c檔案會位於不同的目錄. 又該如何在.c檔案中引用標頭檔案呢?

  —— 可以直接在.c檔案中利用#include“/path/file.h", 通過指定標頭檔案的路徑(可以是絕對路徑,也可以是相對路徑)來包含標頭檔案. 但這明顯降低了程式的可移植性. 在別的系統環境下編譯可能會出現問題.所以還是利用"-I"選項指定標頭檔案完整的包含路徑.

  針對標頭檔案比較多的情況, 最好把它們統一放在一個目錄中, 比如~/project/include. 這樣就不需為不同的標頭檔案指定不同的路徑. 如果你嫌每次輸入這麼多選項太麻煩, 你可以通過設定環境變數來新增路徑:

  $ C_INCLUDE_PATH=/opt/gdbm-1.8.3/include

  $ export C_INCLUDE_PATH

  $ LIBRART_PATH=/opt/gdbm-1.8.3/lib

  $ export LIBRART_PATH

  可一次指定多個搜尋路徑,":"用於分隔它們,"."表示當前路徑,如:

  $ C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include

  $ LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib

  (可以新增多個路徑,路徑之間用:相隔,.代表當前目錄,若.在最前頭,也可省略)

  當然,若想永久地新增這些路徑,可以在.bash_profile中新增上述語句.

  3, 還有一個比較猥瑣的辦法: 系統預設的包含路徑不是/usr/include或/usr/local/include麼? 我把自己的標頭檔案拷貝到其中的一個目錄, 不就可以了麼? 的確可以這樣, 如果你只想在你自己的機器上編譯執行這個程式的話

  前面介紹了三種新增搜尋路徑的方法,如果這三種方法一起使用,優先順序如何呢?

  命令列設定 > 環境變數設定 > 系統預設

  與外部庫連線

  --------------------------------------------------------------------------------

  前面介紹瞭如何包含標頭檔案. 而標頭檔案和庫是息息相關的, 使用庫時, 要在原始碼中包含適當的標頭檔案,這樣才能宣告庫中函式的原型(釋出庫時, 就需要給出相應的標頭檔案).

  和包含路徑一樣, 系統也有預設的連線路徑:

  標頭檔案,包含路徑: /usr/local/include/ or /usr/include/

  庫檔案,連線路徑: /usr/local/lib/ or /usr/lib/

  同樣地, 我們想要使用某個庫裡的函式, 必須將這個庫連線到使用那些函式的程式中.

  有一個例外: libc.a或libc.so (C標準庫,它包含了ANSI C所定義的C函式)是不需要你顯式連線的, 所有的C程式在執行時都會自動載入c標準庫.

  除了C標準庫之外的庫稱之為"外部庫", 它可能是別人提供給你的, 也可能是你自己建立的(後面有介紹如何建立庫的內容).

  外部庫有兩種:(1)靜態連線庫lib.a

  (2)共享連線庫lib.so

  兩者的共同點:

  .a, .so都是.o目標檔案的集合,這些目標檔案中含有一些函式的定義(機器碼),而這些函式將在連線時會被最終的可執行檔案用到。

  兩者的區別:

  靜態庫.a : 當程式與靜態庫連線時,庫中目標檔案所含的所有將被程式使用的函式的機器碼被copy到最終的可執行檔案中.靜態庫有個缺點: 佔用磁碟和記憶體空間. 靜態庫會被新增到和它連線的每個程式中, 而且這些程式執行時, 都會被載入到記憶體中.無形中又多消耗了更多的記憶體空間.

  共享庫.so : 與共享庫連線的可執行檔案只包含它需要的函式的引用表,而不是所有的函式程式碼,只有在程式執行時,那些需要的函式程式碼才被拷貝到記憶體中, 這樣就使可執行檔案比較小,節省磁碟空間(更進一步,作業系統使用虛擬記憶體,使得一份共享庫駐留在記憶體中被多個程式使用).共享庫還有個優點: 若庫本身被更新,不需要重新編譯與它連線的源程式。

  靜態庫

  下面我們來看一個簡單的例子,計算2.0的平方根(假設檔名為sqrt.c):

  #include <math.h>

  #include <stdio.h>

  int

  main (void)

  {

  double x = sqrt (2.0);

  printf ("The square root of 2.0 is %f\n", x);

  return 0;

  }

  用gcc將它編譯為可執行檔案:

  $ gcc -Wall sqrt.c -o sqrt

  編譯成功,沒有任何警告或錯誤資訊。執行結果也正確。

  $ ./sqrt

  The square root of 2.0 is 1.414214

  下面我們來看看剛才使用的gcc版本:

  $ gcc --version

  gcc (GCC) 4.0.2 20050808 (prerelease) (Ubuntu4.0.1-4ubuntu9)

  現在我用2.95版的gcc把sqrt.c再編譯一次:

  $ gcc-2.95 -Wall sqrt.c -o sqrt_2.95

  /tmp/ccVBJd2H.o: In function `main':

  sqrt.c:(.text+0x16): undefined reference to `sqrt'

  collect2: ld returned 1 exit status

  編譯器會給出上述錯誤資訊,這是因為sqrt函式不能與外部數學庫"libm.a"相連。sqrt函式沒有在程式中定義,也不存在於預設C庫"libc.a"中,如果用gcc-2.95,應該顯式地選擇連線庫。上述出錯資訊中的"/tmp/ccVBJd2H.o"是gcc創造的臨時目標檔案,用作連線時用。

使用下列的命令可以成功編譯:

  $ gcc-2.95 -Wall sqrt.c /usr/lib/libm.a -o sqrt_2.95

  它告知gcc:在編譯sqrt.c時,加入位於/usr/lib中的libm.a庫(C數學庫)。

  C庫檔案預設位於/usr/lib, /usr/local/lib系統目錄中; gcc預設地從/usr/local/lib, /usr/lib中搜尋庫檔案。(在我的Ubuntu系統中,C庫檔案位於/urs/lib中。

  這裡還要注意連線順序的問題,比如上述命令,如果我改成:

  $ gcc-2.95 -Wall /usr/lib/libm.a sqrt.c -o sqrt_2.95

  gcc會給出出錯資訊:

  /tmp/cc6b3bIa.o: In function `main':

  sqrt.c:(.text+0x16): undefined reference to `sqrt'

  collect2: ld returned 1 exit status

  正如讀取目標檔案的順序,gcc也在命令列中從左向右讀取庫檔案——任何包含某函式定義的庫檔案必須位於呼叫該函式的目標檔案之後!

  指定庫檔案的絕對路徑比較繁瑣,有一種簡化方法,相對於上述命令,可以用下面的命令來替代:

  $ gcc-2.95 -Wall sqrt.c -lm -o sqrt_2.95

  其中的"-l"表示與庫檔案連線,"m"代表"libm.a"中的m。一般而言,"-lNAME"選項會使gcc將目標檔案與名為"libNAME.a"的庫檔案相連。(這裡假設使用預設目錄中的庫,對於其他目錄中的庫檔案,參考後面的“搜尋路徑”。)

  上面所提到的"libm.a"就是靜態庫檔案,所有靜態庫檔案的副檔名都是.a!

  $ whereis libm.a

  libm: /usr/lib/libm.a /usr/lib/libm.so

  正如前面所說,預設的庫檔案位於/usr/lib/或/usr/local/lib/目錄中。其中,libm.a是靜態庫檔案,libm.so是後面會介紹的動態共享庫檔案。

  如果呼叫的函式都包含在libc.a中(C標準庫被包含在/usr/lib/libc.a中,它包含了ANSIC所定義的C函式)。那麼沒有必要顯式指定libc.a:所有的C程式執行時都自動包含了C標準庫!(試試 $ gcc-2.95 -Wallhello.c -o hello)。

  共享庫

  正因為共享庫的優點,如果系統中存在.so庫,gcc預設使用共享庫(在/usr/lib/目錄中,庫檔案以共享和靜態兩種版本存在)。

  執行:$ gcc -Wall -L. hello.c -lNAME -o hello

  gcc先檢查是否有替代的libNAME.so庫可用。

  正如前面所說,共享庫以.so為副檔名(so == shared object)。

  那麼,如果不想用共享庫,而只用靜態庫呢?可以加上 -static選項

  $ gcc -Wall -static hello.c -lNAME -o hello

  它等價於:

  $ gcc -Wall hello.c libNAME.a -o hello

  $ gcc-2.95 -Wall sqrt.c -static -lm -o sqrt_2.95_static

  $ gcc-2.95 -Wall sqrt.c -lm -o sqrt_2.95_default

  $ gcc-2.95 -Wall sqrt.c /usr/lib/libm.a -o sqrt_2.95_a

  $ gcc-2.95 -Wall sqrt.c /usr/lib/libm.so -o sqrt_2.95_so

  $ ls -l sqrt*

  -rwxr-xr-x 1 zp zp 21076 2006-04-25 14:52 sqrt_2.95_a

  -rwxr-xr-x 1 zp zp 7604 2006-04-25 14:52 sqrt_2.95_default

  -rwxr-xr-x 1 zp zp 7604 2006-04-25 14:52 sqrt_2.95_so

  -rwxr-xr-x 1 zp zp 487393 2006-04-25 14:52 sqrt_2.95_static

  上述用四種方式編譯sqrt.c,並比較了可執行檔案的大小。奇怪的是,-static -lm 和 /lib/libm.a為什麼有區別?有知其原因著,懇請指明,在此謝謝了! :)

  如果libNAME.a在當前目錄,應執行下面的命令:

  $ gcc -Wall -L. hello.c -lNAME -o hello

  -L.表示將當前目錄加到連線路徑。

  利用GNU archiver建立庫

  $ ar cr libhello.a hello_fn.o by_fn.o

  從hello_fn.o和by_fn.o建立libihello.a,其中cr表示:creat & replace

  $ ar t libhello.a

  列出libhello.a中的內容,t == table

  (也可建立libhello.so)

  關於建立庫的詳細介紹,可參考本blog的GNU binutils筆記

  除錯

  --------------------------------------------------------------------------------

  一般地,可執行檔案中是不包含任何對原始碼的參考的,而debugger要工作,就要知道目標檔案/可執行檔案中的機器碼對應的原始碼的資訊(如:哪條語句、函式名、變數名...).debugger工作原理:將函式名、變數名,對它們的引用,將所有這些物件對應的程式碼行號儲存到目標檔案或可執行檔案的符號表中。

  GCC提供-g選項,將除錯資訊加入到目標檔案或可執行檔案中。

  $ gcc -Wall -g hello.c -o hello

  注意:若發生了段錯誤,但沒有core dump,是由於系統禁止core檔案的生成!

  $ ulimit -c  ,若顯示為0,則系統禁止了core dump

  解決方法:

  $ ulimit -c unlimited  (只對當前shell程式有效)

  或在~/.bashrc 的最後加入: ulimit -c unlimited (一勞永逸)

  優化

  --------------------------------------------------------------------------------

  GCC具有優化程式碼的功能,程式碼的優化是一項比較複雜的工作,它可歸為:原始碼級優化、速度與空間的權衡、執行程式碼的排程。

  GCC提供了下列優化選項:

  -O0 : 預設不優化(若要生成除錯資訊,最好不優化)

  -O1 : 簡單優化,不進行速度與空間的權衡優化;

  -O2 : 進一步的優化,包括了排程。(若要優化,該選項最適合,它是GNU釋出軟體的預設優化級別;

  -O3 : 雞肋,興許使程式速度更慢;

  -funroll-loops : 展開迴圈,會使可執行檔案增大,而速度是否增加取決於特定環境;

  -Os : 生成最小執行檔案;

  一般來說,除錯時不優化,一般的優化選項用-O2(gcc允許-g與-O2聯用,這也是GNU軟體包釋出的預設選項),embedded可以考慮-Os。

  注意:此處為O!(非0或小寫的o,-o是指定可執行檔名)。

  檢驗優化結果的方法:$ time ./prog

  time測量指定程式的執行時間,結果由三部分組成:

  real : 程式總的執行時間, 它和系統負載有關(包括了程式排程,切換的時間)

  user: 被測量程式中使用者指令的執行時間

  sys : 被測量程式中核心代使用者指令執行的時間

  user和sys的和被稱為CPU時間.

  注意:對程式碼的優化可能會引發警告資訊,移出警告的辦法不是關閉優化,而是調整程式碼。