0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux下C语言共享库的位置无关实现原理分析

Linux阅码场 来源:未知 2019-11-28 16:20 次阅读

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-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

    文章

    11225

    浏览量

    208905
  • C语言
    +关注

    关注

    180

    文章

    7598

    浏览量

    136162
  • main
    +关注

    关注

    0

    文章

    38

    浏览量

    6140

原文标题:吴章金: 深度剖析 Linux共享库的“位置无关”实现原理

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C语言-文件编程

    这篇文章介绍C语言的文件编程函数,案例代码是在Linux环境运行测试的分别介绍了C语言标准
    的头像 发表于 09-09 11:33 1985次阅读

    Linux操作系统-C语言编程入门-pdf

    Linux操作系统-C语言编程入门介绍在LINUX 进行C
    发表于 12-08 09:55 193次下载
    <b class='flag-5'>Linux</b>操作系统-<b class='flag-5'>C</b><b class='flag-5'>语言</b>编程入门-pdf

    linuxc语言编程pdf

    linuxc语言编程内容为::基础知识,进程介绍,文件操作,时间概念,信号处理,消息管理,线程操作,网络编程,Linux
    发表于 12-08 10:00 0次下载

    Linux系统共享编程

    一、说明 类似Windows系统中的动态链接Linux中也有相应的共享用以支持代码的复用。Windows中为*.dll,而Linux
    发表于 09-13 16:49 24次下载

    Linux静态和动态共享)的制作与使用

    Linux静态和动态共享)的制作与使用Linux
    发表于 07-09 14:39 1154次阅读

    LINUX环境CLIPS动态链接实现方法

    LINUX环境,为了简便、快捷地制作出CLIPS动态链接,本文采用了CNU AUTOTOOLS把CLIPS嵌入式高级语言编译成动态链接
    发表于 04-14 21:18 30次下载

    LinuxC语言编程概述

    分享到:标签:C语言编程 Linux 编译链接器 调试器 操作系统 3.1 LinuxC
    发表于 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操作系统C语言编程入门

    基于Linux操作系统C语言编程入门
    发表于 10-27 15:36 11次下载
    基于<b class='flag-5'>Linux</b>操作系统<b class='flag-5'>下</b><b class='flag-5'>C</b><b class='flag-5'>语言</b>编程入门

    linux静态和动态分析

    的二进制是不兼容的。 本文仅限于介绍linux。 2.的种类 linux
    发表于 11-02 10:12 1次下载

    Linux操作系统C语言编程入门.pdf

    Linux操作系统C语言编程入门
    发表于 05-17 10:08 96次下载

    Linux的常用C函数中文手册免费下载

    本文档的主要内容详细介绍的是Linux的常用C函数中文手册免费下载,包含几乎所有LinuxC
    发表于 10-28 08:00 8次下载
    <b class='flag-5'>Linux</b>的常用<b class='flag-5'>C</b>函数<b class='flag-5'>库</b>中文手册免费下载

    LinuxC语言编程入门教程详细说明

    本文是Linux C 语言编程入门教程。主要介绍了Linux 的发展与特点、C
    发表于 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>编程入门教程详细说明

    C++基础语法知识之链接装载Linux共享

    Linux共享(Shared Library) Linux 共享
    的头像 发表于 11-01 10:15 2872次阅读

    Linux中的静态共享

    是一个二进制文件,包含的代码可被程序调用。例如标准C、数学、线程等等。有源码,可下载后
    的头像 发表于 05-10 09:34 996次阅读

    C 语言的头文件路径位置问题

    的朋友们来说,一些系统的文件路径根本就不知道在什么地方。 所以本文我们就来聊一 C 语言的头文件路径相关的问题 ,包括系统路径位置,绝
    的头像 发表于 06-22 10:05 6188次阅读
    <b class='flag-5'>C</b> <b class='flag-5'>语言</b>的头文件路径<b class='flag-5'>位置</b>问题