久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

linux 下動態(tài)鏈接實現(xiàn)原理

 astrotycoon 2017-06-02

符號重定位

講動態(tài)鏈接之前,,得先說說符號重定位。

c/c++ 程序的編譯是以文件為單位進行的,,因此每個 c/cpp 文件也叫作一個編譯單元(translation unit), 源文件先是被編譯成一個個目標文件, 再由鏈接器把這些目標文件組合成一個可執(zhí)行文件或庫,,鏈接的過程,其核心工作是解決模塊間各種符號(變量,,函數(shù))相互引用的問題,對符號的引用本質是對其在內(nèi)存中具體地址的引用,,因此確定符號地址是編譯,,鏈接,加載過程中一項不可缺少的工作,,這就是所謂的符號重定位,。本質上來說,符號重定位要解決的是當前編譯單元如何訪問「外部」符號這個問題,。

因為編譯是以源文件為單位進行的,,編譯器此時并沒有一個全局的視野,因此對一個編譯單元內(nèi)的符號它是無力確定其最終地址的,,而對于可執(zhí)行文件來說,,在現(xiàn)代操作系統(tǒng)上,程序加載運行的地址是固定或可以預期的,,因此在鏈接時,,鏈接器可以直接計算分配該文件內(nèi)各種段的絕對或相對地址。所以對于可執(zhí)行文件來說,符號重定位是在鏈接時完成的(如果可執(zhí)行文件引用了動態(tài)庫里的函數(shù),,則情況稍有不同),。但對于動態(tài)鏈接庫來說,因為動態(tài)庫的加載是在運行時,,且加載的地址不固定,,因此沒法事先確定該模塊的起始地址,所以對動態(tài)庫的符號重定位,,只能推遲,。

符號重定位既指在當前目標文件內(nèi)進行重定位,也包括在不同目標文件,,甚至不同模塊間進行重定位,,這里面有什么不同嗎?如果是同一個目標文件內(nèi),,或者在同一個模塊內(nèi),,鏈接后,各個符號的相對地址就已經(jīng)確定了,,看起來似乎不用非得要知道最后的絕對地址才能引用這些符號,,這說起來好像也有道理,但事實不是這樣,,x86 上 mov 之類訪問程序中數(shù)據(jù)段的指令,,它要求操作數(shù)是絕對地址,而對于函數(shù)調(diào)用,,雖然是以相對地址進行調(diào)用,,但計算相對地址也只限于在當前目標文件內(nèi)進行,跨目標文件跨模塊間的調(diào)用,,編譯期也是做不到的,,只能等鏈接時或加載時才能進行相對地址的計算,因此重定位這個過程是不能缺少的,,事實上目前來說,,對于動態(tài)鏈接即使是當前目標文件內(nèi),如果是全局非靜態(tài)函數(shù),,那么它也是需要進行重定位的,,當然這里面有別的原因,比如說使得能實現(xiàn) LD_PRELOAD 的功能等,。

鏈接時符號重定位

鏈接時符號重定位指的是在鏈接階段對符號進行重定位,,一般來說,構建一個可執(zhí)行文件可以簡單分為兩個步驟:編譯及鏈接,,如下例子,,我們嘗試使用靜態(tài)鏈接的方式構建一個可執(zhí)行文件:


// file: a.c
int g_share = 1;

int g_func(int a)
{
   g_share += a;
   return a * 3;
}

// file: main.c
extern int g_share;
extern int g_func(int a);

int main()
{
  int a = 42;
  a = g_func(a);
  return 0;
}

正如前面所說,,此時符號的重定位在鏈接時進行,那么在編譯時,,編譯器是怎么生成代碼來引用那些還沒有重定位的符號呢,?讓我們先編譯一下,再來看看目標文件的內(nèi)容:

// x86_64, linux 2.6.9
-bash-3.00$ gcc -c a.c main.c -g
-bash-3.00$ objdump -S a.o

然后得到如下輸出(對于 main.o 中對 g_func 的引用,,實現(xiàn)是一樣的,,故略):

