Linux下Gcc生成和使用靜態庫和動態庫及相關問題總結

  最近一階段項目中編譯過程中涉及到動態與靜態庫,從網上收集了一些資料,並做個總結。

一、基本概念

1.1 什麼是庫

在windows和linux平臺下存在着大量的庫。本質上來說庫是一種可執行代碼的二進制形式,可以被操作系統載入內存執行。由於windows和linux的平臺不同(主要是編譯器、彙編器和連接器的不同),因此二者庫的二進制是不兼容的。本文僅限於介紹linux下的庫。

1.2 靜態庫、動態庫的概念

      庫文件是一些函數、變量的集合,已編譯過的代碼。一般分爲靜態庫和動態庫兩種,二者的不同點在於代碼被載入的時刻不同。

靜態庫: 靜態是指每個用到該庫的應用程序都擁有一份自己的庫拷貝;應用程序運行的時候,即使將庫刪除也沒有問題,因爲應用程序自己已經有了自己的拷貝。但是這也稱爲了它的缺點,因爲靜態庫如果發生改變的話,那麼應用程序也就需要重新進行編譯了

動態庫: 一個共享庫有可能被多個所有應用程序共享。因此,對每個應用程序來說,即使不再使用某個共享庫,也不應將其刪除。此外,應用程序需要正確的環境變量設置(LD_LIBRARY_PATH),從而找到共享庫所在的位置,否則,應用程序運行時會報告找不到這個庫。

1.3 庫存在的意義

庫是別人寫好的現有的,成熟的,可以複用的代碼,你可以使用但要記得遵守許可協議。現實中每個程序都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,因此庫的存在意義非同尋常。共享庫的好處是,不同的應用程序如果調用相同的庫,那麼在內存裏只需要有一份該共享庫的實例。

1.4 在linux下庫文件是如何產生的

靜態庫的後綴是.a,它的產生分兩步

Step 1.由源文件編譯生成一堆.o,每個.o裏都包含這個編譯單元的符號表

Step 2.ar命令將很多.o轉換成.a,成爲靜態庫

動態庫的後綴是.so,它由gcc加特定參數編譯產生。具體方法參見後文實例。

1.5 庫文件的命名規範

在linux下,庫文件一般放在/usr/lib和/lib下,靜態庫的名字一般爲libxxxx.a,其中xxxx是該lib的名稱;動態庫的名字一般爲libxxxx.so.major.minor,xxxx是該lib的名稱,major是主版本號, minor是副版本號。

1.6 如何知道一個可執行程序依賴哪些庫

ldd命令可以查看一個可執行程序依賴的共享庫,

例如# ldd /bin/lnlibc.so.6

=> /lib/libc.so.6 (0×40021000)/lib/ld-linux.so.2

=> /lib/ld- linux.so.2 (0×40000000)

可以看到ln命令依賴於libc庫和ld-linux庫

 1.7靜態庫、動態庫的環境變量配置

庫文件在鏈接(靜態庫和共享庫)和運行(僅限於使用共享庫的程序)時被使用,其搜索路徑是在系統中進行設置的
一般 Linux 系統把 /lib 和 /usr/lib 兩個目錄作爲默認的庫搜索路徑,所以使用這兩個目錄中的庫時不需要進行設置搜索路徑即可直接使用。對於處於默認庫搜索路徑之外的庫,需要將庫的位置添加到庫的搜索路徑之中。設置庫文件的搜索路徑有下列兩種方式,可任選其一使用: 
1)在環境變量 LD_LIBRARY_PATH 中指明庫的搜索路徑。 比如一個libtest.a庫在/home/test/lib目錄下,在終端內

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/test/lib
2)在 /etc/ld.so.conf 文件中添加庫的搜索路徑。添加方法也極其簡單,將庫文件的絕對路徑直接寫進去就OK了,一行一個,

例如:

編輯/etc/ld.so.conf文件,加入庫文件所在目錄的路徑

/usr/local/lib 
/opt/lib

運行ldconfig,該命令會重建/etc/ld.so.cache文件

