1. 程式人生 > >學c之gcc編譯過程及其常用編譯選項

學c之gcc編譯過程及其常用編譯選項

上篇文章,知道了,C程式碼編譯後存放在記憶體中的位置,那麼C程式碼的整個編譯過程又是怎樣的呢?一條命令gcc hello.c就可以編譯成可執行程式a.out,然後./a.out之後就可以執行hello.c這個程式的程式碼了。下面的文章分析的不錯,就整理了下。

hello.c:

1.   #include<stdio.h>

2.   int main()

3.   {

4.   printf(“Hello World\n”);

5.   return 0;

6.   }

#include<stdio.h>

int main()

{

        printf(“Hello World\n”);

        return 0;

}

實際上gcc hello.c可以分解為4個步驟,分別是預處理(Preprocess),編譯(Compilation),彙編(Assembly)和連結(Linking)。

一、預處理

預處理過程主要讀取c源程式,對偽指令和特殊符號進行處理。包括巨集,條件編譯,包含的標頭檔案,以及一些特殊符號。基本上是一個replace的過程。

1.   gcc –E hello.c –o hello.i

gcc –E hello.c –o hello.i

以下為預處理後的輸出檔案hello.i的內容

1.   # 1"hello.c"

2.   # 1"<built-in>

"

3.   # 1"<command-line>"

4.   # 1"hello.c"

5.   # 1 "/usr/include/stdio.h"1 3 4

6.   # 28"/usr/include/stdio.h" 3 4

7.   /***** 省略了部分內容,包括stdio.h中的一些宣告及定義 *****/

8.   # 2"hello.c" 2

9.   int main()

10.  {

11.  printf("Hello World\n");

12.  return 0;

13.  }

# 1"hello.c"

# 1"<built-in>"

# 1"<command-line>"

# 1"hello.c"

# 1 "/usr/include/stdio.h"1 3 4

# 28"/usr/include/stdio.h" 3 4

/***** 省略了部分內容,包括stdio.h中的一些宣告及定義  *****/

# 2"hello.c" 2

int main()

{

 printf("Hello World\n");

 return 0;

}

預處理過程主要處理規則如下:

1、將所有的#define刪除,並且展開所有的巨集定義;

2、處理所有條件編譯指令,如#if,#ifdef等;

3、處理#include預編譯指令,將被包含的檔案插入到該預編譯指令的位置。該過程遞迴進行,及被包含的檔案可能還包含其他檔案。

4、刪除所有的註釋//和 /**/;

5、新增行號和檔案標識,如#2 “hello.c” 2,以便於編譯時編譯器產生除錯用的行號資訊及用於編譯時產生編譯錯誤或警告時能夠顯示行號資訊;

6、保留所有的#pragma編譯器指令,因為編譯器須要使用它們;

二、編譯

編譯過程通過詞法和語法分析,確認所有指令符合語法規則(否則報編譯錯),之後翻譯成對應的中間碼,在linux中被稱為RTL(Register Transfer Language),通常是平臺無關的,這個過程也被稱為編譯前端。編譯後端對RTL樹進行裁減,優化,得到在目標機上可執行的彙編程式碼。gcc採用as作為其彙編器,所以彙編碼是AT&T格式的,而不是Intel格式,所以在用gcc編譯嵌入式彙編時,也要採用AT&T格式。

1.   gcc –S hello.i –o hello.s

gcc –S hello.i –o hello.s

以下為編譯後的輸出檔案hello.s的內容

1.   .file "hello.c"

2.   .section .rodata

3.   .LC0:

4.   .string "HelloWorld"

5.   .text

6.   .globl main

7.   .type main, @function

8.   main:

9.   pushl %ebp

10.  movl %esp, %ebp

11.  andl $-16, %esp

12.  subl $16, %esp

13.  movl $.LC0, (%esp)

14.  call puts

15.  movl $0, %eax

16.  leave

17.  ret

18.  .size main, .-main

19.  .ident "GCC: (GNU)4.4.0 20090506 (Red Hat 4.4.0-4)"

20.  .section .note.GNU-stack,"",@progbits

    .file  "hello.c"

        .section    .rodata

.LC0:

        .string      "HelloWorld"

        .text

.globl main

        .type         main, @function

main:

        pushl         %ebp

        movl          %esp, %ebp

        andl $-16, %esp

        subl  $16, %esp

        movl          $.LC0, (%esp)

        call   puts

        movl          $0, %eax

        leave

        ret

        .size main, .-main

        .ident        "GCC: (GNU)4.4.0 20090506 (Red Hat 4.4.0-4)"

        .section   .note.GNU-stack,"",@progbits

三、彙編

彙編器是將彙編程式碼轉變成機器可以執行的命令,每一個彙編語句幾乎都對應一條機器指令。彙編相對於編譯過程比較簡單,根據彙編指令和機器指令的對照表一一翻譯即可。

1.   gcc –c hello.c –o hello.o

 gcc –c hello.c –o hello.o

由於hello.o的內容為機器碼,不能以文字形式方便的呈現。