a.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <g_func>:
int g_share = 1;

int g_func(int a)
{
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
    g_share += a;
   7:   8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
   a:   01 05 00 00 00 00       add    %eax,0(%rip)        # 10 <g_func+0x10>
    return a * 2;
  10:   8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
  13:   01 c0                   add    %eax,%eax
}
  15:   c9                      leaveq
  16:   c3                      retq

從中可以看到,目標文件里的 .txt 段地址從 0 開始,,其中地址為7的指令用于把參數(shù) a 放到寄存器 %eax 中,,而地址 a 處的指令則把 %eax 中的內(nèi)容與 g_share 相加,注意這里 g_share 的地址為:0(%rip). 顯然這個地址是錯的,,編譯器當前并不知道 g_share 這個變量最后會被分配到哪個地址上,,因此在這兒只是隨便用一個假的來代替,等著到接下來鏈接時,,再把該處地址進行修正,。那么,鏈接器怎么知道目標文件中哪些地方需要修正呢,?很簡單,編譯器編譯文件時時,,會建立一系列表項,,用來記錄哪些地方需要在重定位時進行修正,這些表項叫作“重定位表”(relocatioin table):

-bash-3.00$ objdump -r a.o
a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
000000000000000c R_X86_64_PC32     g_share+0xfffffffffffffffc

如上最后一行,,這條記錄記錄了在當前編譯單元中,,哪兒對 g_share 進行了引用,其中 offset 用于指明需要修改的位置在該段中的偏移,,TYPE 則指明要怎樣去修改,,因為 cpu 的尋址方式不是唯一的,尋址方式不同,,地址的形式也有所不同,,這個 type 用于指明怎么去修改, value 則是配合 type 來最后計算該符號地址的。

有了如上信息,,鏈接器在把目標文件合并成一個可執(zhí)行文件并分配好各段的加載地址后,,就可以重新計算那些需要重定位的符號的具體地址了, 如下我們可以看到在可執(zhí)行文件中,對 g_share(0x40496處), g_func(0x4047a處)的訪問已經(jīng)被修改成了具體的地址:

-bash-3.00$ gcc -o am a.o main.o
-bash-3.00$ objdump -S am
// skip some of the ouput
 
extern int g_func(int a);

int main()
{
  400468:       55                      push   %rbp
  400469:       48 89 e5                mov    %rsp,%rbp
  40046c:       48 83 ec 10             sub    $0x10,%rsp
    int a = 42;
  400470:       c7 45 fc 2a 00 00 00    movl   $0x2a,0xfffffffffffffffc(%rbp)
    a = g_func(a);
  400477:       8b 7d fc                mov    0xfffffffffffffffc(%rbp),%edi
  40047a:       e8 0d 00 00 00          callq  40048c <g_func>
  40047f:       89 45 fc                mov    %eax,0xfffffffffffffffc(%rbp)
    return 0;
  400482:       b8 00 00 00 00          mov    $0x0,%eax
}
  400487:       c9                      leaveq
  400488:       c3                      retq
  400489:       90                      nop
  40048a:       90                      nop
  40048b:       90                      nop

000000000040048c <g_func>:
int g_share = 1;

int g_func(int a)
{
  40048c:       55                      push   %rbp
  40048d:       48 89 e5                mov    %rsp,%rbp
  400490:       89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
    g_share += a;
  400493:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
  400496:       01 05 dc 03 10 00       add    %eax,1049564(%rip)        # 500878 <g_share>
    return a * 2;
  40049c:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
  40049f:       01 c0                   add    %eax,%eax
}
  4004a1:       c9                      leaveq
  4004a2:       c3                      retq

// skip some of the ouput

當然,,重定位時修改指令的具體方式還牽涉到比較多的細節(jié)很啰嗦,,這里就不細說了。

加載時符號重定位

