gcc

elkd發表於2011-11-17

http://gcc.gnu.org/ 

gcc 是 gnu compiler collecti 編譯器,是GNU推出的功能強大、效能優越的多平臺編譯器,是GNU的代表作品之一。

              gcc 和g++什麼關係:事實上只有一個c++編譯器,那就是g++。g++不僅僅是一個c++前處理器,而是一個實實在在的c++編譯器。由於它的名字 gnu c++ compiler 也能縮寫成gcc,所以有時候有人叫它gcc也並不錯。而我們通常所說的gcc是一個編譯器套裝,gcc命令只是一個呼叫各個實際編譯器的快捷方式而已。

              gcc將生成一個名為 a.out的檔案。在Linux系統中,可執行檔案沒有統一的字尾,系統從檔案的屬性來區分可執行檔案和不可執行檔案。而gcc則通過字尾來區別輸入檔案的類別,下面我們來介紹gcc所遵循的部分約定規則。

.c為字尾的檔案,C語言原始碼檔案;

.a為字尾的檔案,是由目標檔案構成的檔案庫檔案;

.C,.cc或.cxx 為字尾的檔案,是C++原始碼檔案;

.h為字尾的檔案,是程式所包含的標頭檔案;

.i 為字尾的檔案,是已經預處理過的C原始碼檔案;

.ii為字尾的檔案,是已經預處理過的C++原始碼檔案;

.m為字尾的檔案,是Objective-C原始碼檔案;

.o為字尾的檔案,是編譯後的目標檔案;

.s為字尾的檔案,是組合語言原始碼檔案;

.S為字尾的檔案,是經過預編譯的組合語言原始碼檔案。

 

gcc的執行過程

gcc的編譯流程分為四個步驟:

         預處理(也稱預編譯,Preprocessing):前處理器CPP將對原始檔中的巨集進行展開。

         編譯(Compilation) :gcc將c檔案編譯成彙編檔案。

         彙編(Assembly) :as將彙編檔案編譯成機器碼。

         連線(Linking) :ld將目標檔案和外部符號進行連線,得到一個可執行二進位制檔案。

(1)預處理階段

        命令gcc首先呼叫cpp進行預處理,在預處理過程中,對原始碼檔案中的檔案包含(include)、預編譯語句(如巨集定義define等)進行分析。編譯器將程式碼中的stdio.h編譯進來,並且使用者可以使用gcc的選項”-E”進行檢視,該選項的作用是讓gcc在預處理結束後停止編譯過程。
    《深入理解計算機系統》中說的:
     前處理器(cpp)根據以字元#開頭的命令(directives),修改原始的C程式。如hello.c中#include <stdio.h>指令告訴前處理器讀系統標頭檔案stdio.h的內容,並把它直接插入到程式文字中去。結果就得到另外一個C程式,通常是以.i作為副檔名的。
   注意:
        Gcc指令的一般格式為:Gcc [選項] 要編譯的檔案 [選項] [目標檔案]    其中,目標檔案可預設,Gcc預設生成可執行的檔名為:編譯檔案.out
(2)編譯階段              
            在這個編譯階段中,Gcc首先要檢查程式碼的規範性、是否有語法錯誤等,以確定程式碼的實際要做的工作,在檢查無誤後,Gcc把程式碼翻譯成組合語言。使用者可以使用”-S”選項來進行檢視,該選項只進行編譯而不進行彙編,生成彙編程式碼。組合語言是非常有用的,它為不同高階語言不同編譯器提供了通用的語言。如:C編譯器和Fortran編譯器產生的輸出檔案用的都是一樣的組合語言。
     這個階段根據輸入檔案生成以.o為字尾的目標檔案。彙編過程是針對組合語言的步驟,呼叫as進行工作,一般來講,.S為字尾的組合語言原始碼檔案和彙編、.s為字尾的組合語言檔案經過預編譯和彙編之後都生成以.o為字尾的目標檔案。

(3)彙編階段
           彙編階段是把編譯階段生成的”.s”檔案轉成目標檔案,讀者在此可使用選項”-c”就可看到彙編程式碼已轉化為”.o”的二進位制目的碼了。

