1. 程式人生 > >Linux程式編譯連結動態庫版本的問題

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