淺談函式庫
比較shared/static library
程式在執行的時候,大部分都會需要引用函式庫(library),library有分shared和static,兩者代表不同的引用方式。
static library | shared library | |
---|---|---|
優點 | 不需要考慮執行環境的相依性問題 | 使用空間小(檔案和記憶體)、更換library不用重build |
缺點 | 執行檔極大、更換library需重build | 在異地執行可能會因為相依性無法執行 |
動態函式庫
在開始前,先確定幾個名詞
- soname:代表特定library的名稱,如libmylib.so.1,最後面的1是version
- real name:實際放有library程式的檔案名稱,名稱會包含三個版號,分別為version, minor和release,如libmylib.so.1.0.0
- version代表原介面有移除或改變,與舊版本不相容
- minor代表新增介面,舊介面沒改
- release代表程式修正,介面沒改
- linker name:用於連結時的名稱,不含版號的soname,如libmylib。通常會link到實際的real name。
如何編譯
首先我們先把.c
編譯成.o
,這邊要加上-fPIC的參數
這個原因是要產生Position Independent code,確保code segment在動態連結時不用花時間重新定位,而且重新定位會造成無法和其他process共享.text區段。
事實上,如果不加-fPIC也是可以產生library,但是產生的執行檔就需要另外存有重新定位的資訊(.rel.dyn區段),而且會有上述的問題。
1 | $ gcc -c -fPIC hello.c world.c |
接下來就是產生shared library了,解釋一下參數的意思
-shared
:代表要編成shared library-Wl
:是用來傳遞參數給linker,讓-soname和libmylib.so.1傳給linker處理-soname
:用來指名soname為libmylib.so.1-o
:最後library會被輸出成libmylib.so.1.0.0soname很重要,就如同前面所提,可以讓開發者和應用程式表示兼容標準,可以用objdump確認soname1
$ gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 hello.o world.o
完成後再用ln建立soname和linker name兩個檔案1
2$ objdump -p libmylib.so.1.0.0 | grep SONAME
SONAME libmylib.so.11
2$ ln -s libmylib.so.1.0.0 libmylib.so
$ ln -s libmylib.so.1.0.0 libmylib.so.1
如何使用
如果有人要使用的話,下列兩種方式都可以。不過要注意目錄下如果同時有static和shared會使用shared為主,如果要static就要加上-static編靜態函式庫
1 | $ gcc main.c libmylib.so -o a.out |
但是shared library執行的時候還是需要有library才能執行,所以要把.so安裝到系統中,有三種方法:
- 把libmylib.so.1 放到系統常見的library目錄,如/lib, /usr/lib
- 設定
/etc/ld.so.conf
,加入一個新的library搜尋目錄,並執行ldconfig更新/etc/ld.so.cache
- 設定LD_LIBRARY_PATH 環境變數來搜尋library,如
LD_LIBRARY_PATH=. ./a.out
這邊提一下一般而言找library的順序
LD_LIBRARY_PATH
或LD_AOUT_LIBRARY_PATH
環境變數所指的路徑- 從
ld.so.cache
的記錄來找shared library。 /lib
,/usr/lib
內的檔案
查看shared library的關係 - ldd
我們要怎麼知道某個執行檔有使用到哪些library呢?這時候就要用到ldd這個指令了。
ldd其實是一個shell script,它會把檔案所用到library一一列出,包括library會用到的library。
舉例來說,如果我們不用ldd,其實是可以從ELF的Dynamic Section獲得shared library資訊
1 | $ readelf -d /bin/cat |
我們看到NEEDED就是需要的dynamic library,但是這個library可能也需要其他library。
1 | $ readelf -d /lib/x86_64-linux-gnu/libc.so.6 |
因此我們知道/bin/cat需要libc.so.6,而libc.so.6還需要ld-linux-x86-64.so.2。這樣尋找實在太麻煩了,其實我們可以直接用ldd
1 | $ ldd /bin/cat |
看,是不是很輕鬆呢?
靜態函式庫
會有static library的概念是,如果我有很多.o檔,那每次要引用其實都不是很方便,所以最好的方法還是可以打包起來,也就是使用ar指令。
如何編譯
static libary建立方式很簡單,一樣要先建立.o
1 | $ gcc -c test1.c test2.c |
接下來開始打包,參數意義如下
- r:代表加入新檔案或取代現有檔案
- c:.a檔不存在時不會跳錯誤訊息
- u:根據timestamp保留檔案
- s:建立索引,加快連結速度如果要顯示函式庫 libstack.a 的內容
1
$ ar rcs libtest.a test1.o test2.o
如果要從libtest.a中取出test1.o1
2
3$ ar -tv libtest.a
rw-r--r-- 0/0 1464 Jan 1 00:00 1970 test1.o
rw-r--r-- 0/0 1464 Jan 1 00:00 1970 test2.o1
ar -x libtest.a test1.o
如何使用
編譯方法一樣很簡單,有兩種
1 | gcc main.c libtest.a |
symbol衝突
假設我們在創建library時遇到symbol衝突會發生什麼事呢?這邊我們分三種情況探討
首先先創三個檔案
hello.c
1 | void test() |
world.c
1 | void test() |
main.c
1 | void test(); |
shared library連結時,object file有衝突
嘗試編譯與連結
1 | $ gcc -c -fPIC hello.c world.c |
會發現出現錯誤,原因是動態連結跟一般編譯一樣會檢查symbol是否重複
static library打包時,object file有衝突
那如果是用static library呢?
1 | $ gcc -c hello.c world.c |
發現居然沒事,這個原因是因為ar只有打包功能不負責檢查。可是問題來了,到底是執行哪個呢?答案是看順序。
1 | $ gcc -o main.out main.c libhello.a libworld.a |
使用shared library時,不同library有衝突
那如果是兩個shared library彼此間有函數衝突的現象呢?
1 | $ gcc -fPIC -shared -o libhello.so hello.c |
結果一樣沒有錯誤,原因是在動態連結時會使用最先看到的symbol,所以順序不同就有不同結果
1 | $ gcc -o main.out libhello.so libworld.so main.c |
這個特性也跟LD_PRELOAD有關,我們可以用LD_PRELOAD來抽換shared library就是因為連結時會先使用先看到的symbol。當然這也曾經造成了一些危害,例如goahead的CVE-2017-17562。
執行中載入library
除了執行開始時載入library外,我們也可以用程式來載入
1 | // 動態載入所需的header |
範例:dltest.c
1 |
|
記得編譯時要連結dl library
1 | $ gcc dltest.c -ldl |
library公開symbols管理
有時候我們並不希望所提供的library會把所有symbol都洩漏出去,這時候大部分的人都會使用static限制外部呼叫。但是當這個函式在library中其他檔案會引用到,那就沒辦法設為static了。
那該怎麼辦呢?這邊有兩個方法:
使用 version script
首先我們先創兩個檔案當範例
test.c
1 | void test() |
func.c
1 | void test(); |
然後我們編成shared library,並且看看symbol
1 | $ gcc -fPIC -c test.c func.c |
可以看到test還是被暴露出來了,但是明明test應該只想要在library中被使用而已。
這時候我們可以試試GNU linker的version script。
libmylib.map
1 | { |
這個意思是只要顯示func,其他function都要隱藏。然後我們link的時候加上version script試看看:
1 | $ gcc -shared -o libmylib.so test.o func.o -Wl,--version-script,libmylib.map |
成功隱藏test了!
使用__attribute__語法
除了使用version script以外,也可以用gcc特有的語法,__attribute__((visibility("default")))
首先我們先改寫要公開的函式,代表我們只要暴露func()給外界看到
func.c
1 | void test(); |
然後在編譯成.o時要記得加上-fvisibility=hidden
,把其他function都隱藏起來。
1 | $ gcc -c -fPIC test.c func.c -fvisibility=hidden |
達到的效果和version script一樣!
用version script控制版本
這邊我們再多談談version script其他的用法,其實他除了管理要暴露出來的symbol外,我們也可以依照版本控制library要暴露出來的function。
首先我們先出第一版程式
libtest.c
1 |
|
libtest1.h
1 | void func(int num); |
version1.c
1 |
|
然後正常編譯執行
1 | $ gcc -fPIC -c libtest.c |
很順利正常執行,那我們假設現在要出第二個版本可以怎麼做
libtest2.c
1 |
|
稍微解釋一下,首先先實作兩個function,然後再用後面兩個__asm__
的symver
來把同樣symbol加上版號,至於第二行@@
的意思代表為預設版本。
接下來的部分就一樣撰寫新的程式
libtest2.h
1 | void func(int num1, int num2); |
version2.c
1 |
|
然後這時候就要出動version script了
libtest2.map
1 | LIBTEST_1.0 { |
然後我們編譯並執行看看
1 | $ gcc -fPIC -c libtest2.c |
可以看到兩者執行結果不同,為什麼會這樣呢?我們先看一下他們連結到的symbol
1 | $ readelf -a version1.out | grep func |
可以看到version1.out是使用func
,而version2.out的symbol就是func@@LIBTEST_2.0
。那同樣是引用相同library,到底是怎麼知道要呼叫哪個func呢?在呼叫func
的情況下,會自動找到最初的版本也就是func@LIBTEST_1.0
。而之後的程式編譯時link library則會去找default的版本,也就是有兩個@的func@@LIBTEST_2.0
,所以就不會有搞混的情況發生了。
這個方法在要維持兼容性的情況下非常好用,可以在不影響舊版的情況下改變函式規格。