Linux下C編程基礎(chǔ)之:gcc編譯器
GNUCC(簡稱為gcc)是GNU項目中符合ANSIC標(biāo)準(zhǔn)的編譯系統(tǒng),能夠編譯用C、C++和ObjectC等語言編寫的程序。gcc不僅功能強大,而且可以編譯如C、C++、ObjectC、Java、Fortran、Pascal、Modula-3和Ada等多種語言,而且gcc是一個交叉平臺編譯器,它能夠在當(dāng)前CPU平臺上為多種不同體系結(jié)構(gòu)的硬件平臺開發(fā)軟件,因此尤其適合在嵌入式領(lǐng)域的開發(fā)編譯。本章中的示例,除非特別注明,否則均采用4.x.x的gcc版本。
表3.6所示為gcc支持編譯源文件的后綴及其解釋。
表3.6 gcc所支持后綴名解釋
后綴名
所對應(yīng)的語言
后綴名
所對應(yīng)的語言
.c
C原始程序
.s/.S
匯編語言原始程序
.C/.cc/.cxx
C++原始程序
.h
預(yù)處理文件(頭文件)
.m
Objective-C原始程序
.o
目標(biāo)文件
.i
已經(jīng)過預(yù)處理的C原始程序
.a/.so
編譯后的庫文件
.ii
已經(jīng)過預(yù)處理的C++原始程序
…
…
3.3.1gcc編譯流程解析如本章開頭提到的,gcc的編譯流程分為了4個步驟,分別為:
n 預(yù)處理(Pre-Processing);
n 編譯(Compiling);
n 匯編(Assembling);
n 鏈接(Linking)。
下面就具體來查看一下gcc是如何完成以上4個步驟的。
首先看一下hello.c的源代碼:
#include<stdio.h>
intmain()
{
printf("Hello!Thisisourembeddedworld!\n");
return0;
}
(1)預(yù)處理階段。
在該階段,對包含的頭文件(#include)和宏定義(#define、#ifdef等)進行處理。在上述代碼的預(yù)處理過程中,編譯器將包含的頭文件stdio.h編譯進來,并且用戶可以使用gcc的選項“-E”進行查看,該選項的作用是讓gcc在預(yù)處理結(jié)束后停止編譯過程。
注意
gcc指令的一般格式為:gcc[選項]要編譯的文件[選項][目標(biāo)文件]
其中,目標(biāo)文件可缺省,gcc默認(rèn)生成可執(zhí)行的文件,名為:編譯文件.out
[root@localhostgcc]#gcc–Ehello.c–ohello.i
在此處,選項“-o”是指目標(biāo)文件,由表3.6可知,“.i”文件為已經(jīng)過預(yù)處理的C程序。以下列出了hello.i文件的部分內(nèi)容:
typedefint(*__gconv_trans_fct)(struct__gconv_step*,
struct__gconv_step_data*,void*,
__constunsignedchar*,
__constunsignedchar**,
__constunsignedchar*,unsignedchar**,
size_t*);
…
#2"hello.c"2
intmain()
{
printf("Hello!Thisisourembeddedworld!\n");
return0;
}
由此可見,gcc確實進行了預(yù)處理,它把“stdio.h”的內(nèi)容插入hello.i文件中。
(2)編譯階段。
接下來進行的是編譯階段,在這個階段中,gcc首先要檢查代碼的規(guī)范性、是否有語法錯誤等,以確定代碼實際要做的工作,在檢查無誤后,gcc把代碼翻譯成匯編語言。用戶可以使用“-S”選項來進行查看,該選項只進行編譯而不進行匯編,結(jié)果生成匯編代碼。
[root@localhostgcc]#gcc–Shello.i–ohello.s
以下列出了hello.s的內(nèi)容,可見gcc已經(jīng)將其轉(zhuǎn)化為匯編代碼了,感興趣的讀者可以分析一下這一個簡單的C語言小程序是如何用匯編代碼實現(xiàn)的。
.file"hello.c"
.section.rodata
.align4
.LC0:
.string"Hello!Thisisourembeddedworld!"
.text
.globlmain
.typemain,@function
main:
pushl%ebp
movl%esp,%ebp
subl$8,%esp
andl$-16,%esp
movl$0,%eax
addl$15,%eax
addl$15,%eax
shrl$4,%eax
sall$4,%eax
subl%eax,%esp
subl$12,%esp
pushl$.LC0
callputs
addl$16,%esp
movl$0,%eax
leave
ret
.sizemain,.-main
.ident"GCC:(GNU)4.0.0200XYZ19(RedHat4.0.0-8)"
.section.note.GNU-stack,"",@progbits
(3)匯編階段。
匯編階段是把編譯階段生成的“.s”文件轉(zhuǎn)成目標(biāo)文件,讀者在此使用選項“-c”就可看到匯編代碼已轉(zhuǎn)化為“.o”的二進制目標(biāo)代碼了,如下所示:
[root@localhostgcc]#gcc–chello.s–ohello.o
(4)鏈接階段。
在成功編譯之后,就進入了鏈接階段。這里涉及一個重要的概念:函數(shù)庫。
讀者可以重新查看這個小程序,在這個程序中并沒有定義“printf”的函數(shù)實現(xiàn),且在預(yù)編譯中包含進的“stdio.h”中也只有該函數(shù)的聲明,而沒有定義函數(shù)的實現(xiàn),那么,是在哪里實現(xiàn)“printf”函數(shù)的呢?最后的答案是:系統(tǒng)把這些函數(shù)的實現(xiàn)都放到名為libc.so.6的庫文件中去了,在沒有特別指定時,gcc會到系統(tǒng)默認(rèn)的搜索路徑“/usr/lib”下進行查找,也就是鏈接到libc.so.6函數(shù)庫中去,這樣就能調(diào)用函數(shù)“printf”了,而這也正是鏈接的作用。
函數(shù)庫有靜態(tài)庫和動態(tài)庫兩種。靜態(tài)庫是指編譯鏈接時,將庫文件的代碼全部加入可執(zhí)行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。其后綴名通常為“.a”。動態(tài)庫與之相反,在編譯鏈接時并沒有將庫文件的代碼加入可執(zhí)行文件中,而是在程序執(zhí)行時加載庫,這樣可以節(jié)省系統(tǒng)的開銷。一般動態(tài)庫的后綴名為“.so”,如前面所述的libc.so.6就是動態(tài)庫。gcc在編譯時默認(rèn)使用動態(tài)庫。
完成了鏈接之后,gcc就可以生成可執(zhí)行文件,如下所示。
[root@localhostgcc]#gcchello.o–ohello
運行該可執(zhí)行文件,出現(xiàn)的正確結(jié)果如下。
[root@localhostgcc]#./hello
Hello!Thisisourembeddedworld!
3.3.2gcc編譯選項分析gcc有超過100個可用選項,主要包括總體選項、告警和出錯選項、優(yōu)化選項和體系結(jié)構(gòu)相關(guān)選項。以下對每一類中最常用的選項進行講解。
(1)常用選項。
gcc的常用選項如表3.7所示,很多在前面的示例中已經(jīng)有所涉及。
表3.7 gcc常用選項列表
選項
含義
-c
只編譯不鏈接,生成目標(biāo)文件“.o”
-S
只編譯不匯編,生成匯編代碼
-E
只進行預(yù)編譯,不做其他處理
-g
在可執(zhí)行程序中包含標(biāo)準(zhǔn)調(diào)試信息
-ofile
將file文件指定為輸出文件
-v
打印出編譯器內(nèi)部編譯各過程的命令行信息和編譯器的版本
-Idir
在頭文件的搜索路徑列表中添加dir目錄
前一小節(jié)已經(jīng)講解了“-c”、“-E”、“-o”、“-S”選項的使用方法,在此主要講解另外2個非常常用的庫依賴選項“-Idir”。
n “-Idir”
正如上表中所述,“-Idir”選項可以在頭文件的搜索路徑列表中添加dir目錄。由于Linux中頭文件都默認(rèn)放到了“/usr/include/”目錄下,因此,當(dāng)用戶希望添加放置在其他位置的頭文件時,就可以通過“-Idir”選項來指定,這樣,gcc就會到相應(yīng)的位置查找對應(yīng)的目錄。
比如在“/root/workplace/gcc”下有兩個文件:
/*hello1.c*/
#include<my.h>
intmain()
{
printf("Hello!!\n");
return0;
}
/*my.h*/
#include<stdio.h>
這樣,就可在gcc命令行中加入“-I”選項:
[root@localhostgcc]gcchello1.c–I/root/workplace/gcc/-ohello1
這樣,gcc就能夠執(zhí)行出正確結(jié)果。
小知識
在include語句中,“<>”表示在標(biāo)準(zhǔn)路徑中搜索頭文件,““””表示在本目錄中搜索。故在上例中,可把hello1.c的“#include<my.h>”改為“#include“my.h””,就不需要加上“-I”選項了。
(2)庫選項。
gcc庫選項如表3.8所示。
表3.8 gcc庫選項列表
選項
含義
-static
進行靜態(tài)編譯,即鏈接靜態(tài)庫,禁止使用動態(tài)庫
-shared
1.可以生成動態(tài)庫文件
2.進行動態(tài)編譯,盡可能地鏈接動態(tài)庫,只有當(dāng)沒有動態(tài)庫時才會鏈接同名的靜態(tài)庫(默認(rèn)選項,即可省略)
-Ldir
在庫文件的搜索路徑列表中添加dir目錄
-lname
鏈接稱為libname.a(靜態(tài)庫)或者libname.so(動態(tài)庫)的庫文件。若兩個庫都存在,則根據(jù)編譯方式(-static還是-shared)而進行鏈接
-fPIC(或-fpic)
生成使用相對地址的位置無關(guān)的目標(biāo)代碼(PositionIndependentCode)。然后通常使用gcc的-static選項從該PIC目標(biāo)文件生成動態(tài)庫文件
我們通常需要將一些常用的公共函數(shù)編譯并集成到二進制文件(Linux的ELF格式文件),以便其他程序可重復(fù)地使用該文件中的函數(shù),此時將這種文件叫做函數(shù)庫,使用函數(shù)庫不僅能夠節(jié)省很多內(nèi)存和存儲器的空間資源,而且更重要的是大大降低開發(fā)難度和開銷,提高開發(fā)效率并增強程序的結(jié)構(gòu)性。實際上,在Linux中的每個程序都會鏈接到一個或者多個庫。比如使用C函數(shù)的程序會鏈接到C運行時庫,Qt應(yīng)用程序會鏈接到Qt支持的相關(guān)圖形庫等。
函數(shù)庫有靜態(tài)庫和動態(tài)庫兩種,靜態(tài)庫是一系列的目標(biāo)文件(.o文件)的歸檔文件(文件名格式為libname.a),如果在編譯某個程序時鏈接靜態(tài)庫,則鏈接器將會搜索靜態(tài)庫,從中提取出它所需要的目標(biāo)文件并直接復(fù)制到該程序的可執(zhí)行二進制文件(ELF格式文件)之中;動態(tài)庫(文件名格式為libname.so[.主版本號.次版本號.發(fā)行號])在程序編譯時并不會被鏈接到目標(biāo)代碼中,而是在程序運行時才被載入。
下面舉一個簡單的例子,講解如何怎么創(chuàng)建和使用這兩種函數(shù)庫。
首先創(chuàng)建unsgn_pow.c文件,它包含unsgn_pow()函數(shù)的定義,具體代碼如下所示。
/*unsgn_pow.c:庫程序*/
unsignedlonglongunsgn_pow(unsignedintx,unsignedinty)
{
unsignedlonglongres=1;
if(y==0)
{
res=1;
}
elseif(y==1)
{
res=x;
}
else
{
res=x*unsgn_pow(x,y-1);
}
returnres;
}
然后創(chuàng)建pow_test.c文件,它會調(diào)用unsgn_pow()函數(shù)。
/*pow_test.c*/
#include<stdio.h>
#include<stdlib.h>
intmain(intargc,char*argv[])
{
unsignedintx,y;
unsignedlonglongres;
if((argc<3)||(sscanf(argv[1],"%u",&x)!=1)
||(sscanf(argv[2],"%u",&y))!=1)
{
printf("Usage:powbaseexponent\n");
exit(1);
}
res=unsgn_pow(x,y);
printf("%u^%u=%u\n",x,y,res);
exit(0);
}
我們用unsgn_pow.c文件可以制作一個函數(shù)庫。下面分別講解怎么生成靜態(tài)庫和動態(tài)庫。
n 靜態(tài)庫的創(chuàng)建和使用。
創(chuàng)建靜態(tài)庫比較簡單,使用歸檔工具ar將一些目標(biāo)文件集成在一起。
[root@localhostlib]#gcc-cunsgn_pow.c
[root@localhostlib]#arrcsvlibpow.aunsgn_pow.o
a-unsgn_pow.o
下面編譯主程序,它將會鏈接到剛生成的靜態(tài)庫libpow.a。具體運行結(jié)果如下所示。
[root@localhostlib]#gcc-opow_testpow_test.c-L.–lpow
[root@localhostlib]#./pow_test210
2^10=1024
其中,選項“-Ldir”的功能與“-Idir”類似,能夠在庫文件的搜索路徑列表中添加dir目錄,而“-lname”選項指示編譯時鏈接到庫文件libname.a或者libname.so。本實例中,程序pow_test.c需要使用當(dāng)前目錄下的一個靜態(tài)庫libpow.a。
n 動態(tài)庫的創(chuàng)建和使用。
首先使用gcc的-fPIC選項為動態(tài)庫構(gòu)造一個目標(biāo)文件
[root@localhostlib]#gcc-fPIC-Wall-cunsgn_pow.c
接下來,使用-shared選項和已創(chuàng)建的位置無關(guān)目標(biāo)代碼,生成一個動態(tài)庫libpow.so。
[root@localhostlib]#gcc-shared-olibpow.sounsgn_pow.o
下面編譯主程序,它將會鏈接到剛生成的動態(tài)庫libpow.so。
[root@localhostlib]#gcc-opow_testpow_test.c-L.–lpow
在運行可執(zhí)行程序之前,需要注冊動態(tài)庫的路徑名。其方法有幾種:修改/etc/ld.so.conf文件,或者修改LD_LIBRARY_PATH環(huán)境變量,或者將庫文件直接復(fù)制到/lib或者/usr/lib目錄下(這兩個目錄為系統(tǒng)的默認(rèn)的庫路徑名)。
[root@localhostlib]#cplibpow.so/lib
[root@localhostlib]#./pow_test210
2^10=1024
動態(tài)庫只有當(dāng)使用它的程序執(zhí)行時才被鏈接使用,而不是將需要的部分直接編譯入可執(zhí)行文件中,并且一個動態(tài)庫可以被多個程序使用故可稱為共享庫,而靜態(tài)庫將會整合到程序中,因此在程序執(zhí)行時不用加載靜態(tài)庫。從而可知,鏈接到靜態(tài)庫會使用戶的程序臃腫,并且難以升級,但是可能會比較容易部署。而鏈接到動態(tài)庫會使用戶的程序輕便,并且易于升級,但是會難以部署。
(3)告警和出錯選項。
gcc的告警和出錯選項如表3.9所示。
表3.9 gcc警告和出錯選項選項列表
選項
含義
-ansi
支持符合ANSI標(biāo)準(zhǔn)的C程序
-pedantic
允許發(fā)出ANSIC標(biāo)準(zhǔn)所列的全部警告信息
-pedantic-error
允許發(fā)出ANSIC標(biāo)準(zhǔn)所列的全部錯誤信息
-w
關(guān)閉所有告警
-Wall
允許發(fā)出gcc提供的所有有用的報警信息
-werror
把所有的告警信息轉(zhuǎn)化為錯誤信息,并在告警發(fā)生時終止編譯過程
下面結(jié)合實例對這幾個告警和出錯選項進行簡單的講解。
有以下程序段:
#include<stdio.h>
voidmain()
{
longlongtmp=1;
printf("Thisisabadcode!\n");
return0;
}
這是一個很糟糕的程序,讀者可以考慮一下有哪些問題。
n “-ansi”
該選項強制gcc生成標(biāo)準(zhǔn)語法所要求的告警信息,盡管這還并不能保證所有沒有警告的程序都是符合ANSIC標(biāo)準(zhǔn)的。運行結(jié)果如下所示:
[root@localhostgcc]#gcc–ansiwarning.c–owarning
warning.c:在函數(shù)“main”中:
warning.c:7警告:在無返回值的函數(shù)中,“return”帶返回值
warning.c:4警告:“main”的返回類型不是“int”
可以看出,該選項并沒有發(fā)現(xiàn)“longlong”這個無效數(shù)據(jù)類型的錯誤。
n “-pedantic”
打印ANSIC標(biāo)準(zhǔn)所列出的全部警告信息,同樣也保證所有沒有警告的程序都是符合ANSIC標(biāo)準(zhǔn)的。其運行結(jié)果如下所示:
[root@localhostgcc]#gcc–pedanticwarning.c–owarning
warning.c:在函數(shù)“main”中:
warning.c:5警告:ISOC90不支持“longlong”
warning.c:7警告:在無返回值的函數(shù)中,“return”帶返回值
warning.c:4警告:“main”的返回類型不是“int”
可以看出,使用該選項查出了“longlong”這個無效數(shù)據(jù)類型的錯誤。
n “-Wall”
打印gcc能夠提供的所有有用的報警信息。該選項的運行結(jié)果如下所示:
[root@localhostgcc]#gcc–Wallwarning.c–owarning
warning.c:4警告:“main”的返回類型不是“int”
warning.c:在函數(shù)“main”中:
warning.c:7警告:在無返回值的函數(shù)中,“return”帶返回值
warning.c:5警告:未使用的變量“tmp”
使用“-Wall”選項找出了未使用的變量tmp,但它并沒有找出無效數(shù)據(jù)類型的錯誤。
另外,gcc還可以利用選項對單獨的常見錯誤分別指定警告。
(4)優(yōu)化選項。
gcc可以對代碼進行優(yōu)化,它通過編譯選項“-On”來控制優(yōu)化代碼的生成,其中n是一個代表優(yōu)化級別的整數(shù)。對于不同版本的gcc來講,n的取值范圍及其對應(yīng)的優(yōu)化效果可能并不完全相同,比較典型的范圍是從0變化到2或3。
不同的優(yōu)化級別對應(yīng)不同的優(yōu)化處理工作。如使用優(yōu)化選項“-O”主要進行線程跳轉(zhuǎn)(ThreadJump)和延遲退棧(DeferredStackPops)兩種優(yōu)化。使用優(yōu)化選項“-O2”除了完成所有“-O1”級別的優(yōu)化之外,同時還要進行一些額外的調(diào)整工作,如處理器指令調(diào)度等。選項“-O3”則還包括循環(huán)展開和其他一些與處理器特性相關(guān)的優(yōu)化工作。
雖然優(yōu)化選項可以加速代碼的運行速度,但對于調(diào)試而言將是一個很大的挑戰(zhàn)。因為代碼在經(jīng)過優(yōu)化之后,原先在源程序中聲明和使用的變量很可能不再使用,控制流也可能會突然跳轉(zhuǎn)到意外的地方,循環(huán)語句也有可能因為循環(huán)展開而變得到處都有,所有這些對調(diào)試來講都將是一場噩夢。所以筆者建議在調(diào)試的時候最好不使用任何優(yōu)化選項,只有當(dāng)程序在最終發(fā)行的時候才考慮對其進行優(yōu)化。
(5)體系結(jié)構(gòu)相關(guān)選項。
gcc的體系結(jié)構(gòu)相關(guān)選項如表3.10所示。
表3.10 gcc體系結(jié)構(gòu)相關(guān)選項列表
選項
含義
-mcpu=type
針對不同的CPU使用相應(yīng)的CPU指令。可選擇的type有i386、i486、pentium及i686等
-mieee-fp
使用IEEE標(biāo)準(zhǔn)進行浮點數(shù)的比較
-mno-ieee-fp
不使用IEEE標(biāo)準(zhǔn)進行浮點數(shù)的比較
-msoft-float
輸出包含浮點庫調(diào)用的目標(biāo)代碼
-mshort
把int類型作為16位處理,相當(dāng)于shortint
-mrtd
強行將函數(shù)參數(shù)個數(shù)固定的函數(shù)用retNUM返回,節(jié)省調(diào)用函數(shù)的一條指令
這些體系結(jié)構(gòu)相關(guān)選項在嵌入式的設(shè)計中會有較多的應(yīng)用,讀者需根據(jù)不同體系結(jié)構(gòu)將對應(yīng)的選項進行組合處理。在本書后面涉及具體實例時將會有針對性的講解。