GCC的静态链接顺序和符号解析

443 阅读2分钟

注意:链接过程虽然由链接器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加入链接。并进行如下操作

  1. 按前述处理.o的方式处理

  2. 重新扫描.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

原因是:

  1. 链接器遇到 libfunc.a,由于 func.o 中的 func 并不在未决表中,因此 func.o 不会加入链接
  2. 链接器遇到 main.o,发现用到外部符号 func,因此 func 加入未决表
  3. 链接器到达末尾,并且 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

man ld

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

6. 参考