一、什么是庫?
在windows平台和linux平台下都大量存在着庫。一般是軟件作者為了發布方便、替換方便或二次開發目的,而發布的一組可以單獨與應用程序進行compile time或runtime鏈接的二進制可重定位目標碼文件。
本質上來說庫是一種可執行代碼的二進制形式,這個文件可以在編譯時由編譯器直接鏈接到可執行程序中,也可以在運行時由操作系統的runtime enviroment根據需要動態加載到內存中。
一組庫,就形成了一個發布包,當然,具體發布多少個庫,完全由庫提供商自己決定。
由於windows和linux的本質不同,因此二者庫的二進制是不兼容的。
現實中每個程序都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,因此庫的存在意義非同尋常。
共享庫的好處是,不同的應用程序如果調用相同的庫,那么在內存里只需要有一份該共享庫的實例。
本文僅討論linux下的庫。
二、庫的分類
庫有兩種:靜態庫和共享庫(動態庫)。
win32平台下,靜態庫通常后綴為.lib,動態庫為.dll ;
linux平台下,靜態庫通常后綴為.a,動態庫為.so 。
從本質上來說,由同一段程序編譯出來的靜態庫和動態庫,在功能上是沒有區別的。不同之處僅僅在於其名字上,也就是“靜態”和“動態”。
二者均以文件的形式存在,其本質上是一種可執行代碼的二進制格式,可以被載入內存中執行。 無論是動態鏈接庫還是靜態鏈接庫,它們無非是向其調用者提供變量、函數和類。
1. 靜態庫
所謂靜態庫,就是在靜態編譯時由編譯器到指定目錄尋找並且進行鏈接,一旦鏈接完成,最終的可執行程序中就包含了該庫文件中的所有有用信息,包括代碼段、數據段等。
2. 動態庫
所謂動態庫,就是在應用程序運行時,由操作系統根據應用程序的請求,動態到指定目錄下尋找並裝載入內存中,同時需要進行地址重定向。
3. 區別
我們以編譯鏈接、載入時刻兩點來討論靜態庫和動態庫的區別。
編譯鏈接
靜態鏈接庫在程序編譯時會被鏈接到目標代碼中,目標程序運行時將不再需要改動態庫,移植方便,體積較大,浪費控件和資源,因為所有相關的對象文件與牽涉到庫都被鏈接合成一個可執行文件,這樣導致可執行文件的體積較大。
動態庫在程序編譯時並不會被鏈接到目標代碼中,而是在程序運行時才被載入,因為可執行文件體積較小。有了動態庫,程序的升級會相對比較簡單,比如某個動態庫升級了,只需要更換這個動態庫的文件,而不需要去更換可執行文件。但要注意的是,可執行程序在運行時需要能找到動態庫文件。可執行文件時動態庫的調用者。
載入時刻
二者的不同點在於代碼被載入的時刻不同。
靜態庫的代碼在編譯過程中已經被載入可執行程序,因此體積較大。
共享庫的代碼是在可執行程序運行時才載入內存的,在編譯過程中僅簡單的引用,因此代碼體積較小。
4. 優缺點
相對於動態庫,靜態庫的優點在於直接被鏈接進可執行程序中,之后,該可執行程序就不再依賴於運行環境的設置了(當然仍然會依賴於 CPU指令集和操作系統支持的可執行文件格式等硬性限制)。
而動態庫的優點在於,用戶甚至可以在程序運行時隨時替換該動態庫,這就構成了動態插件系統的基礎。具體使用靜態庫和動態庫,由程序員根據需要自己決定。
另外,需要說明的一點是,從底層實現上,動態庫的效率可能會比靜態庫稍差一點點,注意,這里用了“可能”二字,具體差不差,還得看寫程序的人。之所以可能會差,主要原因在於,程序總無法直接調用動態庫中的函數符號,而只能通過調用操作系統的runtime enviroment接口來動態載入某個函數符號,同時獲得該函數符號在內存中的地址,將其保存為函數指針進行調用,這就在函數調用時增加了一次間接尋址的過程。
三、庫文件的制作
1. 庫文件命名
靜態庫的名字一般為libxxxx.a,其中xxxx是該lib的名稱;
動態庫的名字一般為libxxxx.so.x.y.z,含義如下圖所示:
2. 制作庫文件常用參數
首先需要了解gcc編譯庫要用到一些參數,很重要。
參數 | 含義 |
---|---|
-shared | 指定生成動態鏈接庫。 |
-static | 指定生成靜態鏈接庫。 |
-fPIC | 表示編譯為位置獨立的代碼,用於編譯共享庫。目標文件需要創建成位置無關碼,概念上就是在可執行程序裝載它們的時候,它們可以放在可執行程序的內存里的任何地方。 |
-L | 表示要連接的庫在當前目錄中。 |
-l | 指定鏈接時需要的動態庫。編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,后面加上.so來確定庫的名稱。 |
-Wall | 生成所有警告信息。 |
-ggdb | 此選項將盡可能的生成gdb的可以使用的調試信息。 |
-g | 編譯器在編譯的時候產生調試信息。 |
-c | 只激活預處理、編譯和匯編,也就是把程序做成目標文件(.o文件)。 |
-Wl,options | 把參數(options)傳遞給鏈接器ld。如果options中間有逗號,就將options分成多個選項,然后傳遞給鏈接程序。 |
3. 庫源文件
假定我們要將以下兩個文件制作成庫文件
add.c
int add(int x,int y)
{
return x+y;
}
int sub(int x,int y)
{
return x-y;
}
add.h
int add(int x,int y);
int sub(int x,int y);
4. 制作靜態庫並使用
- 需要把 add.c 編譯成.o文件
gcc -c add.c
- 使用 ar 命令生成靜態庫libadd.a
ar -rc libadd.a add.o 遵循靜態庫命名的規則 lib + 名字 + .a
- 使用靜態庫
要是用靜態庫libadd.a,只需要包含add.h,就可以使用函數add()、sub()。
#include <stdio.h>
#include "add.h"
void main()
{
printf("add(5,4) is %d\n",add(5,4));
printf("sub(5,4) is %d\n",sub(5,4));
}
靜態庫的文件可以放在任意的位置,編譯時只需要找到該庫文件即可。
gcc test.c -o run libadd.a
- 庫和頭文件如果在其他目錄下
使用一下命令編譯:
gcc -c -I /home/xxxx/include test.c //假設test.c要使用對應的靜態庫
gcc -o test -L /home/xxxxx/lib test.o libadd.a
或者
gcc -c -I /home/xxxx/include -L /home/xxxxx/lib libadd.a test.c
1). 通過-I(是大i)指定對應的頭文件
2). 通過-L制定庫文件的路徑,libadd.a就是要用的靜態庫。
3). 在test.c中要包含靜態庫的頭文件。
5. 制作動態庫並使用
- 把add.c編譯成動態鏈接庫libadd.so
gcc -fPIC -o libadd.o -c add.c
gcc -shared -o libadd.so libadd.o
也可以直接使用一條命令
gcc -fPIC -shared -o libadd.so add.c
- 動態庫的安裝
通常動態庫拷貝到/lib下即可:
sudo cp libadd.so /lib
- 使用動態庫
#include <stdio.h>
#include "add.h"
void main()
{
printf("add(5,4) is %d\n",add(5,4));
printf("sub(5,4) is %d\n",sub(5,4));
}
編譯動態庫:
gcc static -o run -ladd
注意觀察編譯時動態庫的名字與庫文件對應關系
libadd.so<--------->-ladd
去掉 .so, lib簡化成l,其他字母保留。
6. 動態加載的函數庫Dynamically Loaded (DL) Libraries
動態加載的函數庫Dynamically loaded (DL) libraries是一類函數庫,它可以在程序運行過程中的任何時間加載。它們特別適合在函數中加載一些模塊和plugin擴展模塊的場合,因為它可以在當程序需要某個plugin模塊時才動態的加載。
Linux系統下,DL函數庫與其他函數庫在格式上沒有特殊的區別,它們創建的時候是標准的object格式。主要的區別就是這些函數庫不是在程序鏈接的時候或者啟動的時候加載,而是通過一個API來打開一個函數庫,尋找符號表,處理錯誤和關閉函數庫。通常C語言環境下,需要包含這個頭文件。
dlopen()
dlopen函數打開一個函數庫然后為后面的使用做准備。C語言原形是:
void * dlopen(const char *filename, int flag);
參數
filename
如果文件名filename是以“/”開頭,也就是使用絕對路徑,那么dlopne就直接使用它,而不去查找某些環境變量或者系統設置的函數庫所在的目錄了。否則dlopen()就會按照下面的次序查找函數庫文件:
1. 環境變量LD_LIBRARY指明的路徑。
2. /etc/ld.so.cache中的函數庫列表。
3. /lib目錄,然后/usr/lib。
一些很老的a.out的loader則是采用相反的次序,也就是先查 /usr/lib,然后是/lib。
flag
的值必須是RTLD_LAZY或者RTLD_NOW,RTLD_LAZY的意思是resolve undefined symbols as code from the dynamic library is executed,而RTLD_NOW的含義是resolve all undefined symbols before dlopen() returns and fail if this cannot be done'。
返回值
dlopen()函數的返回值是一個句柄,然后后面的函數就通過使用這個句柄來做進一步的操作。如果打開失敗dlopen()就返回一個NULL。如果一個函數庫被多次打開,它會返回同樣的句柄。
如果有好幾個函數庫,它們之間有一些依賴關系的話,例如X依賴Y,那么你就要先加載那些被依賴的函數。
例如先加載Y,然后加載X。
dlerror()
通過調用dlerror()函數,我們可以獲得最后一次調用dlopen(),dlsym(),或者dlclose()的錯誤信息。
dlsym()
如果你加載了一個DL函數庫而不去使用當然是不可能的了,使用一個DL函數庫的最主要的一個函數就是dlsym(),這個函數在一個已經打開的函數庫里面查找給定的符號。這個函數如下定義:
void * dlsym(void *handle, char *symbol);
參數
handle
就是由dlopen打開后返回的句柄,
symbol
是一個以NIL結尾的字符串。
功能:
如果dlsym()函數沒有找到需要查找的symbol,則返回NULL。如果你知道某個symbol的值不可能是NULL或者0,那么就很好,你就可以根據這個返回結果判斷查找的symbol是否存在了;不過,如果某個symbol的值就是NULL,那么這個判斷就有問題了。標准的判斷方法是先調用dlerror(),清除以前可能存在的錯誤,然后調用dlsym()來訪問一個symbol,然后再調用dlerror()來判斷是否出現了錯誤。
dlclose()
dlopen()函數的反過程就是dlclose()函數,dlclose()函數用力關閉一個DL函數庫。
Dl函數庫維持一個資源利用的計數器,當調用dlclose的時候,就把這個計數器的計數減一,如果計數器為0,則真正的釋放掉。真正釋放的時候,如果函數庫里面有_fini()這個函數,則自動調用_fini()這個函數,做一些必要的處理。
Dlclose()返回0表示成功,其他非0值表示錯誤。
舉例
#include <stdio.h>
#include <dlfcn.h>
void main()
{
int (*add)(int x,int y);
int (*sub)(int x,int y);
void *libptr;
libptr=dlopen("./libadd.so",RTLD_LAZY); //加載動態庫
add=dlsym(libptr,"add"); //獲取函數地址
sub=dlsym(libptr,"sub");
printf("add(5,4) is %d\n",add(5,4));
printf("sub(5,4) is %d\n",sub(5,4));
dlclose(libptr);
}
四、庫的兩個查看命令
- 查看依賴庫命令ldd
使用ldd命令可以查看一個可執行程序依賴哪些庫。
這個命令非常有用,實際工作中經常會一直各種庫,而有些程序的執行需要依賴好幾種庫,各種庫的版本又很多歷史版本,經常會出現庫不兼容的情況,我們需要根據實際情況,適當的降低版本或者升級版本。
例如:
可以看到線程庫libpthread-2.23.so依賴於libc庫和ld-linux庫。
- nm
nm工具可以打印出庫中的涉及到的所有符號,下面是我們查看我們創建的動態庫libadd.a:
五、庫的安裝
在新安裝一個庫之后如何讓系統能夠找到他,有以下幾種方法:
1. 拷貝到/lib或者/usr/lib下
如果安裝在/lib或者/usr/lib下,那么ld默認能夠找到,無需其他操作。
如果安裝在其他目錄,需要將其添加到/etc/ld.so.cache文件中,步驟如下
2.通過配置文件/etc/profile
永久生效的環境變量設置,編輯/etc/profile即可。
vi /etc/profile
在文件里末尾加上對應的環境變量信息。
動態庫環境變量設置:
export LD_LIBRARY_PATH=/home/peng/mylib/
/home/peng/mylib/指的是動態庫文件夾所在位置。即,.so等文件在/home/peng/mylib/下。
編輯完成,保存編輯並退出;
使配置即時生效:
source /etc/profile
3./etc/ld.so.conf
編輯/etc/ld.so.conf文件,加入庫文件所在目錄的路徑
vim /etc/ld.so.conf
在里面添加動態庫所在路徑即可,例如
/usr/local/lib/
運行ldconfig,該命令會重建/etc/ld.so.cache文件
七、常見庫的移植
1.jpeg庫,用於jpeg圖像處理
下載地址:
解壓
tar xvzf jpegsrc.v6b.tar.gz
cd jpeg-6b
生成Makefile
./configure --host=arm-linux-gnueabihf --prefix=$PWD/temp_install
編譯, 安裝
make
make install
注意這個庫的安裝程序有BUG,不會自動創建發布的lib,include,man等,因此要手工創建,要不先把其它庫做好,再安裝這個庫
mkdir -p /home/peng/jpeg-6b/temp_install/include
mkdir -p /home/peng/jpeg-6b/temp_install/lib
mkdir -p /home/peng/jpeg-6b/temp_install/man/man1
更多Linux知識,請關注 一口Linux。