如下所示:


(4)連結階段
           在成功編譯之後,就進入了連結階段。在這裡涉及到一個重要的概念:函式庫。
          程式中並沒有定義”printf”的函式實現,且在預編譯中包含進的”stdio.h”中也只有該函式的宣告,而沒有定義函式的實現,那麼,是在哪裡實現”printf”函式的呢?最後的答案是:系統把這些函式實現都被做到名為libc.so.6的庫檔案中去了,在沒有特別指定時,gcc會到系統預設的搜尋路徑”/usr/lib”下進行查詢,也就是連結到libc.so.6庫函式中去,這樣就能實現函式”printf” 了,而這也就是連結的作用。

函式庫一般分為靜態庫和動態庫兩種:

             靜態庫是指編譯連結時,把庫檔案的程式碼全部加入到可執行檔案中,因此生成的檔案比較大,但在執行時也就不再需要庫檔案了。其字尾名一般為”.a”。動態庫與之相反,在編譯連結時並沒有把庫檔案的程式碼加入到可執行檔案中,而是在程式執行時由執行時連結檔案載入庫,這樣可以節省系統的開銷。

            使用靜態連結的好處是,依賴的動態連結庫較少,對動態連結庫的版本不會很敏感,具有較好的相容性;缺點是生成的程式比較大。

             動態庫一般字尾名為”.so”,如前面所述的libc.so.6就是動態庫。gcc在編譯時預設使用動態庫。使用動態連結的好處是,生成的程式比較小,佔用較少的記憶體。

完成了連結之後,gcc就可以生成可執行檔案,如下所示:

           執行該可執行檔案,出現正確的結果如下。當所有的目標檔案都生成之後,gcc就呼叫 ld來完成最後的關鍵性工作,這個階段就是連線。在連線階段,所有的目標檔案被安排在可執行程式中的恰當的位置,同時,該程式所呼叫到的庫函式也從各自所在的檔案庫中連到合適的地方。

 

用一個經典的hello.c程式為例:

#include<stdio.h>
int main(void)
{
        printf("welcome to linux world!\n");
        return 0;
}

[redhat@bogon ~]$ gcc -E hello.c -o hello.i  //這個階段處理預定義和標頭檔案包含#include,並做語法檢查。——預編譯過程
[redhat@bogon ~]$ gcc -E hello.c  // 用-E來檢視預編譯詳細過程.

[redhat@bogon ~]$ gcc -S hello.c -o hello.s //這個階段,生成彙編程式碼。——編譯過程
[redhat@bogon ~]$ as hello.s -o hello.o //這個階段,生成目的碼。——彙編過程  
[redhat@bogon ~]$ gcc hello.o -o hl  //連結過程。生成可執行程式碼.連結過程,連結分兩種:靜態連結和動態連結.見上.
[redhat@bogon ~]$ ./hl    //生成了可執行檔案,程式執行:
welcome to linux world!

上面過程描述了使用gcc進行預處理(生成*.i檔案)、編譯(生成*.s檔案)、彙編(生成*.o檔案)、連結(聲稱可執行檔案)的四個步驟,其實上面四個步驟可由下面一條語句完成:

[redhat@bogon ~]$ gcc hello.c -o hl
[redhat@bogon ~]$ ./hl
welcome to linux world!

 

說明:

[redhat@bogon ~]$gcc -E hello.c -o hello.i  //編譯階段,輸入的是中間檔案*.i  ,-o 是生成可執行檔案的輸出選項。
[redhat@bogon ~]$gcc -S hello.c -o hello.s  //編譯後生成組合語言檔案*.s
[redhat@bogon ~]$gcc -c hello.c -o hello.o  //在彙編階段,將輸入的彙編檔案*.s轉換成機器語言*.o
[redhat@bogon ~]$gcc hello.o -o hello       //最後,在連線階段將輸入的機器程式碼檔案*.s(與其它的機器程式碼檔案和庫檔案)彙整合一個可執行的二進位制程式碼檔案。