前面描述了靜態(tài)鏈接時,,怎么解決符號重定位的問題,,那么當我們使用動態(tài)鏈接來構建程序時,,這些符號重定位問題是怎么解決的呢?目前來說,,Linux 下 ELF 主要支持兩種方式:加載時符號重定位及地址無關代碼,。地址無關代碼接下來會講,對于加載時重定位,,其原理很簡單,,它與鏈接時重定位是一致的,只是把重定位的時機放到了動態(tài)庫被加載到內(nèi)存之后,,由動態(tài)鏈接器來進行,。

int g_share = 1;

int g_func(int a)
{
    g_share += a;
    return a * 2;
}

int g_func2()
{
    int a = 2;
    int b = g_func(3);

    return a + b;
}
// compile on 32bit linux OS
-bash-3.00$ gcc -c a.c main.c
-bash-3.00$ gcc -shared -o liba.so a.o
-bash-3.00$ gcc -o am main.o -L. -la
-bash-3.00$ objdump -S liba.so
// skip some of the output
000004f4 <g_func>:
int g_share = 1;

int g_func(int a)
{
 4f4:   55                      push   %ebp
 4f5:   89 e5                   mov    %esp,%ebp
    g_share += a;
 4f7:   8b 45 08                mov    0x8(%ebp),%eax
 4fa:   01 05 00 00 00 00       add    %eax,0x0
    return a * 2;
 500:   8b 45 08                mov    0x8(%ebp),%eax
 503:   d1 e0                   shl    %eax
}
 505:   c9                      leave  
 506:   c3                      ret    

00000507 <g_func2>:

int g_func2()
{
 507:   55                      push   %ebp
 508:   89 e5                   mov    %esp,%ebp
 50a:   83 ec 08                sub    $0x8,%esp
    int a = 2;
 50d:   c7 45 fc 02 00 00 00    movl   $0x2,0xfffffffc(%ebp)
    int b = g_func(3);
 514:   6a 03                   push   $0x3
 516:   e8 fc ff ff ff          call   517 <g_func2+0x10>
 51b:   83 c4 04                add    $0x4,%esp
 51e:   89 45 f8                mov    %eax,0xfffffff8(%ebp)

    return a + b;
 521:   8b 45 f8                mov    0xfffffff8(%ebp),%eax
 524:   03 45 fc                add    0xfffffffc(%ebp),%eax
}
 527:   c9                      leave  

// skip some of the output

注意其中地址 4fa 及 516 處的指令:此兩處分別對 g_share 及 g_func 進行了訪問,顯然此時它們的地址仍然是假地址,,這些地址在動態(tài)庫加載完成后會被動態(tài)鏈接器進行重定位,,最終修改為正確的地址,這看起來與靜態(tài)鏈接時進行重定位是一樣的過程,,但實現(xiàn)上有幾個關鍵的不同之處:

  1. 因為不允許對可執(zhí)行文件的代碼段進行加載時符號重定位,,因此如果可執(zhí)行文件引用了動態(tài)庫中的數(shù)據(jù)符號,則在該可執(zhí)行文件內(nèi)對符號的重定位必須在鏈接階段完成,,為做到這一點,,鏈接器在構建可執(zhí)行文件的時候,會在當前可執(zhí)行文件的數(shù)據(jù)段里分配出相應的空間來作為該符號真正的內(nèi)存地址,,等到運行時加載動態(tài)庫后,,再在動態(tài)庫中對該符號的引用進行重定位:把對該符號的引用指向可執(zhí)行文件數(shù)據(jù)段里相應的區(qū)域。

  2. ELF 文件對調(diào)用動態(tài)庫中的函數(shù)采用了所謂的"延遲綁定"(lazy binding)策略, 只有當該函數(shù)在其第一次被調(diào)用發(fā)生時才最終被確認其真正的地址,,因此我們不需要在調(diào)用動態(tài)庫函數(shù)的地方直接填上假的地址,,而是使用了一些跳轉地址作為替換,這樣一來連修改動態(tài)庫和可執(zhí)行程序中的相應代碼都不需要進行了,,當然延遲綁定的目的不是為了這個,,具體先不細說。

