《電子技術(shù)應(yīng)用》
您所在的位置:首頁(yè) > 可編程邏輯 > 其他 > Linux教學(xué)——Linux 動(dòng)態(tài)鏈接過程中的【重定位】底層原理

Linux教學(xué)——Linux 動(dòng)態(tài)鏈接過程中的【重定位】底層原理

2022-11-02
作者:道哥
來源:電子技術(shù)應(yīng)用專欄作家 一口Linux

  動(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)庫(kù)中記錄了一些必要的信息。

  真正的重定位過程,,是在這個(gè)時(shí)間點(diǎn)來完成的:可執(zhí)行程序,、動(dòng)態(tài)庫(kù)被加載之后,調(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è)矛盾,!

  微信截圖_20221102164425.png

  解決這個(gè)矛盾的方案,,就是Linux系統(tǒng)中動(dòng)態(tài)鏈接器的核心工作!

  解決矛盾:增加一層間接性

  David Wheeler有一句名言:“計(jì)算機(jī)科學(xué)中的大多數(shù)問題,,都可以通過增加一層間接性來解決,。”

  解決動(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è)矛盾了嗎?!

  如下圖所示:

  微信截圖_20221102164500.png

  理解了上圖的解決思路,,基本上就理解了動(dòng)態(tài)鏈接過程中重定位的核心思想,。

  示例代碼

  我們需要3個(gè)源文件來討論動(dòng)態(tài)鏈接中重定位的過程:main.c、a.c,、b.c,,其中的a.c和b.c被編譯成動(dòng)態(tài)庫(kù),然后main.c與這兩個(gè)動(dòng)態(tài)庫(kù)一起動(dòng)態(tài)鏈接成可執(zhí)行程序,。

  它們之間的依賴關(guān)系是:

  微信截圖_20221102164521.png

  b.c

  代碼如下:

  #include <stdio.h>

  int b = 30;

  void func_b(void)

  {

  printf("in func_b. b = %d \n", b);

  }

  代碼說明:

  定義一個(gè)全局變量和一個(gè)全局函數(shù),,被 a.c 調(diào)用。

  a.c

  代碼如下(稍微復(fù)雜一些,,主要是為了探索:不同類型的符號(hào)如何處理重定位):

  #include <stdio.h>

  // 內(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");

  }

  // 內(nèi)部定義的【非靜態(tài)】函數(shù)

  void func_a3(void)

  {

  printf("in func_a3 \n");

  }

  // 被 main 調(diào)用

  void func_a1(void)

  {

  printf("in func_a1 \n");

  // 操作內(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 <stdio.h>

  #include <unistd.h>

  #include <dlfcn.h>

  // 聲明外部變量

  extern int a2;

  extern void func_a1();

  typedef void (*pfunc)(void);

  int main(void)

  {

  printf("in main \n");

  // 打印此進(jìn)程的全局符號(hào)表

  void *handle = dlopen(0, RTLD_NOW);

  if (NULL == handle)

  {

  printf("dlopen failed! \n");

  return -1;

  }

  printf("\n------------ main ---------------\n");

  // 打印 main 中變量符號(hào)的地址

  pfunc addr_main = dlsym(handle, "main");

  if (NULL != addr_main)

  printf("addr_main = 0x%x \n", (unsigned int)addr_main);

  else

  printf("get address of main failed! \n");

  printf("\n------------ liba.so ---------------\n");

  // 打印 liba.so 中變量符號(hào)的地址

  unsigned int *addr_a1 = dlsym(handle, "a1");

  if (NULL != addr_a1)

  printf("addr_a1 = 0x%x \n", *addr_a1);

  else

  printf("get address of a1 failed! \n");

  unsigned int *addr_a2 = dlsym(handle, "a2");

  if (NULL != addr_a2)

  printf("addr_a2 = 0x%x \n", *addr_a2);

  else

  printf("get address of a2 failed! \n");

  // 打印 liba.so 中函數(shù)符號(hào)的地址

  pfunc addr_func_a1 = dlsym(handle, "func_a1");

  if (NULL != addr_func_a1)

  printf("addr_func_a1 = 0x%x \n", (unsigned int)addr_func_a1);

  else

  printf("get address of func_a1 failed! \n");

  pfunc addr_func_a2 = dlsym(handle, "func_a2");

  if (NULL != addr_func_a2)

  printf("addr_func_a2 = 0x%x \n", (unsigned int)addr_func_a2);

  else

  printf("get address of func_a2 failed! \n");

  pfunc addr_func_a3 = dlsym(handle, "func_a3");

  if (NULL != addr_func_a3)

  printf("addr_func_a3 = 0x%x \n", (unsigned int)addr_func_a3);

  else

  printf("get address of func_a3 failed! \n");

  printf("\n------------ libb.so ---------------\n");

  // 打印 libb.so 中變量符號(hào)的地址

  unsigned int *addr_b = dlsym(handle, "b");

  if (NULL != addr_b)

  printf("addr_b = 0x%x \n", *addr_b);

  else

  printf("get address of b failed! \n");

  // 打印 libb.so 中函數(shù)符號(hào)的地址

  pfunc addr_func_b = dlsym(handle, "func_b");

  if (NULL != addr_func_b)

  printf("addr_func_b = 0x%x \n", (unsigned int)addr_func_b);

  else

  printf("get address of func_b failed! \n");

  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)鏈接庫(kù)

  把以上幾個(gè)源文件編譯成動(dòng)態(tài)庫(kù)以及可執(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)庫(kù)是在運(yùn)行時(shí)加載,,那為什么在編譯的時(shí)候還需要指明?

  因?yàn)樵诰幾g的時(shí)候,,需要知道每一個(gè)動(dòng)態(tài)庫(kù)中提供了哪些符號(hào)。Windows 中的動(dòng)態(tài)庫(kù)的顯性的導(dǎo)出和導(dǎo)入標(biāo)識(shí),,更能體現(xiàn)這個(gè)概念(__declspec(dllexport), __declspec(dllimport)),。

  此時(shí),就得到了如下幾個(gè)文件:

  微信截圖_20221102164840.png

  動(dòng)態(tài)庫(kù)的依賴關(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)庫(kù)加載到內(nèi)存中,,然后才能開始執(zhí)行,。

  對(duì)于我們的實(shí)例代碼來說:main程序依賴于liba.so庫(kù),而liba.so庫(kù)又依賴于libb.so庫(kù),。

  可以用ldd工具來分別看一下動(dòng)態(tài)庫(kù)之間的依賴關(guān)系:

 微信截圖_20221102165434.png

  可以看出:

  在 liba.so 動(dòng)態(tài)庫(kù)中,,記錄了信息:依賴于 libb.so;

  在 main 可執(zhí)行文件中,記錄了信息:依賴于 liba.so, libb.so;

  也可以使用另一個(gè)工具patchelf來查看一個(gè)可執(zhí)行程序或者動(dòng)態(tài)庫(kù),,依賴于其他哪些模塊,。例如:

  微信截圖_20221102165456.png

  那么,動(dòng)態(tài)庫(kù)的加載是由誰來完成的呢,?動(dòng)態(tài)鏈接器,!

  動(dòng)態(tài)庫(kù)的加載過程

  動(dòng)態(tài)鏈接器加載動(dòng)態(tài)庫(kù)

  當(dāng)執(zhí)行main程序的時(shí)候,操作系統(tǒng)首先把main加載到內(nèi)存,,然后通過.interp段信息來查看該文件依賴哪些動(dòng)態(tài)庫(kù):

  微信截圖_20221102165702.png

  上圖中的字符串/lib/ld-linux.so.2,,就表示main依賴動(dòng)態(tài)鏈接庫(kù)。

  ld-linux.so.2也是一個(gè)動(dòng)態(tài)鏈接庫(kù),,在大部分情況下動(dòng)態(tài)鏈接庫(kù)已經(jīng)被加載到內(nèi)存中了(動(dòng)態(tài)鏈接庫(kù)就是為了共享),,操作系統(tǒng)此時(shí)只需要把動(dòng)態(tài)鏈接庫(kù)所在的物理內(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)存中,,示意圖如下所示:

  微信截圖_20221102165727.png

  動(dòng)態(tài)鏈接器自身也是一個(gè)動(dòng)態(tài)庫(kù),而且是一個(gè)特殊的動(dòng)態(tài)庫(kù):它不依賴于其他的任何動(dòng)態(tài)庫(kù),,因?yàn)楫?dāng)它被加載的時(shí)候,,沒有人幫它去加載依賴的動(dòng)態(tài)庫(kù),否則就形成雞生蛋,、蛋生雞的問題了,。

  動(dòng)態(tài)庫(kù)的加載地址

  一個(gè)進(jìn)程在運(yùn)行時(shí)的實(shí)際加載地址(或者說虛擬內(nèi)存區(qū)域),可以通過指令:$ cat /proc/[進(jìn)程的 pid]/maps 讀取出來,。

  例如:我的虛擬機(jī)中執(zhí)行main程序時(shí),,看到的地址信息是:

  微信截圖_20221102165755.png

  黃色部分分別是:main, liba.so, libb.so 這3個(gè)模塊的加載信息。

  另外,,還可以看到c庫(kù)(libc-2.23.so),、動(dòng)態(tài)鏈接器(ld-2.23.so)以及動(dòng)態(tài)加載庫(kù)libdl-2.23.so的虛擬地址區(qū)域,布局如下:

  微信圖片_20221102165852.png

  可以看出出來:main可執(zhí)行程序是位于低地址,,所有的動(dòng)態(tài)庫(kù)都位于4G內(nèi)存空間的最后1G空間中,。

  還有另外一個(gè)指令也很好用 $ pmap [進(jìn)程的 pid],也可以打印出每個(gè)模塊的內(nèi)存地址:

  微信截圖_20221102170205.png

  符號(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)槊總€(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)庫(kù)中的代碼段,、數(shù)據(jù)段被加載的內(nèi)存地址,,因此動(dòng)態(tài)鏈接器也會(huì)維護(hù)一個(gè)全局符號(hào)表,其中存放著每一個(gè)動(dòng)態(tài)庫(kù)中導(dǎo)出的符號(hào)以及它們的內(nèi)存地址信息,。

  在示例代碼main.c函數(shù)中,我們通過dlopen返回的句柄來打印進(jìn)程中的一些全局符號(hào)的地址信息,,輸出內(nèi)容如下:

  微信截圖_20221102170236.png

  上文已經(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)庫(kù)的時(shí)候,,沒有導(dǎo)出到符號(hào)表中。

  既然提到了符號(hào)表,,就來看看這 3 個(gè)ELF文件中的動(dòng)態(tài)符號(hào)表信息:

  動(dòng)態(tài)鏈接庫(kù)中保護(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)(需要重定位);

  微信截圖_20221102170520.png      微信截圖_20221102170533.png

  全局偏移表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)庫(kù),。

  前文說過:代碼重定位需要修改代碼段中的符號(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)閘iba.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)镚OT表在編譯階段是可以確定的,,使用的是相對(duì)地址。

  這樣,,就可以在不修改liba.so代碼段的前提下,,動(dòng)態(tài)的對(duì)符號(hào)b進(jìn)行了重定位!

  其實(shí),,在一個(gè)動(dòng)態(tài)庫(kù)中存在 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)庫(kù)文件中,,有兩個(gè)特殊的段(.rel.dyn和.rel.plt)來告訴鏈接器:.got和.got.plt這兩個(gè)表中,,有哪些符號(hào)需要進(jìn)行重定位,這個(gè)問題下面會(huì)深入討論,。

  liba.so動(dòng)態(tài)庫(kù)文件的布局

  為了更深刻的理解.got和.got.plt這兩個(gè)表,,有必要來拆解一下liba.so動(dòng)態(tài)庫(kù)文件的內(nèi)部結(jié)構(gòu)。

  通過readelf -S liba.so指令來看一下這個(gè)ELF文件中都有哪些section:

  微信截圖_20221102170606.png

  可以看到:一共有28個(gè)section,,其中的21,、22就是兩個(gè)GOT表。

  另外,,從裝載的角度來看,,裝載器并不是把這些sections分開來處理,而是根據(jù)不同的讀寫屬性,,把多個(gè)section看做一個(gè)segment,。

  再次通過指令 readelf -l liba.so ,來查看一下segment信息:

  微信截圖_20221102170627.png

  也就是說:

  這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)庫(kù)文件的內(nèi)部結(jié)構(gòu)如下:

  微信截圖_20221102170644.png

  liba.so動(dòng)態(tài)庫(kù)的虛擬地址

  來繼續(xù)觀察liba.so文件segment信息中的AirtAddr列,它表示的是被加載到虛擬內(nèi)存中的地址,,重新貼圖如下:

  微信截圖_20221102170710.png

  因?yàn)榫幾g動(dòng)態(tài)庫(kù)時(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)存模型圖,如下所示:

  微信截圖_20221102170726.png

  GOT表的內(nèi)部結(jié)構(gòu)

  現(xiàn)在,,我們已經(jīng)知道了liba.so庫(kù)的文件布局,,也知道了它的虛擬地址,此時(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來查看重定位信息:

  微信截圖_20221102170746.png

  從黃色和綠色的矩形框中可以看出:

  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)存模型圖就是下面這樣:

  微信截圖_20221102170844.png

  暫時(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ù)的反匯編代碼:

  微信截圖_20221102170900.png

  第一個(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的地方,,看一下反匯編代碼:

  微信截圖_20221102170917.png

  jmp指令調(diào)用了%ebx + 0xc處的那個(gè)函數(shù)指針,,從上面的.got.plt布局圖中可以看出,重定位之后這個(gè)表項(xiàng)中存儲(chǔ)的正是func_b函數(shù)的地址(libb.so中代碼段的某個(gè)位置),,所以就正確的跳轉(zhuǎn)到該函數(shù)中了,。

更多信息可以來這里獲取==>>電子技術(shù)應(yīng)用-AET<<


微信圖片_20210517164139.jpg

本站內(nèi)容除特別聲明的原創(chuàng)文章之外,轉(zhuǎn)載內(nèi)容只為傳遞更多信息,,并不代表本網(wǎng)站贊同其觀點(diǎn),。轉(zhuǎn)載的所有的文章,、圖片、音/視頻文件等資料的版權(quán)歸版權(quán)所有權(quán)人所有,。本站采用的非本站原創(chuàng)文章及圖片等內(nèi)容無法一一聯(lián)系確認(rèn)版權(quán)者,。如涉及作品內(nèi)容、版權(quán)和其它問題,,請(qǐng)及時(shí)通過電子郵件或電話通知我們,,以便迅速采取適當(dāng)措施,避免給雙方造成不必要的經(jīng)濟(jì)損失,。聯(lián)系電話:010-82306118,;郵箱:[email protected]