Linux程序編譯鏈接動態庫版本號的問題
不同版本號的動態庫可能會不兼容,假設程序在編譯時指定動態庫是某個低版本號。執行是用的一個高版本號,可能會導致無法執行。
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
(中間沒有空格),-Wl
選項告訴編譯器將後面的參數傳遞給鏈接器,
-soname
則指定了動態庫的soname(簡單共享名。Short for shared object name)
如今我們生成了libhello.so.0.0.1,當我們執行ldconfig -n .
命令時,當前文件夾會多一個軟連接
$ 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才會有一個這種軟連接
再看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指定的動態庫,這樣依賴這個動態庫的可執行程序不需又一次編譯就能使用新版動態庫的特性
可執行程序的編譯
還是以hello動態庫為例,我們寫一個簡單的程序
// filename:main.c
#include "hello.h"
int main()
{
hello("handy");
return 0;
}
如今文件夾下是例如以下結構
├── hello.c
├── hello.h
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── main.c
libhello.so.0.0.1是我們編譯生成的動態庫,libhello.so.0是通過ldconfig生成的鏈接。採用例如以下命令編譯main.c
$ gcc main.c -L. -lhello -o main
/usr/bin/ld: cannot find -lhello
報錯找不到hello動態庫,在Linux下,編譯時指定-lhello,鏈接器會去尋找libhello.so這種文件。當前文件夾下沒有這個文件,所以報錯。
建立這樣一個軟鏈。文件夾結構例如以下
├── hello.c
├── hello.h
├── libhello.so -> libhello.so.0.0.1
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── main.c
讓libhello.so鏈接指向實際的動態庫文件libhello.so.0.0.1。再編譯main程序
gcc main.c -L. -lhello -o main
這樣可執行文件就生成了。
通過以上測試我們發現,在編譯可執行程序時。鏈接器會去找它依賴的libxxx.so這種文件。因此必須保證libxxx.so的存在
用ldd查看其依賴的動態庫
$ ldd main
linux-vdso.so.1 => (0x00007fffe23f2000)
libhello.so.0 => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fb6cd084000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb6cd427000)
我們發現main程序依賴的動態庫名字是libhello.so.0。既不是libhello.so也不是libhello.so.0.0.1。事實上在生成main程序的過程有例如以下幾步
- 鏈接器通過編譯命令
-L. -lhello
在當前文件夾查找libhello.so文件 - 讀取libhello.so鏈接指向的實際文件。這裏是libhello.so.0.0.1
- 讀取libhello.so.0.0.1中的SONAME,這裏是libhello.so.0
- 將libhello.so.0記錄到main程序的二進制數據裏
也就是說libhello.so.0是已經存儲到main程序的二進制數據裏的,無論這個程序在哪裏,通過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)
可執行程序的執行
如今測試文件夾結果例如以下
├── hello.c
├── hello.h
├── libhello.so -> libhello.so.0.0.1
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
├── main
└── main.c
這裏我們把編譯環境和執行環境混在一起了。只是沒關系。僅僅要我們知道當中原理。就能夠將其理清楚
前面我們已經通過ldd查看了main程序依賴的動態庫,並且指定了LD_LIBRARY_PATH變量,如今就能夠直接執行了
$ ./main
hello Handy!
看起來非常順利。那麽假設我們要部署執行環境,該怎麽部署呢。
顯然,源碼是不須要的,我們僅僅須要動態庫和可執行程序。這裏新建一個執行文件夾。並拷貝相關文件,文件夾結構例如以下
├── libhello.so.0.0.1
└── main
這時執行會main會發現
$ ./main
./main: 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
└── main
再執行程序,結果就會符合預期了
從以上測試看出,程序在執行時並不須要知道libxxx.so,而是須要程序本身記載的該動態庫的SONAME。所以main程序的執行環境僅僅須要以上三個文件就可以
動態庫版本號更新
假設動態庫須要做一個小小的修改,例如以下
// filename:hello.c
#include <stdio.h>
void hello(const char* name)
{
printf("hello %s, welcom 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
└── main
此時文件夾下有兩個版本號的動態庫。但libhello.so.0指向的是老本版,執行ldconfig -n .
後我們發現,鏈接指向了新版本號,例如以下
├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── main
再執行程序
$ ./main
hello Handy, 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
└── main
這時候發現,生成了新的鏈接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,執行可執行程序
EOF
Linux程序編譯鏈接動態庫版本號的問題