至此,,我們可以發(fā)現(xiàn)加載時重定位實際上是一個重新修改動態(tài)庫中數(shù)據(jù)符號地址的過程(函數(shù)符號的地址因為延遲綁定的存在不需要在代碼段中重定位),,但我們知道,不同的進程即使是對同一個動態(tài)庫也很可能是加載到不同地址上,,因此當以加載時重定位的方式來使用動態(tài)庫時,,該動態(tài)庫就沒法做到被各個進程所共享,而只能在每個進程中 copy 一份:因為符號重定位后,,該動態(tài)庫與在別的進程中就不同了,,可見此時動態(tài)庫節(jié)省內(nèi)存的優(yōu)勢就不復存在了。

地址無關代碼(PIC, position independent code)

從前面的介紹我們知道裝載時重定位有重大的缺點:

  1. 它不能使動態(tài)庫的指令代碼被共享,。
  2. 程序啟動加載動態(tài)庫后,,對動態(tài)庫中的符號引用進行重定位會比較花時間,,特別是動態(tài)庫多且復雜的情況下。

為了克服這些缺陷,,ELF 引用了一種叫作地址無關代碼的實現(xiàn)方案,,該解決方案通過對變量及函數(shù)的訪問加一層跳轉來實現(xiàn),非常的靈活,。

1.模塊內(nèi)部符號的訪問

模塊內(nèi)部符號在這里指的是:static 類型的變量與函數(shù),,這種類型的符號比較簡單,對于 static 函數(shù)來說,,因為在動態(tài)庫編譯完后,,它在模塊內(nèi)的相對地址就已經(jīng)確定了,而 x86 上函數(shù)調(diào)用只用到相對地址,,因此此時根本連重定位都不需要進行,,編譯時就能確定地址,稍微麻煩一點的是訪問數(shù)據(jù),,因為訪問數(shù)據(jù)需要絕對地址,,但動態(tài)庫未被加載時,絕對地址是沒法得知的,,怎么辦呢,?

ELF 在這里使用了一個小技巧,根據(jù)當前 IP 值來動態(tài)計算數(shù)據(jù)的絕對地址,,它的原理很簡單,,當動態(tài)庫編譯好之后,庫中的數(shù)據(jù)段,,代碼段的相對位置就已經(jīng)固定了,此時對任意一條指令來說,,該指令的地址與數(shù)據(jù)段的距離都是固定的,,那么,只要程序在運行時獲取到當前指令的地址,,就可以直接加上該固定的位移,,從而得到所想要訪問的數(shù)據(jù)的絕對地址了,下面我們用實例驗證一下:

int g_share = 1;
static int g_share2 = 2;

int g_func(int a)
{
    g_share += a;
    return a * 2;
}

int g_func2()
{
    int a = 2;
    int b = g_func(3);

    return a + b;
}

static int g_fun3()
{
    g_share2 += 3;
    return g_share2 - 1;
}

static int g_func4()
{
    int a = g_fun3();

    a + 2;
    return a;
}

以上代碼在x86 linux 下編譯,,再反匯編看看得到如下結果:

-bash-3.00$ gcc -o liba.so -fPIC -shared a.c
-bash-3.00$ objdump -S liba.so 
// skip some of the output
00000564 <g_fun3>:
 564:   55                      push   %ebp
 565:   89 e5                   mov    %esp,%ebp
 567:   e8 00 00 00 00          call   56c <g_fun3+0x8>
 56c:   59                      pop    %ecx
 56d:   81 c1 60 11 00 00       add    $0x1160,%ecx
 573:   83 81 20 00 00 00 03    addl   $0x3,0x20(%ecx)
 57a:   8b 81 20 00 00 00       mov    0x20(%ecx),%eax
 580:   48                      dec    %eax
 581:   c9                      leave  
 582:   c3                      ret    
// skip some of the output