GCC常用的兩種模式:編譯模式和編譯連線模式。

[redhat@bogon ~]$ gcc hello.c  //作用:將hello.c預處理、彙編、編譯並連結形成可執行檔案。這裡未指定輸出檔案,預設輸出為a.out。編譯成功後可以看到生成了一個a.out的檔案。在命令列輸入./a.out 執行程式。./表示在當前目錄,a.out為可執行程式檔名。

[redhat@bogon ~]$gcc hello.c -o hello //作用:將hello.c預處理、彙編、編譯並連結形成可執行檔案hello。-o選項用來指定輸出檔案的檔名。輸入./hello執行程式。
[redhat@bogon ~]$gcc -E hello.c -o hello.i //作用:將hello.c預處理輸出hello.i檔案。
[redhat@bogon ~]$gcc -S hello.i  //作用:將預處理輸出檔案hello.i彙編成hello.s檔案。
[redhat@bogon ~]$gcc -c hello.s //作用:將彙編輸出檔案hello.s編譯輸出hello.o檔案。
[redhat@bogon ~]$gcc hello.o -o hello  //作用:將編譯輸出檔案hello.o連結成最終可執行檔案hello。輸入./hello執行程式。
[redhat@bogon ~]$gcc -O1 hello.c -o hello //作用:使用編譯優化級別1編譯程式。級別為1~3,級別越大優化效果越好,但編譯時間越長。輸入./hello執行程式。
[redhat@bogon ~]$gcc hello.cpp -o hello-lstdc++ //作用:將hello.cpp編譯連結成hello可執行檔案。-lstdc++指定連結std c++庫。.編譯使用C++ std庫的程式。


 下面用gcc來編譯程式檢視過程。

用#define定義一個常量。實際上編譯器的工作分為兩個步驟,
先是預處理(Preprocess),然後才是編譯,用gcc的-E選項可以看到預處理之後、編譯之前的程式。

#include<stdio.h>
#include<stdlib.h>
#define N 20
int a[N];
void gen_random(int upper_bound)
{
        int i;
        for(i = 0;i < N; i++){
                a[i] = rand() % upper_bound;
        }
}
void print_rndom()
{
        int i;
        for(i = 0;i < N; i++){
                printf("%d ",a[i]);
        }
        printf("\n");
}
int main(void)
{
        gen_random(20);
        print_rndom();
        return 0;
}

// run
[redhat@localhost ~]$ ./a.out
3 6 17 15 13 15 6 12 9 1 2 7 10 19 3 6 0 6 12 16

[redhat@localhost ~]$ gcc -E rand.c  //或者 cpp rand.c

extern void exit (int __status) __attribute__ ((__nothrow__)) __attribute__ ((__noreturn__));

# 658 "/usr/include/stdlib.h" 3 4

........//部分略


 extern int rpmatch (__const char *__response) __attribute__ ((__nothrow__)) __attribute__ ((__nonnull__ (1))) ;
# 926 "/usr/include/stdlib.h" 3 4
extern int posix_openpt (int __oflag) ;
# 961 "/usr/include/stdlib.h" 3 4
extern int getloadavg (double __loadavg[], int __nelem)
     __attribute__ ((__nothrow__)) __attribute__ ((__nonnull__ (1)));
# 977 "/usr/include/stdlib.h" 3 4

# 3 "rand.c" 2

int a[20];
void gen_random(int upper_bound)
{
 int i;
 for(i = 0;i < 20; i++){
  a[i] = rand() % upper_bound;
 }
}
void print_rndom()
{
 int i;
 for(i = 0;i < 20; i++){
  printf("%d ",a[i]);
 }
 printf("\n");
}
int main(void)
{
 gen_random(20);
 print_rndom();
 return 0;
}

由此可見:前處理器做了兩件事情:
一是把標頭檔案stdio.h和stdlib.h在程式碼中展開,
二是把#define定義的識別符號N替換成它的定義20
(在程式碼中做了三處替換,分別位於陣列的定義中和兩個函式中)。
以#號開頭的語法元素稱為預處理指示(Preprocessing Directive)。
此處,用cpp rand.c命令也可以達到同樣的效果,只做預處理而不編譯,cpp表示C preprocessor。

 

