GCC調試基礎知識

來源:互聯網
上載者:User
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) (Ubuntu 4.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時間.

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.