現(xiàn)在我們來分析驗證一下:首先是地址 567 的指令有些怪,,這兒不深究,簡單來說,,x86 下沒有指令可以取當前 ip 的值,,因此這兒使了個技巧通過函數(shù)調(diào)用來獲取 ip 值(x86_64 下就不用這么麻煩),這個技巧的原理在于進行函數(shù)調(diào)用時要將返回地址壓到棧上,,此時通過讀這個棧上的值就可以獲得下一條指令的地址了,,在這兒我們只要知道指令 56c 執(zhí)行后,,%ecx 中包含了當前指令的地址,也就是 0x56c,,再看 56d 及 573 兩條指令,,得知 %ecx + 0x1160 + 0x20 = 0x16ec 就是 573 指令所需要訪問的地址,這個地址指向哪里了呢,?

-bash-3.00$ objdump -s liba.so
Contents of section .data:
 16e0 e0160000 f4150000 01000000 02000000  ................

結果是數(shù)據(jù)段里的第二個 int,,也就是 g_share2!

2.模塊間符號的訪問

模塊間的符號訪問比模塊內(nèi)的符號訪問要麻煩很多,因為動態(tài)庫運行時被加載到哪里是未知的,,為了能使得代碼段里對數(shù)據(jù)及函數(shù)的引用與具體地址無關,,只能再作一層跳轉,ELF 的做法是在動態(tài)庫的數(shù)據(jù)段中加一個表項,,叫作 GOT(global offset table), GOT 表格中放的是數(shù)據(jù)全局符號的地址,,該表項在動態(tài)庫被加載后由動態(tài)加載器進行初始化,動態(tài)庫內(nèi)所有對數(shù)據(jù)全局符號的訪問都到該表中來取出相應的地址,,即可做到與具體地址了,,而該表作為動態(tài)庫的一部分,訪問起來與訪問模塊內(nèi)的數(shù)據(jù)是一樣的,。

仍然使用前面的例子,,我們來看看 g_func 是怎么訪問 g_share 變量的。

00000504 <g_func>:
 504:   55                      push   %ebp
 505:   89 e5                   mov    %esp,%ebp
 507:   53                      push   %ebx
 508:   e8 00 00 00 00          call   50d <g_func+0x9>
 50d:   5b                      pop    %ebx
 50e:   81 c3 bf 11 00 00       add    $0x11bf,%ebx
 514:   8b 8b f0 ff ff ff       mov    0xfffffff0(%ebx),%ecx
 51a:   8b 93 f0 ff ff ff       mov    0xfffffff0(%ebx),%edx
 520:   8b 45 08                mov    0x8(%ebp),%eax
 523:   03 02                   add    (%edx),%eax
 525:   89 01                   mov    %eax,(%ecx)
 527:   8b 45 08                mov    0x8(%ebp),%eax
 52a:   d1 e0                   shl    %eax
 52c:   5b                      pop    %ebx
 52d:   c9                      leave  
 52e:   c3                      ret    

上面的輸出中,,508 與 50d 處的指令用于獲取 ip 值,, 執(zhí)行完 50d 后, %ebx 中放的是 0x50d,, 地址 50e 用于計算 g_share 在 GOT 中的地址 0x50d + 0x11bf + 0xfffffff0 = 0x16bc, 我們檢查一下該地址是不是 GOT:

-bash-3.00$ objdump -h liba.so
liba.so:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
//skip some of the output

 16 .got          00000010  000016bc  000016bc  000006bc  2**2
                  CONTENTS, ALLOC, LOAD, DATA

顯然,,0x16bc 就是 GOT 表的第一項。

事實上,,ELF 文件中還包含了一個重定位段,,里面記錄了哪些符號需要進行重定位,我們可以通過它驗證一下上面的計算是否與之匹配:

-bash-3.00$ objdump -R liba.so
liba.so:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
000016e0 R_386_RELATIVE    *ABS*
000016e4 R_386_RELATIVE    *ABS*
000016bc R_386_GLOB_DAT    g_share
000016c0 R_386_GLOB_DAT    __cxa_finalize
000016c4 R_386_GLOB_DAT    _Jv_RegisterClasses
000016c8 R_386_GLOB_DAT    __gmon_start__
000016d8 R_386_JUMP_SLOT   g_func
000016dc R_386_JUMP_SLOT   __cxa_finalize

