背景
目前后臺(tái)服務(wù)器都是由bin+so的方式構(gòu)成筒占,ServerFrame(bin)提供網(wǎng)絡(luò)通信赋铝,內(nèi)存管理,配置管理等基礎(chǔ)的通用服務(wù),so實(shí)現(xiàn)各個(gè)服務(wù)器的特有邏輯良哲。bin文件和So公用一個(gè)全局變量G進(jìn)行數(shù)據(jù)共享,在G在bin中讀取相應(yīng)的配置筑凫,so中根據(jù)G中的參數(shù)進(jìn)行一系列的操作巍实。bin和so還公用一些基礎(chǔ)庫(kù)的代碼哩牍。
![](http://static.zybuluo.com/happyxgang/9ws3c06gn6cknroq53sb4q3k/image_1aj2337qi8qmt9o8v1jv1a749.png)
最近一件事情引起了后臺(tái)同學(xué)的注意膝昆,服務(wù)器so更新后,同學(xué)執(zhí)行bin文件纬朝,加載so后會(huì)發(fā)生錯(cuò)誤共苛,導(dǎo)致服務(wù)器會(huì)出現(xiàn)異常俄讹。
經(jīng)過(guò)查看svn代碼绕德,發(fā)現(xiàn)了幾個(gè)可疑的地方:
1.在bin和so公用的代碼庫(kù)里耻蛇,有一個(gè)函數(shù)的定義發(fā)生了變化臣咖,增加了一個(gè)帶有默認(rèn)值的參數(shù)夺蛇。這個(gè)函數(shù)會(huì)在so中被調(diào)用,由于帶有默認(rèn)參數(shù)娶聘,因此調(diào)用方式與從前一樣丸升,并沒有發(fā)生改變狡耻。
2.最近的代碼提交中夷狰,G的定義發(fā)生了變化郊霎,在其定義的中間添加了幾個(gè)變量歹篓。
因?yàn)閟o編譯時(shí)用的是最新代碼,而bin文件并沒有重新編譯毙籽,因此其中的函數(shù)以及結(jié)構(gòu)體的定義還都是老版本的定義坑赡,因此現(xiàn)在程序的結(jié)構(gòu)圖大致如下所示:
![image_1aj23ji9ion3eq010ft1st81usu9.png-13.2kB](http://static.zybuluo.com/happyxgang/52y4oz42zaqzt7lfchskffsq/image_1aj23ji9ion3eq010ft1st81usu9.png)
問題
- Q1:因?yàn)檎{(diào)用除的代碼不會(huì)變毅否,是否可能so中隊(duì)函數(shù)的調(diào)用依舊會(huì)走到了bin中所定義的舊版本的函數(shù)呢螟加?
A1:不會(huì)捆探。程序中調(diào)用哪一個(gè)函數(shù)是在編譯時(shí)決定的黍图。默認(rèn)參數(shù)一般只定義在頭文件中奴烙,只有編譯器看到了函數(shù)有默認(rèn)參數(shù)的聲明或定義之后切诀,才會(huì)在函數(shù)調(diào)用處根據(jù)情況趾牧,添加默認(rèn)的參數(shù)翘单,否則可能會(huì)出現(xiàn)編譯問題哄芜。
在so中編譯時(shí)確定了調(diào)用名為foo且有兩個(gè)int類型參數(shù)的函數(shù)后认臊,是不會(huì)再調(diào)用到bin中定義的foo函數(shù)的失晴。
關(guān)于帶默認(rèn)值的參數(shù),還需要注意一點(diǎn):Never redefine an inherited default parameter value
- Q2:現(xiàn)在兩邊看到的定義已經(jīng)不同了,那么程序會(huì)如何運(yùn)行儒旬。
A2:bin會(huì)按照老的偏移去給變量賦值帖族,而so會(huì)依照新的偏移去獲取數(shù)值并使用竖般。因?yàn)橹虚g添加了新的變量涣雕,導(dǎo)致取出的一部分變量沒問題胞谭,一部分變量數(shù)值不正確丈屹。也正是因?yàn)檫@個(gè)問題旺垒,導(dǎo)致了更換so之后先蒋,服務(wù)器無(wú)法正常啟動(dòng)竞漾。
臨時(shí)解決方案
因?yàn)閱栴}的根本原因是由于數(shù)據(jù)結(jié)構(gòu)的改變引起的不兼容,想要解決問題就必須更新bin文件鳞仙,用最新的版本的數(shù)據(jù)結(jié)構(gòu)定義編譯后內(nèi)存才能夠匹配上棍好。
延伸問題
這次問題的出現(xiàn)使一個(gè)沒有注意到的問題浮出水面:
由于bin文件和so公用一些基礎(chǔ)的代碼庫(kù)借笙。而兩者又是分開編譯的业稼,如果編譯So時(shí)一個(gè)公用的函數(shù)已經(jīng)更新盼忌,那么bin和so中的函數(shù)執(zhí)行就可能出現(xiàn)問題谦纱。
實(shí)例:
橙色函數(shù)表示foo的新版本跨嘉,期望的運(yùn)行方式如圖:
image_1aj4pro0c1kvthu2b1n1g769ge13.png-12.2kB
但實(shí)際可能出現(xiàn)的運(yùn)行方式有可能是一下兩種:
image_1aj4ps3sq2j815ltrug8l31ogh1g.png-13.6kB
為了明白上面的問題梦重,我們要搞清楚一下幾個(gè)問題:
- 為什么在bin文件中和so中函數(shù)原型相同的兩個(gè)函數(shù)有著不同的實(shí)現(xiàn)還能夠正常的鏈接和運(yùn)行琴拧。
我們的bin文件和so在編譯時(shí)互相并不知道對(duì)方的存在蚓胸。bin中通過(guò)dlopen打開so沛膳,除了在bin中調(diào)用的幾個(gè)有限的接口之外汛聚,編譯時(shí)不會(huì)知道so中其他的信息倚舀。因此在編譯時(shí)痕貌,兩者都可以編譯通過(guò)芯侥。
那么為什么通過(guò)dlopen打開之后柱查,bin和so中明明有相同函數(shù)原型的函數(shù)唉工,卻不會(huì)出現(xiàn)在編譯時(shí)經(jīng)沉芟酰可以看到的錯(cuò)誤信息呢?
symbol "x" redefined: first defined in "./main.obj"; redefined in ***.c
- 是什么決定了bin或者so調(diào)用到哪一個(gè)函數(shù)谣膳。
根據(jù)分析继谚,既然運(yùn)行時(shí)有多個(gè)定義花履,那么bin或者so又是如何決定選取哪一個(gè)定義诡壁,我們又是否有辦法來(lái)控制bin和so的行為妹卿,讓他們按照我們的希望選擇相應(yīng)的定義呢纽帖?
動(dòng)態(tài)鏈接庫(kù)相關(guān)知識(shí)
Position Independent Code
目前我們編譯so時(shí)都會(huì)用到-fPIC選項(xiàng)懊直,這表示生成的動(dòng)態(tài)鏈接庫(kù)(SO)是地址無(wú)關(guān)代碼(Position Independent Code)室囊,那么地址有關(guān)無(wú)關(guān)到底有什么關(guān)系呢融撞?
//libdep.c
int g = 1;
int foo(int a){
return g + a ;
}
由于動(dòng)態(tài)鏈接庫(kù)無(wú)法知道自己將會(huì)被加載到內(nèi)存的哪一個(gè)位置饶火,因此也就無(wú)法在編譯時(shí)決定g的地址肤寝。對(duì)于非PIC的so鲤看,當(dāng)libdep.so被不同的程序都用到的時(shí)候义桂,g的地址也就不一樣慷吊,導(dǎo)致ptr的賦值代碼會(huì)不同罢浇。
如果so代碼在不同的程序都不同嚷闭,因此這種方式?jīng)]有辦法做到同一份指令被多個(gè)進(jìn)程所共享胞锰。
![](http://static.zybuluo.com/happyxgang/n5so8knkja9s8eq4jh6fs61f/image_1aj93dd9v1jl115e48p3flfci013.png)
而地址無(wú)關(guān)代碼的so的內(nèi)存結(jié)構(gòu)如下所示:
![](http://static.zybuluo.com/happyxgang/gv45jwcrltcijqwmux2slw2e/image_1aj93hkm2ijsl92uqh1f6p1kuc2a.png)
地址無(wú)關(guān)的實(shí)現(xiàn)
PIC的實(shí)現(xiàn)基本想法是把指令中那些需要被修改的部分分離出來(lái)顺饮,跟數(shù)據(jù)放在一起兼雄,這樣指令部分就可以保持不變赦肋,而數(shù)據(jù)部分可以在各進(jìn)程中擁有一個(gè)副本佃乘。
這個(gè)想法之所以能夠?qū)崿F(xiàn)的前提是趣避,在鏈接階段程帕,鏈接器可以知道數(shù)據(jù)段和程序段的相對(duì)偏移骆捧。
還是這段代碼為例
//libdep.c
int g = 1;
int foo(int a){
return g + a ;
}
如果不是地址無(wú)關(guān)代碼敛苇,那么生成的程序可能會(huì)是這么描述獲取g值的過(guò)程枫攀。
將1234地址的內(nèi)容放到ax寄存器来涨。
而PIC生成的代碼會(huì)是這樣的
從數(shù)據(jù)段中用來(lái)實(shí)現(xiàn)PIC的數(shù)據(jù)結(jié)構(gòu)中獲取g的地址,并存到bx寄存器
將bx中地址的內(nèi)容放到ax
圖示如下:
![](http://static.zybuluo.com/happyxgang/qdxzxz2yzptcqnmebimwclhh/image_1aj97llsi1vi81g29ev61v0tjgcm.png)
數(shù)據(jù)段中用來(lái)實(shí)現(xiàn)PIC的數(shù)據(jù)結(jié)構(gòu)有一個(gè)名字叫做全局偏移表(Global Offfset Table)卧抗。
GOT中包含哪些內(nèi)容
我們可以把共享庫(kù)中對(duì)地址的引用分為四類
- 模塊內(nèi)部的函數(shù)調(diào)用社裆,跳轉(zhuǎn)等
- 模塊內(nèi)部的數(shù)據(jù)訪問,包括模塊內(nèi)定義的全局變量嗜傅,靜態(tài)變量
- 模塊外部的函數(shù)調(diào)用吕嘀,跳轉(zhuǎn)等
- 模塊外部的數(shù)據(jù)訪問币他,比如定義在其他模塊中定義的全局變量
static int a;
extern int b;
extern void ext();
void bar()
{
a = 1; // Type2, 模塊內(nèi)部數(shù)據(jù)訪問
b = 2; // Type4, 模塊外部數(shù)據(jù)訪問
}
void foo()
{
bar(); // Type1, 模塊內(nèi)部函數(shù)調(diào)用
ext(); // Type3, 模塊外部函數(shù)調(diào)用
}
具體的分析可以看程序員的自我修養(yǎng)7.3.3,7.3.4節(jié)蝴悉。這里我只說(shuō)一下結(jié)論:
PIC代碼中通過(guò)GOT訪問的地址引用包括:
- 類型1中沒有限制為static的函數(shù)
- 類型2中模塊內(nèi)部的全局變量
- 類型3尿这,4的地址引用
GOT中的內(nèi)容何時(shí)確定
GOT中的內(nèi)容無(wú)法在編譯時(shí)確定射众,當(dāng)動(dòng)態(tài)鏈接器將動(dòng)態(tài)鏈接庫(kù)加載到內(nèi)存之后叨橱,會(huì)進(jìn)行符號(hào)解析和重定位工作罗洗。當(dāng)動(dòng)態(tài)鏈接器發(fā)現(xiàn)了某個(gè)需要在GOT中保存的符號(hào)后,會(huì)將這個(gè)符號(hào)對(duì)應(yīng)的地址填到GOT中贩绕。
通過(guò)GOT訪問數(shù)據(jù)會(huì)增加一層間接的地址獲取步驟淑倾。但是也帶來(lái)了一定的好處踊淳。
- 在非PIC的代碼中迂尝,每對(duì)一個(gè)符號(hào)進(jìn)行引用都會(huì)產(chǎn)生一處重定位的地方垄开。而GOT訪問的方式使得重定位次數(shù)從每次引用一次變?yōu)槊恳粋€(gè)符號(hào)一次。
- 通過(guò)GOT可以使得代碼段成為地址無(wú)關(guān)的锻梳,從而可以在多個(gè)進(jìn)程中共享代碼疑枯。
舉一個(gè)例子:
$ cat test.c
extern int foo;
int function(void) {
return foo;
}
$ gcc -shared -fPIC -o libtest.so test.c
$ objdump --disassemble libtest.so
[...]
00000000000005ac <function>:
5ac: 55 push %rbp
5ad: 48 89 e5 mov %rsp,%rbp
5b0: 48 8b 05 71 02 20 00 mov 0x200271(%rip),%rax # 200828 <_DYNAMIC+0x1a0>
5b7: 8b 00 mov (%rax),%eax
5b9: 5d pop %rbp
5ba: c3 retq
$ readelf --sections libtest.so
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[...]
[20] .got PROGBITS 0000000000200818 00000818
0000000000000020 0000000000000008 WA 0 0 8
$ readelf --relocs libtest.so
Relocation section '.rela.dyn' at offset 0x418 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
[...]
000000200828 000400000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0
延遲綁定
從上面的描述可以看出废亭,為了保證程序的正常運(yùn)行,GOT中的信息需要在動(dòng)態(tài)鏈接庫(kù)被程序加載后立刻填寫正確骂删。這就給采用動(dòng)態(tài)鏈接庫(kù)的程序在啟動(dòng)時(shí)帶來(lái)了一定額外開銷掌动,從而減緩了啟動(dòng)速度。ELF采用了做延遲綁定的做法來(lái)解決這一問題桃漾』捣耍基本思想就是通過(guò)增加另外一個(gè)間接層,使得函數(shù)第一次被用到時(shí)才進(jìn)行綁定撬统,這就是PLT(Procedure Linkage Table)的作用。
通過(guò)PLT進(jìn)行函數(shù)調(diào)用的過(guò)程如下圖所示:
![image_1ajb5n5706nc6frg3eo4rdt29.png-13.4kB](http://static.zybuluo.com/happyxgang/2kxy3scuq1dvgqkdryhikbmb/image_1ajb5n5706nc6frg3eo4rdt29.png)
- 當(dāng)func被調(diào)用時(shí)敦迄,編譯器會(huì)生成相關(guān)代碼func@plt,表示跳轉(zhuǎn)到plt中表示func的那一項(xiàng)罚屋。
- 假設(shè)func在plt中為第n項(xiàng)苦囱,其內(nèi)容如圖,這時(shí)會(huì)繼續(xù)跳轉(zhuǎn)到GOT[n]所指向的地址脾猛。
- 而在第一次調(diào)用時(shí)撕彤,GOT[n]內(nèi)的地址會(huì)指回PLT[n]中,這里會(huì)做一些初始化的工作猛拴,然后跳轉(zhuǎn)到PLT[0]羹铅,PLT[0]指向了動(dòng)態(tài)鏈接器中解析符號(hào)的函數(shù)去,根據(jù)準(zhǔn)備好的數(shù)據(jù)愉昆,解析func的地址并將其填寫到GOT[n]中
在第一次調(diào)用func之后职员,再對(duì)func進(jìn)行函數(shù)調(diào)用時(shí)的流程如下:
![image_1ajb63cqg3mle34180qq5119dam.png-14.7kB](http://static.zybuluo.com/happyxgang/mqin4vw0arnz9hpw8dam1373/image_1ajb63cqg3mle34180qq5119dam.png)
這時(shí)GOT[n]中的地址已經(jīng)是正確的函數(shù)地址,因此會(huì)直接跳轉(zhuǎn)到正確的地址去跛溉。
下面看一個(gè)例子:
$ cat test.c
int foo(void);
int function(void) {
return foo();
}
$ gcc -shared -fPIC -o libtest.so test.c
$ objdump --disassemble libtest.so
[...]
00000000000005bc <function>:
5bc: 55 push %rbp
5bd: 48 89 e5 mov %rsp,%rbp
5c0: e8 0b ff ff ff callq 4d0 <foo@plt>
5c5: 5d pop %rbp
$ objdump --disassemble-all libtest.so
00000000000004d0 <foo@plt>:
4d0: ff 25 82 03 20 00 jmpq *0x200382(%rip) # 200858 <_GLOBAL_OFFSET_TABLE_+0x18>
4d6: 68 00 00 00 00 pushq $0x0
4db: e9 e0 ff ff ff jmpq 4c0 <_init+0x18>
$ readelf --relocs libtest.so
Relocation section '.rela.plt' at offset 0x478 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200858 000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0
動(dòng)態(tài)符號(hào)表
前面解釋了PIC的動(dòng)態(tài)鏈接庫(kù)的內(nèi)部實(shí)現(xiàn)原理焊切,那么動(dòng)態(tài)鏈接庫(kù)如何被外部的bin或者其他的so調(diào)用呢?
想要引用so中的函數(shù)或者獲取so中的全局變量芳室,那么bin或者其他的so就必須要知道so中有什么內(nèi)容专肪。無(wú)論是變量還是函數(shù)都可以看做是一個(gè)符號(hào),符號(hào)有其對(duì)應(yīng)的值堪侯,對(duì)于變量和函數(shù)來(lái)說(shuō)嚎尤,符號(hào)值是他們的地址。
ELF格式的so文件中會(huì)有一個(gè)段叫做動(dòng)態(tài)符號(hào)表抖格。動(dòng)態(tài)符號(hào)表中包含了動(dòng)態(tài)鏈接庫(kù)需要的導(dǎo)入函數(shù)(本so沒有定義的)和導(dǎo)出函數(shù)(本so定義可以給其他so或bin實(shí)用的)
// libso.c
int a;
extern int test();
void bar()
{
a = 1;
}
void foo()
{
test();
bar();
}
$ gcc -shared -fPIC -o libso.so libso.c
$ readelf --dyn-syms libso.so
Symbol table '.dynsym' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
[...]
8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND test
9: 0000000000000735 19 FUNC GLOBAL DEFAULT 11 bar
10: 0000000000000748 26 FUNC GLOBAL DEFAULT 11 foo
11: 0000000000201044 4 OBJECT GLOBAL DEFAULT 22 a
[...]
可以看到默認(rèn)所有的非static符號(hào)都會(huì)被導(dǎo)出a,bar,foo,而引用的test作為導(dǎo)入符號(hào)也在動(dòng)態(tài)符號(hào)表中诺苹,其Ndx為UND表示未定義咕晋。
程序運(yùn)行后,動(dòng)態(tài)鏈接器會(huì)按照寬度優(yōu)先的順序加載其依賴的動(dòng)態(tài)鏈接庫(kù)以及動(dòng)態(tài)鏈接庫(kù)的依賴收奔。如果存在多個(gè)相同的符號(hào)掌呜,那么最先被加載的符號(hào)將會(huì)干涉(interpose)后面的符號(hào)。也就是說(shuō)坪哄,如果有多個(gè)相同的符號(hào)质蕉,那么最先加載的符號(hào)定義將會(huì)被采用,而后面的鏈接庫(kù)中對(duì)該符號(hào)的引用都會(huì)指向最早被加載的那一個(gè)符號(hào)翩肌。
可執(zhí)行文件中的符號(hào)默認(rèn)并沒有導(dǎo)出模暗,因此不會(huì)參與到動(dòng)態(tài)鏈接庫(kù)的符號(hào)解析過(guò)程中去,但是如果在編譯時(shí)添加了-rdynamic選項(xiàng)念祭,會(huì)將bin文件中的符號(hào)導(dǎo)出兑宇,從而使得bin中的符號(hào)可以被so調(diào)用到。
下面看一個(gè)例子:
// main.c
#include <stdio.h>
#include <dlfcn.h>
typedef void (*func_ptr)();
short g = 1;
short x = 1;
int main()
{
printf("g in main:%d\n", g);
printf("x in main:%d\n",x );
void* handler = dlopen("./so",RTLD_NOW|RTLD_LOCAL);
func_ptr ptr = (func_ptr)dlsym(handler, "call_back");
(*ptr)();
printf("g in main:%d\n", g);
printf("x in main:%d\n", x);
return 0;
}
// libso.c
#include <stdio.h>
int g;
extern "C"{
void call_back()
{
printf("g in so:%d\n", g);}
g = 3;
}
$ g++ -shared -fPIC libso.c -o so -g
$ g++ -o main main.c -g -ldl
$ ./main
g in main:1
x in main:1
g int so:0
g in main:1
x in main:1
可以看到一開始編譯main的時(shí)候沒有加上-rdynamic粱坤,雖然main和so中都有一個(gè)符號(hào)叫做g隶糕,但是兩者互不影響。
下面采用新的方式重新編譯
$ g++ -shared -fPIC libso.c -o so -g
$ g++ -o main main.c -g -ldl -rdynamic
$ ./main
g in main:1
x in main:1
g int so:65537
g in main:3
x in main:0
這時(shí)候我們發(fā)現(xiàn)站玄,so中g(shù)的初始值變成了65537也就是0x00010001,也就是short g short x的內(nèi)存布局枚驻,bin中的符號(hào)g現(xiàn)在和so中的符號(hào)為同一個(gè),且因?yàn)閟o中的類型為int株旷,在編譯時(shí)決定了so中對(duì)g的賦值時(shí)按照4個(gè)字節(jié)的再登,因此在so中對(duì)g賦值時(shí)會(huì)覆蓋掉bin中x的內(nèi)容。
符號(hào)查找順序
當(dāng)存在多個(gè)相同符號(hào)時(shí)晾剖,先被加載的符號(hào)會(huì)覆蓋掉后面的符號(hào)锉矢,那么不同的加載順序就會(huì)影響符號(hào)的解析內(nèi)容,從而改變程序的行為钞瀑。
默認(rèn)的符號(hào)查找范圍(lookup scope)是全局查找范圍(global lookup scope),一開始包括bin中的符號(hào)沈撞,之后動(dòng)態(tài)鏈接器會(huì)按照寬度優(yōu)先的順序遍歷可執(zhí)行文件所依賴的動(dòng)態(tài)鏈接庫(kù)以及動(dòng)態(tài)鏈接庫(kù)所依賴的庫(kù)文件,并將其中的符號(hào)添加到全局查找范圍中雕什。
當(dāng)動(dòng)態(tài)鏈接庫(kù)通過(guò)dlopen打開時(shí)so時(shí)缠俺,so以及其依賴的庫(kù)文件形成另外一個(gè)局部查找范圍(local lookup scope)〈叮可以通過(guò)RTLD_GLOBAL改變這一行為壹士,使其添加到全局查找范圍中。
默認(rèn)情況下偿警,符號(hào)的查找先從全局查找范圍開始躏救,然后再查找局部查找范圍。
有以下幾個(gè)例外:
- 當(dāng)時(shí)用dlsym查找so中的符號(hào)時(shí)則是從局部查找范圍開始
- 當(dāng)dlopen中有RTLD_DEEPBIND時(shí)so中的符號(hào)從局部范圍開始查找
- 當(dāng)編譯so時(shí)添加了-Wl,-Bsymbolic參數(shù),使得so具有DF_DYNAMIC flag時(shí)盒使,在該so進(jìn)行符號(hào)查找是崩掘,依然會(huì)按照先全局在局部的順序查找,但是該so自己會(huì)被添加到全局符號(hào)中的第一個(gè)少办。
下面是一個(gè)例子:
app依賴于一個(gè)libone.so libdl.so libc.so, 并且通過(guò)dlopen打開了libdynamic.so苞慢,libdynamic.so又依賴于libtwo.so。現(xiàn)在的符號(hào)查找范圍如圖所示:
Global: app-->libone.so-->libdl.so-->libc.so
Local: libdynamic.so-->libtwo.so-->libc.so
如果libone.so 和libtwo.so中都定義了一個(gè)變量g英妓,app和libone.so中都有對(duì)g的引用挽放。
按照默認(rèn)的順序,先全局再局部蔓纠,這時(shí)libdynamic.so中引用的變量將是libone.so中的辑畦。
如果dlopen時(shí)添加了RTLD_DEEPBIND則先從局部范圍開始查找因此在libdnamic.so中查找到的g是libtwo.so中的,而app中g(shù)則是定義在libone.so中的腿倚。
假如libdynamic.so具有DT_DYNAMIC纯出,那么這是的查找libdynamic.so中符號(hào)的順序會(huì)變成如下所示:
libdynamic.so-->app-->libone.so-->libdl.so-->libc.so--> libtwo.so-->libc.so
libdynamic.so和app中所引用的g都會(huì)是libone.so中定義的。
遺留問題解答
- 為什么在bin文件中和so中函數(shù)原型相同的兩個(gè)函數(shù)有著不同的實(shí)現(xiàn)還能夠正常的鏈接和運(yùn)行敷燎。
- 是什么決定了bin或者so調(diào)用到哪一個(gè)函數(shù)潦刃。
在存在多個(gè)相同符號(hào)的時(shí)候,動(dòng)態(tài)鏈接器會(huì)選擇最先加載的哪一個(gè)符號(hào)懈叹,PIC程序由于采用GOT的方式訪問數(shù)據(jù)和函數(shù)因此可以在運(yùn)行時(shí)決定對(duì)應(yīng)符號(hào)加載的地址,從而實(shí)現(xiàn)全局符號(hào)介入(Global Symbol Interposition)分扎。因此先被加載的符號(hào)會(huì)被采用,執(zhí)行順序會(huì)如下圖所示:
![image_1ajc157bt8hs11j5qot1gm012qg1g.png-7.1kB](http://static.zybuluo.com/happyxgang/pvy0qelmx808y11fpub0f840/image_1ajc157bt8hs11j5qot1gm012qg1g.png)
如何防止出現(xiàn)這樣的問題
數(shù)據(jù)結(jié)構(gòu)定義不一致
在查閱一番資料后澄成,對(duì)于數(shù)據(jù)結(jié)構(gòu)定義不一致的問題,沒有發(fā)現(xiàn)可以解決的辦法畏吓,畢竟數(shù)據(jù)結(jié)構(gòu)的定義不同直接導(dǎo)致內(nèi)存布局不同墨状。程序按照原來(lái)的內(nèi)存結(jié)構(gòu)去操作數(shù)據(jù)肯定會(huì)出現(xiàn)問題。
在bin和so公用的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)定義發(fā)生變化時(shí)菲饼,感覺比較好的做法就是禁止程序啟動(dòng)肾砂,要是像這一次導(dǎo)致程序無(wú)法啟動(dòng)還好,如果正常啟動(dòng)但是還有一些隱藏的問題直接會(huì)導(dǎo)致運(yùn)行出現(xiàn)莫名奇怪的問題宏悦,甚至寫錯(cuò)數(shù)據(jù)镐确。
函數(shù)定義不一致
通過(guò)-Wl,-Bsymbolic的編譯選項(xiàng)可以使函數(shù)調(diào)用到希望的函數(shù),但是也會(huì)有一些問題饼煞。因?yàn)樵赽in和so之間會(huì)通過(guò)全局變量進(jìn)行通信源葫,Bsymbolic使得無(wú)法這樣做,因?yàn)閟o會(huì)采用自己的全局變量?jī)?nèi)容砖瞧,和bin中的全局變量?jī)?nèi)容不同息堂。為了解決這一個(gè)問題我們可以使用編譯選項(xiàng)(-Wl,-Bsymbolic-functions),這樣只對(duì)函數(shù)的查找改變順序,但是如果有函數(shù)指針的在bin和so中傳遞的情況出現(xiàn)荣堰,可能出現(xiàn)同一個(gè)函數(shù)的指針不同的情況床未。Bsymbolic引起的問題主要原因是采用這一個(gè)符號(hào)后會(huì)使得so和bin中本應(yīng)該是只有一份的內(nèi)容產(chǎn)生兩個(gè)副本。是否使用這一標(biāo)記需要待定振坚。
RTLD_DEEPBIND標(biāo)記和Bsymbolic的功能有一相似薇搁,在這里也可以解決我們的問題,但是RTLD_DEEPBIND同Bsymbolic一樣屡拨,同樣會(huì)有一些問題只酥。
示例如下:
// main.c
#include <stdio.h>
#include <dlfcn.h>
void foo(int a){}
typedef void(*foo_ptr)(int);
typedef void (*func_ptr)(foo_ptr);
int main()
{
void* handler = dlopen("./so",RTLD_NOW|RTLD_LOCAL);
func_ptr ptr = (func_ptr)dlsym(handler, "call_back");
(*ptr)(&foo);
return 0;
}
// libso.c
#include <stdio.h>
int g = 1;
void foo(int a){}
typedef void(*foo_ptr)(int);
extern "C"{
void call_back(foo_ptr p)
{
printf("foo in main:%p\n", p);
printf("foo in so:%p\n", &foo);
if(p == &foo){
printf("ptr is equal \n");
}else{
printf("ptr not equal\n");
}
}
}
// Normal
$ g++ -shared -fPIC libso.c -o so -g
$ g++ -o main main.c -g -ldl -rdynamic
$ ./main
foo in main:0x4008bd
foo in so:0x4008bd
ptr is equal
// Bsymbolic
$ g++ -shared -fPIC libso.c -o so -g -Wl,-Bsymbolic
$ g++ -o main main.c -g -ldl -rdynamic
$ ./main
foo in main:0x4008bd
foo in so:0x7f1e57910755
ptr not equal
// dlopen添加 RTLD_DEEPBIND
$ g++ -shared -fPIC libso.c -o so -g
$ ++ -o main main.c -g -ldl -rdynamic
$ ./main
foo in main:0x4008bd
foo in so:0x7f0a2329a785
ptr not equal
可以看到,本來(lái)應(yīng)該是相等的函數(shù)指針呀狼,在采用了Bsymbolic和RTLD_DEEPBIND之后都變得不相等了裂允。
gcc還提供了可見性控制,gcc默認(rèn)將符號(hào)全部都導(dǎo)出可見哥艇,如果我們僅導(dǎo)出需要使用的函數(shù)就可以防止so中的函數(shù)被bin中的定義所覆蓋绝编。在編譯時(shí)我們可以通過(guò)添加-fvisibility=[default,hidden,internal,protected]來(lái)控制導(dǎo)出符號(hào)默認(rèn)值,同時(shí)在程序中添加__attribute__((visibility ("hidden/default")));
來(lái)改變默認(rèn)的函數(shù)或者變量的可見性貌踏。但這就需要對(duì)所有需要導(dǎo)出的函數(shù)去修改代碼十饥,添加控制相關(guān)的內(nèi)容。尤其是GameSvr中判斷是否支持某個(gè)新的函數(shù)時(shí)用的是dlsym是否存在來(lái)判斷祖乳,如果游戲so代碼沒有即使正確的控制可見性逗堵,會(huì)導(dǎo)致即使游戲so中已經(jīng)有了新的函數(shù),但是dlsym依舊會(huì)查找不到的情況出現(xiàn)眷昆。
gcc還提供了version script可以定義符號(hào)的版本以及控制是否導(dǎo)出等蜒秤,但是和可見性控制一樣,會(huì)增加維護(hù)的成本亚斋,當(dāng)維護(hù)不當(dāng)時(shí)容易出現(xiàn)一些難以發(fā)現(xiàn)的問題作媚。
目前我們?cè)趕o和bin中都有版本控制,當(dāng)檢測(cè)到版本不匹配時(shí)會(huì)彈出提示帅刊。但是這種提示還不夠明確纸泡,大部分情況下并不清楚版本不匹配到底會(huì)帶來(lái)什么問題。我們可以引入主版本號(hào)次版本號(hào)的概念赖瞒,當(dāng)此版本號(hào)不同時(shí)彈出提示女揭。當(dāng)修改了so和bin中的關(guān)鍵數(shù)據(jù)后,修改主版本號(hào)冒黑,強(qiáng)制bin在加載so后啟動(dòng)失敗田绑。
遺留問題
目前我們使用PIC代碼編譯的so在dlopen時(shí)都會(huì)采用RTLD_NOW來(lái)加載so,不會(huì)用到PLT所提供的lazy binding功能抡爹。而為此掩驱,我們每次函數(shù)訪問都會(huì)付出一次從plt轉(zhuǎn)到got讀取地址的過(guò)程,必然就帶來(lái)了性能損失。
在新版本的gcc新版本(may be 6.0)為此添加了編譯選項(xiàng)(-fno-plt)可以不通過(guò)plt來(lái)訪問函數(shù)欧穴。
再進(jìn)一步民逼,如果我們根本不關(guān)心so是否為PIC的來(lái)節(jié)約內(nèi)存,我們可以不使用PIC代碼編譯so涮帘,直接采用裝載時(shí)重定位的方式加載so拼苍,將通過(guò)got訪問的間接層也去掉。不過(guò)64位環(huán)境中默認(rèn)so必須是PIC的调缨,需要通過(guò)添加 -mcmodel=large選項(xiàng)來(lái)進(jìn)行編譯疮鲫。
相關(guān)閱讀:
How to write shared library
What exactly does -Bsymblic do?
What exactly does -Bsymblic do? -- update
Load-time relocation of shared libraries
Position Independent Code (PIC) in shared libraries
Position Independent Code (PIC) in shared libraries on x64
PLT and GOT - the key to code sharing and dynamic libraries
Redirecting functions in shared ELF libraries
Bsymbolic與plt
當(dāng)使用Bsymbolic編譯so后我們會(huì)發(fā)現(xiàn)原來(lái)通過(guò)plt訪問的函數(shù)調(diào)用都變成了直接通過(guò)相對(duì)地址訪問。這是因?yàn)閘inker在發(fā)現(xiàn)了Bsymbolic標(biāo)記后知道so中的符號(hào)都不會(huì)被外部所調(diào)用弦叶,因此會(huì)將原本通過(guò)plt/got調(diào)用的函數(shù)改為直接通過(guò)相對(duì)地址調(diào)用俊犯。
Bsymbolic與Visibility
Bsymbolic會(huì)使得so內(nèi)部的函數(shù)不會(huì)被外部的函數(shù)所干涉,但是so中的函數(shù)依舊會(huì)導(dǎo)出伤哺,給其他的模塊使用燕侠。
而設(shè)置visibility hidden之后,內(nèi)部函數(shù)不僅不會(huì)被外部函數(shù)干涉立莉,而且也無(wú)法被其他的模塊使用绢彤。