GCC : 什么是编译?什么是静态库?什么是动态库?怎么生成?优先级?
本文旨在讓大家真正明白C程序如何運行、庫文件的種類區別、如何生成、如何使用等!
?
一、簡介:
gcc 最初是 "GNU C Compiler" 的簡稱,只是當作一個 C 語言的編譯器,現在已經變成了 "GNU Compiler Collection",可以編譯多種語言。
常用的命令參數選項:
二、編譯的四個階段:
在使用 gcc 編譯程序時,編譯過程可以被細分為 4 個階段:
◆?預處理(Pre-Processing)
◆?編譯(Compiling)
◆?匯編(Assembling)
◆?鏈接(Linking)
三、舉例
下面以一份簡單的 C 代碼來說明:
#include <stdio.h> int main() {printf("Hello,World!\n"); }?
1)首先,預處理生成test.i文件:
gcc -E -o test.i test.c
從上面我們可以知道,預處理其實就是 展開頭文件、宏替換、去掉注釋、條件編譯
2)編譯,解析詞義、檢查語法、解析語義。生成匯編語言。
gcc -S -o test.s test.i
從上面我們可以知道,編譯其實就是檢查語法、解析語義、生成匯編代碼。
3)匯編
gcc -c -o test.o test.s
從上面我們可以知道,匯編其實就是生成二進制0-1代碼,計算機所能識別的代碼。
4)鏈接(這一步可以鏈接靜態庫和動態庫,后面會講解到),生成計算機可執行文件。
gcc ?-o test?test.o
四、那么什么是庫文件?
?
在說明Linux的.a、.so和.o文件關系之前,我們先來看看windows下obj,lib,dll,exe的關系!
windows下obj,lib,dll,exe的關系
??? lib是和dll對應的。lib是靜態鏈接庫的庫文件,dll是動態鏈接庫的庫文件。?
??? 所謂靜態就是link的時候把里面需要的東西抽取出來安排到你的exe文件中,以后運行你的exe的時候不再需要lib。
??? 所謂動態就是exe運行的時候依賴于dll里面提供的功能,沒有這個dll,你的exe無法運行。?
????
??? lib,dll,exe都算是最終的目標文件,是最終產物。而c/c++屬于源代碼。源代碼和最終目標文件中過渡的就是中間代碼obj,實際上之所以需要中間代碼,是你不可能一次得到目標文件。比如說一個exe需要很多的cpp文件生成。而編譯器一次只能編譯一個cpp文件。這樣編譯器編譯好一個cpp以后會將其編譯成obj,當所有必須要的cpp都編譯成obj以后,再統一link成所需要的exe,應該說缺少任意一個obj都會導致exe的鏈接失敗。(win下面的obj相當于linux里的.o文件)
????
??? 1.obj里存的是編譯后的代碼跟數據,并且有名稱,所以在連接時有時會出現未解決的外部符號的問題。當連成exe后便不存在名稱的概念了,只有地址。lib就是一堆obj的組合。
??? 2.理論上可以連接obj文件來引用其他工程(可以認為一個obj文件等價于編譯生成它的cpp文件,可以引用obj來替換cpp,也可以添加cpp來替換obj ),但實際中通常用lib來實現工程間相互引用。
??? 3.編譯器會默認鏈接一些常用的庫,其它的需要你自己指定。
????
lib和DLL的區別
??? (1)lib是編譯時需要的,dll是運行時需要的。如果要完成源代碼的編譯,有lib就夠了。如果也使動態連接的程序運行起來,有dll就夠了。在開發和調試階段,當然最好都有。
??? (2) 一般的動態庫程序有lib文件和dll文件。lib文件是必須在編譯期就連接到應用程序中的,而dll文件是運行期才會被調用的。如果有dll文件,那么對應的lib文件一般是一些索引信息,具體的實現在dll文件中。如果只有lib文件,那么這個lib文件是靜態編譯出來的,索引和實現都在其中。 靜態編譯的lib文件有好處:給用戶安裝時就不需要再掛動態庫了。但也有缺點,就是導致應用程序比較大,而且失去了動態庫的靈活性,在版本升級時,同時要發布新的應用程序才行。
??? (3)在動態庫的情況下,有兩個文件,一個是引入庫(.LIB)文件(實際上也算是一個靜態庫,只是在鏈接時只能把函數在DLL的入口鏈接到exe中,而不像真正靜態鏈接庫那樣將函數體真正鏈接到exe中 ,通過lib進行的動態鏈接實際上也使用了靜態鏈接來實現 ),一個是DLL文件,引入庫文件包含被DLL導出的函數的名稱和位置,DLL包含實際的函數和數據,應用程序使用LIB文件鏈接到所需要使用的DLL文件,庫中的函數和數據并不復制到可執行文件中,因此在應用程序的可執行文件中,存放的不是被調用的函數代碼,而是DLL中所要調用的函數的內存地址,這樣當一個或多個應用程序運行是再把程序代碼和被調用的函數代碼鏈接起來,從而節省了內存資源(共享屬性)。從上面的說明可以看出,DLL和.LIB文件必須隨應用程序一起發行,否則應用程序將會產生錯誤。
DLL內的函數分為兩種:?
??? (1)DLL導出函數,可供應用程序調用;
??? (2)DLL內部函數,只能在DLL程序使用,應用程序無法調用它們
創建靜態鏈接庫和創建動態鏈接庫
??? VC6中創建[Win32 Dynamic-Link Library]工程便可以創建出一個空的DLL工程.
??? VC6中創建[Win32 Static Library]工程便可以創建出一個空的LIB工程(靜態鏈接庫工程,僅生成一個lib文件).
添加lib文件的常用辦法有二個:?
??? 1、把*.lib放在VC的Lib目錄中?
??? 2、修改project setting的Link->Input中的Addtional library path,加入你的目錄dll:是可實際運行的二進制代碼,有定位代碼的!
??? 3、也可以在object/library中直接寫上lib文件路徑.(這里實際上是可以寫上任意obj文件或者lib文件的).
?
以上說完了windows下的庫文件,現在來說說linux下的庫文件:
linux? ? ?.o? ?.a? ? .so
??????? .o,是目標文件,相當于windows中的.obj文件?
.so 為共享庫,是shared object,用于動態連接的,相當于windows下的dll?
.a為靜態庫,是好多個.o合在一起,用于靜態連接?
?
靜態函數庫
特點:實際上是簡單的普通目標文件的集合,在程序執行前就加入到目標程序中。
優點:可以用以前某些程序兼容;描述簡單;允許程序員把程序link起來而不用重新編譯代碼,節省了重新編譯代碼的時間(該優勢目前已不明顯);開發者可以對源代碼保密;理論上使用ELF格式的靜態庫函數生成的代碼可以比使用共享或動態函數庫的程序運行速度快(大概1%-5%)
生成:使用ar程序(archiver的縮寫)。
例子:ar rcs my_lib.a f1.o f2.o? ? ? 把目標代碼f1.o和f2.o加入到my_lib.a這個函數庫文件中(如果my_lib.a不存在則創建)
使用:用gcc生成可執行代碼時,使用-l參數指定要加入的庫函數。也可以用ld命令的-l和-L參數。
?
共享函數庫
??? 共享函數庫在可執行程序啟動的時候加載,所有程序重新運行時都可自動加載共享函數庫中的函數。.so文件感覺很復雜,光是命名規則就已經看得我很暈了~整理一下,共享庫需要:soname、real name,另外編譯的時候名字也有說法。依次解釋下:
soname:必須的格式:lib+函數庫名+.so+版本號信息(但是記住,非常底層的C庫函數都不是以lib開頭命名的)。
例子:/usr/lib/libreadline.so.3
real name:顧名思義是真正的名字啦,有主版本號和發行版本號。但是沒找到實例……
編譯器編譯的時候需要的函數庫的名字就是不包含版本號信息的soname,例如上面的例子把最后的.3去掉就可以了。
位置:共享函數庫文件必須放在特定目錄,對于開放源碼來說,GNU標準建議所有的函數庫文件都放在/usr/local/lib目錄下,而且建議命令、可執行程序都放在/usr/local/bin目錄下。
? ? ?不過這個只是習慣啦,可以改變,具體的位置信息可以看/etc/ld.so.conf里面的配置信息。當然,也可以修改這個文件,加入自己的一些特殊的路徑要求。
創建:在網上找到了gcc方式和easyeclipse環境下兩種創建方式。
gcc方式:
??? 首先創建object文件,這個文件將加入通過gcc –fPIC 參數命令加入到共享函數庫里面,標準格式:gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list(說實話這個標準格式看起來好復雜,我找了個實例,但是好像和那個標準格式稍有不同:gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so)
在easyeclipse環境下生成.so文件:
??????? 1.選擇新建工程,建立一個c++工程
??????? 2.在工程類型選項里選擇 Shared Library,然后填入工程名字PXXX點擊完成即可。
??????? 3.編寫程序,然后編譯就會在debug或者release里生成一個libPXXX.so文件,如果不要lib的起頭標記點擊project菜單的Properties選項,然后在彈出的界面的右邊點擊Build artifact頁面,將Output prefix選項的內容清空即可。
??????? 4.如果是C++程序,注意在接口函數的前面加上extern "C"標記,在頭文件加上如下標記:
#ifdef?? __cplusplus??
#extern?? "C"{??
#endif??
???
頭文件主體??
???
#ifdef?? __cplusplus??
}??
#endif??
???? 如果不加以上標記,經過編譯后,so里的函數名并非你編寫程序時設定的函數名,在開發環境左側的工程文件列表中點開debug項里的PXXX.o可以看到so文件里的函數名都是在你設定的函數名后面加了一個__Fi標記,比如你用的設定的函數名稱是Func(), 而so里的函數名則為Func__Fi()或者其他的名稱。
安裝:拷貝共享庫文件到指定的標準的目錄,然后運行ldconfig。如果沒有權限這樣做,那么就只好通過修改環境變量來實現這些函數庫的使用了。方法不再說了,很復雜。
查看:可以通過運行ldd來看某個程序使用的共享函數庫。例如ldd /bin/ls。查看.so文件使用nm命令,如nm libXXX.so。(注意,nm對于靜態的函數庫和共享的函數庫都起作用)
關于覆蓋:如果想用自己的函數覆蓋某個庫中的一些函數,同時保留該庫中其他的函數的話,可以在/etc/ld.so.preload中加入要替換的庫(.o結尾的文件),這些preloading的庫函數將有優先加載的權利。
關于更新:每次新增加動態加載的函數庫、刪除某個函數庫或者修改某個函數庫的路徑時,都要重新運行ldconfig來更新緩存文件/etc/ld.so.cache,此文件保存已排好序的動態鏈接庫名字列表
(在Linux下,共享庫的加載是由/lib/ld.so完成的,ld.so加載共享庫時,會從ld.so.cache查找)
?
五、庫文件例子
1)靜態庫。
我們通常把一些公用函數制作成函數庫,供其它程序使用。函數庫分為靜態庫和動態庫兩種。靜態庫在程序編譯時會被連接到目標代碼中,程序運行時將不再需要該靜態庫。動態庫在程序編譯時并不會被連接到目標代碼中,而是在程序運行是才被載入,因此在程序運行時還需要動態庫存在。本文主要通過舉例來說明在Linux中如何創建靜態庫和動態庫,以及使用它們。
第一步:在創建函數庫前,我們先來準備舉例用的源程序,并將函數庫的源程序編譯成.o文件。
程序1: hello.h
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif //HELLO_H
程序2: hello.c
#include <stdio.h>
void hello(const char *name)
{
printf("Hello %s!\n", name);
}
程序3: main.c
#include "hello.h"
int main()
{
hello("everyone");
return 0;
}
第2步:將hello.c編譯成.o文件;
第3步:由.o文件創建靜態庫;
靜態庫文件名的命名規范是以lib為前綴,緊接著跟靜態庫名,擴展名為.a。例如:我們將創建的靜態庫名為myhello,則靜態庫文件名就是libmyhello.a。在創建和使用靜態庫時,需要注意這點。
創建靜態庫用ar命令。在系統提示符下鍵入以下命令將創建靜態庫文件libmyhello.a。
ar -cr libmyhello.a hello.o
第4步:在程序中使用靜態庫;
靜態庫制作完了,如何使用它內部的函數呢?只需要在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然后在用gcc命令生成目標文件時指明靜態庫名,gcc將會從靜態庫中將公用函數連接到目標文件中。注意,gcc會在靜態庫名前加上前綴lib,然后追加擴展名.a得到的靜態庫文件名來查找靜態庫文件。
在程序3:main.c中,我們包含了靜態庫的頭文件hello.h,然后在主程序main中直接調用公用函數hello。下面先生成目標程序main,然后運行main程序看看結果如何。
gcc -o main main.c libmyhello.a
還可以用以下命令,等價的。
法一 # gcc -o main main.c -L. –lmyhello,或gcc ?main.c -L. –lmyhello?-o main自定義的庫時,main.c還可放在-L.和 –lmyhello之間,但是不能放在它倆之后,否則會提示myhello沒定義,但是是系統的庫時,如g++ -o main(-L/usr/lib) -lpthread main.cpp就不出錯。
法二 #gcc main.c libmyhello.a -o main或gcc ?-o main main.c libmyhello.a
法三:先生成main.o:gcc -c main.c ,再生成可執行文件:gcc -o main main.o libmyhello.a或gccmain.o libmyhello.a?-o main ,動態庫連接時也可以這樣做。
我們刪除靜態庫文件試試公用函數hello是否真的連接到目標文件 main中了。
會發現還是可以執行,可以見得hello已經連接到目標文件main中了。
2) 動態庫。
我們繼續看看如何在Linux中創建動態庫。我們還是從.o文件開始。
?
第1步:動態庫文件名命名規范和靜態庫文件名命名規范類似,也是在動態庫名增加前綴lib,但其文件擴展名為.so。例如:我們將創建的動態庫名為myhello,則動態庫文件名就是libmyhello.so。用gcc來創建動態庫。在系統提示符下鍵入以下命令得到動態庫文件libmyhello.so。
發現報錯了:解決方案? ?(要在編譯.o文件時就要帶上 -fPIC)
第2步:在程序中使用動態庫;
在程序中使用動態庫和使用靜態庫完全一樣,也是在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然后在用gcc命令生成目標文件時指明動態庫名進行編譯。我們先運行gcc命令生成目標文件,再運行它看看結果。
發現報錯了,原因是在mian運行的時候,在調用hello函數的時候,linux會默認去/usr/lib下去找.so文件。因此需要將libmyhello.so復制到/usr/lib目錄下即可,于是:
至此,動態庫生成成功,鏈接運行完成。
我們回過頭看看,發現使用靜態庫和使用動態庫編譯成目標程序使用的gcc命令完全一樣,那當靜態庫和動態庫同名時,gcc命令會使用哪個庫文件呢?抱著對問題必究到底的心情,來試試看。
先刪除除.c和.h外的所有文件,恢復成我們剛剛編輯完舉例程序狀態。
在來創建靜態庫文件libmyhello.a和動態庫文件libmyhello.so。
在生成動態庫時,需要使用-fPIC,這樣才能生成位置無關的代碼,達到代碼段和數據段共享的目的
# gcc -c -fpic hello.c ?//編譯hello.c時也需要加上-fpic選項,否則rodata' can not be used when making a shared object; recompile with -fPIC
# ar -cr libmyhello.a hello.o (或-cvr )
# gcc -shared -fPIC -o libmyhello.so hello.o
# ls
hello.c hello.h hello.o libmyhello.a libmyhello.so main.c
通過上述最后一條ls命令,可以發現靜態庫文件libmyhello.a和動態庫文件libmyhello.so都已經生成,并都在當前目錄中。然后,我們運行gcc命令來使用函數庫myhello生成目標文件main,并運行程序 main。
# gcc -o main main.c -L. –lmyhello (動態庫和靜態庫同時存在時,優先使用動態庫, 當然,直接#gcc main.c libmyhello.a -o main的話,就是指定為靜態庫了)
# ./hello
./hello: error while loading shared libraries: libmyhello.so: cannot open shar
ed object file: No such file or directory
從程序hello運行的結果中很容易知道,當靜態庫和動態庫同名時,gcc命令將優先使用動態庫,默認去連/usr/lib和/lib等目錄中的動態庫,將文件libmyhello.so復制到目錄/usr/lib中即可。
Note:
編譯參數解析
最主要的是GCC命令行的一個選項:
-shared 該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號),不用該標志外部程序無法連接。相當于一個可執行文件
-fPIC 作用于編譯階段,告訴編譯器產生與位置無關代碼(Position-Independent Code)。那么在產生的代碼中,沒有絕對地址,全部使用相對地址,故而代碼可以被加載器加載到內存的任意位置,都可以正確的執行。這正是共享庫所要求的,共享庫被加載時,在內存的位置不是固定的。
如果不加fPIC,則編譯出來的代碼在加載時需要根據加載到的位置進行重定位(因為它里面的代碼并不是位置無關代碼),如果被多個應用程序共同使用,那么它們必須每個程序維護一份so的代碼副本了.(因為so被每個程序加載的位置都不同,顯然這些重定位后的代碼也不同,當然不能共享)。(使用的是相對路徑)
不用此選項的話編譯后的代碼是位置相關的,所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。
-L. 表示要連接的庫在當前目錄中;(多個庫:在編譯命令行中,將使用的靜態庫文件放在源文件后面就可以了。比如:gcc -L/usr/lib myprop.c libtest.a libX11.a libpthread.a -o myprop
其中-L/usr/lib指定庫文件的查找路徑。編譯器默認在當前目錄下先查找指定的庫文件,如前面的“法二 #gccmain.c libmyhello.a-o hello”)
-lmyhello 編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,后面加上.so或.a來確定庫的名稱libmyhello.so或libmyhello.a。
LD_LIBRARY_PATH這個環境變量指示動態連接器可以裝載動態庫的路徑。
當然如果有root權限的話,可以修改/etc/ld.so.conf文件,然后調用 /sbin/ldconfig來達到同樣的目的,不過如果沒有root權限,那么只能采用輸出LD_LIBRARY_PATH的方法了。
調用動態庫的時候有幾個問題會經常碰到,有時,明明已經將庫的頭文件所在目錄 通過 “-I” include進來了,庫所在文件通過 “-L”參數引導,并指定了“-l”的庫名,但通過ldd命令察看時,就是死活找不到你指定鏈接的so文件,這時你要作的就是通過修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件來指定動態庫的目錄。通常這樣做就可以解決庫無法鏈接的問題了。
?
靜態庫鏈接時搜索路徑順序:
1. ld(GNU linker)會去找GCC命令中的參數-L
? ?編譯過程是分為四個階段:預處理(也稱預編譯,Preprocessing)、編譯(Compilation)、匯編 (Assembly)和連接(link) ?【鏈接】
2. 再找gcc的環境變量LIBRARY_PATH
3. 再找內定目錄 /lib /usr/lib /usr/local/lib 這是當初compile gcc時寫在程序內的
動態鏈接時、執行時搜索路徑順序:
1. 編譯目標代碼時指定的動態庫搜索路徑
2. 環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑
3. 配置文件/etc/ld.so.conf中指定的動態庫搜索路徑
4. 默認的動態庫搜索路徑/lib
5. 默認的動態庫搜索路徑/usr/lib
有關環境變量:
LIBRARY_PATH環境變量:指定程序靜態鏈接庫文件搜索路徑
LD_LIBRARY_PATH環境變量:指定程序動態鏈接庫文件搜索路徑
另:
從上述可知,如何找到生成的動態庫有3種方式:
(1)把庫拷貝到/usr/lib和/lib目錄下。
(2)在LD_LIBRARY_PATH環境變量中加上庫所在路徑。
例如動態庫libhello.so在/home/example/lib目錄下:
export LD_LIBRARY_PATH=LD_LIBRARY_PATH:/home/example/lib
(3) 修改/etc/ld.so.conf文件,把庫所在的路徑加到文件末尾(直接寫在文件末尾,不要在路徑前加include),并執行ldconfig刷新(ldconfig 命令的用途,主要是在默認搜尋目錄(/lib和/usr/lib)以及動態庫配置文件/etc/ld.so.conf內所列的目錄下,搜索出可共享的動態鏈接庫(格式如前介紹,lib*.so*),進而創建出動態裝入程序(ld.so)所需的連接和緩存文件.緩存文件默認為/etc/ld.so.cache,此文件保存已排好序的動態鏈接庫名字列表.)。這樣,加入的目錄下的所有庫文件都可見。
?
?
附:像下面這樣指定路徑去連接系統的靜態庫,會報錯說要連接的庫找不到:
g++ -o main main.cpp -L/usr/lib libpthread.a?
必須這樣g++ -o main main.cpp -L/usr/lib -lpthread才正確 。
自定義的庫考到/usr/lib 下時,
g++ -o main main.cpp -L/usr/lib libpthread.a libthread.a libclass.a會出錯,但是這樣g++ -o main main.cpp -L/usr/lib -lpthread -lthread -lclass就正確了。
參考文章:
1)Linux---C++編譯后各種文件格式
2)c語言編譯過程詳解,預處理,編譯,匯編,鏈接(干貨滿滿)
3)編譯動態庫時遇到relocation R_X86_64_32 against `a local symbol'的錯誤
?
總結
以上是生活随笔為你收集整理的GCC : 什么是编译?什么是静态库?什么是动态库?怎么生成?优先级?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++中const用于函数重载
- 下一篇: Git :LF will be repl