Unit 系統編程手冊-(41-42) 共享庫基礎
一、靜態庫 Vs 共享庫 優缺點
靜態庫 | 共享庫 |
---|---|
可靠,已經包含運行所需的全部庫,與系統無關 | 運行之前需要確保相關共享庫已經存在 |
加載速度更快 | 需要運行之前依次檢索、加載所需的共享庫,以及符號的重新定位 |
每次靜態庫改動,相關引用該庫程序都需要重新編譯 | 運行時動態加載,所以不需要重新編譯 |
浪費磁盤,每被引用一次就會生成一次副本 | 運行是加載,所以不需要 |
浪費內存,每次運行都會在內存生成一次副本 | 只會在內存中生成一次副本 |
庫更加簡單 | |
編譯時必須使用位置獨立的代碼,帶來額外性能開銷 |
二、靜態庫簡單應用
2.1 創建、維護
使用 ar
命令進行
#創建
cc -g -c mod1.c mod2.c mod3.c
ar r libdemo.a mod1.o mod2.o mod3.o
#打印
ar tv libdemo.a
#刪除
ar d libdemo.a mod3.o
2.2 鏈接
標準庫:
/usr/lib 、/lib 、/usr/local/lib
#庫當前目錄
cc -g -o prog prog.o libdemo.a
#庫位於標準庫 使用 -l 去掉 .a
cc -g -o prog prog.o -ldemo
#指定路徑 -L 文件路徑 -l 庫名稱
cc -g -o prog prog.o -Lmydir -ldemo
三、共享庫使用
3.1 創建
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o
#或則
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so
-fPIC
指定編譯器生成位置獨立的代碼,因爲鏈接的時候無法直到共享庫位於內存的何處
#方式一: 是否使用 -fPIC 選項
nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_
#方式二: 若輸出信息,則說明至少有一個目標模塊沒有使用 -fPIC 選項
objump --all-headers libfoo.so | grep TEXTREL
readelf -d libfoo.so | grep TEXTREL
3.2 共享庫 soname
目的: 提供一層間接,使得程序能夠運行時使用與鏈接時使用的庫不同但兼容的共享庫
# 1 創建
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# 2 創建 soname: 將 libfoo.so 的 soname 設爲 libbar.so
gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
# 3 檢查 soname: 確定一個既有共享庫 soname,以下命令二選一
objump -p libfoo.so | grep SONAME
readelf -d libfoo.so | grep SONAME
# 4 soname 嵌入程序: 編譯器檢測到 soname 就將 soname 頂替 libfoo.so 嵌入程序中
gcc -g -Wall -o prog prog.c libfoo.so
# 5 創建 soname: 創建符號鏈接
ln -s libfoo.so libbar.so
3.3 共享庫小工具
ldd
命令:顯示一個程序所需共享庫
objump
/readelf
命令:獲取各類信息(包括反彙編二進制碼),從可執行文件、庫,顯示 ELF 節頭部信息nm 命令:列出目標庫或可執行程序中定義的一組符號
nm -A /usr/lib/lib*.so 2> /dev/null | grep 'crypt$'
3.4 命名規則
真實庫命名:libname.so.major_id.minor_id(次要版本號.補丁號) 如libdemo.1.0.1
soname 命名: libname.so.major_id 如 libdemo.so.1 -> libdemo.so.1.0.2
連接器命名:libname.so -> libdemo.so.1(指向 soname 常見,指向真實庫也可)
3.5 安裝共享庫
方式一:(不推薦)LD_LIBRARY_PATH 所有用戶都可以使用
方式二:標準庫中
方式三:使用 ldconfig
ldconfig :解決庫位置分散帶來的加載緩慢和 soname 鏈接符號因爲庫迭代而失效
1 ldconfig 會依次檢索 /etc/ld.so.conf中指定目錄、/lib、/usr/lib 生成 /etc/ld.so.cache (ldconfig -p 打印)
2 檢查每個庫主要版本的最新次要版本(最大)找出嵌入的 soname,在同一目錄創建相對符號鏈接(命名符合規範) -N 防止緩存重建 -X 阻止 soname 符號鏈接創建
方式四:目標文件中指定庫檢索 -rpath
1 強制指定
gcc -g -Wall -Wl,rpath,/home/mtk/pdir -o prog prog.c libdemo.so
DT_RPATH (優先級高於 LD_LIBRARY_PATH) / DT_RUNPATH (低於LD):
默認情況下,鏈接器會創建 DT_RPATH 標籤,--enable-new-dtags
強制使用 DT_RUNPATH 標籤
gcc -g -Wall -o prog prog.c -Wl,--enable-new-dtags -Wl,-rpath,/home/mtk/pdir/d1 -L/home/mtk/pdir/d1 -lx1
#這樣是可執行文件中將包含兩種標籤,爲了兼容老式動態鏈接器工作
2 不強制指定:使用 $ORIGIN(被解釋成包含應用程序的目錄),允許程序在任意目錄中運行應用程序
gcc -Wl,-rpath,'$ORIGIN'/lib ...
3.6 運行時符號鏈接順序
若主程序定義了與庫相同的全局符號,將會覆蓋庫
gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o
-Bsymbolic 能夠確保庫自身調用與主程序相同的全局符號時,正確調用自身的符號
四、共享庫高級特性
1 動態加載庫
-
dlopen() 將共享庫加載進調用進程的虛擬空間並增加該庫的打開引用次數
-
dlerror() 錯誤信息
-
dlsym() handle 指向庫及所依賴樹的庫中檢索 symbol 的符號(函數或變量)
-
dlclose() 關閉共享庫
-
dladdr() 獲取與加載的符號相關的消息
2 支持回調(庫調用主程序符號)
gcc -Wl,--export-dynamic main.c
使用 gcc -rdynamic / gcc -Wl -E / -Wl,–export-dynamic 含義一樣
3 符號可見性
1 static 符號的可見性侷限於單個源代碼文件中
2 void __attribute__((visibility("hidden"))) fun(void) {}
對所有共享庫不可見
4 鏈接器版本腳本
4.1 控制全局可見的符號
gcc -g -c -fPIC -Wall vis_com.c
gcc -g -shared -o vis.so vis_com.o -Wl,--version-script,vis.map
#vis.map 內容
VER_1 {
global:
vis_f1;
vis_f2;
local:
*;
};
#打印可見符號
readelf --syms --use-dynamic vis.so | grep vis_
4.2 符號版本化
也就是一個共享庫提供同一個函數的多個版本
//cat sv_lib_v2.c
#include <stdio.h>
__asm__(".symver xyz_old,xyz@VER_1");
__asm__(".symver xyz_new,xyz@@VER_2"); //在.symver 指令中只能有一個 @@ 標記
//sv_v2.map
VER_1 {
global: xyz;
local: *;
};
VER_2 {
global: pqr;
}VER_1; //VER_2 依賴 VER_1
4.3 初始化和終止函數
-
老式 _init() 和 _fini(),需要指定
gcc -nostartfiles
也可以通過 -Wl,-init / -Wl,-fini 指定函數 -
使用 gcc ,函數名根據需要替換
void __attribute__((constructor)) some_name_load(void)
{
}
void __attribute__((destructor)) some_name_unload(void)
{
}
4.4 預加載共享庫: LD_PRELOAD
LD_PRELOAD=libalt.so ./prog
這樣的好處就是,本來 prog 會加載指定庫的符號,使用預加載之後,預加載內存在的符號會覆蓋原先的符號被主程序調用
4.5 監控動態鏈接器: LD_DEBUG
LD_DEBUG=libs date
根據關鍵字從動態鏈接器獲得跟蹤信息,以便清楚所檢索的庫