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