理解got和plt

共享庫是現代操作系統的一個重要組成部分,但是我們對它背後的實現知之甚少。當然,很多文檔從各個角度對動態庫進行過介紹。希望我的這邊文章能給對動態庫的理解帶來一種新的理解。
讓我們以此開始——在elf格式中,重定位記錄是一些允許我們稍後填寫的二進制信息——鏈接階段由編譯工具填充或者在運行時刻由動態連接器填寫。一個二進制的重定位記錄從本質上說就是“確定符號X的值,然後把這個值放入二進制文件中的偏移量爲Y的地方”——每一個重定向記錄都有個特定的類型,這個類型在ABI文檔中定義,用來準確的描述在實際中是如何確定X的值。

下面是一個簡單的例子:

$ cat a.c
extern int foo;
 
int function(void) {
    return foo;
}
$ gcc -c a.c
$ readelf --relocs ./a.o
 
Relocation section '.rel.text' at offset 0x2dc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000004  00000801 R_386_32          00000000   foo

在編譯生成a.o文件的時候,編譯器並不知道符號foo的值,所以預留一個重定位記錄(類型爲R_386_32),表示“在最終的二進制文件中,把這個目標文件中符號foo的地址填入偏移量爲4的地方(相對於text 區而言)”。如果你觀察下a.o的彙編結果,你就會發現在text區偏移量爲4的地方,有4個字節爲0,這四個字節最終將會填入真實的地址。

$ objdump --disassemble ./a.o
 
./a.o:     file format elf32-i386
 
 
Disassembly of section .text:
 
00000000 <function>:
   0:    55         push   %ebp
   1:    89 e5                  mov    %esp,%ebp
   3:    a1 00 00 00 00         mov    0x0,%eax
   8:    5d                     pop    %ebp
   9:    c3                     ret

在鏈接的時候,如果你編譯的另外一個目標文件含有foo的地址,並且把這個目標文件與a.o一起編譯爲一個最終的可執行文件,那麼重定位記錄就會消失。但是仍然有很多的東西直到運行的時候才能確定,當編譯一個可執行文件或者動態庫的時候。正如我馬上要解釋的,PIC,與地址無關的代碼是一個很重要的原因(PIC,即Position independent code,直接翻譯就是位置無關代碼,簡單的說就是這個代碼可以被load到內存的任意位置而不用做任何地址修正。這裏指的是代碼段,數據段可能需要地址修正。)。當你觀察一個可執行文件,你會注意到它有一個固定的加載地址:

$ readelf --headers /bin/ls
[...]
ELF Header:
[...]
  Entry point address:               0x8049bb0
 
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
[...]
  LOAD           0x000000 0x08048000 0x08048000 0x16f88 0x16f88 R E 0x1000
  LOAD           0x016f88 0x0805ff88 0x0805ff88 0x01543 0x01543 RW  0x1000

這並不是地址無關。代碼段(權限爲RE,可讀可執行)必須被加載到虛擬地址0x08048000,數據段(RW)必須被加載到0x0805ff88。

這對於可執行文件來說很不錯,因爲每一次你創建一個新的進程(fork,然後exec),都會有一個全新的地址空間。考慮到時間的消耗提前計算好地址並把它們固定到最終的輸出文件中,這種方式是值得考慮的。(當然也可以採取 與地址無關的可執行文件 的方式來實現,但這是另外的一個話題了)

這對於共享庫來說就不是那麼好了。關鍵點是,你可以爲了達到你的目標而對共享庫隨意的組合。如果你的共享庫必須要在固定的地址上運行,32位的系統的地址空間很快就不夠用了。因此當你查看一個共享庫,它們並不指定一個固定的加載地址:

$ readelf --headers /lib/libc.so.6
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
[...]
  LOAD           0x000000 0x00000000 0x00000000 0x236ac 0x236ac R E 0x1000
  LOAD           0x023edc 0x00024edc 0x00024edc 0x0015c 0x001a4 RW  0x1000

共享庫還有第二個目的,代碼分享。如果有一百個進程使用一個共享庫,就沒有必要在內存中產生100分代碼拷貝。如果代碼是完全只讀,並且永遠不會修改,那麼每一個進程就可以分享相同的代碼。然而,對於共享庫有一個約束:對於每一個進程都必須有一份自己的數據實例。然而,在運行時刻,將庫數據放到任何我們想要的地址上也是可行的,這就要我們預留重定義記錄爲代碼段打上“補丁”,告知代碼段到哪裏找到實際的數據——這種方法實際上是不行的,因爲破環了動態庫的代碼只讀屬性和共享性。就如同你從頭文件信息中看到的一樣,解決方案爲:可讀可寫的數據段相對於代碼段有一個固定的偏移量。通過這種方式,利用虛擬內存的魔力,每個進程都有屬於自己的數據段,而共享不可修改的代碼段。所以訪問數據段的算法是很簡單的:我想訪問的數據的地址 = 當前地址+ 固定偏移。

