在Linux中動態庫的確給程序帶來了良好的擴充性,並減少了內存的使用量,但這是有代價的。例如:
#include <stdio.h>
Int main(int argc, char *argv[])
{
Printf(“hello\n”);
Return 0;
}
我們知道printf是在glibc中定義的,如果不適用動態庫,而是將glibc靜態鏈接到進程中的話,那麼printf函數的地址在編譯時就是已知的了,使用很簡單的依據地址轉移,就可以進行函數調用。
可是如果采用動態庫的話,在程序編譯階段,編譯器就無法得知printf的函數地址,因為動態庫加載的內存地址時隨機的。那麼對於動態庫的情況,針對printf是如何尋址的呢?
在程序運行時,當調用printf的時候,程序會將處理權交給linker,由其負責在執行文件以及其連接的動態庫中查找printf的函數地址。由於linker不知道printf具體是在哪個動態庫,所以它將在整個執行文件和動態庫的范圍內查找。更糟糕的是在C++程序中,符號的命名是類名+函數名,這導致在做字符串比較時,往往直到字符串的結尾才能獲得結果。
這也就導致了在進程啟動過程中,符號查找占據了一大部分時間。在Linux的KDE進程啟動中,符號查找甚至占據了進程啟動80%的時間。
因此就針對上述的情況,有以下優化解決方案:
1、減少導出符號的數量
在動態庫編譯和生成時,默認所有的函數和全局變量都是導出的。而實際上有很多函數只是動態庫內部使用,通過去掉那些動態庫中不必要的導出符號,從而減少動態庫在做鏈接時所查找的符號數量,可以加快動態庫鏈接的速度。
可以使用ld的ld --retain-symbols-file --version-script兩個選項實現。寫一個導出符號文件,如 symbol 指定你只導出的函數,如 func1。使用 ld 的--retain-symbols-file 參數可以在 static section 裡取消 func1 以外的所有函數。這時你用 readelf 看編譯好後的 .so 文件 static section 裡沒有了,使用 nm 看 .so文件它無法查出導出函數。但這並不完全。因為在 dynamic section 裡還是會看到所有符號被導出。如果想在 dynsym section 裡也不讓他導出的話,需要再編寫一個 script 文件,指定 global 與 local 在 global 中指定你要導出的函數,簡單的格式如下:
VERSION{
VER_1.0{
global: 導出函數名;
local: *;
};
}
再在 ld 時用 --version-script 選項來 load 你的文件。都完事後再使用 readelf 觀察static 與 dynamic section 發現只導出了你指定的函數名即符號。
例:
ld -shared --retain-symbols-file 符號文件 --version-script 腳本文件 -o 動態庫文件。so filename
2、減少符號的長度
3、使用prelink
在這裡另外在提一個問題,很有趣的東西。
gcc -fvisibility=hidden 只在鏈接時傳入的.c文件起作用,對.o文件不其作用;
比如test.c test1.c,使用以下命令:
gcc -shared -fvisibility=hidden -otest.so test.c test1.c
和命令
gcc -c test.c test1.c
gcc -shared -fvisibility=hidden -otest.so test.o test1.o
生成的test.so中的對應可見性是不一樣的,使用“readelf -s test.so”查看發現:
第一個達到預期目的,即將兩個.c文件中的functions設為HIDDEN,
而第2個則不行,-fvisibility=hidden不起作用;
再用gcc -shared -fvisibility=hidden -o test.so test.o test1.c
生成的so,則可發現test1.c中的函數為HIDDEN,但test.o中的函數仍為DEFAULT;