程序员的自我修养-如何规避动态库与静态库符号冲突

256 阅读9分钟

背景

甲方SDK包含了一份commonSDK(1.0)静态库,而我们的工程里面原本就有一份被乙方SDK依赖的commonSDK(2.0)动态库,根据macho静态链接符号查找的规则,如果静态库在命令行中的链接顺序靠前,则使用静态库中的符号并入可执行文件,如果动态库在命令行中的链接顺序靠前则在可执行文件中标记使用动态库中的符号,所以当commonSDK同时存在动态库和静态库的时候一定会导致可执行文件中的代码调用要么都来自commonSDK静态库,要么都来自commonSDK动态库,而我们希望甲方SDK的代码走commonSDK(1.0)静态库,而乙方SDK的代码继续走commonSDK(2.0)动态库,互不影响,否则乙方SDK/甲方SDK代码调用到了不同版本的commonSDK会导致功能出现问题。

image.png

技术方案

  • 最优的方案肯定是统一使用一份commonSDK,且能通过业务测试

那如果我们没有这样的条件去协调甲方乙方呢,有没有什么方法可以从技术角度进行规避符号错误使用的问题

  • 将甲方SDK与内部的commonSDK(1.0)打包成动态库,并对外只暴露调用甲方的函数符号,这样一来乙方就无法找到commonSDK(1.0)所以只能使用commonSDK(2.0)

image.png

技术原理

不同的平台,不同的可执行文件格式在静态链接和动态链接的处理有所不同,这里主要介绍一些有共性的内容,当前主流操作系统基本都采用了PIC,PIE技术(地址随机装载)保证安全性,Android 自 Jelly Bean 起支持 PIE,并在 Lollipop 中移除非 PIE 链接器; iOS(≥4.3)全面支持 PIE。以下所有内容均基于PIE和PIC

静态链接

把所有的代码源文件编译出来的目标文件合并的过程叫做静态链接,静态链接主要包含两次遍历(two-pass linker)

pass1:符号解析

  1. 创建一份虚拟的已定义符号表(D)和未定义符号表(U),以及一份记录需要加载的目标文件表E,然后根据命令行的链接顺序,从左到右扫描输入的目标文件、静态库、动态库进入多轮扫描,
  • 如果碰到目标文件,直接将当前.o符号表中的已定义的符号先和D表U表进行对比(D表中如果已经有了 直接报错符号重复定义,U表中如果有就把U表里面的符号删除)扔进D表,未定义的符号依然是对比完D表和U表后扔进U表,并将.o记录到E表
  • 如果碰到静态库,扫描静态库的索引文件,遍历索引文件判断里面的符号是否在未定义符号集U表内,在则将对应的目标文件依据上一条进行操作(linux系统会标记当前库已被扫描,macos不会)
  • 如果碰到动态库,则将相关的符号从U表中删除计入D表
  1. 发现U表无内容或者已经扫描到底了且E表没有来自静态库的新的目标文件则判定结束,若结束后U表仍然有内容,对外报错符号未定义
  2. 如果当前轮次扫描结束后E表有新的来自静态库的目标文件则进行入下一轮扫描

image.png

注:mac系统中第一轮扫描所有.o文件会先遍历完,无视命令行的顺序,举个例子:

Ld a.o c.a d.o 假设a中依赖了一个符号foo,且c.a静态库中和d.o静态库中都有符号foo,mac系统工作正常

pass2:合并与重定位

  1. 根据对应的段合并pass1中E表标记的所有目标文件,创建虚拟的重定位表条目R表和B表
  2. 遍历重定位表(这个是.o目标文件编译的时候就已经生成的重定位条目,重定位表条目中有标记具体定位类型,当前目标文件外定义的符号一律标记为动态重定位类型(GOT_LOAD,JUMP_SLOT...),不管这个符号最终来自动态库还是静态库,当前目标文件内定义的符号一律标记为静态重定位(RELATIVE...))
  • 如果碰到静态重定位类型,直接消费掉这一条
  • 如果碰到动态重定位类型,且当前符号来自静态库或者自身其他合并来目标文件,则往rebase表中添加,如果当前符号来自动态库则往Bind表中添加。(这个地方并不是这么简单的判断往哪个表里面添加,比如preemptible,全局变量可覆盖特性是否需要支持,如果要支持就扔到bind表,ELF默认支持,macho默认不支持),对于每一个需要往Bind表中添加的条目还需要为他在DATA段中生成一个GOT表的槽位,然后将对该条目的重定位方式进行修改指向新开的GOT槽,然后在增加一条GOT槽的动态重定位条目。
  1. 生成动态重定位表(macos R表和B表继续分离,linux可执行文件则进行合并,但是内部条目根据不同的类型区分不同的动态重定位类型)
  2. 将D表做出相应的调整以后写入全局符号表.symtab,将来自于其他动态库的符号或者需要对外导出使用的写入.dynsym

