跳到主要内容
AZ Tools

静态库 (.a) 检查器

.a 就是 ar 归档:8 字节魔数之后,每个成员有一个 60 字节定宽 ASCII 头部(名字 16、修改时间 12、uid 6、gid 6、八进制权限 8、十进制大小 10),再跟上该成员的数据,并对齐到偶数偏移。本工具直接从字节里读出这些内容,逐个成员显示头部位置、数据真正开始的位置、大小、以 UTC 表示的时间戳、属主和权限。超过 16 字节的名字要放在别处,而两种约定的做法不同:GNU 把长名收进一个叫 // 的成员,头部里写 /偏移;BSD 和 macOS 则写 #1/长度,把名字放在成员数据的最前面,于是数据并不是从头部结束处开始,大小字段里也算上了名字。15 个字符还能以 "名字/" 的形式放进头部,16 个就放不下了;读错这个边界,后面所有偏移都会错位,所以工具会明确报告采用的是哪种约定,并逐个成员标出名字来自哪里。不过这个工具真正的理由是符号索引。名为 / 的成员(或 /SYM64/,BSD 上是 __.SYMDEF)把符号映射到定义它的那个成员的文件偏移,它只是一份缓存:除了 ranlib,没有别的东西保证它是新的。如果归档被就地修改、由脚本拼装,或者追加成员后没有重建索引,索引就可能指向一个已经不再定义该符号的成员,或者漏掉一个明明就在里面的符号;而信任索引的链接器随后会对归档中确实存在的符号报 undefined reference。因此这里两边都读:索引,以及每个成员自己的 ELF 符号表,逐条列出不一致之处。每个 ELF 成员还会列出它定义的全局符号(沿用 nm 的字母)和它留作未定义的符号,从中可以看出成员之间的依赖顺序。被两个成员强定义的符号单独收集,因为那才是让链接失败的 duplicate symbol;弱符号、common 和唯一符号本来就会重复,不会被标记。不是 ELF 目标文件的成员——别的平台的 Mach-O 或 COFF、嵌套归档、误加进来的文本文件——会被指名说明而不是半解析,成员只是磁盘文件引用的 thin 归档也会被点名。工具无法告诉你的是某次具体链接会拉进哪些成员:那取决于链接顺序,以及走到这个归档时还有什么处于未定义状态。

使用方法

  1. 把 .a 文件拖到方框里。什么都不会上传,由页面里的 JavaScript 读取。
  2. 在概览里看命名约定(GNU 的 // 还是 BSD 的 #1/)、符号索引的种类,以及文件里有多少字节是索引和长名表而不是代码。
  3. 先看“索引与成员的比对”。绿色表示缓存和成员一致;红色会直接点出不一致的符号和成员。
  4. 浏览成员表里的头部与数据位置、大小和保存的日期:可复现构建会把时间、uid、gid 都写成 0,出现别的值就说明归档是用 ar U 生成的。
  5. 在“各成员的符号”里展开某个成员,看它定义了什么、还需要什么,再用重复列表去解释 duplicate symbol 链接错误。

常见问题

"archive has no index; run ranlib to add one" 是什么意思?
说明这个归档是在没有符号表的情况下生成的,通常是用 ar rcS 建的、用 ar q 追加的,或者由自己写 ar 格式的工具生成的。没有索引,链接器就得打开每个成员才知道谁定义了什么,所以多数链接器干脆拒绝并要求运行 ranlib。ranlib 会加上一个名为 / 的首个成员来存放这张映射表。对文件运行 ranlib 或 ar s 可以就地修好,一开始用 ar rcs 也就不会有这个问题。遇到这种归档,本工具会显示“符号索引:无”,而成员列表照样能读,因为索引只是加速用的,并不属于成员本身。
既然 ar 会维护索引,索引怎么还会是错的?
ar 每次重写归档都会重建索引,所以常规命令是安全的。索引变旧是别的东西动了这个文件:脚本就地打了补丁、构建系统用 ar q 追加却从不运行 ranlib、有人手工拼装或用自己写 ar 格式的工具生成、或者在机器之间复制后又编辑过。索引存的是字节偏移而不是名字,所以成员变了而索引没重建时,就会留下一条指向不再定义该符号的字节的记录。链接器信任索引,于是失败出现在很远的地方:对一个用 nm 明明能看到的符号报 undefined reference。
GNU 和 BSD 的 .a 有什么区别,thin 归档又是什么?
区别只在于超过 16 个字符的名字和符号索引怎么存。GNU 把长名收进 // 成员并用偏移引用,索引叫 /,64 位形式叫 /SYM64/;BSD 在名字栏写 #1/长度、把名字放在成员数据开头,索引叫 __.SYMDEF 或 __.SYMDEF SORTED。因此两种格式里成员数据的起点不同,这也是误读归档最常见的原因。thin 归档是 ar T 生成的第三种形式:以 !<thin> 开头,只存头部,数据仍留在磁盘上原来的 .o 里,所以单独搬走 .a 就会坏掉。
成员里有未定义符号,是库坏了吗?
不是,这很正常,而且正是这份列表的意义所在。每个成员都是独立的目标文件,未定义符号是它指望别的成员、别的库或者程序本身提供的。把它们合起来看,就能读出归档内部的依赖顺序。这一点很重要,因为静态库是被搜索而不是被合并的:链接器只沿命令行走一遍,只拉进那些能解决当时尚未定义符号的成员,而被较晚拉进来的成员可能留下一个更早的库本来能满足的未定义符号。这就是链接顺序重要、以及 --start-group 存在的原因。
为什么只有一部分重复符号被当成重复报告?
因为只有一部分会让链接失败。同一个名字在两个成员里都是强定义,就是典型的 duplicate symbol 错误:链接器第二次拉进来的那个会冲突。但弱定义(nm 的 W 和 V)、common 符号(C)和 GNU 的唯一符号(u)本来就是要重复的:C++ 的内联函数、模板和虚表会出现在每个用到它们的目标文件里,链接器只保留一份。真实的 libstdc++.a 里有几千处这样的重复却不会报错,全部列出来只会把真正要紧的那一条淹没。所以这里只收集在多个成员里都有强定义的名字。
归档会被上传到什么地方吗?
不会。它用浏览器的 File API 读取,由页面里的 JavaScript 解析,根本没有可以发送的服务器端。对静态库来说这一点比对多数文件都重要,因为来自供应商或内部构建的 .a 往往是你没有权利上传的东西。页面加载完之后断开网络,工具照样能用,文件也始终不会离开这个标签页。

相关工具