原創 【CentOS Linux 7】實驗4【gcc編譯器】

是您啊,噠噠子前輩!發表於2020-09-30

 

第3章  gcc 編 譯 器

Linux的各發行版中包含了很多軟體開發工具,它們中的很多是用於C和C++應用程式開發的。本章將介紹如何使用Linux下的C 編譯器和其他C程式設計工具。

3.1  gcc 簡 介

在為Linux開發應用程式時,絕大多數情況下使用的都是C語言,因此幾乎每一位Linux程式設計師面臨的首要問題都是如何靈活運用C編譯器。目前Linux下最常用的C語言編譯器是gcc(GNU Compiler Collection),它是GNU專案中符合ANSI C標準的編譯系統,能夠編譯用C、C++和Object C等語言編寫的程式。gcc不僅功能十分強大,結構也異常靈活。最值得稱道的一點就是它可以通過不同的前端模組來支援各種語言,如Java、Fortran、Pascal、Modula-3和Ada等。gcc是可以在多種硬體平臺上編譯出可執行程式的超級編譯器,其執行效率與一般的編譯器相比,平均效率要高20%~30%。gcc支援編譯的一些原始檔的字尾及其解釋見表3-1。

表3-1  gcc所支援的語言

後  綴  名

所支援的語言

.c

C原始程式

.C

C++原始程式

.cc

C++原始程式

.cxx

C++原始程式

.m

Objective-C原始程式

.i

已經過預處理的C原始程式

.ii

已經過預處理的C++原始程式

.s

組合語言原始程式

.S

組合語言原始程式

.h

預處理檔案(標標頭檔案)

.o

目標檔案

.a

存檔檔案

 

開放、自由和靈活是Linux的魅力所在,而這一點在gcc上的體現就是程式設計師通過它能夠更好地控制整個編譯過程。

在使用gcc編譯程式時,編譯過程可以細分為4個階段:

  • 預處理(Pre-Processing)
  • 編譯(Compiling)
  • 彙編(Assembling)
  • 連結(Linking)

Linux程式設計師可以根據自己的需要讓gcc在編譯的任何階段結束,檢查或使用編譯器在該階段的輸出資訊,或者對最後生成的二進位制檔案進行控制,以便通過加入不同數量和種類的除錯程式碼來為今後的除錯做好準備。與其他常用的編譯器一樣,gcc也提供了靈活而強大的程式碼優化功能,利用它可以生成執行效率更高的程式碼。

gcc提供了30多條警告資訊和3個警告級別,使用它們有助於增強程式的穩定性和可移植性。此外,gcc還對標準的C和C++語言進行了大量的擴充套件,提高了程式的執行效率,有助於編譯器進行程式碼優化,能夠減輕程式設計的工作量。

3.2  使 用 gcc

gcc的版本可以使用如下gcc –v命令檢視:

[david@DAVID david]$ gcc -v

Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/specs

Configured with: ../configure --prefix=/usr --mandir=/usr/share/man

--infodir=/

sr/share/info --enable-shared --enable-threads=posix

--disable-checking --with-

ystem-zlib --enable-__cxa_atexit --host=i386-redhat-linux

Thread model: posix

gcc version 3.2.2 20030222 (Red Hat Linux 3.2.2-5)

以上顯示的就是Redhat linux 9.0裡自帶的gcc的版本3.2.2。

下面將以一個例項來說明如何使用gcc編譯器。例3-1能夠幫助大家迅速理解gcc的工作原理,並將其立即運用到實際的專案開發中去。

例項3-1  hello.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

int main (int argc,char **argv) {

printf("Hello Linux\n");

}

要編譯這個程式,只要在命令列下執行如下命令:

[david@DAVID david]$ gcc hello.c -o hello

[david@DAVID david]$ ./hello

Hello Linux

這樣,gcc 編譯器會生成一個名為hello的可執行檔案,然後執行./hello就可以看到程式的輸出結果了。

命令列中 gcc表示用gcc來編譯源程式,-o 選項表示要求編譯器輸出的可執行檔名為hello ,而hello.c是源程式檔案。從程式設計師的角度看,只需簡單地執行一條gcc命令就可以了;但從編譯器的角度來看,卻需要完成一系列非常繁雜的工作。首先,gcc需要呼叫預處理程式cpp,由它負責展開在原始檔中定義的巨集,並向其中插入#include語句所包含的內容;接著,gcc會呼叫ccl和as將處理後的原始碼編譯成目的碼;最後,gcc會呼叫連結程式ld,把生成的目的碼連結成一個可執行程式。

