【CentOS Linux 7】【gcc编译器】
- Linux系統及應用---調研報告
- 實驗4文檔:gcc編譯器、Linux網絡配置基礎
目? ?錄
第3章? gcc 編 譯 器
3.1? gcc 簡 介
3.2? 使 用 gcc
3.3? gcc警告提示功能
3.4? 庫? 依? 賴
3.5? gcc代碼優化
3.6? 加??? 速
3.7? gcc常用選項
3.8? gcc的錯誤類型及對策
第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的內容。
總結
以上是生活随笔為你收集整理的【CentOS Linux 7】【gcc编译器】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【CentOS Linux 7】实验2【
- 下一篇: 【CentOS Linux 7】【Lin