但是,當前的地址有可能不是那麼簡單的知道:

$ cat test.c
static int foo = 100;
 
int function(void) {
    return foo;
}
$ gcc -fPIC -shared -o libtest.so test.c

foo位於數據段,與函數function中的指令有一個固定的偏移量。我們要做的就是找到它。在amd64上,這很簡單:

000000000000056c <function>:
 56c:        55         push   %rbp
 56d:        48 89 e5               mov    %rsp,%rbp
 570:        8b 05 b2 02 20 00      mov    0x2002b2(%rip),%eax        # 200828 <foo>
 576:        5d                     pop    %rbp

上面的代碼的意思是說“把與當前指令地址偏移0x2002b2處的值放入eax”。另一方面,i386並沒有提供訪問當前指令偏移的能力。所以有一些限制:

0000040c <function>:
 40c:    55         push   %ebp
 40d:    89 e5                  mov    %esp,%ebp
 40f:    e8 0e 00 00 00         call   422 <__i686.get_pc_thunk.cx>
 414:    81 c1 5c 11 00 00      add    $0x115c,%ecx                    //%ecx=0x414
 41a:    8b 81 18 00 00 00      mov    0x18(%ecx),%eax
 420:    5d                     pop    %ebp
 421:    c3                     ret
 
00000422 <__i686.get_pc_thunk.cx>:
 422:    8b 0c 24       mov    (%esp),%ecx
 425:    c3                     ret

這裏的魔數是__i686.get_pc_thunk.cx。i386不允許我們得到當前指令的地址,但是我們可以得到一個已知的固定地址——__i686.get_pc_thunk.cx的值,%ecx中的值是call的返回地址,這裏是0x414.我們做一個簡單的算術:0x115c+0x414 = 0x1570.最終的數據和0x1588偏移了0x18個字節,查看彙編代碼:

00001588 <global>:
    1588:       64 00 00                add    %al,%fs:(%eax)

正是100所處的地址。

現在我們越來越接近了,但是還是有很多的問題要處理。如果一個共享庫可以被加載到任意的地址,那麼,一個可執行文件或者其他的共享庫,如何知道怎麼訪問它的數據或者調用它的函數呢?從理論上,我們是可以的,加載庫,然後把數據的地址或者函數的地址填入到庫相應的地方。然後這正如之前所講的,違反了代碼共享性。就如同我們所瞭解的,所有的問題都可以通過增加一箇中間層來解決,在這種情形下,稱之爲全局偏移表或者got。

考慮下面的庫:

$ cat test.c
extern int foo;
 
int function(void) {
    return foo;
}
$ gcc -shared -fPIC -o libtest.so test.c

這和之前的文件很像,但是foo是extern的。假設是由其他的庫提供。讓我們看一下在amd64上它是如何工作的:

$ 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

反彙編的結果顯示返回值位於當前指令偏移0x200271處:0x0200828。查看section header,這個地址位於.got區。接着我們查看重定位記錄,可以發現有一個類型爲R_X86_64_GLOB_DAT的重定位的意思是“找到foo的值,然後把它放在地址0x200828處”。

 

所以,當這個動態庫被加載,動態加載器將會檢查重定位記錄,找到foo的值,並按照要求爲.got中的條目打上“補丁”。當動態庫中的代碼運行並訪問foo的時候,訪內指針將會指向正確的地址,一切都會正常工作,而不用去修改指令的值,以避免代碼的共享性。

 

以上是數據的處理,那麼函數調用呢?函數調用的中間層稱之爲procedure linkage table 或者PLT.代碼不會直接調用外部的函數,而是通過一個plt stub。

$ 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

現在,我們function跳轉到0x4d0.反彙編,我們看到這是一個有趣的調用,我們跳轉到當前rip指針偏移0x200382,也就是0x200858處。可以發現,這個地址存放着符號foo的重定位的記錄。

I讓我們來看一下0x200858的初始值:

$ objdump --disassemble-all libtest.so
 
Disassembly of section .got.plt:
 