為了更好地理解gcc的工作過程,可以把上述編譯過程分成幾個步驟單獨進行,並觀察每步的執行結果。

第一步要進行預編譯,使用-E引數可以讓gcc在預處理結束後停止編譯過程:

[david@DAVID david]$ gcc -E hello.c -o hello.i

此時若檢視hello.i檔案中的內容,會發現stdio.h的內容確實都插到檔案裡去了,而且被預處理的巨集定義也都作了相應的處理。

# 1 "hello.c"

# 1 "<built-in>"

# 1 "<command line>"

# 1 "hello.c"

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

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

# 1 "/usr/include/features.h" 1 3

# 291 "/usr/include/features.h" 3

# 1 "/usr/include/sys/cdefs.h" 1 3

# 292 "/usr/include/features.h" 2 3

# 314 "/usr/include/features.h" 3

# 1 "/usr/include/gnu/stubs.h" 1 3

# 315 "/usr/include/features.h" 2 3

# 29 "/usr/include/stdio.h" 2 3

# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3

# 213 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 3

typedef unsigned int size_t;

# 35 "/usr/include/stdio.h" 2 3

# 1 "/usr/include/bits/types.h" 1 3

# 28 "/usr/include/bits/types.h" 3

# 1 "/usr/include/bits/wordsize.h" 1 3

# 29 "/usr/include/bits/types.h" 2 3

# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3

# 32 "/usr/include/bits/types.h" 2 3

 

"hello.i" 838L, 16453C                         1,1           Top

下一步是將hello.i編譯為目的碼,這可以通過使用-c引數來完成:

[david@DAVID david]$ gcc -c hello.i -o hello.o

gcc預設將.i檔案看成是預處理後的C語言原始碼,因此上述命令將自動跳過預處理步驟而開始執行編譯過程,也可以使用-x引數讓gcc從指定的步驟開始編譯。最後一步是將生成的目標檔案連結成可執行檔案:

[david@DAVID david]$ gcc hello.o -o hello

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

[david@DAVID david]$ gcc david.c xueer.c -o davidxueer

如果同時處理的檔案不止一個,gcc仍然會按照預處理、編譯和連結的過程依次進行。如果深究起來,上面這條命令大致相當於依次執行如下3條命令:

[david@DAVID david]$ gcc david.c -o david.o

[david@DAVID david]$ gcc  xueer.c -o xueer.o

[david@DAVID david]$ gcc david.o xueer.o -o davidxueer

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

3.3  gcc警告提示功能

gcc包含完整的出錯檢查和警告提示功能,它們可以幫助Linux程式設計師儘快找到錯誤程式碼,從而寫出更加專業和優美的程式碼。先來讀讀例3-2所示的程式,這段程式碼寫得很糟糕,仔細檢查一下不難挑出如下毛病:

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

例項3-2  bad.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

void main(void)

{

  long long int var = 1;

  printf("It is not standard C code!\n");

}

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

[david@DAVID david]$ gcc -pedantic bad.c -o bad

bad.c: In function 'main':

bad.c:4: warning: ISO C89 does not support 'long long'

bad.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產生儘可能多的警告資訊。例如:

[david@DAVID david]$ gcc -Wall bad.c -o bad

bad.c:3: warning: return type of 'main' is not 'int'

bad.c: In function 'main':

bad.c:4: warning: unused variable 'var'

bad.c:6:2: warning: no newline at end of file

gcc給出的警告資訊雖然從嚴格意義上說不能算作是錯誤,但很可能成為錯誤的棲身之所。一個優秀的Linux程式設計師應該儘量避免產生警告資訊,使自己的程式碼始終保持簡潔、優美和健壯的特性。
    在處理警告方面,另一個常用的編譯選項是-Werror,它要求gcc將所有的警告當成錯誤進行處理,這在使用自動編譯工具(如make等)時非常有用。如果編譯時帶上-Werror選項,那麼gcc會在所有產生警告的地方停止編譯,迫使程式設計師對自己的程式碼進行修改。只有當相應的警告資訊消除時,才可能將編譯過程繼續朝前推進。執行情況如下:

[david@DAVID david]$ gcc -Werror bad.c -o bad

