動(dòng)態(tài)鏈接庫(kù)相關(guān)知識(shí)

背景

目前后臺(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ù)的代碼哩牍。

最近一件事情引起了后臺(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
image_1aj23ji9ion3eq010ft1st81usu9.png-13.2kB

問題

  • 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
image_1aj4pro0c1kvthu2b1n1g769ge13.png-12.2kB

但實(shí)際可能出現(xiàn)的運(yùn)行方式有可能是一下兩種:


image_1aj4ps3sq2j815ltrug8l31ogh1g.png-13.6kB
image_1aj4ps3sq2j815ltrug8l31ogh1g.png-13.6kB

為了明白上面的問題梦重,我們要搞清楚一下幾個(gè)問題:

  1. 為什么在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

  1. 是什么決定了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)程所共享胞锰。

而地址無(wú)關(guān)代碼的so的內(nèi)存結(jié)構(gòu)如下所示:


地址無(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

圖示如下:


數(shù)據(jù)段中用來(lái)實(shí)現(xiàn)PIC的數(shù)據(jù)結(jié)構(gòu)有一個(gè)名字叫做全局偏移表(Global Offfset Table)卧抗。

GOT中包含哪些內(nèi)容

我們可以把共享庫(kù)中對(duì)地址的引用分為四類

  1. 模塊內(nèi)部的函數(shù)調(diào)用社裆,跳轉(zhuǎn)等
  2. 模塊內(nèi)部的數(shù)據(jù)訪問,包括模塊內(nèi)定義的全局變量嗜傅,靜態(tài)變量
  3. 模塊外部的函數(shù)調(diào)用吕嘀,跳轉(zhuǎn)等
  4. 模塊外部的數(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
image_1ajb5n5706nc6frg3eo4rdt29.png-13.4kB
  1. 當(dāng)func被調(diào)用時(shí)敦迄,編譯器會(huì)生成相關(guān)代碼func@plt,表示跳轉(zhuǎn)到plt中表示func的那一項(xiàng)罚屋。
  2. 假設(shè)func在plt中為第n項(xiàng)苦囱,其內(nèi)容如圖,這時(shí)會(huì)繼續(xù)跳轉(zhuǎn)到GOT[n]所指向的地址脾猛。
  3. 而在第一次調(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
image_1ajb63cqg3mle34180qq5119dam.png-14.7kB

這時(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è)例外:

  1. 當(dāng)時(shí)用dlsym查找so中的符號(hào)時(shí)則是從局部查找范圍開始
  1. 當(dāng)dlopen中有RTLD_DEEPBIND時(shí)so中的符號(hào)從局部范圍開始查找
  2. 當(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中定義的。

遺留問題解答

  1. 為什么在bin文件中和so中函數(shù)原型相同的兩個(gè)函數(shù)有著不同的實(shí)現(xiàn)還能夠正常的鏈接和運(yùn)行敷燎。
  2. 是什么決定了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
image_1ajc157bt8hs11j5qot1gm012qg1g.png-7.1kB

如何防止出現(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ú)法被其他的模塊使用绢彤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜓耻,隨后出現(xiàn)的幾起案子茫舶,更是在濱河造成了極大的恐慌,老刑警劉巖刹淌,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奇适,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡芦鳍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門葛账,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柠衅,“玉大人,你說(shuō)我怎么就攤上這事籍琳》蒲纾” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵趋急,是天一觀的道長(zhǎng)喝峦。 經(jīng)常有香客問我,道長(zhǎng)呜达,這世上最難降的妖魔是什么谣蠢? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上眉踱,老公的妹妹穿的比我還像新娘挤忙。我一直安慰自己,他們只是感情好谈喳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布册烈。 她就那樣靜靜地躺著,像睡著了一般婿禽。 火紅的嫁衣襯著肌膚如雪赏僧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼埂奈。 笑死斗遏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的良漱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哀卫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起撬槽,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤此改,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后侄柔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體共啃,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年暂题,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了移剪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡薪者,死狀恐怖纵苛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情言津,我是刑警寧澤攻人,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站悬槽,受9級(jí)特大地震影響怀吻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜初婆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一蓬坡、第九天 我趴在偏房一處隱蔽的房頂上張望猿棉。 院中可真熱鬧,春花似錦渣窜、人聲如沸铺根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)位迂。三九已至,卻和暖如春详瑞,著一層夾襖步出監(jiān)牢的瞬間掂林,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工坝橡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泻帮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓计寇,卻偏偏與公主長(zhǎng)得像锣杂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子番宁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容