0000000000200840 <.got.plt>:
  200840:       98                      cwtl
  200841:       06                      (bad)
  200842:       20 00                   and    %al,(%rax)
        ...
  200858:       d6                      (bad)
  200859:       04 00                   add    $0x0,%al
  20085b:       00 00                   add    %al,(%rax)
  20085d:       00 00                   add    %al,(%rax)
  20085f:       00 e6                   add    %ah,%dh
  200861:       04 00                   add    $0x0,%al
  200863:       00 00                   add    %al,(%rax)
  200865:       00 00                   add    %al,(%rax)
        ...

0x200858的初始值是0x4d6,居然是下一條指令的地址!這條指令把0要入棧中,然後跳轉到0x4c0.通過查看代碼我們可以發現,把GOT一個值壓入棧中,然後跳到GOT中的第二個值。

00000000000004c0 <foo@plt-0x10>:
 4c0:   ff 35 82 03 20 00       pushq  0x200382(%rip)        # 200848 <_GLOBAL_OFFSET_TABLE_+0x8>
 4c6:   ff 25 84 03 20 00       jmpq   *0x200384(%rip)        # 200850 <_GLOBAL_OFFSET_TABLE_+0x10>
 4cc:   0f 1f 40 00             nopl   0x0(%rax)

這裏究竟是在做什麼呢?這就是 lazy binding(延遲綁定)——按照約定,動態連接器加載一個動態庫,首先應該在got中的已知地址存放能夠解析符號的默認函數。因此,上面的處理流程大體是這樣子的:當第一次調用一個函數的時候,因爲此時got中還沒有它的地址,所以調用失敗,從而進入默認的stub處理流程,這個stub用來解決符號解析。當找到foo的地址之後,就會把這個值填入到got,這樣下次調用的時候,就直接調用到foo的實際地址。

原文地址:
https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html

 

 

GOT表和PLT表知識詳解

轉:https://blog.csdn.net/qq_18661257/article/details/54694748

GOT表和PLT表在程序中的作用非常巨大,接下來的講解希望大家可以仔細看看

我們用一個非常簡單的例子來講解,代碼如下: 
圖1

這裏寫圖片描述


然後我們編譯

我們直接gdb ./a.out來進行反編譯處理,然後通過disas main查看main函數中的反編譯代碼如下:

圖3

這裏寫圖片描述


我們可以觀察到gets@plt和puts@plt這兩個函數,爲什麼後面加了個@plt,因爲這個爲PLT表中的數據的地址。那爲什麼反編譯中的代碼地址爲PLT表中的地址呢。

原因

爲了更好的用戶體驗和內存CPU的利用率,程序編譯時會採用兩種表進行輔助,一個爲PLT表,一個爲GOT表,PLT表可以稱爲內部函數表,GOT表爲全局偏移表,這兩個表是相對應的,什麼叫做相對應呢,PLT表中的數據就是GOT表中的一個地址,如下圖:

圖44

這裏寫圖片描述


PLT表中的每一項的數據內容都是對應的GOT表中一項的地址,這個是固定不變的,到這裏大家也知道了PLT表中的數據根本不是函數的真實地址,而是GOT表項的地址,好坑啊。

其實在大家進入帶有@plt標誌的函數時,這個函數其實就是個過渡作用,因爲GOT表項中的數據纔是函數最終的地址,而PLT表中的數據又是GOT表項的地址,我們就可以通過PLT表跳轉到GOT表來得到函數真正的地址。

那問題來了,這個@plt函數時怎麼來的,這個函數是編譯系統自己加的,大家可以通過disas gets看看裏面的代碼,如下圖:

圖55

這裏寫圖片描述


大家可以發現,這個函數只有三行代碼,第一行跳轉,第二行壓棧,第三行又是跳轉,解釋: 
第一行跳轉,它的作用是通過PLT表跳轉到GOT表,而在第一次運行某一個函數之前,這個函數PLT表對應的GOT表中的數據爲@plt函數中第二行指令的地址,針對圖中來說步驟如下:

  1. jmp指令跳轉到GOT表
  2. GOT表中的數據爲0x400486
  3. 跳轉到指令地址爲0x400486
  4. 執行push 0x3#這個爲在GOT中的下標序號
  5. 在執行jmp 0x400440
  6. 而0x400440爲PLT[0]的地址
  7. PLT[0]的指令會進入動態鏈接器的入口
  8. 執行一個函數將真正的函數地址覆蓋到GOT表中

