Linux動態鏈接庫so版本兼容


1 Linux下so的特性

1.1 So的內容

nm可以看so的導出符號表

nm -C libsayhello.so 
...
00000000000006a0 T sayhello
...

可看到該so導出了一個函數,sayhello

 

1.2 App運行時加載的so名字

app鏈接時用到的so庫,它在運行的時候就會去找同樣名字的so庫。比如app鏈接了libsayhello.so,運行時就會去找libsayhello.so。

我們也可以讓app運行時去找另外的名字的so,方法是在編譯libsayhello.so的時候,指定編譯項soname,比如

-Wl,-soname,libwhatever.so.1

那么當app鏈接了libsayhello.so之后,運行時會去找libwhatever.so.1。

 

2 不同so符號覆蓋問題

2.1 動態庫so同名函數覆蓋

如果app鏈接了兩個so,兩個so里面存在同樣簽名的函數,但是實現不一樣,那么只會保留一個實現。

比如假設libsayhello.so和libsayworld.so里面都有一個函數,叫void say(),

libsayhello.so

void say() { printf("hello\n"); } 

libsayworld.so

void say() { printf("world\n"); } 

當app里面調用了say的時候,要么輸出hello,要么輸出world,不可能即輸出hello,又輸出world。

 

假設是兩個so分別引用了libsayhello.so和libsayworld.so,app再鏈接這四個so。

這種情況和app直接鏈接libsayhello.so和libsayworld.so是一樣的。

 

當然如果是用dlopen,dlclose,dlsym這種動態加載so的方法,還是可以做到既輸出hello,又輸出world的。

 

2.2 動態庫so同名變量覆蓋

同名變量和同名函數也一樣存在覆蓋的問題。

而且更隱晦的是,變量和函數即使不在.h里定義,直接在.c/.cpp里面定義,也會導出到符號表,造成覆蓋。

舉個例子:

p1.so

//p1.h void setInt_1(int i); void sayInt_1(); //p1.cpp #include <stdio.h> #include "p1.h"  int myInt = 1; void setInt_1(int i) { myInt = i; } void sayInt_1() { printif("p1 myInt=%d\n", myInt); }

p2.so

//p2.h void setInt_2(int i); void sayInt_2(); //p2.cpp #include <stdio.h> #include "p2.h"  int myInt = 2; void setInt_2(int i) { myInt = i; } void sayInt_2() { printif("p2 myInt=%d\n", myInt); } 

main.c

#include "p1.h" #include "p2.h"  int main(int argc, char** argv) { setInt_1(100); sayInt_1(); sayInt_2(); return 0; } 

結果為:

p1 myInit=100
p2 myInit=100

p1和p2都使用了一個同名的全局變量myInt,並且只在.c/.cpp文件里面定義,但是鏈接到so之后,就會只剩下一個全局變量myInt。所以,調用了p1.so里面的setInit_1函數之后,同時修改了p2.so里面的myInt值。

 

2.3 動態庫so類靜態變量覆蓋

這個問題就更隱晦了!

如果兩個so庫的cpp文件里都包含了一個類A的定義,類里有一個靜態變量s_a:

// p1.cpp & p2.cpp 新加入以下代碼 class A { public: A() { printf("A() this=%lld\n", (long long)this); m_int = new int(); *m_int = 1; } ~A() { printf("~A()\n"); /*delete m_int;*/ } private: static A s_a; int* m_int; }; A A::s_a; 

main.c保持不變。

輸出:

A() this=140279519260760
A() this=140279519260760
p1 myInit=100
p2 myInit=100
~A()
~A()

可以看出,同一個對象先被構造了兩次,再被析構了兩次!

如果去掉注釋,delete m_int的話將會crash。

 

2.4 靜態庫同名函數和同名變量覆蓋

靜態函數庫的情況和動態庫的情況類似。

 

2.5 導出腳本對符號覆蓋的影響

前面之所以函數和變量會互相覆蓋,是因為兩個so都導出了相同的符號。

可以使用導出腳本,指定要導出的符號,未指定的就不會被導出。

如果我們指定了導出腳本為:

p1.map

{
  global:
  extern "C++"
  {
    "setInt_1(int)";
    "sayInt_1()";
  };

local:
    *;
};

p2.map

{
  global:
  extern "C++"
  {
    "setInt_2(int)";
    "sayInt_2()";
  };

local:
    *;
};

編譯選項如:

g++ -shared -Wl,--version-script=p1.map -o libp1.so p1.o
g++ -shared -Wl,--version-script=p2.map -o libp2.so p2.o

輸出為:

A() this=139883050766416
A() this=139883052871760
p1 myInt=100
p2 myInt=2
~A()
~A()

查看一下導出表,可知未導出變量:

 nm libp2.so | grep " D "

查看一下導出表,可知導出了兩個函數

nm libp2.so | grep " T "
0000000000000756 T _Z8sayInt_2v
0000000000000740 T _Z8setInt_2i

 

