Linux 動態(tài)庫相關知識總結
動態(tài)庫和靜態(tài)庫在C/C++開發(fā)中很常見,相比靜態(tài)庫直接被編譯到可執(zhí)行程序,動態(tài)庫運行時加載使得可執(zhí)行程序的體積更小,更新動態(tài)庫可以不用重新 編譯可執(zhí)行程序等諸多好處。作者是一個Linux后臺開發(fā),這些知識經常用到,所以整理了一下這方面的知識。靜態(tài)庫相對簡單,本文只關心Linux平臺下 的動態(tài)庫。
創(chuàng)建動態(tài)庫
這里我把一個短小卻很有用的哈希函數(shù)編譯成動態(tài)庫做為示例,ELFhash用于對字符串做哈希,返回一個無符號整數(shù)。
- //elfhash.h
- #include
- unsigned long ELFhash(const char* key);
- //elfhash.c
- #include "elfhash.h"
- unsigned long ELFhash(const char* key)
- {
- unsigned long h = 0, g;
- while( *key ) {
- h = ( h > 24;
- h &= ~g;
- }
- return h;
- }
接下來使用gcc編譯以上代碼,并用ld將編譯的目標文件鏈接成動態(tài)庫
- gcc -fPIC -c -Wall elfhash.c
- ld -shared elfhash.o -o libelfhash.so
其中-fPIC
意思是生成位置無關的代碼(Position Independent Code),適用于動態(tài)庫,在多個進程中共享動態(tài)庫的同一份代碼。ld的-shared
選項告訴鏈接器創(chuàng)建的是動態(tài)庫。gcc也可以間接調用ld生成動態(tài)庫
- gcc -fPIC -shared -Wall -o libelfhash.so elfhash.c
使用動態(tài)庫
動態(tài)庫的使用方法有兩種一種是隱式使用,第二種是顯式使用。隱式使用的方法很簡單。
- #include "elfhash.h"
- int main()
- {
- printf("%ldn", ElfHash("key-for-test"));
- return 0;
- }
顯式使用動態(tài)庫需要借助以下幾個函數(shù)
#include
void *dlopen(const char *filename, int flag); //flag可以是RTLD_LAZY,執(zhí)行共享庫中的代碼時解決未定義符號,RTLD_NOW則是dlopen返回前解決未定義符號。
char *dlerror(void); //當發(fā)生錯誤時,返回錯誤信息
void *dlsym(void *handle, const char *symbol); //獲取符號
int dlclose(void *handle); //關閉
應用上面幾個函數(shù),調用ELFhash實現(xiàn)跟隱式調用一樣的功能
- #include "elfhash.h"
- #include
- #include
- int main() {
- void *handle;
- unsigned long (*hash)(const char*);
- char *error;
- handle = dlopen ("./libelfhash.so", RTLD_LAZY);
- if (!handle) {
- fputs (dlerror(), stderr);
- exit(1);
- }
- hash = dlsym(handle, "ElfHash");
- if ((error = dlerror()) != NULL) {
- fputs(error, stderr);
- exit(1);
- }
- printf ("%ldn", (*hash)("key-for-test"));
- dlclose(handle);
- }
至此了解以上的知識就可以創(chuàng)建和使用動態(tài)庫了。 實際應用中我們可能還是會遇到一些問題。
動態(tài)庫的加載
動態(tài)庫創(chuàng)建那一節(jié),我演示如何隱式使用動態(tài)庫,那么編譯運行這段代碼試一下。
gcc main.c -L./ -lelfhash
./a.out //執(zhí)行可執(zhí)行程序
//以下是輸出結果
./a.out: error while loading shared libraries: libelfhash.so: cannot open shared object file: No such file or directory
結果運行時報錯,可執(zhí)行程序找不到動態(tài)庫。 網上有一些說法是編譯時設置-L
選項,但在Linux上面證明是不行的(SunOS上可行),這個選項只能在編譯鏈接時有效, 可以讓你使用-l
如上面的-lelfhash
。使用readelf -d a.out
可以看到可執(zhí)行文件依賴的動態(tài)庫信息。
0x0000000000000001 (NEEDED) Shared library: [libelfhash.so]
可以看到這里面并沒有包含動態(tài)庫的路徑信息。查閱一下動態(tài)鏈接器的文檔man ld-linux.so
可以發(fā)現(xiàn)這樣一句話(有的沒有,版本問題)
If a slash is found, then the dependency string is interpreted as a (relative or absolute) pathname, and the library is loaded using that pathname
這段話太長,我只截取一部分,大致就是說,當依賴中有/
符號,那么會被解析成動態(tài)庫加載的路徑,隱式使用的例子換一種編譯方法。
gcc main.c ./libelfhash.so
./a.out
23621492 //輸出正常
再用readelf -d a.out
查看會發(fā)現(xiàn),依賴信息中有了一個路徑。
0x0000000000000001 (NEEDED) Shared library: [./libelfhash.so]
這種方法雖然解決了問題,但是依賴中的路徑是硬編碼,不是很靈活。 動態(tài)鏈接器是如何查找的動態(tài)庫的需要進一步查閱文檔。關于查找的順序有點長,這里就不直接引用了,大致是這樣:
-
(僅ELF文件) 使用可執(zhí)行文件中DT_RPATH區(qū)域設置的屬性,如果DT_RUNPATH被設置,那么忽略DT_RPATH(在我的Linux對應的是RPATH和RUNPATH)。
-
使用環(huán)境變量LD_LIBRARY_PATH,如果可執(zhí)行文件中有set-user-id/set-group-id, 會被忽略。
-
(僅ELF文件) 使用可執(zhí)行文件中DT_RUNPATH區(qū)域設置的屬性
-
從/etc/ld.so.cache緩存文件中查找
-
從默認路徑/lib, /usr/lib文件目錄中查找
我們需要設置RPATH或者RUNPATH,可以這樣做
gcc main.c -Wl,-rpath,/home/xxx,--enable-new-dtags -L./ -lelfhash
這里的-Wl
選項告訴鏈接器ld
如果如何處理,接下來傳遞的-rpath
(或者使用-R
)告訴ld
動態(tài)庫的路徑信息(注意-Wl,
和后面選項之間不能有空格)。如果沒有--enable-new-dtags
那么只會設置RPATH,反之,RPATH和RUNPATH會同時被設置。使用readelf -d a.out
查看結果:
0x000000000000000f (RPATH) Library rpath: [/home/xxx]
0x000000000000001d (RUNPATH) Library runpath: [/home/xxx]
如果使用環(huán)境變量LD_LIBRARY_PATH,那么一般這樣用 export
export LD_LIBRARY_PATH=/home/xxx;$LD_LIBRARY_PATH
RPATH和RUNPATH指定動態(tài)庫的路徑,用起來簡單,但是也缺乏靈活性,LDLIBRARYPATH在臨時測試的也是很有用的,但是在正式環(huán)境中,直接使用它也不是好的實踐,因為環(huán)境變量跟用戶的環(huán)境關系比較大。動態(tài)庫不僅要考慮自己使用, 還有分發(fā)給別的用戶使用的情況。
更通用的方法是使用ldconfig
,有幾種方法,先在/etc/ld.so.conf.d/
目錄下創(chuàng)建一個文件,然后把你的動態(tài)庫路徑寫進去。或者將你的動態(tài)庫放到/lib,/lib64(64位),/usr/lib,/usr/lib64(64位)
然后運行sudo ldconfig
重建/etc/ld.so.cache
文件。
動態(tài)庫版本
通常在使用第三方給的動態(tài)庫的時候,都是帶有版本(文件命名),可以在/usr/lib64
下看到很多這樣的動態(tài)庫。現(xiàn)在我重新編譯動態(tài)庫,這次加上版本信息。
gcc -fPIC -shared -Wall -Wl,-soname,libelfhash.so.0 -o libelfhash.so.0.0.0 elfhash.c
每個動態(tài)庫都有一個名字,如這里的libelfhash.so.0.0.0,叫real name
,命名規(guī)則跟簡單,通常是libxxx.so.MAJOR.MINOR.VERSION
(有 的時候VERSION會被省略),如果動態(tài)庫在接口上的兼容性,比如刪除了接口或者修改了接口參數(shù),MAJOR增加,如果接口兼容,只是做了更新或者 bug修復那么MINOR和VERSION增加。也就是說MAJOR相同的庫接口都是兼容的,反之不兼容,如果使用不兼容的動態(tài)庫需要重新編譯可執(zhí)行程 序。
編譯動態(tài)庫時,通過給ld
傳遞連接選項-soname
可以指定一個soname
, 如這里的libelfhash.so.0 只保留MAJOR,可執(zhí)行程序運行加載動態(tài)庫時,會加載這個指定名字的庫。
動態(tài)庫還有一個名字是link name
,編譯可執(zhí)行程序時,傳個鏈接器ld
的動態(tài)庫名字,通常是沒有版本號以.so結尾的文件名。 一般作法是對soname創(chuàng)建軟鏈。
按照這個規(guī)則來命名的動態(tài)庫可以ldconfig
識別,我們把libelfhash.so.0.0.0放到/usr/lib64
文件夾中,執(zhí)行以下指令
- $sudo ldconfig -v | grep libelfhash.so
- libelfhash.so.0 -> libelfhash.so.0.0.0
可以發(fā)現(xiàn)ldconfig
根據(jù)libelfhash.so.0.0.0的信息,創(chuàng)建了一個soname指向real name的軟鏈,當動態(tài)庫更新(MINOR,VERSION增加),拷貝新庫到相應的位置,再執(zhí)行sudo ldconfig
會自動更新軟鏈指向***的動態(tài)庫,動態(tài)庫更新就完成了。
總結
OK,關于Linux動態(tài)庫知識整理就到這里了,這些知識雖說都是些基礎,少有涉及動態(tài)庫內部的一些原理,但是卻很常用。整理過程中我?guī)е蓡柸ラ喿x了ld
和ld-linux.so
的文檔,收獲頗豐。同樣,希望本文能幫你解釋遇到的部分問題或疑惑。