這裏我們要提幾個問題: 
1. PLT[0]處到底做了什麼,按照我們之前的思路它不是應該跳轉到GOT[0]嗎? 
2. 爲什麼中間要進行push壓棧操作? 
3. 壓入的序號爲什麼爲0x3,不是最開始應該爲0x0嗎?

解決問題

問題1

看下圖: 
圖66

這裏寫圖片描述


我們嘗試着查看0x400440地址的數據內容發現一個問題,從0x400440−0x400450之間的數據完全不知道是什麼,而真正的PLT[x]中的數據是從0x400450開始的,從這裏纔有了@plt爲後綴的地址,但是我們disas gets看代碼的時候是從0x400440開始的,我們可以通過x /5i 0x400440查看0x400440處的代碼,如下: 
圖77

這裏寫圖片描述


我們看到了後面的#之後又一個16進制數,一看便可以知道是GOT表的地址,爲什麼這麼肯定呢,因爲我們可以通過objdump -R ./a.out查看一個程序的GOT函數的地址,如下圖: 
圖88

這裏寫圖片描述


這裏都是些GOT地址,我們發現都是0x601...這些,所以可以斷定圖77中的也是GOT地址,那麼我們可以猜想出,在正式存儲一個函數的GOT地址前,我們的PLT表前面有一項進行一些處理,我們暫且不具體深入剖析這些代碼有什麼用,但是我們可以肯定puts@plt前面那16個字節也算是PLT表中的內容,這其實就是我們的PLT[0],正如我們之前問題提到的那樣,我們的PLT[0]根本沒有跳轉到GOT[0],它不像我們的PLT[1]這些存儲的是GOT表項的地址,它是一些代碼指令,換句話說,PLT[0]是一個函數,這個函數的作用是通過]GOT[1]和GOT[2]來正確綁定一個函數的正式地址到GOT表中來。

咦,這裏問題好像又產生了,本來按照最開始的思路PLT[1]也是跳轉到GOT[1]的,GOT[2]同理,但是這兩個數據好像被PLT[0]利用了,同時GOT[0]好像消失了,這裏GOT[0]暫且不說它的作用是什麼,針對GOT[1]和GOT[2]被PLT[0]利用,所以我們程序中真實情況其實是從PLT[1]到GOT[3],PPLT[2]到GOT[4],所以我們推翻了我們的圖44,建立一張新的處理表

圖99

這裏寫圖片描述


而plt[0]代碼做的事情則是:由於GOT[2]中存儲的是動態鏈接器的入口地址,所以通過GOT[1]中的數據作爲參數,跳轉到GOT[2]所對應的函數入口地址,這個動態鏈接器會將一個函數的真正地址綁定到相應的GOT[x]中。

這就是PLT表和GOT表,總而言之,我們調用一個函數的時候有兩種方法,一個是通過PLT表調用,一個則是通過GOT表調用,因爲PLT表最終也是跳轉GOT表,GOT表中則是一個函數真正的地址,這裏需要注意的是,在一個函數沒有運行一次之前,GOT表中的數據爲@plt函數中下一條指令的地址,圖55有說。

問題2

中間進行的壓棧是爲了確定PLT對應的GOT表項,即是PLT[1]−>GOT[3],0x3就是GOT的下標3,也就是說壓棧後我們跳轉到PLT[0],接着PLT[0]中的指令會通過這次壓棧的序號來確定操作的GOT表項的位置

問題3

好像都在第一個問題都已經解決了,這裏壓入0x3的原因是因爲,我們的GOT[0],GOT[1],GOT[2]都有額外用處。要從GOT[3]開始

 

 

PIC的實現

轉:https://blog.csdn.net/cody_kai/article/details/6589263

今天研究了下PIC,記錄下。

1)什麼是PIC,爲什麼要PIC