分析整個流程,可知道兩個so都分別只導出了兩個函數, 各自里面的myInt變量和靜態變量A::s_a都保留着,沒有互相覆蓋。

 

2.6 so之間符號覆蓋的解決方案

簡單的說就是不允許so之間出現符號覆蓋,如果有符號覆蓋基本可以肯定是出問題了。

 

那么萬一用到的兩個不同功能的so,比如是兩個不同的開源項目的代碼,由於是各自開發,出現了函數或變量名字相同的情況,應該怎么辦呢?

答案簡單粗暴,也最可靠,那就是改名。

話說回來,沒考慮到符號沖突的so,質量要打個問號,能不用還是不要用。。。

 

如果是我們自己開發的so庫,要注意

(1) 函數/變量/類加名字空間,如果是c函數就需要加前綴

(2) 不導出不需要的函數/變量/類

 

3 相同so版本兼容問題

3.1 新舊版本的兼容問題

動態庫可能有新舊多個版本,並且新舊版本也可能不兼容。

可能有多個app依賴於這些不同版本的so庫。

因此當一個so庫被覆蓋的時候,就可能出問題。

(1) 舊so覆蓋新so,可能導致找不到新函數,

(2) 新so覆蓋舊so,可能導致找不到舊的函數,

(3) 而更加隱蔽的問題是:新舊so里的同一個函數,語義已經不一樣,即前置條件和效果不一樣。

 

3.2 新舊版本的兼容關系

(1) 新版本完全兼容舊版本,只是新增了函數。

這種情況只需要新版本即可。

(2) 新版本刪除了一些舊版函數,並且保持簽名相同的語義相同(可能新增了函數)。

這種情況需要新舊版本同時存在。

(3) 新舊兩個版本有一些相同簽名但是語義不一樣的函數。

這種情況是不予許的。

因為可能出現一個app必須同時依賴新舊兩個版本,由於同一簽名函數只能有一個實現,也就說另一個實現會被覆蓋,就會出錯。

 

3.3 新舊版本兼容的解決方法

由此我們知道,有兩個解決方案:

(1) 新版本完全兼容舊版本,並保證新版本覆蓋舊版本或者新舊版本共存。

這種方法太理想化。

實際情況下,新版本完全兼容舊版本比較難以做到,這要求函數一旦發布就不能改不能刪,並且永遠必須兼容。

(2) 新版本可以刪除一些舊版函數,需保持簽名相同的函數語義相同,並保證新舊版本共存。

這是可行的解決方法。

 

3.4 Linux的版本兼容解決方法

首先加版本號保證新舊版本可以共存,不會互相覆蓋。版本號形如openssl.so.1.0.0。

其次新版本需保持和舊版本簽名相同的函數語義相同。

 

這樣已經可以解決問題了,但是還可以優化。

因為版本號分的太細,導致有很多的版本同時存在,其實不需要這么多版本。

仔細考慮一下:

(1) 如果新版本和舊版本的函數完全相同,只是fix bug:那么新版本需要替換掉舊版本,舊版本不需要保留。

(2) 如果新版本新增了函數:那么新版本可以替換掉舊版本,舊版本不需要保留。

(3) 如果新版本刪除了函數:那么舊版本就需要保留。

 

如果linux系統下有新舊兩個so,它怎么知道可不可以需不需要替換掉舊版本?

答案是通過版本號:

linux規定對於大版本號相同的一系列so,可以選出里面最新的so,用它替換掉其它的so。

這里所謂的替換,其實是建立了一個軟鏈接,型如openssl.so.1,把它指向openssl.so.1.x.x.x系列so里面最新的那一個so。

 

4 Linux下的so規則

總結一下:

 

4.1 so導出規則

(1) 函數/變量/類加名字空間,如果是c函數就需要加前綴

(2) 不導出不需要的函數/變量/類

 

4.2 so版本號規則

版本號升級規則:

(1) 如果新舊版本函數完全相同,那么大版本號不變。

(2) 如果新版本新增了函數,那么大版本號不變。

(3) 如果新版本刪除了函數,那么大版本號需要變。

(4) build號每次都變,小版本號按特性或需求變

 

此外還有兩個版本號相關規則:

(1) 新版本不允許和舊版本函數簽名相同語義不同。

(2) 建立軟鏈接(形如openssl.so.1),指向大版本號相同的一系列so里面最新的so。app應當依賴於so的大版本號,不依賴於更細致的小版本號和build號。

 

4.3 App引用so的規則

(1) 不同庫之間不能有同名全局函數/變量,靜態函數/變量等。

否則會造成符號覆蓋,基本可以肯定會出問題。

(2) 對於一個庫,最好只引用一個版本。

如果需要引用同一庫的多個版本,那么該庫必須保證同名函數/變量的語義一致,除非是動態加載。

但是引用同一庫的多個版本即使編譯鏈接通過了,運行時依然可能會有潛在的問題,比如使用了同名的全局文件,信號量等,所以最好就是只引用一個版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM