description: 本文详细介绍了 linux 下 c 语言共享库的位置无关(pic)实现原理。
背景简介
吴章金:如何创建一个*可执行*的共享库一文谈完了如何让共享库可直接执行,本文再来谈谈共享库的运行时位置无关(pic)是如何做到的。
pic = position independent code
-fpic generate position-independent code (pic) suitable for use in a shared library
共享库有一个很重要的特征,就是可以被多个可执行文件共享,以达到节省磁盘和内存空间的目标:
共享意味着不仅磁盘上只有一份拷贝,加载到内存以后也只有一份拷贝,那么代码部分在运行时也不能被修改,否则就得有多个拷贝存在
同时意味着,需要能够灵活映射在不同的虚拟地址空间,以便适应不同程序,避免地址冲突
这两点要求共享库的代码和数据都是位置无关的,接下来先看看什么是“位置无关”。
什么是位置无关
同样以 hello.c 为例:
#include
intmain(void)
{
printf(hello
);
return0;
}
以普通的方式来编译并反汇编一个可执行文件看看:
$gcc-m32-ohellohello.c
$objdump-dhello|grep-b1call.*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-a2main>:
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-b1call.*main
4bd:68ec040000push$0x4ec
4c2:e8fcffffffcall4c3
作为对比,来看看加上-fpic的效果:
$gcc-m32-shared-fpic-olibhello.sohello.c
$objdump-drlibhello.so|grep-b6call.*puts@plt>
4c8:e828000000call4f5
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:
4f5:8b0424mov(%esp),%eax
4f8:c3ret
这个函数贼简单,从栈顶取了一个数据就跳回去了,取的数据是什么呢?这就要了解调用它的call指令了。
call指令会把下一条指令的eip压栈然后 jump 到目标地址:
callbackward==>pusheip;
jmpbackward
所以,数据地址是运行时计算的,跟运行时的 “eip” 给关联上了。
不难猜测,如果知道当前指令的位置,又提前保存了数据离当前位置的偏移,那么数据地址是可以直接计算的,只是上面那一段代码还是略微复杂了,因为有一堆 “magic number”。
不管怎么样,先来模拟计算一下,假设装载到的地址就是 0x0,那么执行到add指令时存到 eax 的 eip,恰好是call返回后下一条指令的地址,即 0x4cd:
4c8:e828000000call4f5
4cd:05331b0000add$0x1b33,%eax
4d5:8d9010e5fffflea-0x1af0(%eax),%edx
根据上述指令,那么%edx计算出来就是 0x510:
$echoobase=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:
.stringhello
.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
$echoobase=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
计算过程如下:
$echoobase=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地址密切相关,受限于篇幅,暂时不做详细展开。
海底捞正在通过部署机器人来提高特色的高质量服务
河套IT WALK——科技创新无处不在,百度无人驾驶、硅光子MEMS等领域引领潮流
电池散热请认准——导热相变化材料
MiniLED是什么,它的技术原理及应用介绍
基于嵌入式Linux系统的导航软件设计思路
Linux下C语言共享库的位置无关实现原理分析
苹果指纹坏了能修吗_苹果手机修指纹多少钱
基于TLK10002的 SERDES FIFO 溢出解决方案
重锤式表面电阻测试仪的原理和特点
信号源的组成以及应用方案详解
特斯拉国产化速度加快 紧紧抓住中国市场
Apple Pay设置快捷交通卡在银行卡闪付时遇到的问题
凌壹科技:智能网安解决方案解析
百能云芯与Arrow&Verical达成战略合作,正式成为中国区线上代理商
走好职业生涯第一步|中科驭数2023校招生培训“芯星计划” 圆满落幕!
紫光国微发布256G超级SIM卡
容量大反应快的“智慧超级充电宝”
视频眼-短视频服务平台
可编程ISM频段收发器SX1233
EOS C70相机和EF-EOS R卡口适配器将从11月开始在印度发售