用gcc 檢視 警告提示資訊

         GCC包含完整的出錯檢查和警告提示功能,它們可以幫助Linux程式設計師寫出更加專業和優美的程式碼。 

寫一個llcode.c程式為例:

#include<stdio.h>
void main(void)
{
        long long int var = 1;
        printf("It is not standard C code!\n");
}

main函式的返回值被宣告為void,但實際上應該是int;
使用了GNU語法擴充套件,即使用long long來宣告64位整數,不符合ANSI/ISO C語言標準;
main函式在終止前沒有呼叫return語句。

        下面來看看GCC是如何幫助程式設計師來發現這些錯誤的。當GCC在編譯不符合ANSI/ISO C語言標準的原始碼時,如果加上了-pedantic選項,那麼使用了擴充套件語法的地方將產生相應的警告資訊:

[redhat@bogon ~]$ gcc -pedantic llcode.c
llcode.c: In function ‘main’:
llcode.c:4: warning: ISO C90 does not support ‘long long’
llcode.c:3: warning: return type of ‘main’ is not ‘int’

        需要注意的是,-pedantic編譯選項並不能保證被編譯程式與ANSI/ISO C標準的完全相容,它僅僅只能用來幫助Linux程式設計師離這個目標越來越近。或者換句話說,-pedantic選項能夠幫助程式設計師發現一些不符合 ANSI/ISO C標準的程式碼,但不是全部,事實上只有ANSI/ISO C語言標準中要求進行編譯器診斷的那些情況,才有可能被GCC發現並提出警告。

      除了-pedantic之外,GCC還有一些其它編譯選項也能夠產生有用的警告資訊。這些選項大多以-W開頭,其中最有價值的當數-Wall了,使用它能夠使GCC產生儘可能多的警告資訊: 

 [redhat@bogon ~]$ gcc -Wall llcode.c
llcode.c:3: warning: return type of ‘main’ is not ‘int’
llcode.c: In function ‘main’:
llcode.c:4: warning: unused variable ‘var’

     GCC給出的警告資訊雖然從嚴格意義上說不能算作是錯誤,但卻很可能成為錯誤的棲身之所。一個優秀的Linux程式設計師應該儘量避免產生警告資訊,使自己的程式碼始終保持簡潔、優美和健壯的特性。 

     在處理警告方面,另一個常用的編譯選項是-Werror,它要求GCC將所有的警告當成錯誤進行處理,這在使用自動編譯工具(如Make等)時非常有用。如 果編譯時帶上-Werror選項,那麼GCC會在所有產生警告的地方停止編譯,迫使程式設計師對自己的程式碼進行修改。只有當相應的警告資訊消除時,才可能將編 譯過程繼續朝前推進。執行情況如下:

[redhat@bogon ~]$ gcc -Wall -Werror llcode.c
cc1: warnings being treated as errors
llcode.c:3: warning: return type of ‘main’ is not ‘int’
llcode.c: In function ‘main’:
llcode.c:4: warning: unused variable ‘var’

   對Linux程式設計師來講,GCC給出的警告資訊是很有價值的,它們不僅可以幫助程式設計師寫出更加健壯的程式,而且還是跟蹤和除錯程式的有力工具。建議在用GCC編譯原始碼時始終帶上-Wall選項,並把它逐漸培養成為一種習慣,這對找出常見的隱式程式設計錯誤很有幫助。

 

gcc 庫依賴 

         在Linux 下開發軟體時,完全不使用第三方函式庫的情況是比較少見的,通常來講都需要藉助一個或多個函式庫的支援才能夠完成相應的功能。從程式設計師的角度看,函式庫實 際上就是一些標頭檔案(.h)和庫檔案(.so或者.a)的集合。雖然Linux下的大多數函式都預設將標頭檔案放到/usr/include/目錄下,而庫 檔案則放到/usr/lib/目錄下,但並不是所有的情況都是這樣。正因如此,GCC在編譯時必須有自己的辦法來查詢所需要的標頭檔案和庫檔案。

         GCC採用搜尋目錄的辦法來查詢所需要的檔案,-I選項可以向GCC的標頭檔案搜尋路徑中新增新的目錄。例如,如果在/home/xiaowp/include/目錄下有編譯時所需要的標頭檔案,為了讓GCC能夠順利地找到它們,就可以使用-I選項:

