PLT學習

I. 動態鏈接中延遲綁定(Lazy binding)的基本思想和方法
基本思想:函數第一次用到時才進行綁定(符號查找,重定位等),如果不用則不進行綁定。
方法:使用PLT(Procedure Linkage Table)的方法來實現。它使用了一些精巧的指令序列。

II. 理解PLT方法涉及的幾個對象

1.調用外部函數的代碼
代碼裏調用外部函數時,轉向去調用PLT部分的代碼。
2.GOT(global offset table)
保存外部函數的地址(當然也包括外部變量),初始加載時,每個函數的地址不是準確的地址,而是這個函數對應PLT項的Push指令的地址。第一次調用函數後填入真正的地址。
3.PLT
指令序列的集合,存放在虛擬內存空間的代碼區域。分兩大部分:PLT0, PLTn。PLT0的指令作用是壓模塊標誌進棧和調用__dll_runtime_resolve函數。
4.Rel.PLT
PLT的重定位表。保存需要重定位函數的需要的信息,函數符號是函數__dll_runtime_resolve需要的兩個參數(哪個動態鏈接庫,哪個外部函數)中的一個。

III. 一個實例
1)
程序dynlib.c:
extern int foo;
extern int f();

static int g(){
    return f();
}

int function(void) {
    int k = g();
    int c = foo;
    return (c+k);
}

編譯成共享動態庫:
gcc -fIPC -shared dynlib.so dynlib.c

2)
函數g的代碼:
000006d0 <g>:
6d0:   55                        push   %ebp
6d1:   89 e5                   mov    %esp,%ebp
6d3:   53                        push   %ebx
6d4:   83 ec 04                sub    $0x4,%esp
6d7:   e8 00 00 00 00          call   6dc <g+0xc>
6dc:   5b                         pop    %ebx
6dd:   81 c3 a4 11 00 00       add    $0x11a4,%ebx
6e3:   e8 6c fe ff ff          call   554 <_init+0x28>(運行PLT裏f函數對應項的指令去獲取f的地址)
6e8:   89 c0                   mov    %eax,%eax
6ea:   89 c0                   mov    %eax,%eax
6ec:   8b 5d fc                mov    0xfffffffc(%ebp),%ebx
6ef:   c9                      leave
6f0:   c3                      ret
6f1:   8d 76 00                lea    0x0(%esi),%esi

3)
PLT的內容,它是一些指令序列。
00000544 <.plt>:
544:   ff b3 04 00 00 00       pushl  0x4(%ebx)  (push library ID)
54a:   ff a3 08 00 00 00       jmp    *0x8(%ebx) (jump to function  _dll_runtime_resolve())

550:   00 00                   add    %al,(%eax)
552:   00 00                   add    %al,(%eax)
554:   ff a3 0c 00 00 00       jmp    *0xc(%ebx) 
55a:   68 00 00 00 00          push   $0x0
55f:   e9 e0 ff ff ff          jmp    544 <_init+0x18>

564:   ff a3 10 00 00 00       jmp    *0x10(%ebx)
56a:   68 08 00 00 00          push   $0x8
56f:   e9 d0 ff ff ff          jmp    544 <_init+0x18>
574:   ff a3 14 00 00 00       jmp    *0x14(%ebx)
57a:   68 10 00 00 00          push   $0x10
57f:   e9 c0 ff ff ff          jmp    544 <_init+0x18>
584:   ff a3 18 00 00 00       jmp    *0x18(%ebx)
58a:   68 18 00 00 00          push   $0x18
58f:   e9 b0 ff ff ff          jmp    544 <_init+0x18>

用(*0xc(%ebx) )獲取GOT表裏函數f對應保存的值,應該是f的地址值,但使用延遲綁定技術後,初始值時這個函數對應PLT項的push指令值,在本例中就是55a。第一次運行函數時,其實jump *0xc(%ebx),就跳轉到55a,這條指令的效果就是跳轉到下一條指令,相當於什麼也沒做。
接下來55f的指令就跳轉到PL0的指令地址去,544指令的目的是壓模塊ID入棧,就是這個動態庫是哪個動態庫。54a指令去調用函數_dll_runtime_resolve去解析函數f的地址,填入GOT中f對應的項中,並且執行f。
第二次執行函數f時,還會跳轉到指令554去,但這時 *0xc(%ebx)的值就是真正函數f的地址值了,這時554的效果就是轉到函數f的地址去執行f了。

查看幾個指令“55a,56a,57a,58a”,push指令後的整數是“0x0,0x8,0x10,0x18”,爲什麼每兩個數之間差8?
push之後的整數標準這個外部函數在.rel.plt中偏移量。.rel.plt是一個結構數組。結構是:
typedef struct{
    Elf32_Addr r_offset;
    Elf32_Word r_info;
}Elf32_Rel;
sizeof(Elf32_Rel)=8。第一個結構偏移是0,第二個地址偏移就是0x8,第三個就是0x10,所以每兩個整數之間差8。


4)
GOT的內容。
Hex dump of section '.got':
  0x00001880 0000055a(加載時,初始值是PLT中函數f對應項的push指令地址,本例中就是55a;第一次運行函數f後就寫入函數f的地址)           
                    00000000(_dll_runtime_resolve地址) 
                    00000000(加載時寫入庫的名字) 
                    000017b0 ............Z...
  0x00001890 00000000 0000058a 0000057a 0000056a j...z...........
  0x000018a0 00000000 00000000 00000000 00000000 ................

5)
Rel.plt內容
Relocation section '.rel.plt' at offset 0x50c contains 4 entries:
Offset     Info    Type            Symbol's Value  Symbol's Name
0000188c  00001607 R_386_JUMP_SLOT       00000000  f
00001890  00001707 R_386_JUMP_SLOT       00000000  __register_frame_info
00001894  00001907 R_386_JUMP_SLOT       00000000  __deregister_frame_info
00001898  00001d07 R_386_JUMP_SLOT       00000000  __cxa_finalize
保存了Plt需要重定位的函數信息。PLT中push指令後的整數就是外部函數在.rel.plt中對應項的偏移量。

IV參考資料
《程序員的自我修養--鏈接、裝載與庫》作者:俞甲子,石凡,潘愛民
《漫談兼容內核》系列,作者:毛德操

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