PIC,即Position independent code,直接翻譯就是位置無關代碼,簡單的說就是這個代碼可以被load到內存的任意位置而不用做任何地址修正。這裏指的是代碼段,數據段可能需要地址修正。
PIC是share library機制的基礎之一,要實現library在各個process之間可以share,代碼必須是PIC。爲什麼?有了load時的relocation還不夠麼?答案是也可以夠,但是僅僅有load時的relocation,基本等於沒有用。share library的主要目的是讓各個process可以共享common的代碼(指代碼段),這部分代碼只在內存中佔用一次內存,所有process共享這部分代碼,而不需要每個process都有一份拷貝。因爲代碼段是要被share的,所以代碼段的內容不能被改變。load時的relocation是在load時對代碼做地址修正,所以一旦library被load了,這個library在所有共享該library的process的地址空間中的位置也確定了。有人會問這樣有什麼問題麼?OS在第一次load這個library的時候完全可以找個available的地址空間阿,因爲第一次load的時候,是可以被load到內存的任意地方的,所以只要有地址空間就可以。遺憾的是,這種情況下地址空間很可能不夠,而導致沒法load。考慮這樣的問題,如果某人寫了個搗蛋的library,佔用很大的地址空間,一旦這個library被load了,就會導致其他library不能被load,因爲已經沒有available的地址空間了,這樣這個系統就會崩潰。所以要實現share library,僅僅load時的relocation是不夠的,我們需要一種機制,可以讓library被load進process的任意地址空間,或者說library在不同的process中,可以被load到不同的地址空間,然後在OS層,通過OS的地址空間映射,來實現library在物理內存上的share。所以PIC是必須的。

2)怎麼實現PIC

PIC需要解決的問題就是找到一種辦法,避免load時的地址修正(relocation)。以下面的代碼爲例,該代碼把內存中符號one_dword對應的地址的一個dword的內容放到%eax裏。如果一個library包含下面的代碼,則這個代碼不是PIC的。因爲$one_byte會隨着該代碼被load到不同的地址而有不同的值,這就導致了代碼段在load到不同地址時,內容(第一句mov指令)會不同,這就導致了無法share。

		.text
		movl	$one_byte, %ebx
		movl	(%ebx), %eax
		.data
		.align	4
	one_dword:
		.byte	1


以ELF格式爲例來說明PIC如何避免地址修正。在ELF格式中,各個代碼段數據段都有固定的位置,代碼段中某條代碼的位置到數據段的地址都是固定的。所以如果某條指令要引用一個數據的時候,如果能得到當前指令的地址,就可以通過加上到數據段的固定偏移來找到這個數據。ELF格式中引入了Global Offset Table (GOT)來實現這個機制。GOT就是一系列地址的數組,包含了所有全局數據的地址。

			|		|
			|---------------|  
			|  data_A	|
	Data section	|---------------| 
			|  data_B	|
		------->|---------------| 
			|		|
			|      ...	|
			|		|
			|---------------| 
			| addr of data_A|
			|---------------|  
		GOT base| addr of data_B|
		------->|---------------| 
			|		|
			|		|
			|		|
			|      ...	|
	Text section	|		|
		------->|---------------| 


根據GOT,經過下面的三步,就可以尋址到特定數據。
    1)得到當前指令的地址
    2)根據固定的偏移找到GOT的地址
    3)根據數據的符號固定偏移找到該數據的地址

下面的代碼實現了這一過程:

		call 	tmp_label						/* will push EIP to stack */
	tmp_label:	
		popl	%ecx							/* %ecx now has address of $tmp_label */
		addl	$GOT_TABLE_OFFSET_TO_CUR +[. - $tmp_label], %ecx	/* %ecx now has the base address of GOT */


上面的代碼中,GOT_TABLE_OFFSET_TO_CUR是所在代碼的地址到GOT基地址的固定偏移,這個是compiler & linker決定的。執行上面最後一句後,%ecx裏已經是GOT的基地址了,然後就可以根據%ecx尋址數據:

		movl	data_symbol_offset(%ecx), %ebx	/* %ebx now has address of target data */
		movl	(%ebx), %eax			/* move data to %eax */


data_symbol_offset也是在編譯連接的過程中確定,是固定值。

一個library中包含了一個全局的GOT,每個library都有自己的GOT。在load時,GOT對每個進程都是私有的,這個和數據段一樣。如果某個library引用了另一個library的數據,則該library的GOT裏也包含這個數據的地址,只不過這個地址是在dynamic linker在load library的時候負責填入的,編譯鏈接階段無法確定這個值,ELF格式中定義了特定的類型來表示這種數據。

3)一個具體例子

寫一個簡單的例子來驗證PIC的實現。libtest2.so只包含一個數據,被libtest.so引用,main調用libtest.so裏的test函數。

kai@opensolaris-kai:~/src/tmp$ cat test.c
static int data = 1;
extern int test2_data;
 
void test(int p1, int p2, int p3)
{
	data += p2;
	test2_data += p1;
}
 
kai@opensolaris-kai:~/src/tmp$ cat test2.c
int test2_data = 3;
 
kai@opensolaris-kai:~/src/tmp$ cat main.c 
void test(int p1, int p2, int p3);
 
int main(void)
{
	int i = 5;
	
	test(2, i, 2);
}
 
