发现问题
当两个 static library 分别定义了一个相同的符号时,静态链接可能不会有任何异常提示,最终运行时出现问题。
我们来看下面这个例子:
创建两个目录,分别为 add 和 sub,add 目录内容如下:
// add/add.c
#include "add.h"
#include "myfun.h"
#include "add.h"
int add(int a, int b) {
myfun();
return a + b;
}
// add/myfun.c
#include <stdio.h>
void myfun(void) {
printf("myfun in libadd.a\n");
}
// add/add.h
#ifndef add_h
#define add_h
int add(int a, int b);
#endif
// add/myfun.h
#ifndef myfun_h
#define myfun_h
void myfun(void);
#endif
sub 目录内容如下:
// sub/sub.c
#include "myfun.h"
#include "sub.h"
int sub(int a, int b) {
myfun();
return a - b;
}
// sub/myfun.c
#include <stdio.h>
void myfun(void) {
printf("myfun in libsub.a\n");
}
// sub/sub.h
#ifndef sub_h
#define sub_h
int sub(int a, int b);
#endif
// sub/myfun.h
#ifndef myfun_h
#define myfun_h
void myfun(void);
#endif
我们再给根目录添加一个 main.o 调用对应的函数:
#include "add/add.h"
#include "sub/sub.h"
int main() {
add(1, 1);
sub(1, 1);
}
使用以下命令编译:
# 在 add 目录下生成 libadd.a
pushd add
gcc -c add.c myfun.c
ar rcs libadd.a add.o myfun.o
popd
# 在 sub 目录下生成 libsub.a
pushd sub
gcc -c sub.c myfun.c
ar rcs libsub.a sub.o myfun.o
popd
gcc -c main.c
gcc -o prog main.o ./add/libadd.a ./sub/libsub.a
这里既然有两个都是 strong 的符号,应当出现重复定义符号的报错,实际执行会发现没有任何的报错或告警,同时运行结果如下:
$ ./prog
myfun in libadd.a
myfun in libadd.a
如果调换一下上面的 static library 参数顺序,得到的结果如下:
$ ./prog
myfun in libsub.a
myfun in libsub.a
而我们期望运行结果应当是:
- 启动调用到
main函数 -
main函数调用add,add调用libadd.a中的myfun,输出myfun in libadd.a - 接下来,
main函数调用sub,sub调用libsub.a中的myfun,输出myfun in libsub.a
对应的输出为:
myfun in libadd.a
myfun in libsub.a
问题分析
为了了解这里到底发生了什么,我们重新来看 7.6 Symbol Resolution 章节(特别是 7.6.3 How Linkers Use Static Libraries to Resolve References 小节)。
根据这一章可以得到,链接器会一个一个地处理用户的参数,如果是 object file 则将对应的符号添加到 E,同时更新新增(未定义)引用的符号到 U 和定义符号到 D,如果是 archive(static library)则仅从 U 中查找匹配的未定义符号,如果有匹配,则将这个 static library 中对应的 object file 也添加进来(规则和遇到一个 object file 相同)。
这里的规则描述不够完整,详细内容可以从书中查找。
这里我一般通俗地理解为,传递给链接器的 object file 是需求方,表示这些都要添加到最终的可执行文件,缺失的符号由 static library 作为供给方提供。
根据上述规则,我们按步骤模拟例子中的解决符号的过程:
首先输入 main.o,结果如下:
| E | U | D |
|---|---|---|
main |
add |
main |
sub |
下一个输入的参数为 ./add/libadd.a,可以知道这里提供了符号 add,将 (./add/libadd.a)add.o 添加进来,更新符号:
| E | U | D |
|---|---|---|
main |
sub |
main |
add |
myfun |
add |
在 U 中删除了 add,又引入了 myfun,此时 U 还不是空的,也没有遍历完 libadd.a 的内容(准确说没有达到不动点),将 (./add/libadd.a)myfun.o 添加进来,更新符号:
| E | U | D |
|---|---|---|
main |
sub |
main |
add |
add |
|
myfun |
myfun |
接下来读取 ./sub/libsub.a,由于 (./sub/libsub.a)sub.o 提供了未定义的 sub 符号,将这个文件加进来,更新符号:
| E | U | D |
|---|---|---|
main |
main |
|
add |
add |
|
myfun |
myfun |
|
sub |
sub |
因为可以在 D 中找到 myfun,所以我们不会将 sub.o 带来的 myfun 再添加到 U 中。
此时解决符号的过程结束。
我们可以看到,整个符号查找过程中,根本不会打开 (./sub/libsub.a)myfun.o 这个文件,也不会知道这里还有个 myfun 的符号呢!
未解决符号集为空,解决符号的过程就结束了,链接器不一定会读取所有的文件,也无法知道这个冲突的存在。
思考
那么现实中这种情况是否会发生呢?什么场景下会发生?
很遗憾,这个情况可能已经埋很多的项目中。
一个常见的场景:我需要用到某个公开的三方库,同时进行一些修改。当其他人也遇到这个场景时,做了不一样的修改过程,最终生成可执行文件后,抛弃一个三方库的实现,最终一定导致某一个组件使用了对方的实现。(Bug 就来了)
还好,我们可以有多种解决的手段应对该问题,一个是从静态链接的角度考虑,我不能相信所有的静态库都是安全无冲突的,我需要把所有的 static library 都看一看,另一个是从 SDK 开发角度考虑,我使用了一个第三方的开源库,我应当将它以某种方式隐藏起来,避免和其他人的冲突。