概述
最近 REA 项目很火呀,有了 AI 的加持,真是可以有无限想象。这不,最近突发奇想,一个动态库有没有可能使用静态链接到一个应用程序里呢?
这个想法起初是我发现有时候我们拿到供应商分发的 SDK 库是不公开源码的,而有的时候这个库是分发的静态库,有的时候又是分发的动态库,而且还不知道编译工具链的版本。我就想,一个库无非就是一个个编译好的函数,只要能提取出来相关的机器码部分,是不是就可以正常链接了,这样我就可以统统都静态链接,不需要部署很多的依赖了?
问题
事实上,静态库打包成动态库是简单的,有的时候动态库编译的时候就是先打包的静态库,说明动态库的内容相比静态库是有减少的。因此,理论上动态库是无法完整还原成静态库的。(这个跟库无法还原出完整的源代码一样,都属于逆向工程的范畴,我们在这只探讨技术可行性。)
- 信息不可逆。动态库是个“成品”,编译时的中间信息(函数怎么组织、地址怎么规划)早就没了,只能从成品里往回凑。它内部的跳转、寻址在编译时就已经算死,想拆开重排就得有一份“地址表”,可这份表在成品里是不完整的——所以只能尽量保留原有布局,不能想怎么摆就怎么摆。
- 编译器的“懒”招数得拆掉。为了性能和灵活性,现代编译器很多操作用了间接寻址,比如线程局部变量,运行时才真正确定位置。这套“先取个地址再跳过去”的玩法在动态链接里没有问题,但静态链接器不认,得先在代码层面把这些间接访问翻成直接的、固定地址的访问。
尝试
借助 AI 进行 ELF 的分析和一通操作,最终成功实现了其中一种静态链接的方式。核心思路是把动态库先改写成静态链接器能消费的 ET_REL 目标文件,再打包成 .a。步骤简要如下:
- 校验输入:确认输入是 64 位小端 ELF 动态库,并识别架构(
readelf -h)。 - 改类型:用
elfedit把 ELF 类型从ET_DYN改成ET_REL。 - 去程序头:用
objcopy重写一遍,丢掉程序头。 - 重算符号:把所有已定义符号的值从虚拟地址改成“节内偏移”,符合
ET_REL的语义。 - 重定位改写:把
.rela.dyn/.rela.plt按目标节拆成一组组.rela.<section>,并把动态重定位类型统一改写成静态链接器能处理的ABS64;RELATIVE借助一个钉在原地址的合成符号,让链接器把原地址原样写回。 - TLS 改写:把 TLSDESC 的
ldr/add/blr三连就地改成movz/movk/nop(还是 12 个字节),并补上R_AARCH64_TLSLE_*重定位,让静态链接器自己算线程指针偏移。 - 换符号表:用普通
.symtab/.strtab替换.dynsym/.dynstr,这样ar能索引到导出 API,静态链接器也能解析外部(libc)符号。 - 钉地址:把剩下的 alloc 节改名为
.<prefix>.<name>并钉在基址 + 原 .so 地址(通过--section-start),保住.so内部既有的引用;TLS 节不钉,交给链接器并入应用的 TLS 块。 - 打包与记录:把目标文件打包进归档,并把钉地址的
--section-start参数写进.rsp文件,由最终链接用-Wl,@...读入。 - 转换后校验:全量检查转换结果,不允许残留任何动态重定位或 TLSDESC,
.so一旦更新引入新问题,构建期直接报错,而不是留到运行时。
总结
这种方式与其说是“还原”一个库,不如说是“重标定”——把一份已经链接完成的 .so 当作快照,重新包装成静态链接器能消费的 .a。只能说如果供应商 SDK 只提供了动态库,而我们项目想要静态链接,不部署那一堆依赖,是可以简单用一下的。但是没有办法完整还原,实际上用到的“钉地址”技巧也会把几乎整个库链接进去了,也没起到节省程序体积的效果。另外,方法是否能成功跟工具链、架构等都有关系。
只能说是一种尝试吧,而且靠的是 AI 的帮忙,算是一种体验吧。
