注意:链接过程虽然由链接器ld负责,但通常我们不直接使用它,而是由GCC间接驱动之
1. 先谈结论
越底层的库,在链接命令行中的位置应越靠后。如:x.o 或 libA.a 依赖 libB.a 中定义的符号,则在链接命令行中 -lB 应在 x.o 或 -lA 之后
2. 术语
- 导出符号:在.o中,直接定义的符号
- 导入符号:在.o中,用到的外部符号
3. 链接过程
链接器ld维护两个符号表:
- 已决符号表A: 遇到的所有.o/.a的导出符号
- 未决符号表B: 遇到的所有.o/.a需要导入,但尚未找到的符号
当链接器遇到.o时,对其中的每个符号
- 导出符号:若表A已有此符号,则产生“多重定义”错误;否则添加到表A,同时从表B删除(若存在)
- 导入符号:若表A不存此符号,则添加到表B
当链接器遇到.a时,对其中的每个.o(.a是.o的简单集合),若有任何导出符号位于表B(换句话说,能提供前面需要的符号),则将该.o加入链接。并进行如下操作
-
按前述处理.o的方式处理
-
重新扫描.a,从而让同库中的其他.o有机会加入链接
- 这些.o之前没有加入链接,是因为导出符号不在表B中,当后面某个.o加入链接后,表B被更新了
最后,当所有.o/.a处理完成时,若表B仍有未决符号,则抛出“未定义引用”错误
4. 示例
// func.c
int func(int i) { return i * i; }
// main.c
extern int func(int);
int main(int argc, char* argv[]) { return func(argc); }
4.1. .o不关心顺序
gcc -c func.c main.c
gcc func.o main.o
./a.out x; echo $?
# 输出4
由于.o文件始终被纳入链接,因此顺序无所谓。比如 gcc main.o func.o 也可以
4.2. .a关心顺序
gcc -c func.c main.c
ar r libfunc.a func.o && ranlib libfunc.a
gcc main.o -L. -lfunc
./a.out x; echo $?
但若顺序为gcc -L. -lfunc main.o,则报错
main.o: In function 'main':
main.c:(.text+0x15): undefined reference to 'func'
collect2: ld returned 1 exit status
原因是:
- 链接器遇到 libfunc.a,由于 func.o 中的 func 并不在未决表中,因此 func.o 不会加入链接
- 链接器遇到 main.o,发现用到外部符号 func,因此 func 加入未决表
- 链接器到达末尾,并且 func 仍未定义
5. 循环依赖
/*
main.o---+----func.a----+---dep.a--+
╰------|--> func.o <--|------╮ |
| ╰-------|--> dep.o |
| dep2.o <--|------╯ |
+--------------+----------+
*/
// func.c
extern int dep(int);
int func(int i) { return dep(i + 1); }
// dep2.c
int dep2(int i) { return i + 1; }
// dep.c
extern int dep2(int);
extern int func(int);
int dep(int i) { return i > 3 ? dep2(i) : func(i); }
// main.c
extern int func(int);
int main(int argc, char* argv[]) { return func(argc); }
gcc -c func.c dep.c dep2.c main.c
ar r libfunc.a func.o dep2.o && ranlib libfunc.a
ar r libdep.a dep.o && ranlib libdep.a
# 下面两种顺序都会报错
# gcc main.o -L. -lfunc -ldep
# gcc main.o -L. -ldep -lfunc
5.1. 法1:--start-group 和 --end-group
gcc main.o -L. -Wl,--start-group -lfunc -ldep -Wl,--end-group
5.2. 法2:--undefined
gcc main.o -L. -Wl,--undefined=dep -ldep -lfunc