需要注意的是:第二種搜索路徑的設置方式對於程序連接時的庫(包括共享庫和靜態庫)的定位已經足夠了,但是對於使用了共享庫的程序的執行還是不夠的。這是因爲爲了加快程序執行時對共享庫的定位速度,避免使用搜索路徑查找共享庫的低效率,所以是直接讀取庫列表文件 /etc/ld.so.cache 從中進行搜索的。/etc/ld.so.cache 是一個非文本的數據文件,不能直接編輯,它是根據 /etc/ld.so.conf 中設置的搜索路徑由 /sbin/ldconfig 命令將這些搜索路徑下的共享庫文件集中在一起而生成的(ldconfig 命令要以 root 權限執行)。

        因此,爲了保證程序執行時對庫的定位,在 /etc/ld.so.conf 中進行了庫搜索路徑的設置之後,還必須要運行 /sbin/ldconfig 命令更新 /etc/ld.so.cache 文件之後纔可以。ldconfig ,簡單的說,它的作用就是將/etc/ld.so.conf列出的路徑下的庫文件緩存到/etc/ld.so.cache 以供使用。因此當安裝完一些庫文件,(例如剛安裝好glib),或者修改ld.so.conf增加新的庫路徑後,需要運行一下 /sbin/ldconfig使所有的庫文件都被緩存到ld.so.cache中,如果沒做,即使庫文件明明就在/usr/lib下的,也是不會被使用的,結果編譯過程中抱錯,缺少xxx庫,去查看發現明明就在那放着,這時候會很是鬱悶的。

1.8靜態庫、動態庫的的使用
編譯鏈接目標程序的方法是一樣的:
#gcc main.c -L. -ltest -o main
-L.指定現在本目錄下搜索庫,如果沒有,會到系統默認的目錄下搜索,一般爲/lib、/usr/lib下
對於靜態庫,這個步驟之後就可以將libtest.a庫刪掉,因爲它已經被編譯進了目標程序,不再需要它了。而對於動態庫,libtest.so庫只是在目標程序裏做了標記,在運行程序時纔會動態加載,那麼從哪加載呢?加載目錄會由/etc/ld.so.conf來指定,一般默認是/lib、/usr/lib,所以要想讓動態庫順利加載,你可以將庫文件copy到上面的兩個目錄下,或者設置export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX/YYY,後面爲你自己動態庫的目錄,再或者修改/etc/ld.so.conf文件,把庫所在的路徑加到文件末尾,並執行ldconfig刷新。這樣,加入的目錄下的所有庫文件都可見。

1.9靜態庫、動態庫的加載順序

GCC在鏈接過程中,對參數中的庫的順序是有要求的,參數右側的庫會先於左側的庫加載,也就是說參數的解析是從右往左的。

    假設庫B依賴與庫A,則鏈接的時候要寫爲:
       gcc -o bin B A
   如果寫爲:
       gcc -o bin A B
   則在B中引用的A中的內容就會無法鏈接通過。如果在同一目錄下,既有靜態庫又有動態庫,則在默認情況下是先加載動態庫,因爲對gcc來說默認方式是加載動態庫,如果要讓靜態庫優先加載的話,則需要加上-static參數。

二、用gcc生成靜態和動態鏈接庫的示例

2.1準備好測試代碼hello.hhello.cmain.c

hello.h(見程序1)爲該函數庫的頭文件。

hello.c(見程序2)是函數庫的源程序,其中包含公用函數hello,該函數將在屏幕上輸出"Hello XXX!"。

main.c(見程序3)爲測試庫文件的主程序,在主程序中調用了公用函數hello。

 程序1: hello.h

#define HELLO_H 
void hello(const char *name); 
程序2:hello.c
void hello(const char *name) { 
        printf("Hello %s!\n", name); 
}
程序3:main.c
 int main() 
 { 
     hello("everyone"); 
     return 0; 
 }

2.2 問題的提出

注意:這個時候,我們編譯好的hello.o是無法通過gcc –o 編譯的,這個道理非常簡單,

hello.c是一個沒有main函數的.c程序,因此不夠成一個完整的程序,如果使用gcc –o 編譯並連接它,GCC將報錯。

無論靜態庫,還是動態庫,都是由.o文件創建的。因此,我們必須將源程序hello.c通過gcc先編譯成.o文件。

這個時候我們有三種思路:

1)  通過編譯多個源文件,直接將目標代碼合成一個.o文件。

2)  通過創建靜態鏈接庫libmyhello.a,使得main函數調用hello函數時可調用靜態鏈接庫。

