動態(tài)庫(Shared Library)中 PLT 和 GOT 的使用機(jī)制

開篇宿百,首先這篇文章是翻譯的別人的,看后覺得受益匪淺洪添,原文地址垦页,但是文章應(yīng)該是臺灣朋友寫的,有些專業(yè)術(shù)語和我們大陸叫法不同干奢,這里簡單翻譯一下痊焊。

PLT (Procedure Linkage Table) 和 GOT (Global Offset Table) 是 GCC 中生成shared library的重要元素。至于為何一定要這兩個表?

GOT的功用

以gcc內(nèi)建的libc.so 為例忿峻,因為你不可能用到libc.so 里面所有的函數(shù)薄啥,所以其實不用知道所有函數(shù)在內(nèi)存的絕對位置。其中GOT只列出你會用到的function 或者是 global variable的絕對位置逛尚。這樣會節(jié)省許多解析時間垄惧。

以下面的圖為例,圖里面是一個簡化的例子绰寞,這和實際編譯情況不同到逊,但適合說明GOT。
當(dāng)我要從main()內(nèi)去調(diào)用 shared binary 中的foo()方法的時候滤钱,在編譯過程中(調(diào)用$gcc main.c 的時候)編譯器會生成一個可執(zhí)行文件觉壶,假設(shè)生成的可執(zhí)行文件名字為a.out,在這個生成的文件中原先的main.c 中的foo()被替換為 b @GOT+0x14 ,這行代碼的作用是件缸,跳轉(zhuǎn)到GOT內(nèi)所記錄的位置上去铜靶,地址就是GOT表的起始地址加上0x14,內(nèi)容是 0x76fc6578停团,這個地址也就是foo() 在 shared library 的覺得位置旷坦。

image.png

PLT的功用

既然GOT已經(jīng)列出了需要的東西掏熬,那照理說工作就結(jié)束了,為啥還需要PLT?
試想秒梅,當(dāng)你的程序大到和libc.so 庫一樣大的時候旗芬,你可能會調(diào)用上百個libc里面的函數(shù),所以當(dāng)你的程序加載進(jìn)內(nèi)存的時候捆蜀,linker會解析你需要的函數(shù)疮丛,這個過程會消耗一些時間,并導(dǎo)致使用者認(rèn)為程序運行很慢辆它。為了解決這個問題誊薄,所以GCC改為在調(diào)用 共享庫(libxxx.so) 里面的函數(shù)之前,才去吧絕對位置填入到GOT里面锰茉。而PLT的功能就是調(diào)用linker 去填入GOT表里面的表項呢蔫,這個機(jī)制就是延遲綁定(lazy binding)。

要注意 lazy binding和 lazy loading的差異飒筑。Lazy loading 是通過調(diào)用dlopen()等函數(shù)將library(動態(tài)庫)動態(tài)加載進(jìn)內(nèi)存片吊。GCC並沒有自動提供lazy loading的機(jī)制,所以的shared library都是一次加載進(jìn)內(nèi)存协屡,除非你使用dlopen()俏脊。

下面用幾張圖來說明一下:

Step 1: 呼叫 Linker
在解釋動作前,先看一下 GOT表格肤晓,其中 GOT+0x14的內(nèi)容暫時填入 linker 的位置爷贫,這需要 linker 去解析然后回填到GOT+0x14。原先main()要調(diào)用的 foo()被替換成 "foo()@plt" 的函式补憾,而這個函數(shù)又會跳轉(zhuǎn)到 GOT+0x14的地址去漫萄。請仔細(xì)看,這個地址是要跳去 linker余蟹,而非foo()卷胯,因為這時候 foo()的地址還沒有被解析。

Step 1.png

Step 2: 解析 foo() 的地址

Linker "ld-2.so"會把 foo()在 shared library的絕對地址填入 GOT+0x14的內(nèi)存中威酒。請注意,ld代表的意思是 Linker/Loader挺峡。

Step 2.png

Step 3: 跳轉(zhuǎn)到 foo()

