description: "本文詳細(xì)介紹了 Linux 下 C 語言共享庫的位置無關(guān)(PIC)實(shí)現(xiàn)原理。"
背景簡(jiǎn)介
吳章金:如何創(chuàng)建一個(gè)*可執(zhí)行*的共享庫一文談完了如何讓共享庫可直接執(zhí)行,本文再來談?wù)劰蚕韼斓倪\(yùn)行時(shí)位置無關(guān)(PIC)是如何做到的。
PIC = position independent code
-fpic Generate position-independent code (PIC) suitable for use in a shared library
共享庫有一個(gè)很重要的特征,就是可以被多個(gè)可執(zhí)行文件共享,以達(dá)到節(jié)省磁盤和內(nèi)存空間的目標(biāo):
共享意味著不僅磁盤上只有一份拷貝,加載到內(nèi)存以后也只有一份拷貝,那么代碼部分在運(yùn)行時(shí)也不能被修改,否則就得有多個(gè)拷貝存在
同時(shí)意味著,需要能夠靈活映射在不同的虛擬地址空間,以便適應(yīng)不同程序,避免地址沖突
這兩點(diǎn)要求共享庫的代碼和數(shù)據(jù)都是位置無關(guān)的,接下來先看看什么是“位置無關(guān)”。
什么是位置無關(guān)
同樣以 hello.c 為例:
#includeintmain(void) { printf("hello "); return0; }
以普通的方式來編譯并反匯編一個(gè)可執(zhí)行文件看看:
$gcc-m32-ohellohello.c $objdump-dhello|grep-B1"call.*puts@plt>" 8048416:68b0840408push$0x80484b0 804841b:e8c0feffffcall80482e0
可以看到上面?zhèn)鬟f給puts(printf)的字符串地址是“寫死的”,在編譯時(shí)就是確定的,這意味著 Load Address 也必須是固定的:
$readelf-lhello|grepLOAD|head-1 LOAD0x0000000x080480000x080480000x005b00x005b0RE0x1000
上面可以看到 Load Address 為 0x8048000。
如果 Load Address 改變,數(shù)據(jù)地址就指向別的內(nèi)容了,這就是“位置有關(guān)”。
共享庫的話,必須摒棄這種“寫死的”地址,要做到“位置無關(guān)”(注:prelink 是特殊需求,暫且不表)。
如何做到位置無關(guān)(Part1)
位置無關(guān),意味著運(yùn)行時(shí)可以靈活調(diào)整 Load Address,當(dāng) Load Address 在運(yùn)行時(shí)發(fā)生改變后,代碼還能被執(zhí)行到,數(shù)據(jù)也能被正確訪問。
那么代碼和數(shù)據(jù)都變成跟 Load Address 相關(guān)的,不能再是絕對(duì)地址,而需要采用某個(gè)相對(duì) Load Address 的地址。
動(dòng)態(tài)鏈接器會(huì)負(fù)責(zé)找到可執(zhí)行文件的共享庫并裝載它們,所以動(dòng)態(tài)鏈接器是知道這個(gè) Load Address 的,那么函數(shù)符號(hào)其實(shí)是很容易確定的,來看看不帶-fpic時(shí)編譯生成一個(gè)共享庫:
查看main函數(shù)的初始地址
$gcc-m32-shared-olibhello.sohello.c $objdump-dlibhello.so|grep-A2"main>:" 000004a9: 4a9:8d4c2404lea0x4(%esp),%ecx 4ad:83e4f0and$0xfffffff0,%esp
查看“裝載地址”,編譯后初始化為 0
$readelf-llibhello.so|grepLOAD|head-1 LOAD0x0000000x000000000x000000000x0057c0x0057cRE0x1000
確認(rèn)main在文件中的偏移
$readelf--dyn-symslibhello.so|grepm Symboltable'.dynsym'contains12entries: Num:ValueSizeTypeBindVisNdxName 4:000000000NOTYPEWEAKDEFAULTUND__gmon_start__ 9:000004a946FUNCGLOBALDEFAULT11main $hexdump-C-s$((0x4a9))-n10libhello.so 000004a98d4c240483e4f0ff71fc|.L$.....q.| 000004b3
可以看到,對(duì)于main而言,無論把共享庫裝載到哪里,動(dòng)態(tài)鏈接器總能根據(jù) Load Address 以及.dynsym中的偏移把main的運(yùn)行時(shí)地址算出來(見 glibc:_dl_fixup)。
但是,這個(gè)時(shí)候(不用-fpic的話),數(shù)據(jù)地址也是“寫死的”:
$objdump-dlibhello.so|grep-B1"call.*main" 4bd:68ec040000push$0x4ec 4c2:e8fcffffffcall4c3
作為對(duì)比,來看看加上-fpic的效果:
$gcc-m32-shared-fpic-olibhello.sohello.c $objdump-drlibhello.so|grep-B6"call.*puts@plt>" 4c8:e828000000call4f5<__x86.get_pc_thunk.ax> 4cd:05331b0000add$0x1b33,%eax 4d2:83ec0csub$0xc,%esp 4d5:8d9010e5fffflea-0x1af0(%eax),%edx 4db:52push%edx 4dc:89c3mov%eax,%ebx 4de:e8bdfeffffcall3a0
可以看到,用上-fpic以后,傳遞給 puts 的數(shù)據(jù)地址(push %edx)已經(jīng)是通過動(dòng)態(tài)計(jì)算的,那是怎么算的呢?
上面有個(gè)內(nèi)聯(lián)進(jìn)來的函數(shù)很關(guān)鍵:
$objdump-drlibhello.so|grep-A3"__x86.get_pc_thunk.ax>:" 000004f5<__x86.get_pc_thunk.ax>: 4f5:8b0424mov(%esp),%eax 4f8:c3ret
這個(gè)函數(shù)賊簡(jiǎn)單,從棧頂取了一個(gè)數(shù)據(jù)就跳回去了,取的數(shù)據(jù)是什么呢?這就要了解調(diào)用它的call指令了。
call指令會(huì)把下一條指令的eip壓棧然后 jump 到目標(biāo)地址:
callbackward==>pusheip; jmpbackward
所以,數(shù)據(jù)地址是運(yùn)行時(shí)計(jì)算的,跟運(yùn)行時(shí)的 “eip” 給關(guān)聯(lián)上了。
不難猜測(cè),如果知道當(dāng)前指令的位置,又提前保存了數(shù)據(jù)離當(dāng)前位置的偏移,那么數(shù)據(jù)地址是可以直接計(jì)算的,只是上面那一段代碼還是略微復(fù)雜了,因?yàn)橛幸欢?“Magic Number”。
不管怎么樣,先來模擬計(jì)算一下,假設(shè)裝載到的地址就是 0x0,那么執(zhí)行到add指令時(shí)存到 eax 的 eip,恰好是call返回后下一條指令的地址,即 0x4cd:
4c8:e828000000call4f5<__x86.get_pc_thunk.ax> 4cd:05331b0000add$0x1b33,%eax 4d5:8d9010e5fffflea-0x1af0(%eax),%edx
根據(jù)上述指令,那么%edx計(jì)算出來就是 0x510:
$echo"obase=16;$((0x4cd+0x1b33-0x1af0))"|bc 510
再去取數(shù)據(jù):
$hexdump-C-s$((0x510))-n10libhello.so 0000051068656c6c6f000000011b|hello.....| 0000051a
果然是字符串的地址,所以,相對(duì)偏移其實(shí)被拆分成了兩部分:0x1b33和-0x1af0。兩個(gè) "Magic Number" 一加就出來了。
所以,小結(jié)一下,“位置無關(guān)” 是通過運(yùn)行時(shí)動(dòng)態(tài)獲取 “eip” 并加上一個(gè)編譯時(shí)記錄好的偏移計(jì)算出來的,這樣的話,無論加載到什么位置,都能訪問到數(shù)據(jù)。
如何做到位置無關(guān)(Part2)
這對(duì) “Magic Number” 還是需要再看一看,既然是編譯時(shí)確定的,看看匯編狀態(tài)是怎么回事:
$gcc-m32-shared-fpic-Shello.c $cathello.s|grep-v.cfi ... .LC0: .string"hello" .text .globlmain .typemain,@function main: .LFB0: leal4(%esp),%ecx andl$-16,%esp pushl-4(%ecx) pushl%ebp movl%esp,%ebp pushl%ebx pushl%ecx call__x86.get_pc_thunk.ax addl$_GLOBAL_OFFSET_TABLE_,%eax subl$12,%esp leal.LC0@GOTOFF(%eax),%edx pushl%edx movl%eax,%ebx callputs@PLT ...
從 i386 的 archABI 不難找到這塊的定義(P61~P62),name@GOTOFF(%eax)直接表示 name 符號(hào)相對(duì) %eax 保存的 GOT 的偏移地址。
首先,編譯時(shí)要計(jì)算$_GLOBAL_OFFSET_TABLE和.LC0@GOTOFF。
$_GLOBAL_OFFSET_TABLE_為 GOT 相對(duì)eip的偏移,可計(jì)算為:
>
$_GLOBAL_OFFSET_TABLE_ = .got.plt - eip
計(jì)算過程如下:
$readelf-Slibhello.so|grep.got.plt [21].got.pltPROGBITS0000200000100000001004WA004 $echo"obase=16;$((0x2000-0x4cd))"|bc 1B33
接著,計(jì)算.LC0@GOTOFF:
.LC0 - eip =GLOBAL_OFFSET_TABLE+ .LC0@GOTOFF .LC0@GOTOFF = .LC0 - eip -GLOBALOFFSETTABLE+.LC0@GOTOFF.LC0@GOTOFF=.LC0?eip?GLOBAL_OFFSET_TABLE
計(jì)算過程如下:
$echo"obase=16;$((0x510-0x4cd-0x1B33))"|bc -1AF0
反過來,運(yùn)行時(shí)的計(jì)算公式為:
.LC0 =GLOBAL_OFFSET_TABLE+ .LC0@GOTOFF + eip
.LC0 = 0x1B33 + (-1AF0) + eip
.got.plt =GLOBALOFFSETTABLE+.LC0@GOTOFF+eip.LC0=0x1B33+(?1AF0)+eip.got.plt=GLOBAL_OFFSET_TABLE+ eip
.got.plt = 0x1B33 + eip
實(shí)際上,只有 .got.plt 的地址,即ebx需要$_GLOBAL_OFFSET_TABLE_來計(jì)算,這個(gè)是用來做動(dòng)態(tài)地址重定位的,暫且不表。
.LC0的地址,完全可以換一種方式,直接用.LC0到 eip 的偏移即可,匯編代碼改造完如下:
call__x86.get_pc_thunk.ax .eip: #計(jì)算eip+(.LC0-.eip)剛好指向內(nèi)存中的數(shù)據(jù)"hello"所在位置 movl%eax,%ebx leal(.LC0-.eip)(%eax),%edx #計(jì)算 .got.plt 地址,_GLOBAL_OFFSET_TABLE_是相對(duì) eip 的偏移,所以必須加上這個(gè) offset:. - .eip addl$_GLOBAL_OFFSET_TABLE_+[.-.eip],%ebx subl$12,%esp pushl%edx callputs@PLT
驗(yàn)證結(jié)果:
$gcc-m32-g-shared-fpic-olibhello.sohello.s $gcc-m32-g-ohello.noc-L./-lhello $LD_LIBRARY_PATH=$LD_LIBRARY_PATH:././hello.noc hello
小結(jié)
本文詳細(xì)介紹了 Linux 下 C 語言共享庫“位置無關(guān)”(PIC)的核心實(shí)現(xiàn)原理:即用 EIP 相對(duì)地址來取代絕對(duì)地址。
“位置無關(guān)” 代碼會(huì)帶來很大的內(nèi)存使用靈活性,也會(huì)帶來一定的安全性,因?yàn)椤拔恢脽o關(guān)”以后就可以帶來加載地址的隨機(jī)性,給代碼注入帶來一定的難度。
由于有上述好處,各大平臺(tái)的 gcc 都開始默認(rèn)打開可執(zhí)行文件的-pie -fpie了,因?yàn)?gcc 編譯時(shí)開啟了:--enable-default-pie。這也可能導(dǎo)致一些“衰退”,大家可以根據(jù)需要關(guān)閉它:-no-pie,-fno-pie。
當(dāng)然,共享庫的實(shí)現(xiàn)精髓不止于此,最核心的還是函數(shù)符號(hào)地址的動(dòng)態(tài)解析過程,而這些則跟上面的.got.plt地址密切相關(guān),受限于篇幅,暫時(shí)不做詳細(xì)展開。
-
Linux
+關(guān)注
關(guān)注
87文章
11310瀏覽量
209620 -
C語言
+關(guān)注
關(guān)注
180文章
7605瀏覽量
136934 -
main
+關(guān)注
關(guān)注
0文章
38瀏覽量
6168
原文標(biāo)題:吳章金: 深度剖析 Linux共享庫的“位置無關(guān)”實(shí)現(xiàn)原理
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論