cc1: warnings being treated as errors

bad.c: In function 'main':

bad.c:3: warning: return type of 'main' is not 'int'

bad.c:6:2: no newline at end of file

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

3.4  庫  依  賴

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

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

[david@DAVID david]$ gcc david.c -I /home/david/include -o david

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

[david@DAVID david]$ gcc david.c -L /home/david/lib –ldavid -o david

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

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

[david@DAVID david]$ gcc foo.c -L /home/david/lib -static –ldavid -o

david

3.5  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的程式碼優化功能,所用程式如例3-3所示。

例項3-3  count.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

 int main(void)

{  double counter;

   double result;

   double temp;

   for (counter = 0; counter < 4000.0 * 4000.0 * 4000.0  / 20.0 + 2030;   

counter += (5 - 3 +2 + 1 ) / 4)

     {  temp = counter / 1239;

        result  = counter;   

       } 

        printf("Result is %lf\n", result); 

        return 0;

}

首先不加任何優化選項進行編譯:

[david@DAVID david]$ gcc -Wall count.c -o count

藉助Linux提供的time命令,可以大致統計出該程式在執行時所需要的時間:

[david@DAVID david]$ time ./count

Result is 3200002029.000000

real    1m59.357s

user    1m59.140s

sys     0m0.050s

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

[david@DAVID david]$ gcc -Wall count.c -o count2

在同樣的條件下再次測試一下執行時間:

[david@DAVID david]$ time ./count2

Result is 3200002029.000000

real    0m26.573s

user    0m26.540s

sys     0m0.010s

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

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

  • 程式開發的時候:優化等級越高,消耗在編譯上的時間就越長,因此在開發的時候最好不要使用優化選項,只有到軟體發行或開發結束的時候,才考慮對最終生成的程式碼進行優化。
  • 資源受限的時候:一些優化選項會增加可執行程式碼的體積,如果程式在執行時能夠申請到的記憶體資源非常緊張(如一些實時嵌入式裝置),那就不要對程式碼進行優化,因為由這帶來的負面影響可能會產生非常嚴重的後果。
  • 跟蹤除錯的時候:在對程式碼進行優化的時候,某些程式碼可能會被刪除或改寫,或者為了取得更佳的效能而進行重組,從而使跟蹤和除錯變得異常困難。

3.6  加    速

在將原始碼變成可執行檔案的過程中,需要經過許多中間步驟,包含預處理、編譯、彙編和連線。這些過程實際上是由不同的程式負責完成的。大多數情況下gcc可以為Linux程式設計師完成所有的後臺工作,自動呼叫相應程式進行處理。

這樣做有一個很明顯的缺點,就是gcc在處理每一個原始檔時,最終都需要生成好幾個臨時檔案才能完成相應的工作,從而無形中導致處理速度變慢。例如,gcc在處理一個原始檔時,可能需要一個臨時檔案來儲存預處理的輸出,一個臨時檔案來儲存編譯器的輸出,一個臨時檔案來儲存彙編器的輸出,而讀寫這些臨時檔案顯然需要耗費一定的時間。當軟體專案變得非常龐大的時候,花費在這上面的代價可能會變得很大。

解決的辦法是,使用Linux提供的一種更加高效的通訊方式—— 管道。它可以用來同時連線兩個程式,其中一個程式的輸出將直接作為另一個程式的輸入,這樣就可以避免使用臨時檔案,但編譯時卻需要消耗更多的記憶體。

注意:

在編譯過程中使用管道是由gcc的-pipe選項決定的。下面的這條命令就是藉助gcc的管道功能來提高編譯速度的:

[david@DAVID david]$ gcc -pipe david.c -o david

在編譯小型工程時使用管道,編譯時間上的差異可能還不是很明顯,但在原始碼非常多的大型工程中,差異將變得非常明顯。

3.7  gcc常用選項

gcc作為Linux下C/C++重要的編譯環境,功能強大,編譯選項繁多。為了方便大家日後編譯方便,在此將常用的選項及說明羅列出來,見表3-2。

表3-2  gcc的常用選項

選  項  名

作    用

-c

通知gcc取消連線步驟,即編譯原始碼並在最後生成目標檔案

-Dmacro

定義指定的巨集,使它能夠通過原始碼中的#ifdef進行檢驗

-E

不經過編譯預處理程式的輸出而輸送至標準輸出

-g3

