2019年9月30日星期一

动态链接(上)

动态链接(上)

首先解释一下,什么叫做动态链接,动态链接是相对于静态链接来说的,静态链接在生成可执行程序时便已经完成了链接操作,将多个目标文件链接成一个整体,链接后得程序无需依赖一起库便可运行,而动态链接则是在程序运行时才开始链接,之后再进行程序执行。

为什么需要动态链接?

相比于动态链接,静态链接有如下几个问题:
  • 浪费内存和磁盘空间:每个程序内部都保留了一份printf()等等常用的公用库函数和他们所需要得辅助数据结构,大量几乎一样得副本占用了机器内存和磁盘
  • 模块更新困难:更新任意模块都需要对整个程序进行重新的链接,而使用者们则需要下载整个程序
为了解决这两个问题,人们想到可以使用动态链接的方式,通过延迟链接的时机来解决这个问题,系统会根据程序需要,动态的将依赖库加载至内存,并且可以只保留一份依赖库的副本,不同的程序只需要有自己单独的数据额外保存即可。同时,这种方式下,程序更新以模块的方式进行,每次更新只需要发布被更新模块,然后让程序在运行时动态加载更新后模块即可。

动态链接存在的问题

  • 必须要操作系统的支持
  • 新旧模块间接口不兼容
  • 相对于静态链接有速度上的损失

动态链接的基本实现

在linux下,ELF动态链接文件被称为动态共享对象,简称共享对象,一般以.so为扩展名。Windows下动态链接文件被称为动态链接库,一般以.dll作为扩展名
由于linux下动态链接机制较为简单,接下这里分析先从Linux下开始,在linux中,常用c语言运行库glibc,他的动态链接形式的版本保存在"/lib/libc.so",整个系统只保留一份libc.so的副本,所有c语言编写的且动态链接的程序都可以在运行时使用它。
当程序被装载时,系统会先调用动态链接器将程序需要的所有动态链接库装载到进程空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作
一个示例程序如下:
在执行gcc -fPIC -shared -o Lib.so Lib.c 后,产生了一个Lib.so文件,这是一个包含了Lib.c的foobar()的共享文件对象,然后我们分别链接program1和program2
使用命令gcc -o program1 program1.c ./Lib.sogcc -o program2 program2.c ./Lib.so
在这个过程中,并不是像静态链接中那样,直接将Lib.o文件链接进入program文件,当foobar是一个定义在动态共享文件的函数时,那么符号链接器就会将这个符号引用标记为一个动态连接的符号,不对他进行地址重定位,把这个过程留到装载时再进行
链接器如何知道引用是一个静态符号还是动态符号?
这实际就是我们要用到Lib.so的原因,Lib.so中保存了完整的的符号信息,把Lib.so也作为链接的输入文件之一,链接器在解析符号时就可以知道:foobar是一个定义在Lib.so的动态文件。这样链接器就可以对foobar的引用做特殊处理,使他成为一个对动态符号的引用
我们用readelf -l Lib.so 查看Lib.so的装载属性,发现其装载首地址为00000000,可猜测共享对象的最终装载地址在编译时是不确定的,而是在装载时由装载器根据当前地址的空闲情况,动态分配一块足够大小的虚拟地址去加载共享对象

链接时/装载时重定位

因为程序的装载位置不确定,为了不使各个共享模块产生地址的冲突问题,必须使这些模块可以在任意地址装载。思路是到实际装载时由操作系统对程序中的指令和数据中对绝对地址的引用进行重定位。在静态链接中的重定位,由于程序是整个装载到内存中的,所以重定位会简单一些,那时的重定位叫做链接时重定位,而现在我们面临的是装载时重定位,在Windows中又被称为基址重置
但是,如果只是装载时重定位,只能解决我们50%的问题,为什么呢? 对于动态链接库中的可修改数据部分对不同进程有多个副本,可以使用装载时重定位的方法来解决,但是对于指令部分,由于装载时重定位的方法会修改指令,所以没办法做到同一份指令被多个进程共享。所以我们就要想一种方法,使得指令也可以在多个进程间共享。

地址无关代码

其实我们的目的很简单,希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以最直接的思路就是:把程序中需要修改的部分拿出来和数据部分放在一起,其他的指令部分就可以保持不变的被各个进程共享,这种技术被称为地址无关代码技术
那么怎么产生地址无关代码呢?不着急,我们先分析模块中各种类型的地址引用方式,这里我们把共享对象模块中的地址引用按照是否跨模块分两类:模块内引用和模块外引用;按照不同的引用方式又可以分为指令引用和数据访问,于是也就是如下4种:
  • 模块内指令引用(函数调用,跳转等)
  • 模块内数据访问(定义的全局变量,静态变量等)
  • 模块外指令引用(函数调用,跳转等)
  • 模块外数据访问(其他模块中定义的全局变量等)
接下来,我们详细解释一下GOT的实现
当指令访问某个变量时,程序会先找到GOT,然后根据GOT中变量所对应的项找到变量的目标地址。每个变量对用一个4字节的地址,链接器在装载模块时会查找每个变量所在的地址,然后填充GOT中的各个项,以确保每个指针所指向的地址正确。由于GOT本身在数据段,所以可以在装载时进行修改,并且每个进程都可以有独立的副本,互相不受影响
那么现在有个问题,怎么找到GOT表呢,其实这时GOT就变成了模块内数据访问了,采用相对地址的方法(PC+偏移)就可了(GOT中每个地址对应的变量由编译器决定,所以程序知道第几个对应谁)
为了方便理解,可看下图:
img
图里面是一个简化的例子,这和实际编译情况不同,但适合说明GOT。 当我要从main()内去调用 shared binary 中的foo()方法的时候,在编译过程中(调用$gcc main.c 的时候)编译器会生成一个可执行文件,假设生成的可执行文件名字为a.out,在这个生成的文件中原先的main.c 中的foo()被替换为 b @GOT+0x14 ,这行代码的作用是,跳转到GOT内所记录的位置上去,地址就是GOT表的起始地址加上0x14,内容是 0x76fc6578,这个地址也就是foo() 在 shared library 的绝对位置
以上就是GOT的实现机制,但是仔细想想,我们是不是还漏了一种情况,那就是模块内的全局变量应该怎么访问,可能你第一反应是跟模块内静态变量一样不就好了,一般来说确实可以,但是存在一种特殊情况: 当一个模块引用了一个定义的共享对象的全局变量时,但是却无法判断该变量是否定义在同一个模块中。
这时,gcc编译器会把他当作是跨模块的数据访问,并且将GOT中的指针指向这个副本(可执行文件中的该变量中的副本,因为在编译中.bss段中肯定会有一个这个变量的副本)
好了,今天就先到这里,关于动态链接剩下的部分,明天接着说。

今日分享:
每一个不曾起舞的日子 都是对生命的辜负!

没有评论:

发表评论

XSS速查表

今天继续XSS的进一步学习,以下内容转载自Freebuf上的文章,原文章来自OWASP的xss备忘录 1.介绍 这篇文章的主要目的是给专业安全测试人员提供一份跨站脚本漏洞检测指南。文章的初始内容是由RSnake提供给 OWASP,内容基于他的XSS备忘录: http://...