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) 對於一個庫,最好只引用一個版本。
如果需要引用同一庫的多個版本,那么該庫必須保證同名函數/變量的語義一致,除非是動態加載。
但是引用同一庫的多個版本即使編譯鏈接通過了,運行時依然可能會有潛在的問題,比如使用了同名的全局文件,信號量等,所以最好就是只引用一個版本。