在上一篇文章中,我們一起學(xué)習(xí)了Linux
系統(tǒng)中 GCC
編譯器在編譯可執(zhí)行程序時(shí),靜態(tài)鏈接過程中是如何進(jìn)行符號(hào)重定位的。GCC 鏈接過程中的【重定位】過程分析
為了完整性,我們這篇文章來一起探索一下:動(dòng)態(tài)鏈接過程中是如何進(jìn)行符號(hào)重定位的。
老樣子,文中使用大量的【代碼+圖片】的方式,來真實(shí)的感受一下實(shí)際的內(nèi)存模型。
文中使用了大量的圖片,建議您在電腦上閱讀此文。
關(guān)于為什么使用動(dòng)態(tài)鏈接,這里就不展開討論了,無非就幾點(diǎn):
節(jié)省物理內(nèi)存;
可以動(dòng)態(tài)更新;
動(dòng)態(tài)鏈接要解決什么問題?
靜態(tài)鏈接得到的可執(zhí)行程序,被操作系統(tǒng)加載之后就可以執(zhí)行執(zhí)行。
因?yàn)樵阪溄拥臅r(shí)候,鏈接器已經(jīng)把所有目標(biāo)文件中的代碼、數(shù)據(jù)等Section
,都組裝到可執(zhí)行文件中了。
并且把代碼中所有使用的外部符號(hào)(變量、函數(shù)),都進(jìn)行了重定位(即:把變量、函數(shù)的地址,都填寫到代碼段中需要重定位的地方),因此可執(zhí)行程序在執(zhí)行的時(shí)候,不依賴于其它的外部模塊即可運(yùn)行。
詳細(xì)的靜態(tài)鏈接過程,請(qǐng)參考上一篇文章:GCC 鏈接過程中的【重定位】過程分析。
也就是說:符號(hào)重定位的過程,是直接對(duì)可執(zhí)行文件進(jìn)行修改。
但是對(duì)于動(dòng)態(tài)鏈接來說,在編譯階段,僅僅是在可執(zhí)行文件或者動(dòng)態(tài)庫中記錄了一些必要的信息。
真正的重定位過程,是在這個(gè)時(shí)間點(diǎn)來完成的:可執(zhí)行程序、動(dòng)態(tài)庫被加載之后,調(diào)用可執(zhí)行程序的入口函數(shù)之前。
只有當(dāng)所有需要被重定位的符號(hào)被解決了之后,才能開始執(zhí)行程序。
既然也是重定位,與靜態(tài)鏈接過程一樣:也需要把符號(hào)的目標(biāo)地址填寫到代碼段中需要重定位的地方。
矛盾:代碼段不可寫
問題來了!
我們知道,在現(xiàn)代操作系統(tǒng)中,對(duì)于內(nèi)存的訪問是有權(quán)限控制的,一般來說:
代碼段:可讀、可執(zhí)行;
數(shù)據(jù)段:可讀、可寫;
如果進(jìn)行符號(hào)重定位,就需要對(duì)代碼進(jìn)行修改(填寫符號(hào)的地址),但是代碼段又沒有可寫的權(quán)限,這是一個(gè)矛盾!
解決這個(gè)矛盾的方案,就是Linux
系統(tǒng)中動(dòng)態(tài)鏈接器的核心工作!
解決矛盾:增加一層間接性
David Wheeler
有一句名言:“計(jì)算機(jī)科學(xué)中的大多數(shù)問題,都可以通過增加一層間接性來解決?!?/p>
解決動(dòng)態(tài)鏈接中的代碼重定位問題,同樣也可以通過增加一層間接性來解決。
既然代碼段在被加載到內(nèi)存中之后不可寫,但是數(shù)據(jù)段是可寫的。
在代碼段中引用的外部符號(hào),可以在數(shù)據(jù)段中增加一個(gè)跳板:讓代碼段先引用數(shù)據(jù)段中的內(nèi)容,然后在重定位時(shí),把外部符號(hào)的地址填寫到數(shù)據(jù)段中對(duì)應(yīng)的位置,不就解決這個(gè)矛盾了嗎?!
如下圖所示:
理解了上圖的解決思路,基本上就理解了動(dòng)態(tài)鏈接過程中重定位的核心思想。
示例代碼
我們需要3
個(gè)源文件來討論動(dòng)態(tài)鏈接中重定位的過程:main.c
、a.c
、b.c
,其中的a.c
和b.c
被編譯成動(dòng)態(tài)庫,然后main.c
與這兩個(gè)動(dòng)態(tài)庫一起動(dòng)態(tài)鏈接成可執(zhí)行程序。
它們之間的依賴關(guān)系是:
b.c
代碼如下:
#include
int b = 30;
void func_b(void)
{
printf("in func_b. b = %d
", b);
}
代碼說明:
定義一個(gè)全局變量和一個(gè)全局函數(shù),被 a.c 調(diào)用。
a.c
代碼如下(稍微復(fù)雜一些,主要是為了探索:不同類型的符號(hào)如何處理重定位):
#include
// 內(nèi)部定義【靜態(tài)】全局變量
static int a1 = 10;
// 內(nèi)部定義【非靜態(tài)】全局變量
int a2 = 20;
// 聲明外部變量
extern int b;
// 聲明外部函數(shù)
extern void func_b(void);
// 內(nèi)部定義的【靜態(tài)】函數(shù)
static void func_a2(void)
{
printf("in func_a2
");
}
// 內(nèi)部定義的【非靜態(tài)】函數(shù)
void func_a3(void)
{
printf("in func_a3
");
}
// 被 main 調(diào)用
void func_a1(void)
{
printf("in func_a1
");
// 操作內(nèi)部變量
a1 = 11;
a2 = 21;
// 操作外部變量
b = 31;
// 調(diào)用內(nèi)部函數(shù)
func_a2();
func_a3();
// 調(diào)用外部函數(shù)
func_b();
}
代碼說明:
定義了 2 個(gè)全局變量:一個(gè)靜態(tài),一個(gè)非靜態(tài);
定義了 3 個(gè)函數(shù):
func_a2
是靜態(tài)函數(shù),只能在本文件中調(diào)用;
func_a1
和func_a3
是全局函數(shù),可以被外部調(diào)用;
- 在 main.c 中會(huì)調(diào)用
func_a1
。
main.c
代碼如下:
#include
#include
#include
// 聲明外部變量
extern int a2;
extern void func_a1();
typedef void (*pfunc)(void);
int main(void)
{
printf("in main
");
// 打印此進(jìn)程的全局符號(hào)表
void *handle = dlopen(0, RTLD_NOW);
if (NULL == handle)
{
printf("dlopen failed!
");
return -1;
}
printf("
------------ main ---------------
");
// 打印 main 中變量符號(hào)的地址
pfunc addr_main = dlsym(handle, "main");
if (NULL != addr_main)
printf("addr_main = 0x%x
", (unsigned int)addr_main);
else
printf("get address of main failed!
");
printf("
------------ liba.so ---------------
");
// 打印 liba.so 中變量符號(hào)的地址
unsigned int *addr_a1 = dlsym(handle, "a1");
if (NULL != addr_a1)
printf("addr_a1 = 0x%x
", *addr_a1);
else
printf("get address of a1 failed!
");
unsigned int *addr_a2 = dlsym(handle, "a2");
if (NULL != addr_a2)
printf("addr_a2 = 0x%x
", *addr_a2);
else
printf("get address of a2 failed!
");
// 打印 liba.so 中函數(shù)符號(hào)的地址
pfunc addr_func_a1 = dlsym(handle, "func_a1");
if (NULL != addr_func_a1)
printf("addr_func_a1 = 0x%x
", (unsigned int)addr_func_a1);
else
printf("get address of func_a1 failed!
");
pfunc addr_func_a2 = dlsym(handle, "func_a2");
if (NULL != addr_func_a2)
printf("addr_func_a2 = 0x%x
", (unsigned int)addr_func_a2);
else
printf("get address of func_a2 failed!
");
pfunc addr_func_a3 = dlsym(handle, "func_a3");
if (NULL != addr_func_a3)
printf("addr_func_a3 = 0x%x
", (unsigned int)addr_func_a3);
else
printf("get address of func_a3 failed!
");
printf("
------------ libb.so ---------------
");
// 打印 libb.so 中變量符號(hào)的地址
unsigned int *addr_b = dlsym(handle, "b");
if (NULL != addr_b)
printf("addr_b = 0x%x
", *addr_b);
else
printf("get address of b failed!
");
// 打印 libb.so 中函數(shù)符號(hào)的地址
pfunc addr_func_b = dlsym(handle, "func_b");
if (NULL != addr_func_b)
printf("addr_func_b = 0x%x
", (unsigned int)addr_func_b);
else
printf("get address of func_b failed!
");
dlclose(handle);
// 操作外部變量
a2 = 100;
// 調(diào)用外部函數(shù)
func_a1();
// 為了讓進(jìn)程不退出,方便查看虛擬空間中的地址信息
while(1) sleep(5);
return 0;
}
糾正:代碼中本來是想打印變量的地址的,但是不小心加上了 *,變成了打印變量值。最后檢查的時(shí)候才發(fā)現(xiàn),所以就懶得再去修改了。
代碼說明:
利用 dlopen 函數(shù)(第一個(gè)參數(shù)傳入 NULL),來打印此進(jìn)程中的一些符號(hào)信息(變量和函數(shù));
賦值給 liba.so 中的變量 a2,然后調(diào)用 liba.so 中的 func_a1 函數(shù);
編譯成動(dòng)態(tài)鏈接庫
把以上幾個(gè)源文件編譯成動(dòng)態(tài)庫以及可執(zhí)行程序:
$ gcc -m32 -fPIC --shared b.c -o libb.so
$ gcc -m32 -fPIC --shared a.c -o liba.so -lb -L./
$ gcc -m32 -fPIC main.c -o main -ldl -la -lb -L./
有幾點(diǎn)內(nèi)容說明一下:
-fPIC 參數(shù)意思是:生成位置無關(guān)代碼(Position Independent Code),這也是動(dòng)態(tài)鏈接中的關(guān)鍵;
既然動(dòng)態(tài)庫是在運(yùn)行時(shí)加載,那為什么在編譯的時(shí)候還需要指明?
因?yàn)樵诰幾g的時(shí)候,需要知道每一個(gè)動(dòng)態(tài)庫中提供了哪些符號(hào)。Windows 中的動(dòng)態(tài)庫的顯性的導(dǎo)出和導(dǎo)入標(biāo)識(shí),更能體現(xiàn)這個(gè)概念(__declspec(dllexport), __declspec(dllimport))。
此時(shí),就得到了如下幾個(gè)文件:
動(dòng)態(tài)庫的依賴關(guān)系
對(duì)于靜態(tài)鏈接的可執(zhí)行程序來說,被操作系統(tǒng)加載之后,可以認(rèn)為直接從可執(zhí)行程序的入口函數(shù)開始(也就是ELF
文件頭中指定的e_entry
這個(gè)地址),執(zhí)行其中的指令碼。
但是對(duì)于動(dòng)態(tài)鏈接的程序來說,在執(zhí)行入口函數(shù)的指令之前,必須把該程序所依賴的動(dòng)態(tài)庫加載到內(nèi)存中,然后才能開始執(zhí)行。
對(duì)于我們的實(shí)例代碼來說:main
程序依賴于liba.so
庫,而liba.so
庫又依賴于libb.so
庫。
可以用ldd
工具來分別看一下動(dòng)態(tài)庫之間的依賴關(guān)系:
可以看出:
在 liba.so 動(dòng)態(tài)庫中,記錄了信息:依賴于 libb.so;
在 main 可執(zhí)行文件中,記錄了信息:依賴于 liba.so, libb.so;
也可以使用另一個(gè)工具patchelf
來查看一個(gè)可執(zhí)行程序或者動(dòng)態(tài)庫,依賴于其他哪些模塊。例如:
那么,動(dòng)態(tài)庫的加載是由誰來完成的呢?動(dòng)態(tài)鏈接器!
動(dòng)態(tài)庫的加載過程
動(dòng)態(tài)鏈接器加載動(dòng)態(tài)庫
當(dāng)執(zhí)行main
程序的時(shí)候,操作系統(tǒng)首先把main
加載到內(nèi)存,然后通過.interp
段信息來查看該文件依賴哪些動(dòng)態(tài)庫:
上圖中的字符串/lib/ld-linux.so.2
,就表示main
依賴動(dòng)態(tài)鏈接庫。
ld-linux.so.2
也是一個(gè)動(dòng)態(tài)鏈接庫,在大部分情況下動(dòng)態(tài)鏈接庫已經(jīng)被加載到內(nèi)存中了(動(dòng)態(tài)鏈接庫就是為了共享),操作系統(tǒng)此時(shí)只需要把動(dòng)態(tài)鏈接庫所在的物理內(nèi)存,映射到 main
進(jìn)程的虛擬地址空間中就可以了,然后再把控制權(quán)交給動(dòng)態(tài)鏈接器。
動(dòng)態(tài)鏈接器發(fā)現(xiàn):main
依賴liba.so
,于是它就在虛擬地址空間中找一塊能放得下liba.so
的空閑空間,然后把liba.so
中需要加載到內(nèi)存中的代碼段、數(shù)據(jù)段都加載進(jìn)來。
當(dāng)然,在加載liba.so
時(shí),又會(huì)發(fā)現(xiàn)它依賴libb.so
,于是又把在虛擬地址空間中找一塊能放得下libb.so
的空閑空間,把libb.so
中的代碼段、數(shù)據(jù)段等加載到內(nèi)存中,示意圖如下所示:
動(dòng)態(tài)鏈接器自身也是一個(gè)動(dòng)態(tài)庫,而且是一個(gè)特殊的動(dòng)態(tài)庫:它不依賴于其他的任何動(dòng)態(tài)庫,因?yàn)楫?dāng)它被加載的時(shí)候,沒有人幫它去加載依賴的動(dòng)態(tài)庫,否則就形成雞生蛋、蛋生雞的問題了。
動(dòng)態(tài)庫的加載地址
一個(gè)進(jìn)程在運(yùn)行時(shí)的實(shí)際加載地址(或者說虛擬內(nèi)存區(qū)域),可以通過指令:$ cat /proc/[進(jìn)程的 pid]/maps
讀取出來。
例如:我的虛擬機(jī)中執(zhí)行main
程序時(shí),看到的地址信息是:
黃色部分分別是:main
, liba.so
, libb.so
這3
個(gè)模塊的加載信息。
另外,還可以看到c
庫(libc-2.23.so
)、動(dòng)態(tài)鏈接器(ld-2.23.so
)以及動(dòng)態(tài)加載庫libdl-2.23.so
的虛擬地址區(qū)域,布局如下:
可以看出出來:
main
可執(zhí)行程序是位于低地址,所有的動(dòng)態(tài)庫都位于4G
內(nèi)存空間的最后1G
空間中。
還有另外一個(gè)指令也很好用 $ pmap [進(jìn)程的 pid]
,也可以打印出每個(gè)模塊的內(nèi)存地址:
符號(hào)重定位
全局符號(hào)表
在之前的靜態(tài)鏈接中學(xué)習(xí)過,鏈接器在掃描每一個(gè)目標(biāo)文件(.o
文件)的時(shí)候,會(huì)把每個(gè)目標(biāo)文件中的符號(hào)提取出來,構(gòu)成一個(gè)全局符號(hào)表。
然后在第二遍掃描的時(shí)候,查看每個(gè)目標(biāo)文件中需要重定位的符號(hào),然后在全局符號(hào)表中查找該符號(hào)被安排在什么地址,然后把這個(gè)地址填寫到引用的地方,這就是靜態(tài)鏈接時(shí)的重定位。
但是動(dòng)態(tài)鏈接過程中的重定位,與靜態(tài)鏈接的處理方式差別就大很多了,因?yàn)?span>每個(gè)符號(hào)的地址只有在運(yùn)行的時(shí)候才能知道它們的地址。
例如:liba.so
引用了libb.so
中的變量和函數(shù),而libb.so
中的這兩個(gè)符號(hào)被加載到什么位置,直到main
程序準(zhǔn)備執(zhí)行的時(shí)候,才能被鏈接器加載到內(nèi)存中的某個(gè)隨機(jī)的位置。
也就是說:動(dòng)態(tài)鏈接器知道每個(gè)動(dòng)態(tài)庫中的代碼段、數(shù)據(jù)段被加載的內(nèi)存地址,因此動(dòng)態(tài)鏈接器也會(huì)維護(hù)一個(gè)全局符號(hào)表,其中存放著每一個(gè)動(dòng)態(tài)庫中導(dǎo)出的符號(hào)以及它們的內(nèi)存地址信息。
在示例代碼main.c
函數(shù)中,我們通過dlopen
返回的句柄來打印進(jìn)程中的一些全局符號(hào)的地址信息,輸出內(nèi)容如下:
上文已經(jīng)糾錯(cuò)過:本來是想打印變量的地址信息,但是 printf 語句中不小心加上了型號(hào),變成了打印變量值。
可以看到:在全局符號(hào)表中,沒有找到liba.so
中的變量a1
和函數(shù)func_a2
這兩個(gè)符號(hào),因?yàn)樗鼈z都是static
類型的,在編譯成動(dòng)態(tài)庫的時(shí)候,沒有導(dǎo)出到符號(hào)表中。
既然提到了符號(hào)表,就來看看這 3 個(gè)ELF
文件中的動(dòng)態(tài)符號(hào)表信息:
- 動(dòng)態(tài)鏈接庫中保護(hù)兩個(gè)符號(hào)表:.dynsym(動(dòng)態(tài)符號(hào)表: 表示模塊中符號(hào)的導(dǎo)出、導(dǎo)入關(guān)系) 和 .symtab(符號(hào)表: 表示模塊中的所有符號(hào));
.symtab 中包含了 .dynsym;
- 由于圖片太大,這里只貼出 .dynsym 動(dòng)態(tài)符號(hào)表。
綠色矩形框前面的Ndx
列是數(shù)字,表示該符號(hào)位于當(dāng)前文件的哪一個(gè)段中(即:段索引);
紅色矩形框前面的Ndx
列是UND
,表示這個(gè)符號(hào)沒有找到,是一個(gè)外部符號(hào)(需要重定位);
全局偏移表GOT
在我們的示例代碼中,liba.so
是比較特殊的,它既被main
可執(zhí)行程序所依賴,又依賴于libb.so
。
而且,在liba.so
中,定義了靜態(tài)、動(dòng)態(tài)的全局變量和函數(shù),可以很好的概況很多種情況,因此這部分內(nèi)容就主要來分析liba.so
這個(gè)動(dòng)態(tài)庫。
前文說過:代碼重定位需要修改代碼段中的符號(hào)引用,而代碼段被加載到內(nèi)存中又沒有可寫的權(quán)限,動(dòng)態(tài)鏈接解決這個(gè)矛盾的方案是:增加一層間接性。
例如:liba.so
的代碼中引用了libb.so
中的變量b
,在liba.so
的代碼段,并不是在引用的地方直接指向libb.so
數(shù)據(jù)段中變量b
的地址,而是指向了liba.so
自己的數(shù)據(jù)段中的某個(gè)位置,在重定位階段,鏈接器再把libb.so
中變量b
的地址填寫到這個(gè)位置。
因?yàn)?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">liba.so自己的代碼段和數(shù)據(jù)段位置是相對(duì)固定的,這樣的話,liba.so
的代碼段被加載到內(nèi)存之后,就再也不用修改了。
而數(shù)據(jù)段中這個(gè)間接跳轉(zhuǎn)的位置,就稱作:全局偏移表(GOT: Global Offset Table
)。
劃重點(diǎn):
liba.so
的代碼段中引用了libb.so
中的符號(hào)b
,既然b
的地址需要在重定位時(shí)才能確定,那么就在數(shù)據(jù)段中開辟一塊空間(稱作:GOT
表),重定位時(shí)把b
的地址填寫到GOT
表中。
而liba.so
的代碼段中,把GOT
表的地址填寫到引用b
的地方,因?yàn)?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">GOT表在編譯階段是可以確定的,使用的是相對(duì)地址。
這樣,就可以在不修改liba.so
代碼段的前提下,動(dòng)態(tài)的對(duì)符號(hào)b
進(jìn)行了重定位!
其實(shí),在一個(gè)動(dòng)態(tài)庫中存在 2 個(gè)GOT
表,分別用于重定位變量符號(hào)(section
名稱:.got
)和函數(shù)符號(hào)( section
名稱:.got.plt
)。
也就是說:所有變量類型的符號(hào)重定位信息都位于.got
中,所有函數(shù)類型的符號(hào)重定位信息都位于.got.plt
中。
并且,在一個(gè)動(dòng)態(tài)庫文件中,有兩個(gè)特殊的段(.rel.dyn
和.rel.plt
)來告訴鏈接器:.got
和.got.plt
這兩個(gè)表中,有哪些符號(hào)需要進(jìn)行重定位,這個(gè)問題下面會(huì)深入討論。
liba.so動(dòng)態(tài)庫文件的布局
為了更深刻的理解.got
和.got.plt
這兩個(gè)表,有必要來拆解一下liba.so
動(dòng)態(tài)庫文件的內(nèi)部結(jié)構(gòu)。
通過readelf -S liba.so
指令來看一下這個(gè)ELF
文件中都有哪些section
:
可以看到:一共有28
個(gè)section
,其中的21、22
就是兩個(gè)GOT
表。
另外,從裝載的角度來看,裝載器并不是把這些sections
分開來處理,而是根據(jù)不同的讀寫屬性,把多個(gè)section
看做一個(gè)segment
。
再次通過指令 readelf -l liba.so
,來查看一下segment
信息:
也就是說:
這28
個(gè)section
中(關(guān)注綠色線條):
section 0 ~ 16 都是可讀、可執(zhí)行權(quán)限,被當(dāng)做一個(gè) segment;
section 17 ~ 24 都是可讀、可寫的權(quán)限,被動(dòng)作另一個(gè) segment;
再來重點(diǎn)看一下.got
和.got.plt
這兩個(gè)section
(關(guān)注黃色矩形框):
可見:.got
和.got.plt
與數(shù)據(jù)段一樣,都是可讀、可寫的,所以被當(dāng)做同一個(gè) segment
被加載到內(nèi)存中。
通過以上這2
張圖(紅色矩形框),可以得到liba.so
動(dòng)態(tài)庫文件的內(nèi)部結(jié)構(gòu)如下:
liba.so動(dòng)態(tài)庫的虛擬地址
來繼續(xù)觀察liba.so
文件segment
信息中的AirtAddr
列,它表示的是被加載到虛擬內(nèi)存中的地址,重新貼圖如下:
因?yàn)榫幾g動(dòng)態(tài)庫時(shí),使用了代碼位置無關(guān)參數(shù)(-fPIC
),這里的虛擬地址從0x0000_0000
開始。
當(dāng)liba.so
的代碼段、數(shù)據(jù)段被加載到內(nèi)存中時(shí),動(dòng)態(tài)鏈接器找到一塊空閑空間,這個(gè)空間的開始地址,就相當(dāng)于一個(gè)基地址。
liba.so
中的代碼段和數(shù)據(jù)段中所有的虛擬地址信息,只要加上這個(gè)基地址,就得到了實(shí)際虛擬地址。
我們還是把上圖中的輸出信息,畫出詳細(xì)的內(nèi)存模型圖,如下所示:
GOT表的內(nèi)部結(jié)構(gòu)
現(xiàn)在,我們已經(jīng)知道了liba.so
庫的文件布局,也知道了它的虛擬地址,此時(shí)就可以來進(jìn)一步的看一下.got
和.got.plt
這兩個(gè)表的內(nèi)部結(jié)構(gòu)了。
從剛才的圖片中看出:
.got 表的長(zhǎng)度是 0x1c,說明有 7 個(gè)表項(xiàng)(每個(gè)表項(xiàng)占 4 個(gè)字節(jié));
.got.plt 表的長(zhǎng)度是 0x18,說明有 6 個(gè)表項(xiàng);
上文已經(jīng)說過,這兩個(gè)表是用來重定位所有的變量和函數(shù)等符號(hào)的。
那么:liba.so
通過什么方式來告訴動(dòng)態(tài)鏈接器:需要對(duì).got
和.got.plt
這兩個(gè)表中的表項(xiàng)進(jìn)行地址重定位呢?
在靜態(tài)鏈接的時(shí)候,目標(biāo)文件是通過兩個(gè)重定位表.rel.text
和.rel.data
這兩個(gè)段信息來告訴鏈接器的。
對(duì)于動(dòng)態(tài)鏈接來說,也是通過兩個(gè)重定位表來傳遞需要重定位的符號(hào)信息的,只不過名字有些不同:.rel.dyn
和.rel.plt
。
通過指令 readelf -r liba.so
來查看重定位信息:
從黃色和綠色的矩形框中可以看出:
liba.so 引用了外部符號(hào) b,類型是 R_386_GLOB_DAT,這個(gè)符號(hào)的重定位描述信息在 .rel.dyn 段中;
liba.so 引用了外部符號(hào) func_b, 類型是 R_386_JUMP_SLOT,這個(gè)符號(hào)的重定位描述信息在 .rel.plt 段中;
從左側(cè)紅色的矩形框可以看出:每一個(gè)需要重定位的表項(xiàng)所對(duì)應(yīng)的虛擬地址,畫成內(nèi)存模型圖就是下面這樣:
暫時(shí)只專注表項(xiàng)中的紅色部分:.got
表中的b
, .got.plt
表中的func_b
,這兩個(gè)符號(hào)都是libb.so
中導(dǎo)出的。
也就是說:
liba.so
的代碼中在操作變量b
的時(shí)候,就到.got
表中的0x0000_1fe8
這個(gè)地址處來獲取變量b
的真正地址;
liba.so
的代碼中在調(diào)用func_b
函數(shù)的時(shí)候,就到.got.plt
表中的0x0000_200c
這個(gè)地址處來獲取函數(shù)的真正地址;
反匯編liba.so代碼
下面就來反匯編一下liba.so
,看一下指令碼中是如何對(duì)這兩個(gè)表項(xiàng)進(jìn)行尋址的。
執(zhí)行反匯編指令:$ objdump -d liba.so
,這里只貼出func_a1
函數(shù)的反匯編代碼:
第一個(gè)綠色矩形框(call 490 <__x86.get_pc_thunk.bx>
)的功能是:把下一條指令(add
)的地址存儲(chǔ)到%ebx
中,也就是:
%ebx = 0x622
然后執(zhí)行: add $0x19de,%ebx
,讓%ebx
加上0x19de
,結(jié)果就是:%ebx = 0x2000
。
0x2000
正是.got.plt
表的開始地址!
看一下第2
個(gè)綠色矩形框:
mov -0x18(%ebx),%eax
: 先用%ebx
減去0x18
的結(jié)果,存儲(chǔ)到%eax
中,結(jié)果是:%eax = 0x1fe8
,這個(gè)地址正是變量b
在.got
表中的虛擬地址。
movl $0x1f,(%eax)
:在把0x1f
(十進(jìn)制就是31
),存儲(chǔ)到0x1fe8
表項(xiàng)中存儲(chǔ)的地址所對(duì)應(yīng)的內(nèi)存單元中(libb.so
的數(shù)據(jù)段中的某個(gè)位置)。
因此,當(dāng)鏈接器進(jìn)行重定位之后,0x1fe8
表項(xiàng)中存儲(chǔ)的就是變量b
的真正地址,而上面這兩步操作,就把數(shù)值31
賦值給變量b
了。
第3
個(gè)綠色矩形框,是調(diào)用函數(shù)func_b
,稍微復(fù)雜一些,跳轉(zhuǎn)到符號(hào) func_b@plt
的地方,看一下反匯編代碼:
jmp
指令調(diào)用了%ebx + 0xc
處的那個(gè)函數(shù)指針,從上面的.got.plt
布局圖中可以看出,重定位之后這個(gè)表項(xiàng)中存儲(chǔ)的正是func_b
函數(shù)的地址(libb.so
中代碼段的某個(gè)位置),所以就正確的跳轉(zhuǎn)到該函數(shù)中了。
審核編輯 :李倩
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
6715瀏覽量
88308 -
代碼
+關(guān)注
關(guān)注
30文章
4671瀏覽量
67765 -
動(dòng)態(tài)鏈接
+關(guān)注
關(guān)注
0文章
4瀏覽量
5739
原文標(biāo)題:Linux 動(dòng)態(tài)鏈接過程中的【重定位】底層原理
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論