動態鏈接庫的優點比較明顯,主要集中在節省內存,簡化對程序的管理等,對此感興趣的看官可以去閱讀經典的教材
Linker and Loader,國內也有一本經典的教材,俞甲子 石凡 潘愛民編著的程序員的自我修養,講的也非常好。
延遲綁定PLT,我迷惑過很久,終於讓我遇到一篇寫的非常棒的博文,這就是 Position Indepentent code in share library.作者對編譯鏈接的理解十分深刻,寫了一個系列的文章,英文水平好的讀者可以不聽我嘮叨,直接看英文原文。我這篇文章主要參考提到的博文,同時參考的程序員的自我修養,即網上的其他博文,做了下實驗,對位置無關共享庫的理解加深了一大步。說這些的原因是不想陷入版權糾紛,本文所有的東西,基本是學習其他博文
書籍的產物,版權不屬於我,版權屬於前輩。我做的只是將前輩的講解綜合以下,互相補充。
動態鏈接技術,嚴格的說分成兩類,一種是 Load-Time Relocation,這種技術容易理解,但是缺點也比較致命,不能共享,起不到節省內存的目的,目前X86_64已經不提供這種方式;另外一種屬於主流的位置無關(PIC)動態庫。
位置無關動態庫中,有兩個需要解決的問題,一個是動態庫中的變量如何訪問,另一個是動態庫中的函數調用如何訪問。其實,我們一般調用的是動態庫中的函數,別的不說,printf是libc庫提供的函數,當我們調用printf的時候,我們如何找到函數的地址的呢? 這就是本文主要講述的內容。
前面提到了延遲綁定,何爲延遲綁定呢。這個也比較好理解。libc庫中有很多的函數,但是我們編寫程序的時候,並不一定會調用libc庫中的每個函數,更多的情況下,我們只調用了極少數函數,如果我們將每個函數的地址都解析出來,其實是一種浪費。所以採用的方法是用到函數時再進行對函數的位置進行定位。 這種技術就叫做延遲定位。
我下面的代碼用的是Position Indepentent code in share library博文中的代碼,再次強調,版權和榮耀,都屬於前輩。
ml_main.c
-
int myglob = 42;
-
-
int ml_util_func(int a)
-
{
-
return a + 1;
-
}
-
int ml_func(int a, int b)
-
{
-
int c = b + ml_util_func(a);
-
myglob += c;
-
return b + myglob;
-
}
-
gcc -shared -fpic -g libmlpic.so
ml_main.c
解釋下上面的命令,-shared參數表示生成共享庫, -fpic參數表示生成位置無關動態庫,前面也提到了,存在兩種類型的動態庫,如果不帶-fpic,那麼,生成的是裝載時重定位共享庫。當然了fpic和fPIC也有區別,這個不是我們關心的內容,不贅述。
driver.c
-
#define _GNU_SOURCE
-
#include <link.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
#include<time.h>
-
static int header_handler(struct dl_phdr_info* info, size_t
size, void* data)
-
{
-
int j;
-
printf("name=%s (%d segments) address=%p\n",
-
info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);
-
for ( j = 0; j < info->dlpi_phnum; j++) {
-
printf("\t\t header %2d: address=%10p\n", j,
-
(void*) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));
-
printf("\t\t\t type=%u, flags=0x%X\n",
-
info->dlpi_phdr[j].p_type, info->dlpi_phdr[j].p_flags);
-
}
-
printf("\n");
-
return 0;
-
}
-
-
extern int ml_func(int, int);
-
-
int main(int argc, const char* argv[])
-
{
-
dl_iterate_phdr(header_handler, NULL);
-
-
int t = ml_func(argc, argc);
-
sleep(12);
-
return t;
-
}
-
gcc -o driver -g driver.c ./libmlpic.so
我們生成了可執行程序driver,driver調用了共享庫libmlpic.so中的函數ml_func。
-
(gdb) disas main
-
Dump of assembler code for function main:
-
0x08048697 <+0>: push %ebp
-
0x08048698 <+1>: mov %esp,%ebp
-
0x0804869a <+3>: and $0xfffffff0,%esp
-
0x0804869d <+6>: sub $0x20,%esp
-
0x080486a0 <+9>: movl $0x0,0x4(%esp)
-
0x080486a8 <+17>: movl $0x80485c4,(%esp)
-
0x080486af <+24>: call 0x80484fc <dl_iterate_phdr@plt>
-
=> 0x080486b4 <+29>: mov 0x8(%ebp),%eax
-
0x080486b7 <+32>: mov %eax,0x4(%esp)
-
0x080486bb <+36>: mov 0x8(%ebp),%eax
-
0x080486be <+39>: mov %eax,(%esp)
-
0x080486c1 <+42>: call 0x804849c <ml_func@plt>
-
0x080486c6 <+47>: mov %eax,0x1c(%esp)
-
0x080486ca <+51>: movl $0xc,(%esp)
-
0x080486d1 <+58>: call 0x80484ec <sleep@plt>
-
0x080486d6 <+63>: mov 0x1c(%esp),%eax
-
0x080486da <+67>: leave
-
0x080486db <+68>: ret
-
End of assembler dump.
我們要調用ml_func函數,但是@plt表明是動態庫中的函數,0x804849c是地址,我們反彙編下
-
Breakpoint 1, 0x080486c1 in main (argc=1, argv=0xbffff7e4) at
driver.c:27
-
27 int t = ml_func(argc, argc);
-
-
(gdb) disas 0x804849c
-
Dump of assembler code for function ml_func@plt:
-
0x0804849c <+0>: jmp *0x804a000
-
0x080484a2 <+6>: push
$0x0
-
0x080484a7 <+11>:
jmp 0x804848c
-
End of assembler dump.
-
(gdb) x 0x804a000
-
0x804a000 <_GLOBAL_OFFSET_TABLE_+12>: 0x080484a2
理想情況下,0x8048a00存放的是ml_func函數的地址,但是0x80484a2這個地址並不是ml_func函數的地址。所謂延遲綁定的意思就是,並不是一開始就將所有的函數的地址解析好,而是第一次調用庫函數時,將庫函數的實際地址綁定到GOT的指定位置。
我們看到0x8048a00地址是_GLOBAL_OFFSET_TABLE+12,這個可以算出,_GLOBAL_OFFSET_TABLE地址爲 0x80489ff4.
-
(gdb) x/20x 0x8049FF4
-
0x8049ff4 <_GLOBAL_OFFSET_TABLE_>: 0x08049f18 0x0012c8f8 0x00123220 0x080484a2 - -地址並不是真正 的ml_func的地址。
-
0x804a004 <_GLOBAL_OFFSET_TABLE_+16>: 0x080484b2 0x0018fb70 0x00147af0 0x00178130
-
0x804a014 <_GLOBAL_OFFSET_TABLE_+32>: 0x080484f2 0x00234e10 0x00000000 0x00000000
-
0x804a024 <completed.7021>: 0x00000000 0x00000000 0x00000000 0x00000000
-
0x804a034: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) x/30i 0x804848c
0x804848c:
pushl 0x8049ff8
0x8048492:
jmp *0x8049ffc
0x8048498:
add %al,(%eax)
0x804849a:
add %al,(%eax)
0x804849c <ml_func@plt>:
jmp *0x804a000
0x80484a2 <ml_func@plt+6>:
push $0x0 -----跳到此處,發現不是函數地址,跳轉後調 用_dl_runtime_resolve
0x80484a7 <ml_func@plt+11>:
jmp 0x804848c
0x80484ac <__gmon_start__@plt>:
jmp *0x804a004
0x80484b2 <__gmon_start__@plt+6>:
push $0x8
0x80484b7 <__gmon_start__@plt+11>:
jmp 0x804848c
0x80484bc <putchar@plt>:
jmp *0x804a008
0x80484c2 <putchar@plt+6>:
push $0x10
0x80484c7 <putchar@plt+11>:
jmp 0x804848c
0x80484cc <__libc_start_main@plt>:
jmp *0x804a00c
0x80484d2 <__libc_start_main@plt+6>:
push $0x18
0x80484d7 <__libc_start_main@plt+11>:
jmp 0x804848c
0x80484dc <printf@plt>:
jmp *0x804a010
0x80484e2 <printf@plt+6>:
push $0x20
0x80484e7 <printf@plt+11>:
jmp 0x804848c
0x80484ec <sleep@plt>:
jmp *0x804a014
0x80484f2 <sleep@plt+6>:
push $0x28
0x80484f7 <sleep@plt+11>:
jmp 0x804848c
0x80484fc <dl_iterate_phdr@plt>:
jmp *0x804a018
0x8048502 <dl_iterate_phdr@plt+6>:
push $0x30
0x8048507 <dl_iterate_phdr@plt+11>:
jmp 0x804848c
0x804850c:
add %al,(%eax)
0x804850e:
add %al,(%eax)
我們看到0x80484a2處,不是函數地址,而是push 0x0,然後跳轉到了0x804848c,然後,將另一個參數動態庫的模塊ID壓棧後,調用傳說中的_dl_runtime_resolve,來 獲取函數的真正地址。0x123220就是_dl_runtime_resolve的地址,可以看下程序員的自我修養,.got.plt的前三項的特殊含義的說明。
-
(gdb) b * 0x00123220
-
Breakpoint 2 at 0x123220: file ../sysdeps/i386/dl-trampoline.S, line 29.
-
(gdb) c
-
Continuing.
-
-
Breakpoint 2, _dl_runtime_resolve () at ../sysdeps/i386/dl-trampoline.S:29
-
29 ../sysdeps/i386/dl-trampoline.S: 沒有那個文件或目錄.
-
in ../sysdeps/i386/dl-trampoline.S
-
(gdb) disas 0x00123220
-
Dump of assembler code for function _dl_runtime_resolve:
-
=> 0x00123220 <+0>: push %eax
-
0x00123221 <+1>: push %ecx
-
0x00123222 <+2>: push %edx
-
0x00123223 <+3>: mov 0x10(%esp),%edx
-
0x00123227 <+7>: mov 0xc(%esp),%eax
-
0x0012322b <+11>: call 0x11d550 <_dl_fixup>
-
0x00123230 <+16>: pop %edx
-
0x00123231 <+17>: mov (%esp),%ecx
-
0x00123234 <+20>: mov %eax,(%esp)
-
0x00123237 <+23>: mov 0x4(%esp),%eax
-
0x0012323b <+27>: ret $0xc
-
End of assembler dump.
我們看到的,_dl_runtime_resolve調用的_dl_fixup,來修改GOT表中的表項,還記的
-
(gdb) x 0x804a000
-
0x804a000 <_GLOBAL_OFFSET_TABLE_+12>: 0x080484a2
執行完,之後,我們看下
-
(gdb) x/20x 0x8049FF4
-
0x8049ff4 <_GLOBAL_OFFSET_TABLE_>: 0x08049f18 0x0012c8f8 0x00123220
0x0012e4a7 ----這回這個地址就是ml_func的 地址
-
0x804a004 <_GLOBAL_OFFSET_TABLE_+16>: 0x080484b2 0x0018fb70 0x00147af0 0x00178130
-
0x804a014 <_GLOBAL_OFFSET_TABLE_+32>: 0x080484f2 0x00234e10 0x00000000 0x00000000
-
0x804a024 <completed.7021>: 0x00000000 0x00000000 0x00000000 0x00000000
-
0x804a034: 0x00000000 0x00000000 0x00000000 0x00000000
我們看到GOT發生了變化,第四項內容從原來的0x80484a2變成了,0x0012e4a7,這個地址,就是傳說中的ml_func的地址,我們驗證下:
-
(gdb) p &ml_func
-
$1 = (int (*)(int, int)) 0x12e4a7 <ml_func>
Bingo,這就是一個完整的PLT的過程,對_dl_runtime_resolve函數感興趣的兄弟可以繼續深究。
參考文獻:
1 程序員的自我修養