3)  通過創建動態鏈接庫libmyhello.so,使得main函數調用hello函數時可調用靜態鏈接庫。

2.3思路一:編譯多個源文件

在系統提示符下鍵入以下命令得到hello.o文件。

# gcc -c hello.c

爲什麼不使用gcc–o hello hello.cpp 這個道理我們之前已經說了,使用-c是什麼意思呢?這涉及到gcc 編譯選項的常識。

我們通常使用的gcc –o 是將.c源文件編譯成爲一個可執行的二進制代碼(-o選項其實是制定輸出文件文件名,如果不加-c選項,gcc默認會編譯連接生成可執行文件,文件的名稱有-o選項指定),這包括調用作爲GCC內的一部分真正的C編譯器(ccl),以及調用GNU C編譯器的輸出中實際可執行代碼的外部GNU彙編器(as)和連接器工具(ld)。

gcc –c是使用GNU彙編器將源文件轉化爲目標代碼之後就結束,在這種情況下,只調用了C編譯器(ccl)和彙編器(as),而連接器(ld)並沒有被執行,所以輸出的目標文件不會包含作爲Linux程序在被裝載和執行時所必須的包含信息,但它可以在以後被連接到一個程序。

我們運行ls命令看看是否生存了hello.o文件。

# ls

hello.c hello.h hello.o main.c

ls命令結果中,我們看到了hello.o文件,本步操作完成。

同理編譯main

#gcc –c main.c

將兩個文件鏈接成一個.o文件。

#gcc –o hello hello.o main.o

運行

# ./hello

Hello everyone!

完成^ ^!

2.4思路二:靜態鏈接庫

下面我們先來看看如何創建靜態庫,以及使用它。

靜態庫文件名的命名規範是以lib爲前綴,緊接着跟靜態庫名,擴展名爲.a。例如:我們將創建的靜態庫名爲myhello,則靜態庫文件名就是libmyhello.a。在創建和使用靜態庫時,需要注意這點。創建靜態庫用ar命令

在系統提示符下鍵入以下命令將創建靜態庫文件libmyhello.a

# ar rcs libmyhello.a hello.o

我們同樣運行ls命令查看結果:

# ls

hello.c  hello.h hello.o libmyhello.a main.c

ls命令結果中有libmyhello.a

靜態庫製作完了,如何使用它內部的函數呢?只需要在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然後在用gcc命令生成目標文件時指明靜態庫名,gcc將會從靜態庫中將公用函數連接到目標文件中。注意,gcc會在靜態庫名前加上前綴lib,然後追加擴展名.a得到的靜態庫文件名來查找靜態庫文件,因此,我們在寫需要連接的庫時,只寫名字就可以,如libmyhello.a的庫,只寫:-lmyhello

在程序3:main.c中,我們包含了靜態庫的頭文件hello.h,然後在主程序main中直接調用公用函數hello。下面先生成目標程序hello,然後運行hello程序看看結果如何。

# gcc -o hello main.c -static -L. -lmyhello

# ./hello

Hello everyone!

我們刪除靜態庫文件試試公用函數hello是否真的連接到目標文件 hello中了。

# rm libmyhello.a