接著Linker會跳轉(zhuǎn)到foo(),大功告成

Step 3.png

一個真實的例子的概述

上面介紹了GOT 和PLT的概念葵孤,下面搞一個實際的例子來看下結(jié)果。
例子是參考《程序員的自我修養(yǎng)--鏈接橱赠、裝載與庫.pdf》這本書中的地7.3.3 節(jié)的列子尤仍。列子的代碼可以在GitHub上下載點我

例子.png

例子雖然簡單,但是目的卻很有趣狭姨,一共四個:

  • Type 1: Inner-module call (模塊內(nèi)部函數(shù)調(diào)用宰啦,跳轉(zhuǎn))
  • Type 2: Inner-module data access(模塊內(nèi)部數(shù)據(jù)訪問苏遥,比如模塊中定義的全局變量,靜態(tài)變量等)
  • Type 3: Inter-module call(模塊外部的函數(shù)調(diào)用赡模,跳轉(zhuǎn)等)
  • Type 4: Inter-module data access(模塊外部數(shù)據(jù)訪問田炭,比如其他模塊中定義的全局變量)

在觀察這幾個例子之前,我們先來編譯這幾個c文件漓柑,然后反編譯生成的文件 (實驗的環(huán)境是 arm cortex-a7 32bits教硫、gcc 4.6.3)。

首先生成 Lib_a.o 和 Lib_b.o(不知道為啥辆布,在Mac上這個東西編譯不過)

$ 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)備工作之后锋玲,先來看下 function call 相關(guān)的 Type1 和 Type3 的流程景用,也就是 inner-module call和 inter-module call。在開始之前惭蹂,我們照直觀的想法丛肢,inter-module call一定會用到GOT,而 inner-module call 因為不需要跳轉(zhuǎn)剿干,所以應(yīng)該不需要用到GOT蜂怎。我們可以使用 $ readelf -r 這個命令行工具去看看 relocation section,這個section 的功能就是表示GOT表中每個表項的定義。

注:objdump 和 readelf 是兩個命令行工具置尔,在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 :每個表項對應(yīng)了除了外部過程調(diào)用的符號以外的所有重定位對象,(比如通過全局函數(shù)指針來調(diào)用外部函數(shù))
  • .rel.plt表項對應(yīng)了所有外部過程(function)調(diào)用符號的重定位信息
    也可以從"R_ARM_GLOB_DAT" 和 "R_ARM_JUMP_SLOT" 看出來。

進(jìn)一步來看一下各個symbol:

  1. __gmon_start__: 如果用gcc編譯的時候榜轿,加上-pg這個參數(shù)選項幽歼,那么這個symbol就會起作用(比如 gcc -pg main.c),具體介紹看這里
  2. __libc_start_main: 這是c程序運行之前一定會執(zhí)行的一個函數(shù)谬盐,問的是加載需要的library 甸私,具體看這里
  3. foo:這個是Lib_a.o 里面的程序
  4. abort: 這是c90標(biāo)準(zhǔn)定義里面的預(yù)設(shè)function 看這里

雖然在上面查看到條目里面有很多沒遇到過的function,但是foo() 還是按照我們預(yù)期出現(xiàn)了飞傀。

接下來看 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)了一些新的東西:

  1. __cxa_finalize : 當(dāng)shared library unload時,會調(diào)用他砸烦。(參考資料)
  2. b : 這是Lib_b.o 內(nèi)部全局變量
  3. bar: 這是Lib_a.o 內(nèi)的function
  4. ext : 這是Lib_b.o 內(nèi)部function

有趣的是弃鸦,即便bar() 定義在Lib_a.o 內(nèi),也需要GOT幢痘,和之前猜測不一樣哦唬格,所以"Type 2: Inner-module data access"是需要GOT的。另外,變量 "static int a" 并沒有在GOT內(nèi)购岗,非常合理汰聋。

我們繼續(xù)看最后一個 動態(tài)庫,"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__

其余的符號不解釋了喊积,只看我們感興趣的兩個:

  1. b : Lib_b.o本身的全域變量
  2. printf : libc提供的function