[redhat@bogon ~]$ gcc yh.c -I /home/xiaowp/include -o yh

       同樣,如果使用了不在標準位置的庫檔案,那麼可以通過-L選項向GCC的庫檔案搜尋路徑中新增新的目錄。例如,如果在/home/xiaowp/lib/目錄下有連結時所需要的庫檔案libfoo.so,為了讓GCC能夠順利地找到它,可以使用下面的命令:

[redhat@bogon ~]$ gcc yh.c -L /home/xiaowp/lib -lyh -o yh

         值得好好解釋一下的是-l選項,它指示GCC去連線庫檔案libfoo.so。Linux下的庫檔案在命名時有一個約定,那就是應該以lib三個字母開頭, 由於所有的庫檔案都遵循了同樣的規範,因此在用-l選項指定連結的庫檔名時可以省去lib三個字母,也就是說GCC在對-lfoo進行處理時,會自動去 連結名為libfoo.so的檔案。

        Linux下的庫檔案分為兩大類分別是動態連結庫(通常以.so結尾)和靜態連結庫(通常以.a 結尾),兩者的差別僅在程式執行時所需的程式碼是在執行時動態載入的,還是在編譯時靜態載入的。預設情況下,GCC在連結時優先使用動態連結庫,只有當動態 連結庫不存在時才考慮使用靜態連結庫,如果需要的話可以在編譯時加上-static選項,強制使用靜態連結庫。例如,如果在 /home/xiaowp/lib/目錄下有連結時所需要的庫檔案libfoo.so和libfoo.a,為了讓GCC在連結時只用到靜態連結庫,可以使 用下面的命令:
[redhat@bogon ~]$ gcc yh.c -L /home/xiaowp/lib -static -lyh -o yh

gcc 程式碼優化

        程式碼優化指的是編譯器通過分析原始碼,找出其中尚未達到最優的部分,然後對其重新進行組合,目的是改善程式的執行效能。GCC提供的程式碼優化功能非常強大, 它通過編譯選項-On來控制優化程式碼的生成,其中n是一個代表優化級別的整數。對於不同版本的GCC來講,n的取值範圍及其對應的優化效果可能並不完全相 同,比較典型的範圍是從0變化到2或3。

        編譯時使用選項-O可以告訴GCC同時減小程式碼的長度和執行時間,其效果等價於-O1。在這 一級別上能夠進行的優化型別雖然取決於目標處理器,但一般都會包括執行緒跳轉(Thread Jump)和延遲退棧(Deferred Stack Pops)兩種優化。選項-O2告訴GCC除了完成所有-O1級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令排程等。選項-O3則除了完 成所有-O2級別的優化之外,還包括迴圈展開和其它一些與處理器特性相關的優化工作。通常來說,數字越大優化的等級越高,同時也就意味著程式的執行速度越 快。許多Linux程式設計師都喜歡使用-O2選項,因為它在優化長度、編譯時間和程式碼大小之間,取得了一個比較理想的平衡點。


下面通過具體例項來感受一下GCC的程式碼優化功能,所用程式如yh.c。

#include <stdio.h> 
int main(void)
{
	double counter;
	double result;
	double temp;
	for (counter = 0; 
	counter < 2000.0 * 2000.0 * 2000.0 / 20.0 + 2020; 
	counter += (5 - 1) / 4) {
		temp = counter / 1979;
		result = counter; 
	}
	printf("Result is %lf\n", result);
	return 0;
}

首先不加任何優化選項進行編譯:
[redhat@bogon ~]$ gcc -Wall yh.c -o yh
藉助Linux提供的time命令,可以大致統計出該程式在執行時所需要的時間:

[redhat@bogon ~]$ time ./yh
Result is 400002019.000000

real    0m11.023s
user    0m9.714s
sys     0m0.010s

接下去使用優化選項來對程式碼進行優化處理:

[redhat@bogon ~]$ gcc -Wall -O yh.c -o yh
在同樣的條件下再次測試一下執行時間:

[redhat@bogon ~]$ time ./yh
Result is 400002019.000000

real    0m1.769s
user    0m1.610s
sys     0m0.005s

 

        對比兩次執行的輸出結果不難看出,程式的效能的確得到了很大幅度的改善,由原來的14秒縮短到了3秒。這個例子是專門針對GCC的優化功能而設計的,因此優 化前後程式的執行速度發生了很大的改變。儘管GCC的程式碼優化功能非常強大,但作為一名優秀的Linux程式設計師,首先還是要力求能夠手工編寫出高質量的代 碼。如果編寫的程式碼簡短,並且邏輯性強,編譯器就不會做更多的工作,甚至根本用不著優化。

       優化雖然能夠給程式帶來更好的執行效能,但在如下一些場合中應該避免優化程式碼: 

        程式開發的時候 優化等級越高,消耗在編譯上的時間就越長,因此在開發的時候最好不要使用優化選項,只有到軟體發行或開發結束的時候,才考慮對最終生成的程式碼進行優化。 

       資源受限的時候 一些優化選項會增加可執行程式碼的體積,如果程式在執行時能夠申請到的記憶體資源非常緊張(如一些實時嵌入式裝置),那就不要對程式碼進行優化,因為由這帶來的負面影響可能會產生非常嚴重的後果。

        跟蹤除錯的時候 在對程式碼進行優化的時候,某些程式碼可能會被刪除或改寫,或者為了取得更佳的效能而進行重組,從而使跟蹤和除錯變得異常困難。


#define 定義的常量與列舉定義的常量的區別??

   首先,define有僅用於定義常量,也可以定義更復雜的語法結構,稱為巨集(Macro)定義。
   其次,define定義是在預處理階段處理的,而列舉是在編譯階段處理的。
如:

#include<stdio.h>
#define PECTANGULAR 1
#define POLAR 2
int main(void)
{
        int RECTANGULAR;
        printf("%d %d\n",RECTANGULAR,POLAR);
        return 0;
}

 

上面用gcc編譯的只是一個檔案,現如何用gcc來編譯多個檔案呢??

       在採用模組化的設計思想進行軟體開發時,通常整個程式是由多個原始檔組成的,相應地也就形成了多個編譯單元,使用GCC能夠很好地管理這些編譯單元。假設有一個由test1.c和test2.c兩個原始檔組成的程式,為了對它們進行編譯,並最終生成可執行程式test,可以使用下面這條命令:

[redhat@localhost ~]$ test1.c test2.c -o test

同時處理的檔案不止一個,GCC仍然會按照預處理、編譯和連結的過程依次進行。如上面這條命令大致相當於依次執行如下三條命令:
[redhat@localhost ~]$ gcc -c test1.c -o test1.o

[redhat@localhost ~]$ gcc -c test2.c -o test2.o 

[redhat@localhost ~]$ gcc test1.o test2.o -o test

           在編譯一個包含許多原始檔的工程時,若只用一條GCC命令來完成編譯是非常浪費時間的。假設專案中有100個原始檔需要編譯,並且每個原始檔中都包含 10000行程式碼,如果像上面那樣僅用一條GCC命令來完成編譯工作,那麼GCC需要將每個原始檔都重新編譯一遍,然後再全部連線起來。很顯然,這樣浪費 的時間相當多,尤其是當使用者只是修改了其中某一個檔案的時候,完全沒有必要將每個檔案都重新編譯一遍,因為很多已經生成的目標檔案是不會改變的。要解決這個問題,關鍵是要靈活運用GCC,同時還要藉助像Make這樣的工具。

詳見Makefile節。

 

gcc編譯器如何工作要更為詳細的資訊請參考編譯器手冊。