獲得有關除錯程式的詳細資訊,它不能與-o選項聯合使用

-Idirectory

在包含檔案搜尋路徑的起點處新增指定目錄

-llibrary

提示連線程式在建立最終可執行檔案時包含指定的庫

-O、-O2、-O3

將優化狀態開啟,該選項不能與-g選項聯合使用

-S

要求編譯程式生成來自原始碼的彙編程式輸出

-v

啟動所有警報

.h

預處理檔案(標標頭檔案)

-Wall

在發生警報時取消編譯操作,即將警報看作是錯誤

-w

禁止所有的報警

 

3.8  gcc的錯誤型別及對策

如果gcc編譯器發現源程式中有錯誤,就無法繼續進行,也無法生成最終的可執行檔案。為了便於修改,gcc給出錯誤資訊,必須對這些錯誤資訊逐個進行分析、處理,並修改相應的原始碼,才能保證原始碼的正確編譯連線。.gcc給出的錯誤資訊一般可以分為四大類,下面我們分別討論其產生的原因和對策。

  • 第一類:C語法錯誤

錯誤資訊:檔案source.c中第n行有語法錯誤(syntex errror)。這種型別的錯誤,一般都是C語言的語法錯誤,應該仔細檢查原始碼檔案中第n行及該行之前的程式,有時也需要對該檔案所包含的標頭檔案進行檢查。有些情況下,一個很簡單的語法錯誤,gcc會給出一大堆錯誤,我們最主要的是要保持清醒的頭腦,不要被其嚇倒,必要的時候再參考一下C語言的基本教材。在這裡推薦一本由Andrew Koenig寫的《C 陷阱與缺陷》(此書已由人民郵電出版社翻譯出版),說得誇張一點就是此書可以幫助你減少C程式碼和初級C++程式碼中的90%的bug。

  • 第二類:標頭檔案錯誤

錯誤資訊:找不到標頭檔案head.h(Can not find include file head.h)。這類錯誤是原始碼檔案中包含的標頭檔案有問題,可能的原因有標頭檔案名錯誤、指定的標頭檔案所在目錄名錯誤等,也可能是錯誤地使用了雙引號和尖括號。

  • 第三類:檔案庫錯誤

錯誤資訊:連線程式找不到所需的函式庫,例如:

ld: -lm: No such file or directory

這類錯誤是與目標檔案相連線的函式庫有錯誤,可能的原因是函式庫名錯誤、指定的函式庫所在目錄名稱錯誤等。檢查的方法是使用find命令在可能的目錄中尋找相應的函式庫名,確定檔案庫及目錄的名稱並修改程式中及編譯選項中的名稱。

  • 第四類:未定義符號

錯誤資訊:有未定義的符號(Undefined symbol)。這類錯誤是在連線過程中出現的,可能有兩種原因:一是使用者自己定義的函式或者全域性變數所在原始碼檔案,沒有被編譯、連線,或者乾脆還沒有定義,這需要使用者根據實際情況修改源程式,給出全域性變數或者函式的定義體;二是未定義的符號是一個標準的庫函式,在源程式中使用了該庫函式,而連線過程中還沒有給定相應的函式庫的名稱,或者是該檔案庫的目錄名稱有問題,這時需要使用檔案庫維護命令ar檢查我們需要的庫函式到底位於哪一個函式庫中,確定之後,修改gcc連線選項中的-l和-L項。

排除編譯、連線過程中的錯誤,應該說只是程式設計中最簡單、最基本的一個步驟,可以說只是開了個頭。這個過程中的錯誤,只是我們在使用C語言描述一個演算法中所產生的錯誤,是比較容易排除的。我們寫一個程式,到編譯、連線通過為止,應該說剛剛開始,程式在執行過程中所出現的問題,是演算法設計有問題,說得嚴重點兒是對問題的認識和理解不夠,還需要更加深入地測試、除錯和修改。一個程式,稍為複雜的程式,往往要經過多次的編譯、連線、測試和修改。 gcc是在Linux下開發程式時必須掌握的工具之一。

以上對gcc作了一個簡要的介紹,主要講述瞭如何使用gcc編譯程式、產生警告資訊、和加快gcc的編譯速度。對所有希望早日跨入Linux開發者行列的人來說,gcc就是成為一名優秀的Linux程式設計師的起跑線。關於除錯 C 程式的更多資訊請看第4章關於gdb的內容。

相關文章