如上輸出,, g_share 的地址在 0x16bc,,與前面的計算完全吻合!
致此,,模塊間的數(shù)據(jù)訪問就介紹完了,,模塊間的函數(shù)調(diào)用在實現(xiàn)原理上是一樣的,也需要經(jīng)過一個類似 GOT 的表格進行跳轉,,但在具體實現(xiàn)上,,ELF 為了實現(xiàn)所謂延遲綁定而作了更精細的處理,接下來會介紹,。值得一提的是,,PIC 也可在編譯可執(zhí)行文件時指定,,此時可執(zhí)行文件中的代碼對外部符號的引用方式會改變,不再是直接(絕對地址或相對地址)引用該符號,,而是也通過 GOT 來間接的引用,。

延遲加載

我們知道,動態(tài)庫是在進程啟動的時候加載進來的,,加載后,,動態(tài)鏈接器需要對其作一系列的初始化,如符號重定位(動態(tài)庫內(nèi)以及可執(zhí)行文件內(nèi)),,這些工作是比較費時的,,特別是對函數(shù)的重定位,那么我們能不能把對函數(shù)的重定位延遲進行呢,?這個改進是很有意義的,,畢竟很多時候,一個動態(tài)庫里可能包含很多的全局函數(shù),,但是我們往往可能只用到了其中一小部分而已,,而且在這用到的一小部分里,很可能其中有些還壓根不會執(zhí)行到,,因此完全沒必要把那些沒用到的函數(shù)也過早進行重定位,,具體來說,就是應該等到第一次發(fā)生對該函數(shù)的調(diào)用時才進行符號綁定 -- 此謂之延遲綁定,。

延遲綁定的實現(xiàn)步驟如下:

  1. 建立一個 GOT.PLT 表,,該表用來放全局函數(shù)的實際地址,,但最開始時,該里面放的不是真實的地址而是一個跳轉,,接下來會講,。
  2. 對每一個全局函數(shù),鏈接器生成一個與之相對應的影子函數(shù),,如 fun@plt
  3. 所有對 fun 的調(diào)用,,都換成對 fun@plt 的調(diào)用,,每個fun@plt 長成如下樣子:

    fun@plt:
    jmp *([email protected])
    push index
    jmp _init

    其中第一條指令直接從 got.plt 中去拿真實的函數(shù)地址,如果已經(jīng)之前已經(jīng)發(fā)生過調(diào)用,,got.plt 就已經(jīng)保存了真實的地址,,如果是第一次調(diào)用,則 got.plt 中放的是 fun@plt 中的第二條指令,,這就使得當執(zhí)行第一次調(diào)用時,,fun@plt中的第一條指令其實什么事也沒做,,直接繼續(xù)往下執(zhí)行,,第二條指令的作用是把當前要調(diào)用的函數(shù)在 got.plt 中的編號作為參數(shù)傳給 _init(),而 _init() 這個函數(shù)則用于把 fun 進行重定位,,然后把結果寫入到 got.plt 相應的地方,,最后直接跳過去該函數(shù)。

仍然是使用前面的例子,,我們看看 g_func2 是怎樣調(diào)用 g_func 的:

