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

    文章

    10973

    浏览量

    206669
  • C语言
    +关注

    关注

    180

    文章

    7521

    浏览量

    127270
  • main
    +关注

    关注

    0

    文章

    38

    浏览量

    5964

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

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

收藏 人收藏

    评论

    相关推荐

    Linux操作系统-C语言编程入门介绍在LINUX 进行C 语言编程所需要的基础知识.在这篇

    Linux操作系统-C语言编程入门介绍在LINUX 进行C
    发表于 12-08 09:56

    [推荐]linuxc语言编程简介

    第一章本章将简要介绍一什么是LinuxC语言的特点,程序开发的预备知识,Linux
    发表于 04-29 13:50

    LinuxC语言编程基础知识

    LinuxC语言编程基础知识
    发表于 08-20 17:21

    如何在linux进行c语言编程

    如何在linux进行c语言编程
    发表于 08-20 22:55

    Linux动态和静态的制作及使用

    不必从零开始,我们要做的只是在恰当的位置调用合适的库函数去实现相应的功能,充分利用前人的劳动成果,就是“站在巨人的肩膀上”。本文主要简述Linux
    发表于 11-18 17:05

    Samba实现Linux与Windows共享详解

    、重新启动Samba服务 7、Linux 网络设备IP 8、Windows 运行 9、弹出对话框,输入用户名和密码 10、进入共享 11、本次共享目录为
    发表于 07-03 08:59

    位置无关的代码

    ) }--------------------------------------------这里把代码段等链接到外存地址,那么确实在引导代码里要注意“位置无关的代码”问题,个人查了一些资料,只是提到bl,adr等相对pc的一些指令用法 .rodata ALIGN(4
    发表于 06-17 05:45

    Linux如何使用C语言编程

    LinuxC语言编程一般由四个部分组成:一.编辑器如Vim来进行代码的编写二.编译器如gcc来进行程序的编译,产生可执行文件。需要注意的一些实际的IDE中其实默认是集成了像gcc
    发表于 07-05 08:13

    利用C语言Linux怎么实现一个Sniffer?

    扫描相比,嗅探的行为更加难以被察觉,操作起来也不是很复杂!对于网络管理人员来说,可以利用嗅探技术对网络活动进行监控,并及时发现各种攻击行为!在这篇文章里,我们主要探讨在Linux如何利用C
    发表于 03-27 07:04

    linux系统C语言开发学习

    本课程是全套课程的第0.2.3课(预科第三课程),主题linux系统C语言开发学习,总共25小时左右的课程。该视频是我在联嵌科技代课期间随堂真实录制,***均为根本没接触过
    发表于 12-15 09:10

    Linux系统编程重点学习标准I/O

    1. 嵌入式 Linux 工程师的自我修养 ?编号概览1C语言2Linux基础Linux的目录
    发表于 12-15 06:45

    Linux系统的链接原理是什么?有哪些应用呢

    Linux 使用 gcc 创建一个动态。由于动态可以被多个进程共享加载,所以需要生成位置
    发表于 06-21 17:05

    linux共享,调用共享库函数时,程序卡死在函数中的pid=fork()这里的原因?

    linux共享,调用共享库函数时,程序卡死在函数中的pid=fork()这里,来个大佬指导指导原因...
    发表于 06-20 06:55

    LinuxC语言编程入门教程

    u3000本文是Linux C 语言编程入门教程。主要介绍了Linux 的发展与特点、C
    发表于 09-22 06:56

    如何开发与存储位置无关的STM32应用?

    如何开发与存储位置无关的STM32应用?
    的头像 发表于 10-18 16:46 333次阅读
    如何开发与存储<b class='flag-5'>位置</b><b class='flag-5'>无关</b>的STM32应用?