linux c 同一共享庫文件多個版本共存帶來的運行錯誤的原理 not found
不同版本的同一動態庫(so文件)可能會不兼容。如果程序在編譯時指定動態庫是某個低版本,運行是用的一個高版本,可能會導致無法運行,如果系統內存在同一個so文件的多個版本,在運行時也可能產生錯誤。
Linux上對動態庫的命名採用libxxx.so.a.b.c的格式,其中a代表大版本號,b代表小版本號,c代表更小的版本號。
以Linux自帶的cp程序爲例,通過ldd查看其依賴的動態庫:
$ ldd /bin/cp
linux-vdso.so.1 => (0x00007ffff59df000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fb3357e0000)
librt.so.1 => /lib64/librt.so.1 (0x00007fb3355d7000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007fb3353cf000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007fb3351ca000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb334e35000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fb334c31000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb335a0d000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb334a14000)
左邊是依賴的動態庫名字,右邊是鏈接指向的文件,再查看libacl.so相關的動態庫
$ ll /lib64/libacl.so*
lrwxrwxrwx. 1 root root 15 1月 7 2015 /lib64/libacl.so.1 -> libacl.so.1.1.0
-rwxr-xr-x. 1 root root 31280 12月 8 2011 /lib64/libacl.so.1.1.0
我們發現libacl.so.1實際上是一個軟鏈接,它指向的文件是libacl.so.1.1.0,命名方式符合上面的描述。當然,也有不按這種方式命名的,比如
$ ll /lib64/libc.so*
lrwxrwxrwx 1 root root 12 8月 12 14:18 /lib64/libc.so.6 -> libc-2.12.so
不管怎樣命名,只要按照規定的方式來生成和使用動態庫,就不會有問題。而且我們往往是在機器A上編譯程序,在機器B上運行程序,編譯和運行的環境其實是有略微不同的(有意和無意的)。
下面就說說動態庫在生成和使用過程中的一些問題。
動態庫的編譯
我們以一個簡單的程序作爲例:
// filename:hello.c
#include <stdio.h>
void hello(const char* name)
{
printf("hello %s!\n", name);
}
// filename:hello.h
void hello(const char* name);
採用如下命令進行編譯:
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.1
需要注意的參數是 -Wl,-soname,libhello.so.0(中間沒有空格),-Wl選項告訴編譯器將後面的參數傳遞給鏈接器,-soname則指定了動態庫的soname(簡單共享名,Short for shared object name),現在我們生成了libhello.so.0.0.1。
運行 ldconfig -n . 命令,在當前目錄創建一個軟連接libhello.so.0
$ ll libhello.so.0
lrwxrwxrwx 1 handy handy 17 8月 17 14:18 libhello.so.0 -> libhello.so.0.0.1
個軟鏈接是如何生成的呢,並不是截取libhello.so.0.0.1名字的前面部分,而是根據libhello.so.0.0.1編譯時指定的-soname生成的。也就是說我們在編譯動態庫時通過-soname指定的名字,已經記載到了動態庫的二進制數據裏面。不管程序是否按libxxx.so.a.b.c格式命名,但Linux上幾乎所有動態庫在編譯時都指定了-soname,我們可以通過readelf工具查看soname,比如文章開頭列舉的兩個動態庫:
$ readelf -d /lib64/libacl.so.1.1.0
Dynamic section at offset 0x6de8 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libattr.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libacl.so.1]
這裏省略了一部分,可以看到最後一行SONAME爲libacl.so.1,所以/lib64纔會有一個這樣的軟連接
也可以查看libhello.so.0.0.1這個文件裏面的soname字段。
再看libc-2.12.so文件,該文件並沒有採用我們說的命名方式:
$ readelf -d /lib64/libc-2.12.so
Dynamic section at offset 0x18db40 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME) Library soname: [libc.so.6]
樣可以看到最後一行SONAME爲libc.so.6,即便該動態庫沒有按版本號的方式命名,但仍舊有一個軟鏈指向該動態庫,而該軟鏈的名字就是soname指定的名字。
所以關鍵就是這個soname,它相當於一箇中間者,當我們的動態庫只是升級一個小版本時,我們可以讓它的soname相同,而可執行程序只認soname指定的動態庫,這樣依賴這個動態庫的可執行程序不需重新編譯就能使用新版動態庫的特性。值得注意的是,如果我們不更改動態庫的 SONAME
,只更改動態庫文件的文件名,然後指定給鏈接器更改文件名後的動態庫文件,那麼在運行時,二進制文件可能會報錯:找不到指定的庫(因爲soname對應的名字實際是軟連接連到了改名前的動態庫文件)。
我們再寫一個測試文件:
#include "hello.h"
int main(int argc, char** argv)
{
hello("jsc");
return 0;
}
因爲我們在gcc編譯時使用-l只認庫名(如libhello.so不認libhello.so.x),所以我們先給libhello.so.0建立一個軟鏈接:
ln -s libhello.so.0 libhello.so
執行:
gcc main.c -L. -lhello
得到a.out可執行文件。
運行a.out發現無法找到libhello.so這個文件,使用ldd a.out也會發現找不到這個文件。但是這個文件明明就在本目錄下,爲啥還找不到呢?
這是因爲gcc編譯鏈接動態庫時,很有可能編譯通過,但是執行時,找不到動態鏈接庫,那是因爲-L選項指定的路徑只在編譯時有效,編譯出來的可執行文件不知道-L選項後面的值,當然找不到。可以用ldd <your_execute>看看是不有 ‘not found’在你鏈接的庫後面,解決方法是通過-Wl,rpath=<your_lib_dir>,使得execute記住鏈接庫的位置。這樣的缺點是綁定死了運行庫的位置,如果這個庫文件移動了位置,就可能無法運行了。
由於種種原因,Linux 下寫 c 代碼時要用到一些外部庫(不屬於標準C的庫),可是由於沒有權限,無法將這寫庫安裝到系統目錄,只好安裝用戶目錄下如 /home/youname/lib,可是怎麼編譯才能讓程序正常編譯,並且正常運行呢。這樣使用gcc:
gcc -I/path/to/include/dir -L/path/to/lib/dir -llibname -Wl,-rpath,/path/to/lib/dir -o test test.c
解釋一下,-I ,-L ,-l 這三個經常用,分別表示編譯時include目錄,庫目錄和所用的庫,而-Wl,-rpath,是什麼呢,它就是指定編譯好的程序在運行時動態庫的目錄(可以 man gcc 搜索 -Wl查看),當編譯好程序後用 ldd 就可以看到你指定的路徑了。
當然也可以不用-Wl,-rpath,而用–static 採用靜態編譯,這樣程序在哪都能正常運行,不過代價是程序要大很多。
還有一種方法是用LD_LIBRARY_PATH,不過很多人不推薦用這個,所以最好的方法還是用 -Wl,-rpath=/path/to/lib/dir。
執行gcc main.c -L. -Wl,-rpath=. -lhello -o test
得到可執行程序test。然後可以在本地執行。
使用readelf -d test查看該文件的屬性信息,發現有一個RUNPATH指定了運行時連接的庫路徑爲.,這樣如果test換個路徑,就無法運行。
如果我們把上面的編譯main.c的rpath換成絕對路徑:gcc main.c -L. -Wl,-rpath=/home/ok/code/sofileTest/ -lhello -o test
再查看test的信息,發現RUNPATH是一個絕對路徑,這樣的話,只要libhello.so不換地方,把test換到其他地方,還是能正常運行的。
其實在生成test程序的過程有如下幾步:
- 鏈接器通過編譯命令 -L. -lhello 在當前目錄查找libhello.so文件
- 讀取libhello.so鏈接指向的實際文件,這裏是libhello.so.0.0.1
- 讀取libhello.so.0.0.1中的SONAME,這裏是libhello.so.0
- 將libhello.so.0記錄到test程序的二進制數據裏
- 也就是說libhello.so.0是已經存儲到test程序的二進制數據裏的,不管這個程序在哪裏,通過ldd查看它依賴的動態庫都是libhello.so.0
而爲什麼這裏ldd查看main顯示libhello.so.0爲not found呢,因爲ldd是從環境變量$LD_LIBRARY_PATH指定的路徑裏來查找文件的,我們指定環境變量再運行如下
$ export LD_LIBRARY_PATH=. && ldd main
linux-vdso.so.1 => (0x00007fff7bb63000)
libhello.so.0 => ./libhello.so.0 (0x00007f2a3fd39000)
libc.so.6 => /lib64/libc.so.6 (0x00007f2a3f997000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2a3ff3b000)
可執行程序的運行
現在測試目錄結果如下這裏我們把編譯環境和運行環境混在一起了,不過沒關係,只要我們知道其中原理,就可以將其理清楚。前面我們已經通過ldd查看了main程序依賴的動態庫,並且指定了LD_LIBRARY_PATH變量,現在就可以直接運行了
$ ./test
hello jsc!
看起來很順利。那麼如果我們要部署運行環境,該怎麼部署呢。顯然,源代碼是不需要的,我們只需要動態庫和可執行程序。這裏新建一個運行目錄,並拷貝相關文件,目錄結構如下
├── libhello.so.0.0.1
└── test
時運行會main會發現
$ ./test
./test: error while loading shared libraries: libhello.so.0: cannot open shared object file: No such file or directory
報錯說libhello.so.0文件找不到,也就是說 程序運行時需要尋找的動態庫文件名其實是動態庫編譯時指定的SONAME,這也和我們用ldd查看的一致。通過 ldconfig -n . 建立鏈接,如下
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── test
運行程序,結果就會符合預期了。
從上面的測試看出,程序在運行時並不需要知道libxxx.so,而是需要程序本身記載的該動態庫的SONAME,所以test程序的運行環境只需要以上三個文件即可
動態庫版本更新
假設動態庫需要做一個小小的改動,如下
// filename:hello.c
#include <stdio.h>
void hello(const char* name)
{
printf("hello %s, welcome to our world!\n", name);
}
由於改動較小,我們編譯動態庫時仍然指定相同的soname
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.2
新的動態庫拷貝到運行目錄,此時運行目錄結構如下
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── test
時目錄下有兩個版本的動態庫,但libhello.so.0指向的是老本版,運行 ldconfig -n . 後我們發現,鏈接指向了新版本,如下
├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── test
再運行程序
$ ./test
hello jsc, welcom to our world!
沒有重新編譯就使用上了新的動態庫, wonderful!
同樣,假如我們的動態庫有大的改動,編譯動態庫時指定了新的soname,如下
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0
動態庫文件拷貝到運行目錄,並執行 ldconfig -n .,目錄結構如下
├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
├── libhello.so.1 -> libhello.so.1.0.0
├── libhello.so.1.0.0
└── test
這時候發現,生成了新的鏈接libhello.so.1,而main程序還是使用的libhello.so.0,所以無法使用新版動態庫的功能,需要重新編譯纔行
最後
在實際生產環境中,程序的編譯和運行往往是分開的,但只要搞清楚這一系列過程中的原理,就不怕被動態庫的版本搞暈。簡單來說,按如下方式來做
- 編譯動態庫時指定-Wl,-soname,libxxx.so.a,設置soname爲libxxx.so.a,生成實際的動態庫文件libxxx.so.a.b.c
- 編譯可執行程序時保證libxx.so存在,如果是軟鏈,必須指向實際的動態庫文件libxxx.so.a.b.c
- 運行可執行文件時保證libxxx.so.a.b.c文件存在,通過ldconfig生成libxxx.so.a鏈接指向libxxx.so.a.b.c
- 設置環境變量LD_LIBRARY_PATH,運行可執行程序
再說一下人爲修改so文件名的事情。對於上述生成的libhello.so和libhello.so.0以及libhello.so.0.0.1這3個庫文件及軟連接。如果修改了libhello.so.0,也就是那個SONAME對應的文件,那麼在使用時就會造成not found的錯誤。具體原因是:libhello.so是一個軟鏈接,它連向的是libhello.so.0。所以後面一定會找libhello.so.0,因此,修改了libhello.so這個文件名沒關係,但是修改了libhello.so.0就會造成無法鏈過去的錯誤。同理,修改了libhello.so.0.0.1也會造成問題。