開(kāi)篇,首先這篇文章是翻譯的別人的,看后覺(jué)得受益匪淺,原文地址,但是文章應(yīng)該是臺(tái)灣朋友寫(xiě)的,有些專業(yè)術(shù)語(yǔ)和我們大陸叫法不同,這里簡(jiǎn)單翻譯一下。
PLT (Procedure Linkage Table) 和 GOT (Global Offset Table) 是 GCC 中生成shared library的重要元素。至于為何一定要這兩個(gè)表?
GOT的功用
以gcc內(nèi)建的libc.so 為例,因?yàn)槟悴豢赡苡玫絣ibc.so 里面所有的函數(shù),所以其實(shí)不用知道所有函數(shù)在內(nèi)存的絕對(duì)位置。其中GOT只列出你會(huì)用到的function 或者是 global variable的絕對(duì)位置。這樣會(huì)節(jié)省許多解析時(shí)間。
以下面的圖為例,圖里面是一個(gè)簡(jiǎn)化的例子,這和實(shí)際編譯情況不同,但適合說(shuō)明GOT。
當(dāng)我要從main()內(nèi)去調(diào)用 shared binary 中的foo()方法的時(shí)候,在編譯過(guò)程中(調(diào)用$gcc main.c 的時(shí)候)編譯器會(huì)生成一個(gè)可執(zhí)行文件,假設(shè)生成的可執(zhí)行文件名字為a.out,在這個(gè)生成的文件中原先的main.c 中的foo()被替換為 b @GOT+0x14 ,這行代碼的作用是,跳轉(zhuǎn)到GOT內(nèi)所記錄的位置上去,地址就是GOT表的起始地址加上0x14,內(nèi)容是 0x76fc6578,這個(gè)地址也就是foo() 在 shared library 的覺(jué)得位置。

PLT的功用
既然GOT已經(jīng)列出了需要的東西,那照理說(shuō)工作就結(jié)束了,為啥還需要PLT?
試想,當(dāng)你的程序大到和libc.so 庫(kù)一樣大的時(shí)候,你可能會(huì)調(diào)用上百個(gè)libc里面的函數(shù),所以當(dāng)你的程序加載進(jìn)內(nèi)存的時(shí)候,linker會(huì)解析你需要的函數(shù),這個(gè)過(guò)程會(huì)消耗一些時(shí)間,并導(dǎo)致使用者認(rèn)為程序運(yùn)行很慢。為了解決這個(gè)問(wèn)題,所以GCC改為在調(diào)用 共享庫(kù)(libxxx.so) 里面的函數(shù)之前,才去吧絕對(duì)位置填入到GOT里面。而PLT的功能就是調(diào)用linker 去填入GOT表里面的表項(xiàng),這個(gè)機(jī)制就是延遲綁定(lazy binding)。
要注意 lazy binding和 lazy loading的差異。Lazy loading 是通過(guò)調(diào)用dlopen()等函數(shù)將library(動(dòng)態(tài)庫(kù))動(dòng)態(tài)加載進(jìn)內(nèi)存。GCC並沒(méi)有自動(dòng)提供lazy loading的機(jī)制,所以的shared library都是一次加載進(jìn)內(nèi)存,除非你使用dlopen()。
下面用幾張圖來(lái)說(shuō)明一下:
Step 1: 呼叫 Linker
在解釋動(dòng)作前,先看一下 GOT表格,其中 GOT+0x14的內(nèi)容暫時(shí)填入 linker 的位置,這需要 linker 去解析然后回填到GOT+0x14。原先main()要調(diào)用的 foo()被替換成 "foo()@plt" 的函式,而這個(gè)函數(shù)又會(huì)跳轉(zhuǎn)到 GOT+0x14的地址去。請(qǐng)仔細(xì)看,這個(gè)地址是要跳去 linker,而非foo(),因?yàn)檫@時(shí)候 foo()的地址還沒(méi)有被解析。

Step 2: 解析 foo() 的地址
Linker "ld-2.so"會(huì)把 foo()在 shared library的絕對(duì)地址填入 GOT+0x14的內(nèi)存中。請(qǐng)注意,ld代表的意思是 Linker/Loader。

Step 3: 跳轉(zhuǎn)到 foo()
接著Linker會(huì)跳轉(zhuǎn)到foo(),大功告成

一個(gè)真實(shí)的例子的概述
上面介紹了GOT 和PLT的概念,下面搞一個(gè)實(shí)際的例子來(lái)看下結(jié)果。
例子是參考《程序員的自我修養(yǎng)--鏈接、裝載與庫(kù).pdf》這本書(shū)中的地7.3.3 節(jié)的列子。列子的代碼可以在GitHub上下載點(diǎn)我

例子雖然簡(jiǎn)單,但是目的卻很有趣,一共四個(gè):
- Type 1: Inner-module call (模塊內(nèi)部函數(shù)調(diào)用,跳轉(zhuǎn))
- Type 2: Inner-module data access(模塊內(nèi)部數(shù)據(jù)訪問(wèn),比如模塊中定義的全局變量,靜態(tài)變量等)
- Type 3: Inter-module call(模塊外部的函數(shù)調(diào)用,跳轉(zhuǎn)等)
- Type 4: Inter-module data access(模塊外部數(shù)據(jù)訪問(wèn),比如其他模塊中定義的全局變量)
在觀察這幾個(gè)例子之前,我們先來(lái)編譯這幾個(gè)c文件,然后反編譯生成的文件 (實(shí)驗(yàn)的環(huán)境是 arm cortex-a7 32bits、gcc 4.6.3)。
首先生成 Lib_a.o 和 Lib_b.o(不知道為啥,在Mac上這個(gè)東西編譯不過(guò))
$ gcc -g -shared -fPIC Lib_b.c -o Lib_b.o
$ gcc -g -shared -fPIC Lib_a.c -o Lib_a.o
然后生成可執(zhí)行文件:
$ gcc -g main.c ./Lib_a.o ./Lib_b.o
然后反編譯Lib_a.o、Lib_b.o、a.out:
$ objdump -sSdD a.out > objdump.txt
$ objdump -sSdD Lib_a.o > objdump.txt-Lib_a
$ objdump -sSdD Lib_b.o > objdump.txt-Lib_b
做完上面的準(zhǔn)備工作之后,先來(lái)看下 function call 相關(guān)的 Type1 和 Type3 的流程,也就是 inner-module call和 inter-module call。在開(kāi)始之前,我們照直觀的想法,inter-module call一定會(huì)用到GOT,而 inner-module call 因?yàn)椴恍枰D(zhuǎn),所以應(yīng)該不需要用到GOT。我們可以使用 $ readelf -r 這個(gè)命令行工具去看看 relocation section,這個(gè)section 的功能就是表示GOT表中每個(gè)表項(xiàng)的定義。
注:objdump 和 readelf 是兩個(gè)命令行工具,在Linux系統(tǒng)上可以搜索安裝,Mac下我是用的greadelf 和 gobjdump
先看 main.c的GOT
$ readelf -r a.out
Relocation section '.rel.dyn' at offset 0x41c contains 1 entries:
Offset Info Type Sym.Value Sym. Name
00010708 00000115 R_ARM_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x424 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
000106f8 00000d16 R_ARM_JUMP_SLOT 00000000 __libc_start_main
000106fc 00000116 R_ARM_JUMP_SLOT 00000000 __gmon_start__
00010700 00000516 R_ARM_JUMP_SLOT 00000000 foo
00010704 00000916 R_ARM_JUMP_SLOT 00000000 abort
-
.rel.dyn:每個(gè)表項(xiàng)對(duì)應(yīng)了除了外部過(guò)程調(diào)用的符號(hào)以外的所有重定位對(duì)象,(比如通過(guò)全局函數(shù)指針來(lái)調(diào)用外部函數(shù)) -
.rel.plt表項(xiàng)對(duì)應(yīng)了所有外部過(guò)程(function)調(diào)用符號(hào)的重定位信息
也可以從"R_ARM_GLOB_DAT" 和 "R_ARM_JUMP_SLOT" 看出來(lái)。
進(jìn)一步來(lái)看一下各個(gè)symbol:
-
__gmon_start__: 如果用gcc編譯的時(shí)候,加上-pg這個(gè)參數(shù)選項(xiàng),那么這個(gè)symbol就會(huì)起作用(比如gcc -pg main.c),具體介紹看這里 -
__libc_start_main: 這是c程序運(yùn)行之前一定會(huì)執(zhí)行的一個(gè)函數(shù),問(wèn)的是加載需要的library ,具體看這里 -
foo:這個(gè)是Lib_a.o 里面的程序 -
abort: 這是c90標(biāo)準(zhǔn)定義里面的預(yù)設(shè)function 看這里
雖然在上面查看到條目里面有很多沒(méi)遇到過(guò)的function,但是foo() 還是按照我們預(yù)期出現(xiàn)了。
接下來(lái)看 Lib_a.o的relocation section
$ readelf -r Lib_a.o
Relocation section '.rel.dyn' at offset 0x3bc contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00008598 00000017 R_ARM_RELATIVE
0000859c 00000017 R_ARM_RELATIVE
000086b8 00000017 R_ARM_RELATIVE
000086a8 00000315 R_ARM_GLOB_DAT 00000000 __cxa_finalize
000086ac 00000415 R_ARM_GLOB_DAT 00000000 b
000086b0 00000515 R_ARM_GLOB_DAT 00000000 __gmon_start__
000086b4 00000715 R_ARM_GLOB_DAT 00000000 _Jv_RegisterClasses
Relocation section '.rel.plt' at offset 0x3f4 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
00008698 00000316 R_ARM_JUMP_SLOT 00000000 __cxa_finalize
0000869c 00000a16 R_ARM_JUMP_SLOT 00000530 bar
000086a0 00000516 R_ARM_JUMP_SLOT 00000000 __gmon_start__
000086a4 00000616 R_ARM_JUMP_SLOT 00000000 ext
和上面a.out 相比這里少了abort() ,但是出現(xiàn)了一些新的東西:
- __cxa_finalize : 當(dāng)shared library unload時(shí),會(huì)調(diào)用他。(參考資料)
- b : 這是Lib_b.o 內(nèi)部全局變量
- bar: 這是Lib_a.o 內(nèi)的function
- ext : 這是Lib_b.o 內(nèi)部function
有趣的是,即便bar() 定義在Lib_a.o 內(nèi),也需要GOT,和之前猜測(cè)不一樣哦,所以"Type 2: Inner-module data access"是需要GOT的。另外,變量 "static int a" 并沒(méi)有在GOT內(nèi),非常合理。
我們繼續(xù)看最后一個(gè) 動(dòng)態(tài)庫(kù),"Lib_b.o"的relocation section:
$ readelf -r Lib_b.o
Relocation section '.rel.dyn' at offset 0x3a4 contains 7 entries:
Offset Info Type Sym.Value Sym. Name
00008594 00000017 R_ARM_RELATIVE
00008598 00000017 R_ARM_RELATIVE
000086b0 00000017 R_ARM_RELATIVE
000086a0 00000315 R_ARM_GLOB_DAT 00000000 __cxa_finalize
000086a4 00000e15 R_ARM_GLOB_DAT 000086b8 b
000086a8 00000515 R_ARM_GLOB_DAT 00000000 __gmon_start__
000086ac 00000615 R_ARM_GLOB_DAT 00000000 _Jv_RegisterClasses
Relocation section '.rel.plt' at offset 0x3dc contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00008694 00000316 R_ARM_JUMP_SLOT 00000000 __cxa_finalize
00008698 00000416 R_ARM_JUMP_SLOT 00000000 printf
0000869c 00000516 R_ARM_JUMP_SLOT 00000000 __gmon_start__
其余的符號(hào)不解釋了,只看我們感興趣的兩個(gè):
- b : Lib_b.o本身的全域變量
- printf : libc提供的function
即便 int b就在Lib_b.o內(nèi),也需要GOT來(lái)存取。
跟蹤反編譯后的代碼 ( main.c)
實(shí)際 Trace Code 來(lái)看看 GOT + PLT 的用途
先看 main.c的反編譯結(jié)果
513 00008540 <main>:
514 #include <stdio.h>
515 #include "Lib_a.h"
516
517 int main(int argc, char* argv[])
518 {
519 8540: e92d4800 push {fp, lr}
520 8544: e28db004 add fp, sp, #4
521 8548: e24dd008 sub sp, sp, #8
522 854c: e50b0008 str r0, [fp, #-8]
523 8550: e50b100c str r1, [fp, #-12]
524 foo();
525 8554: ebffffc8 bl 847c <foo@plt>
526 }
527 8558: e1a00003 mov r0, r3
528 855c: e24bd004 sub sp, fp, #4
529 8560: e8bd8800 pop {fp, pc}
Line 525 可以看到為了調(diào)用foo()直接跳到0x847c的位置,但是注解寫(xiě)的function名稱是foo@plt,有點(diǎn)奇怪。不過(guò)直接去看0x847c
450 0000847c <foo@plt>:
451 847c: e28fc600 add ip, pc, #0, 12
452 8480: e28cca08 add ip, ip, #8, 20 ; 0x8000
453 8484: e5bcf27c ldr pc, [ip, #636]! ; 0x27c
這個(gè)arm的代碼有點(diǎn)煩,不過(guò)一行行解讀就行了
Line 451: add ip, pc, #0, 12
其中pc指的是下兩行指令的地址,也就是 Line 453標(biāo)注的位置 0x8484。整個(gè)指令的作用為 "ip = pc + 0x0 << 12",所以 ip = 0x8484 + 0x0 = 0x8484。
稍微解釋一下:因?yàn)?arm 處理器使用 3 級(jí)流水線(取指,譯碼,執(zhí)行),所以無(wú)論處理器處于何種狀態(tài),程序計(jì)數(shù)器R15(PC)總是指向“正在取指”的指令,而不是指向“正在執(zhí)行”的指令或者正在“譯碼”的指令。
處理器處于ARM狀態(tài)時(shí),每條指令為4個(gè)字節(jié),所以PC值為正在執(zhí)行的指令地址加8字節(jié),即是:PC值 = 當(dāng)前程序執(zhí)行位置 + 8字節(jié);
處理器處于Thumb狀態(tài)時(shí),每條指令為2字節(jié),所以PC值為正在執(zhí)行的指令地址加4字節(jié),即是:PC值 = 當(dāng)前程序執(zhí)行位置 + 4字節(jié)。
人們一般會(huì)習(xí)慣性的將正在執(zhí)行的指令作為參考點(diǎn),即當(dāng)前第1條指令。所以,PC總是指向第3條指令,或者說(shuō)PC總是指向當(dāng)前正在執(zhí)行的指令地址再加2條指令的地址。
接著往下一行看:
Line 452: add ip, ip, #8, 20
指令等價(jià)于 "ip = ip + 0x8 << 20",因?yàn)槲沂褂玫臋C(jī)器是 32bit arm cortex-a7,所以向右做circular bit shift等于是向右位移 (32-20 = 12) bit,所以指令變?yōu)?"ip = ip + 0x8 << 12 = ip + 0x8000 = 0x8484 + 0x8000 = 0x10484"
再往下一行看
Line 453: ldr pc, [ip, #636]!
pc = [ip + d'636] = [0x10484 + d'636] = [0x10484+0x27c] =[0x10700]
看一下0x10700內(nèi)存的值是什么:
126 Contents of section .got:
127 106ec ec050100 00000000 00000000 50840000 ............P...
128 106fc 50840000 50840000 50840000 00000000 P...P...P.......
所以 [0x10700]是0x8450,注意這是little endian的排列方式。所以pc會(huì)載入0x8450嗎??
記得這只是反編譯的內(nèi)容,而非 linker載入程序后的結(jié)果,有可能linker會(huì)去修改GOT內(nèi)的值,保險(xiǎn)起見(jiàn),還是通過(guò) gdb去看看這個(gè)值。