即便 int b就在Lib_b.o內(nèi)烹困,也需要GOT來存取。

跟蹤反編譯后的代碼 ( main.c)

實際 Trace Code 來看看 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的位置注服,但是注解寫的function名稱是foo@plt韭邓,有點奇怪。不過直接去看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

這個arm的代碼有點煩溶弟,不過一行行解讀就行了

Line 451: add ip, pc, #0, 12
其中pc指的是下兩行指令的地址女淑,也就是 Line 453標(biāo)注的位置 0x8484。整個指令的作用為 "ip = pc + 0x0 << 12"辜御,所以 ip = 0x8484 + 0x0 = 0x8484鸭你。

稍微解釋一下:因為 arm 處理器使用 3 級流水線(取指,譯碼擒权,執(zhí)行)袱巨,所以無論處理器處于何種狀態(tài),程序計數(shù)器R15(PC)總是指向“正在取指”的指令碳抄,而不是指向“正在執(zhí)行”的指令或者正在“譯碼”的指令愉老。

處理器處于ARM狀態(tài)時,每條指令為4個字節(jié)剖效,所以PC值為正在執(zhí)行的指令地址加8字節(jié)嫉入,即是:PC值 = 當(dāng)前程序執(zhí)行位置 + 8字節(jié);
處理器處于Thumb狀態(tài)時,每條指令為2字節(jié)璧尸,所以PC值為正在執(zhí)行的指令地址加4字節(jié)咒林,即是:PC值 = 當(dāng)前程序執(zhí)行位置 + 4字節(jié)。
人們一般會習(xí)慣性的將正在執(zhí)行的指令作為參考點爷光,即當(dāng)前第1條指令垫竞。所以,PC總是指向第3條指令蛀序,或者說PC總是指向當(dāng)前正在執(zhí)行的指令地址再加2條指令的地址欢瞪。

接著往下一行看:
Line 452: add ip, ip, #8, 20
指令等價于 "ip = ip + 0x8 << 20",因為我使用的機(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會載入0x8450嗎??
記得這只是反編譯的內(nèi)容檬姥,而非 linker載入程序后的結(jié)果曾我,有可能linker會去修改GOT內(nèi)的值,保險起見健民,還是通過 gdb去看看這個值抒巢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市秉犹,隨后出現(xiàn)的幾起案子蛉谜,更是在濱河造成了極大的恐慌,老刑警劉巖崇堵,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件型诚,死亡現(xiàn)場離奇詭異,居然都是意外死亡鸳劳,警方通過查閱死者的電腦和手機(jī)狰贯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赏廓,“玉大人涵紊,你說我怎么就攤上這事♂C” “怎么了摸柄?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長既忆。 經(jīng)常有香客問我驱负,道長,這世上最難降的妖魔是什么尿贫? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任电媳,我火速辦了婚禮,結(jié)果婚禮上庆亡,老公的妹妹穿的比我還像新娘妄帘。我一直安慰自己,他們只是感情好岂贩,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布妈踊。 她就那樣靜靜地躺著,像睡著了一般彰亥。 火紅的嫁衣襯著肌膚如雪咧七。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天任斋,我揣著相機(jī)與錄音继阻,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛瘟檩,可吹牛的內(nèi)容都是我干的抹缕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼墨辛,長吁一口氣:“原來是場噩夢啊……” “哼卓研!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起睹簇,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤奏赘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后太惠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磨淌,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年垛叨,在試婚紗的時候發(fā)現(xiàn)自己被綠了伦糯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡嗽元,死狀恐怖敛纲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剂癌,我是刑警寧澤淤翔,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站佩谷,受9級特大地震影響旁壮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谐檀,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一抡谐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桐猬,春花似錦麦撵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惫撰,卻和暖如春羔沙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厨钻。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工扼雏, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留坚嗜,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓呢蛤,卻偏偏與公主長得像惶傻,于是被迫代替她去往敵國和親棍郎。 傳聞我的和親對象是個殘疾皇子其障,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容