0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux下C語言共享庫的位置無關(guān)實(shí)現(xiàn)原理分析

Linux閱碼場(chǎng) ? 來源:未知 ? 2019-11-28 16:20 ? 次閱讀

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 為例:

#include

intmain(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ì)展開。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 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)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    C語言-文件編程

    這篇文章介紹C語言的文件編程函數(shù),案例代碼是在Linux環(huán)境運(yùn)行測(cè)試的分別介紹了C語言標(biāo)準(zhǔn)
    的頭像 發(fā)表于 09-09 11:33 ?2018次閱讀

    Linux操作系統(tǒng)-C語言編程入門-pdf

    Linux操作系統(tǒng)-C語言編程入門介紹在LINUX 進(jìn)行C
    發(fā)表于 12-08 09:55 ?193次下載
    <b class='flag-5'>Linux</b>操作系統(tǒng)-<b class='flag-5'>C</b><b class='flag-5'>語言</b>編程入門-pdf

    linuxc語言編程pdf

    linuxc語言編程內(nèi)容為::基礎(chǔ)知識(shí),進(jìn)程介紹,文件操作,時(shí)間概念,信號(hào)處理,消息管理,線程操作,網(wǎng)絡(luò)編程,Linux
    發(fā)表于 12-08 10:00 ?0次下載

    Linux系統(tǒng)共享編程

    一、說明 類似Windows系統(tǒng)中的動(dòng)態(tài)鏈接,Linux中也有相應(yīng)的共享用以支持代碼的復(fù)用。Windows中為*.dll,而Linux
    發(fā)表于 09-13 16:49 ?24次下載

    Linux靜態(tài)和動(dòng)態(tài)共享)的制作與使用

    Linux靜態(tài)和動(dòng)態(tài)共享)的制作與使用Linux
    發(fā)表于 07-09 14:39 ?1178次閱讀

    LINUX環(huán)境CLIPS動(dòng)態(tài)鏈接實(shí)現(xiàn)方法

    LINUX環(huán)境,為了簡(jiǎn)便、快捷地制作出CLIPS動(dòng)態(tài)鏈接,本文采用了CNU AUTOTOOLS把CLIPS嵌入式高級(jí)語言編譯成動(dòng)態(tài)鏈接
    發(fā)表于 04-14 21:18 ?30次下載

    LinuxC語言編程概述

    分享到:標(biāo)簽:C語言編程 Linux 編譯鏈接器 調(diào)試器 操作系統(tǒng) 3.1 LinuxC
    發(fā)表于 10-18 14:36 ?0次下載
    <b class='flag-5'>Linux</b><b class='flag-5'>下</b><b class='flag-5'>C</b><b class='flag-5'>語言</b>編程概述

    基于Linux操作系統(tǒng)C語言編程入門

    基于Linux操作系統(tǒng)C語言編程入門
    發(fā)表于 10-27 15:36 ?11次下載
    基于<b class='flag-5'>Linux</b>操作系統(tǒng)<b class='flag-5'>下</b><b class='flag-5'>C</b><b class='flag-5'>語言</b>編程入門

    linux靜態(tài)和動(dòng)態(tài)分析

    的二進(jìn)制是不兼容的。 本文僅限于介紹linux。 2.的種類 linux
    發(fā)表于 11-02 10:12 ?1次下載

    Linux操作系統(tǒng)C語言編程入門.pdf

    Linux操作系統(tǒng)C語言編程入門
    發(fā)表于 05-17 10:08 ?96次下載

    Linux的常用C函數(shù)中文手冊(cè)免費(fèi)下載

    本文檔的主要內(nèi)容詳細(xì)介紹的是Linux的常用C函數(shù)中文手冊(cè)免費(fèi)下載,包含幾乎所有LinuxC
    發(fā)表于 10-28 08:00 ?8次下載
    <b class='flag-5'>Linux</b>的常用<b class='flag-5'>C</b>函數(shù)<b class='flag-5'>庫</b>中文手冊(cè)免費(fèi)下載

    LinuxC語言編程入門教程詳細(xì)說明

    本文是Linux C 語言編程入門教程。主要介紹了Linux 的發(fā)展與特點(diǎn)、C
    發(fā)表于 08-25 18:05 ?39次下載
    <b class='flag-5'>Linux</b><b class='flag-5'>下</b><b class='flag-5'>C</b><b class='flag-5'>語言</b>編程入門教程詳細(xì)說明

    C++基礎(chǔ)語法知識(shí)之鏈接裝載Linux共享

    Linux共享(Shared Library) Linux 共享
    的頭像 發(fā)表于 11-01 10:15 ?2911次閱讀

    Linux中的靜態(tài)共享

    是一個(gè)二進(jìn)制文件,包含的代碼可被程序調(diào)用。例如標(biāo)準(zhǔn)C、數(shù)學(xué)、線程等等。有源碼,可下載后
    的頭像 發(fā)表于 05-10 09:34 ?1046次閱讀

    C 語言的頭文件路徑位置問題

    的朋友們來說,一些系統(tǒng)的文件路徑根本就不知道在什么地方。 所以本文我們就來聊一 C 語言的頭文件路徑相關(guān)的問題 ,包括系統(tǒng)路徑位置,絕
    的頭像 發(fā)表于 06-22 10:05 ?6308次閱讀
    <b class='flag-5'>C</b> <b class='flag-5'>語言</b>的頭文件路徑<b class='flag-5'>位置</b>問題