注:每一条动态重定位表内的条目在linux系统下默认不会记录符号具体来自哪个库,但是macos会

image.png

动态链接

其实大部分的工作在静态链接器中已经做的差不多了,但是为了解决库共享,物理内存共享节省内存的问题,就有了动态库,因为动态库和可执行文件本质上差不多,为了安全也为了实际运行时能分配出足够的虚拟内存地址,动态库以及可执行文件每一次的实际装载地址都是不同的,装载地址不同就会导致程序每次运行的实际虚拟内存地址不同,因此需要每一次都对来自动态库的符号进行重定位,为了解决装载时重定位就有了动态链接。

动态库的代码段是所有进程共享的,但是数据段不是。每一个进程都有自己独立的数据段的备份来写自己的数据,因此所有的代码段的对外部符号的引用都需要改成通过数据段的间接引用,因为无法在运行时对代码段内的偏移做出修改,因此PIC和PIE下的所有外部符号都会在编译期间就直接使用GOT访问的方式来确定外部符号的地址。但是在静态链接的Pass2阶段正式确认需要使用GOT槽的条目。

动态链接干的活也很简单,根据动态重定位表(Rebase表和Bind表),如果是R表,则只根据基址对所有条目做偏移调整,不进行符号查找,如果是Bind表则依据一定的规则去找符号并写回GOT槽,macos下可以直接根据条目中指定的动态库录入符号地址,linux下则只能依据主程序-动态库的链接顺序去查找。

image.png

实现

分析

现在我们先来分析一下给出的技术方案是否是正确的:

将甲方SDK以及里面的commonSDK(1.0)静态库封装到一个FutureWrapper动态库中

  1. 工程源代码与甲方SDK+commonSDK静态库共同组装成一个动态库,在静态链接的pass2 阶段会把所有来自于commonSDK的符号加入动态重定位表的rebase表,因为能确定他们来自内部且默认不需要支持全局符号覆盖,因此不存在符号查找到过程,所有来自甲方SDK的调用都会走自己携带的commonSDK(1.0)静态库的符号不会去引用乙方的commonSDK(2.0)动态库
  2. 抹掉FutureWrapper对外导出的commonSDK .dynsym动态符号表,这样一来即便是FutureWrapper在链接顺序中靠前,主工程静态链接的时候乙方SDK的代码也会因为无法从FutureWrapper的符号表中获取任何未知的commonSDK的符号而只能使用原来就配置好的commonSDK(2.0)动态库。

具体步骤

第一步,创建一个动态库工程,将甲方SDk依赖的静态库导入工程,

第二步,新建对外暴露的封装类,将项目中用到甲方SDK的方法封装之后再暴露出去,

第三步,设置编译参数:"-Wl,--exported_symbols_list,exported_symbols.txt",将要导出的符号放入exported_symbols.txt 中,在这里要导出的符号就是第二步中自己创建的类,

第四步,编译工程,产生动态库产物,

第五步,将动态库引入我们自己的项目工程,将原来依赖甲方SDK的地方改为第四步产生的动态库。

还需解决的问题:

当两个动态库拥有相同的类以及方法的时候,如果不正常的使用objc_getclass或者NSSClassFromString这种动态运行的调用方式会导致发生不确定的系统行为,就比如如下的提示一样,One of two will be used, Which one is undefined,当然按照苹果的尿性,所谓的未定义的系统行为也很可能只是不想告诉你而已,其实里面还是有顺序的,比如依据动态库的加载顺序。

解决方法其实也蛮简单的,用fishhook 去hook objc_getClass这两函数就行,然后在里面根据类的名称去判断调用来源和返回正确的方法指针,长度有限就不继续往下写了

程序员的自我修养--链接、装载与库

dyld3