StevenPZChan
StevenPZChan
1 min read

Categories

Tags

概述

最近 REA 项目很火呀,有了 AI 的加持,真是可以有无限想象。这不,最近突发奇想,一个动态库有没有可能使用静态链接到一个应用程序里呢?

这个想法起初是我发现有时候我们拿到供应商分发的 SDK 库是不公开源码的,而有的时候这个库是分发的静态库,有的时候又是分发的动态库,而且还不知道编译工具链的版本。我就想,一个库无非就是一个个编译好的函数,只要能提取出来相关的机器码部分,是不是就可以正常链接了,这样我就可以统统都静态链接,不需要部署很多的依赖了?

问题

事实上,静态库打包成动态库是简单的,有的时候动态库编译的时候就是先打包的静态库,说明动态库的内容相比静态库是有减少的。因此,理论上动态库是无法完整还原成静态库的。(这个跟库无法还原出完整的源代码一样,都属于逆向工程的范畴,我们在这只探讨技术可行性。)

  1. 信息不可逆。动态库是个“成品”,编译时的中间信息(函数怎么组织、地址怎么规划)早就没了,只能从成品里往回凑。它内部的跳转、寻址在编译时就已经算死,想拆开重排就得有一份“地址表”,可这份表在成品里是不完整的——所以只能尽量保留原有布局,不能想怎么摆就怎么摆。
  2. 编译器的“懒”招数得拆掉。为了性能和灵活性,现代编译器很多操作用了间接寻址,比如线程局部变量,运行时才真正确定位置。这套“先取个地址再跳过去”的玩法在动态链接里没有问题,但静态链接器不认,得先在代码层面把这些间接访问翻成直接的、固定地址的访问。

尝试

借助 AI 进行 ELF 的分析和一通操作,最终成功实现了其中一种静态链接的方式。核心思路是把动态库先改写成静态链接器能消费的 ET_REL 目标文件,再打包成 .a。步骤简要如下:

  1. 校验输入:确认输入是 64 位小端 ELF 动态库,并识别架构(readelf -h)。
  2. 改类型:用 elfedit 把 ELF 类型从 ET_DYN 改成 ET_REL。
  3. 去程序头:用 objcopy 重写一遍,丢掉程序头。
  4. 重算符号:把所有已定义符号的值从虚拟地址改成“节内偏移”,符合 ET_REL 的语义。
  5. 重定位改写:把 .rela.dyn / .rela.plt 按目标节拆成一组组 .rela.<section>,并把动态重定位类型统一改写成静态链接器能处理的 ABS64;RELATIVE 借助一个钉在原地址的合成符号,让链接器把原地址原样写回。
  6. TLS 改写:把 TLSDESC 的 ldr/add/blr 三连就地改成 movz/movk/nop(还是 12 个字节),并补上 R_AARCH64_TLSLE_* 重定位,让静态链接器自己算线程指针偏移。
  7. 换符号表:用普通 .symtab/.strtab 替换 .dynsym/.dynstr,这样 ar 能索引到导出 API,静态链接器也能解析外部(libc)符号。
  8. 钉地址:把剩下的 alloc 节改名为 .<prefix>.<name> 并钉在 基址 + 原 .so 地址(通过 --section-start),保住 .so 内部既有的引用;TLS 节不钉,交给链接器并入应用的 TLS 块。
  9. 打包与记录:把目标文件打包进归档,并把钉地址的 --section-start 参数写进 .rsp 文件,由最终链接用 -Wl,@... 读入。
  10. 转换后校验:全量检查转换结果,不允许残留任何动态重定位或 TLSDESC,.so 一旦更新引入新问题,构建期直接报错,而不是留到运行时。

总结

这种方式与其说是“还原”一个库,不如说是“重标定”——把一份已经链接完成的 .so 当作快照,重新包装成静态链接器能消费的 .a。只能说如果供应商 SDK 只提供了动态库,而我们项目想要静态链接,不部署那一堆依赖,是可以简单用一下的。但是没有办法完整还原,实际上用到的“钉地址”技巧也会把几乎整个库链接进去了,也没起到节省程序体积的效果。另外,方法是否能成功跟工具链、架构等都有关系。

只能说是一种尝试吧,而且靠的是 AI 的帮忙,算是一种体验吧。