四、連結

連結器ld將各個目標檔案組裝在一起,解決符號依賴,庫依賴關係,並生成可執行檔案。

1.   ld –static crt1.o crti.o crtbeginT.ohello.o –start-group –lgcc –lgcc_eh –lc-end-group crtend.o crtn.o

ld –static crt1.o crti.o crtbeginT.ohello.o –start-group –lgcc –lgcc_eh –lc-end-group crtend.o crtn.o


(省略了檔案的路徑名)。

當然連結的時候還會用到靜態連結庫,和動態連線庫。靜態庫和動態庫都是.o目標檔案的集合。

靜態庫是在連結過程中將相關程式碼提取出來加入可執行檔案的庫(即在連結的時候將函式的程式碼將從其所在地靜態連結庫中被拷貝到最終的可執行程式中),ar只是將一些別的檔案集合到一個檔案中。可以打包,當然也可以解包。

1.   ar -v -q test.a test.o

ar -v -q  test.a test.o

上面指令可以生成靜態連結庫test.a

動態庫在連結時只建立一些符號表,而在執行的時候才將有關庫的程式碼裝入記憶體,對映到執行時相應程序的虛地址空間。如果出錯,如找不到對應的.so檔案,會在執行的時候報動態連線錯(可用LD_LIBRARY_PATH指定路徑)。用file test.so可以看到test.so是shared object的ELF檔案。

1.   gcc -sharedtest.so test.o

gcc -sharedtest.so test.o

上面指令可以生成動態連線庫test.so

好了,整個編譯過程就如上所示了,那麼對於gcc還有一些編譯的選項的。具體如下:

GCC編譯選項

1. -c

編譯產生物件檔案(*.obj)而不連結成可執行檔案,當編譯幾個獨立的模組,而待以後由連結程式把它們連結在一起時,就可以使用這個選項,如:

1.   gcc -chello.c ===> hello.o

2.   gcc hello.o

         gcc -c hello.c ===> hello.o

         gcc hello.o

2. -o

允許使用者指定輸出檔名,如

1.   gcc hello.c -o hello.o

2.   or

3.   gcc hello.c -o hello

           gcc hello.c -o hello.o

           or

           gcc hello.c -o hello

3. -g

指明編譯程式在編譯的輸出中應產生除錯資訊.這個除錯資訊使原始碼和變數名引用在除錯程式中或者當程式異常退出後在分析core檔案時可被使用.

4. -D

允許從編譯程式命令列定義巨集符號

一共有兩種情況:一種是用-DMACRO,相當於在程式中使用#define MACRO,另一種是用-DMACRO=A,相當於程式中的#define MACRO A.如對下面這程式碼:

1.   #ifdef DEBUG

2.   printf("debugmessage\n");

3.   #endif

           #ifdef DEBUG

                printf("debugmessage\n");

           #endif

編譯時可加上-DDEBUG引數,執行程式則打印出編譯資訊

5. -I

可指定查詢include檔案的其他位置.例如,如果有些include檔案位於比較特殊的地方,比如/usr/local/include,就可以增加此選項如下:

1.   gcc -c -I/usr/local/include -I/opt/include hello.c

           gcc -c -I/usr/local/include -I/opt/include hello.c

此時目錄搜尋會按給出的次序進行.

6. -E

這個選項是相對標準的,它允許修改命令列以使編譯程式把預先處理的C檔案發到標準輸出,而不實際編譯程式碼.在檢視C預處理偽指令和C巨集時,這是很有用的.可能的編譯輸出可重新定向到一個檔案,然後用編輯程式來分析:

1.   gcc -c -E hello.c>cpp.out

           gcc -c -E hello.c >cpp.out  

此命令使include檔案和程式被預先處理並重定向到檔案cpp.out.以後可以用編輯程或者分頁命令分析這個檔案,並確定最終的C語言程式碼看起來如何.

7. -O

優化選項,這個選項不是標準的

-O和 -O1指定1級優化

-O2 指定2級優化

-O3 指定3級優化

-O0指定不優化

gcc -c O3 -O0 hello.c

當出現多個優化時,以最後一個為準!!

8. -Wall

以最高級別使用GNU編譯程式,專門用於顯示警告用!!

1.   gcc -Wall hello.c

           gcc -Wall hello.c

9. -L

指定連線庫的搜尋目錄,-l(小寫L)指定連線庫的名字

1.   gcc main.o -L/usr/lib -lqt -o hello

           gcc main.o -L/usr/lib -lqt -o hello

1.    

10.-share   

此選項將盡量使用動態庫,所以生成檔案比較小,但是需要系統由動態庫

11.-static  

此選項將禁止使用動態庫,所以,編譯出來的東西,一般都很大,也不需要什麼動態連線庫

12.-fPIC

表示編譯為位置獨立的程式碼,不用此選項的話編譯後的程式碼是位置相關的所以動態載入時是通過程式碼拷貝的方式來滿足不同程序的需要,而不能達到真正程式碼段共享的目的。