rm: remove regular file `libmyhello.a'? y

# ./hello

Hello everyone!

程序照常運行,靜態庫中的公用函數已經連接到目標文件中了。

靜態鏈接庫的一個缺點是,如果我們同時運行了許多程序,並且它們使用了同一個庫函數,這樣,在內存中會大量拷貝同一庫函數。這樣,就會浪費很多珍貴的內存和存儲空間。使用了共享鏈接庫的Linux就可以避免這個問題。

共享函數庫和靜態函數在同一個地方,只是後綴有所不同。比如,在一個典型的Linux系統,標準的共享數序函數庫是/usr/lib/libm.so。

當一個程序使用共享函數庫時,在連接階段並不把函數代碼連接進來,而只是鏈接函數的一個引用。當最終的函數導入內存開始真正執行時,函數引用被解析,共享函數庫的代碼才真正導入到內存中。這樣,共享鏈接庫的函數就可以被許多程序同時共享,並且只需存儲一次就可以了。共享函數庫的另一個優點是,它可以獨立更新,與調用它的函數毫不影響。

2.5思路三、動態鏈接庫(共享函數庫)

我們繼續看看如何在Linux中創建動態庫。我們還是從.o文件開始。

動態庫文件名命名規範和靜態庫文件名命名規範類似,也是在動態庫名增加前綴lib,但其文件擴展名爲.so。例如:我們將創建的動態庫名爲myhello,則動態庫文件名就是libmyhello.so。用gcc來創建動態庫。

在系統提示符下鍵入以下命令得到動態庫文件libmyhello.so

# gcc -shared -fPIC -o libmyhello.so hello.o

 “PIC”命令行標記告訴GCC產生的代碼不要包含對函數和變量具體內存位置的引用,這是因爲現在還無法知道使用該消息代碼的應用程序會將它連接到哪一段內存地址空間。這樣編譯出的hello.o可以被用於建立共享鏈接庫。建立共享鏈接庫只需要用GCC”-shared”標記即可。

我們照樣使用ls命令看看動態庫文件是否生成。

# ls

hello.cpp hello.h hello.o libmyhello.so main.cpp

調用動態鏈接庫編譯目標文件。

在程序中使用動態庫和使用靜態庫完全一樣,也是在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然後在用gcc命令生成目標文件時指明動態庫名進行編譯。我們先運行gcc命令生成目標文件,再運行它看看結果。

如果直接用如下方法進行編譯,並連接:

# gcc -o hello main.c -L. -lmyhello

(使用”-lmyhello”標記來告訴GCC驅動程序在連接階段引用共享函數庫libmyhello.so”-L.”標記告訴GCC函數庫可能位於當前目錄。否則GNU連接器會查找標準系統函數目錄:它先後搜索1.elf文件的 DT_RPATH段—2.環境變量LD_LIBRARY_PATH—3./etc/ld.so.cache文件列表—4./lib/,/usr/lib目錄找到庫文件後將其載入內存,但是我們生成的共享庫在當前文件夾下,並沒有加到上述的4個路徑的任何一箇中,因此,執行後會出現錯誤)

# ./hello

./hello: error while loading shared libraries: libmyhello.so: cannot open shared object file: No such file or directory

#

錯誤提示,找不到動態庫文件libmyhello.so。程序在運行時,會在/usr/lib/lib等目錄中查找需要的動態庫文件。若找到,則載入動態庫,否則將提示類似上述錯誤而終止程序運行。有多種方法可以解決,

(1)我們將文件 libmyhello.so複製到目錄/usr/lib中,再試試。

# mv libmyhello.so /usr/lib

# ./hello

成功!

(2)既然連接器會搜尋LD_LIBRARY_PATH所指定的目錄,那麼我們可以將這個環境變量設置成當前目錄:

先執行:

export LD_LIBRARY_PATH=$(pwd)

再執行:

./hello

成功!

(3)執行:  

ldconfig   /usr/zhsoft/lib   /*庫文件存放路徑,可以使任意目錄*/  
  注:   當用戶在某個目錄下面創建或拷貝了一個動態鏈接庫,若想使其被系統共享,可以執行一下"ldconfig   目錄名"這個命令.此命令的功能在於讓ldconfig將指定目錄下的動態鏈接庫被系統共享起來,意即:在緩存文件/etc/ld.so.cache中追加進指定目錄下的共享庫.本例讓系統共享了/usr/zhsoft/lib目錄下的動態鏈接庫.該命令會重建/etc/ld.so.cache文件

成功!

可以查看程序執行時調用動態庫的過程:

# ldd hello
執行 test,可以看到它是如何調用動態庫中的函數的。
[pin@localhost 20090505]$ ldd hello
        linux-gate.so.1 => (0x00110000)
        libmyhello.so => /usr/lib/libmyhello.so (0x00111000)
        libc.so.6 => /lib/libc.so.6 (0x00859000)
        /lib/ld-linux.so.2 (0x0083a000)

我們回過頭看看,發現使用靜態庫和使用動態庫編譯成目標程序使用的gcc命令完全一樣,
那當靜態庫和動態庫同名時,gcc命令會使用哪個庫文件呢?抱着對問題必究到底的心情,
來試試看。

先刪除除.c和.h外的所有文件,恢復成我們剛剛編輯完舉例程序狀態。

# rm -f hello hello.o /usr/lib/libmyhello.so

# ls

hello.c hello.h main.c

#

在來創建靜態庫文件libmyhello.a和動態庫文件libmyhello.so。

# gcc -c hello.c

# ar rcs libmyhello.a hello.o

# gcc -shared -fPCI -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生成目標文件hello,並運行程序 hello。

# gcc -o hello main.c -L. -lmyhello

# ./hello

./hello: error while loading shared libraries: libmyhello.so: cannot open shar
ed object file: No such file or directory

#

從程序hello運行的結果中很容易知道,當靜態庫和動態庫同名時, gcc命令將優先使用動態庫。


Note:

編譯參數解析

最主要的是GCC命令行的一個選項:
-shared 該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號),不用該標誌外部程序無法連接。相當於一個可執行文件
 -fPIC:表示編譯爲位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。
 -L.:表示要連接的庫在當前目錄中
 -ltest:編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.so來確定庫的名稱
 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會去找GCC命令中的參數-L

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環境變量:指定程序動態鏈接庫文件搜索路徑


附註:

作爲Linux程序開發員,最好對開發工具和資源的位置有個初步瞭解。下面簡要介紹一下主要的文件夾和應用程序。

1.應用程序(Applications)

應用程序通常都有固定的文件夾,系統通用程序放在/usr/bin,日後系統管理員在本地計算機安裝的程序通常放在/usr/local/bin或者/opt文件夾下。除了系統程序外,大部分個人用到的程序都放在/usr/local下,所以保持/usr的整潔十分重要。當升級或者重裝系統的時候,只要把/usr/local的程序備份一下就可以了。

一些其他的程序有自己特定的文件夾,比如X Window系統,通常安裝在/usr/X11中,或者/usr/X11R6。GNU的編譯器GCC,通常放置在/usr/bin或者/usr/local/bin中,不同的Linux版本可能位置稍有不同。

2.頭文件(Head Files)

在C語言和其他語言中,頭文件聲明瞭系統函數和庫函數,並且定義了一些常量。對於C語言,頭文件基本上散落於/usr/include和它的子文件夾下。其他的編程語言的庫函數分佈在編譯器定義的地方,比如在一些Linux版本中,X Window系統庫函數分佈在/usr/include/X11,GNU C++的庫函數分佈在/usr/include/g++。這些系統庫函數的位置對於編譯器來說都是“標準位置”,即編譯器能夠自動搜尋這些位置。

如果想引用位於標準位置之外的頭文件,我們需要在調用編譯器的時候加上-I標誌,來顯式的說明頭文件所在文件夾。比如,

$ gcc -I/usr/openwin/include hello.c

會告訴編譯器除了標準位置外,還要去/usr/openwin/include看看有沒有所需的頭文件。詳細情況見編譯器的使用手冊(man gcc)。

庫函數(Library Files)

庫函數就是函數的倉庫,它們都經過編譯,重用性不錯。通常,庫函數相互合作,來完成特定的任務。比如操控屏幕的庫函數(cursers和ncursers庫函數),數據庫讀取庫函數(dbm庫函數)等。

系統調用的標準庫函數一般位於/lib以及/usr/lib。C編譯器(精確點說,連接器)需要知道庫函數的位置。默認情況下,它只搜索標準C庫函數。

庫函數文件通常開頭字母是lib。後面的部分標示庫函數的用途(比如C庫函數用c標識, 數學庫函數用m標示),小數點後的後綴表明庫函數的類型:

  • .a 指靜態鏈接庫
  • .so 指動態鏈接庫

去/usr/lib看一下,你會發現,庫函數都有動態和靜態兩個版本。

與頭文件一樣,庫函數通常放在標準位置,但我們也可以通過-L標識符,來添加新的搜索文件夾,-l指定特定的庫函數文件。比如

 $ gcc -o x11fred -L/usr/openwin/lib x11fred.c -lX11

上述命令就會在編譯期間,鏈接位於/usr/openwin/lib文件夾下的libX11函數庫,編譯生成x11fred。

靜態鏈接庫(Static Libraries)

最簡單的函數庫就是一些函數的簡單集合。調用庫函數中的函數時,需要在調用函數中include定義庫函數的頭文件。我們用-l選項添加標準函數庫之外的函數庫。

主要摘抄地址:http://blog.chinaunix.net/uid-23592843-id-223539.html
















發佈了5 篇原創文章 · 獲贊 12 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章