0000052f <g_func2>:
 52f:   55                      push   %ebp
 530:   89 e5                   mov    %esp,%ebp
 532:   53                      push   %ebx
 533:   83 ec 14                sub    $0x14,%esp
 536:   e8 00 00 00 00          call   53b <g_func2+0xc>
 53b:   5b                      pop    %ebx
 53c:   81 c3 91 11 00 00       add    $0x1191,%ebx
 542:   c7 45 f8 02 00 00 00    movl   $0x2,0xfffffff8(%ebp) // a = 2
 549:   83 ec 0c                sub    $0xc,%esp
 54c:   6a 03                   push   $0x3 // push argument 3 for g_func.
 54e:   e8 d5 fe ff ff          call   428 <g_func@plt>
 553:   83 c4 10                add    $0x10,%esp
 556:   89 45 f4                mov    %eax,0xfffffff4(%ebp)
 559:   8b 45 f4                mov    0xfffffff4(%ebp),%eax
 55c:   03 45 f8                add    0xfffffff8(%ebp),%eax
 55f:   8b 5d fc                mov    0xfffffffc(%ebp),%ebx
 562:   c9                      leave  
 563:   c3                      ret 

如上匯編,指令 536, 53b, 53c, 用于計算 got.plt 的具體位置,,計算方式與前面對數(shù)據(jù)的訪問原理是一樣的,經(jīng)計算此時, %ebx = 0x53b + 0x1191 = 0x16cc, 注意指令 54e,, 該指令調(diào)用了函數(shù) g_func@plt:

00000428 <g_func@plt>:
 428:   ff a3 0c 00 00 00       jmp    *0xc(%ebx)
 42e:   68 00 00 00 00          push   $0x0
 433:   e9 e0 ff ff ff          jmp    418 <_init+0x18>

注意到此時,, %ebx 中放的是 got.plt 的地址,g_func@plt 的第一條指令用于獲取 got.plt 中 func 的具體地址,, func 放在 0xc + %ebx = 0xc + 0x16cc = 0x16d8, 這個地址里放的是什么呢,?我們查一下重定位表:

-bash-3.00$ objdump -R liba.so

liba.so:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
000016e0 R_386_RELATIVE    *ABS*
000016e4 R_386_RELATIVE    *ABS*
000016bc R_386_GLOB_DAT    g_share
000016c0 R_386_GLOB_DAT    __cxa_finalize
000016c4 R_386_GLOB_DAT    _Jv_RegisterClasses
000016c8 R_386_GLOB_DAT    __gmon_start__
000016d8 R_386_JUMP_SLOT   g_func
000016dc R_386_JUMP_SLOT   __cxa_finalize

可見,,該地址里放的就是 g_func 的具體地址,,那此時 0x16d8 放的是真正的地址了嗎,?我們再看看 got.plt:

Contents of section .got.plt:
 16cc fc150000 00000000 00000000 2e040000  ................
 16dc 3e040000 

16d8 處的內(nèi)容是: 2e040000, 小端序,,換回整形就是 0x000042e, 該地址就是 fun@plt 的第二條指令,!是不是覺得有點兒繞?你可以定下心來再看一遍,,其實不繞,而是很巧妙,。

后話

對動態(tài)鏈接庫來說,加載時重定位與鏈接時重定位各有優(yōu)缺點,,前者使得動態(tài)庫的代碼段不能被多個進程間所共享,加載動態(tài)庫時也比較費時,,但是加載完成后,,因為對符號的引用不需要進行跳轉,,程序運行的效率相對是較高的。而對地址無關的代碼,,它的缺點是動態(tài)庫的體積相對較大,畢竟增加了很多表項及相關的函數(shù),,另外就運行時對全局符號的引用需要通過表格進行跳轉,程序執(zhí)行的效率不可避免有所損失,,優(yōu)點嘛,,就是動態(tài)庫加載比較快,,而且代碼可以在多個進程間共享,,對整個系統(tǒng)而言,,可以大大節(jié)約對內(nèi)存的使用,這個好處的吸引力是非常大的,,所以你可以看到,目前來說在常用的動態(tài)庫使用上,,PIC 相較而言是更加被推崇的,,道理在此,。

【引用】

  1. 《程序員的自我修養(yǎng)》

  2. http://eli./2011/08/25/load-time-relocation-of-shared-libraries/

  3. http://www./linker/linker10.html

  4. https://www./c/position-independent-code-and-x86-64-libraries.html

    本站是提供個人知識管理的網(wǎng)絡存儲空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導購買等信息,謹防詐騙,。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報,。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多