kai@opensolaris-kai:~/src/tmp$ make
gcc -nostdlib -shared -fPIC -s -o libtest2.so test2.c
gcc -nostdlib -shared -fPIC -s -o libtest.so test.c -ltest2
gcc -o main main.c -ltest
objdump -D libtest2.so > test2.S
objdump -D libtest.so > test.S
objdump -D main > main.S


反彙編後的test.S:

Disassembly of section .text:
 
0000056c <test>:
 56c:   55                      push   %ebp			/* %ebp指向caller的stack frame base pointer */
 56d:   89 e5                   mov    %esp,%ebp		/* %esp指向test的stack frame base pointer,存進%ebp,用來訪問傳給test的參數 */
 56f:   53                      push   %ebx			
 570:   e8 00 00 00 00          call   575 <test+0x9>		/* 地址575會被壓入stack */
 575:   5b                      pop    %ebx			/* %ebx現在等於地址575 */
 576:   81 c3 2b 00 01 00       add    $0x1002b,%ebx		/* %ebx現在等於GOT的base address,0x1002b是compiler & linker計算的 */
 57c:   8b 45 0c                mov    0xc(%ebp),%eax		/* 把p2的值move到%eax,傳給test的三個參數分別在%ebp+0x8, %ebp + 0xc, %ebp + 0x10 */
 57f:   01 83 10 00 00 00       add    %eax,0x10(%ebx)		/* %ebp+0x10直接指向了libtest.so裏的data,這裏沒有經過GOT去尋址,猜測應該是經過優化了 */
 585:   8b 8b 0c 00 00 00       mov    0xc(%ebx),%ecx		/* %ecx, %edx都指向libtest2.so裏的test2_data */	
 58b:   8b 93 0c 00 00 00       mov    0xc(%ebx),%edx
 591:   8b 45 08                mov    0x8(%ebp),%eax		/* move test2_data到%eax */
 594:   03 02                   add    (%edx),%eax		/* add p1 to %eax */
 596:   89 01                   mov    %eax,(%ecx)		/* store %eax back to test2_data */
 598:   5b                      pop    %ebx
 599:   83 c4 00                add    $0x0,%esp		/* ? */
 59c:   c9                      leave
 59d:   c3                      ret
 
Disassembly of section .got:
 
000105a0 <_GLOBAL_OFFSET_TABLE_>:
   105a0:       94                      xchg   %eax,%esp
        ...
 
Disassembly of section .data:
 
000105b0 <_edata-0x4>:
   105b0:       01 00                   add    %eax,(%eax)
        ...


一個問題:對於library自身數據的訪問,似乎不需要GOT?因爲可以數據段到代碼的偏移也是固定的,完全可以直接得到數據段基地址。

給出執行了push %ebx後的stack的情況(期間有一次地址575的push和pop)。
SFBP = stack frame base pointer     

					|			|  
	stack top when enter main  --->	|-----------------------| 
					| 			|  
					|			|  					
					|			|  
					| 			|
					|-----------------------| 
					|  	p3		|
					|-----------------------| 
					|	p2		|
					|-----------------------|
					|	p1		|
					|-----------------------| 
					| return address in main|
	stack top when enter test  --->	|-----------------------|  
					| SFBP of main  	| 
					|-----------------------|  <--- EBP 
					| original %ebx		|
					|-----------------------|  <--- ESP
					|			|
					|      			|


4) Advantage & disadvantage of PIC

PIC的好處顯然是在load時可以被load到任意位置而不需要代碼段的地址修正,代碼可以被不同process share而只留有一份代碼在內存中。壞處是增加了額外的對GOT的引用,以及一系列必須的額外的開銷(比如對代碼段地址的call 和pop等),使得代碼運行速度比飛PIC的慢。
另外,由於GOT裏的數據地址也是需要在load時計算的,所以對於一些擁有大量數據的library,load的時間也會變慢。

一個問題:對於library自身數據的訪問,似乎不需要GOT?因爲可以數據段到代碼的偏移也是固定的,完全可以直接得到數據段基地址。這似乎可以大大減少load時對GOT裏地址的修正所帶來的額外時間的花銷。

5) Reference

a. Intel IA-32 Architectures Manual Volume1 Basic Architecture, CHAPTER 6, PROCEDURE CALLS, INTERRUPTS, AND EXCEPTIONS
b. Linkers & Loaders, Chapter 8, Loading and overlays, Position indenpendent code
c. http://bottomupcs.sourceforge.net/csbu/x3824.htm            

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章