深入理解 C++静态库依赖顺序

参考

https://blog.xizhibei.me/2019/02/24/why-library-order-matters-in-cpp-static-linking/
https://eli.thegreenplace.net/2013/07/09/library-order-in-static-linking

前言

之前只是了解 C++ 在链接时对静态库的顺序有要求,被依赖库的要放在后面。但是并不理解其中的原理,今天参考了两篇文章做了下面总结。

原理

C++ 生成可执行文件分为四个步骤:预处理、编译、汇编、链接, 前三个步骤将单个C/C++文件生成 目标文件,里面包含各种符号信息, nm命名可以查看文件中的符号信息,里面会包括依赖其他源文件的未定义符号,这就需要链接过程,将多个目标文件和静态库组装,对符号地址的引用加以修正。
静态库其实就是一组目标文件的集合,但是在链接过程中,对源文件的目标文件和 库中的目标文件处理还是有一些差异。

ar t libfunc.a
func2.o
func1.o
可以看到libfunc静态库 包含两个.o文件
  1. 首先 目标文件和库在命令行上按特定顺序从左到右提供。这个是链接顺序,链接器按照这个链接顺序 挨个处理目标文件和库。

  2. 链接器维护一个符号表(symbol table),这个符号表包含 两个列表 导出符号列表 和 未定义符号表:
    * 导出符号表(exported symbol list):这个表记录着链接器 到目前为止遇到的所有目标文件和库 导出的符号列表。
    * 未定义符号表(undefined symbol list): 到目前遇到 “需要被导入的”的目标文件和库中未定义且在当前导出表中不存在的符号都会加入到符号列表中。

  3. 如果链接器遇到的是目标文件:
    3.1 目标文件的所有导出符号(nm命令显示为T或t 表示定义的符号)都会添加到导出符号表;如果导出符号之前在未定义符号表中,则将此符号从未定义符号表中移出;如果导出符号之前在导出符号表存在,则会报"multiple definition" 错误,两个不同目标文件导出相同 符号会导致链接器混乱
    3.2 目标文件中未定义符号(nm命令显示为U的符号),如果在 导出符号列表中找不到,就被添加到未定义符号列表

  4. 如果链接器遇到的是库,虽然库是一堆目标文件,链接器会遍历库中所有的目标文件,对于每一个目标文件,链接器先查看所有目标文件的导出符号:
    4.1 只要目标文件 任一一个符号在 未定义符号列表中, 目标文件就会被添加到链接器中,然后执行4.2, 否则执行4.3;
    4.2 如果目标文件被添加到链接器中,那么库中的目标文件就按照源文件的目标文件一样处理, 未定义符号和导出符号都添加到符号表中;
    4.3 所有目标文件遍历一遍后,最后,只要库中任一一个目标文件被添加到链接库中,链接库就重新扫描一遍 库中的目标文件 -- 被添加到链接的目标文件的未定义符号 可能包含在相同库中其他的目标文件中。

  5. 链接器查看了一个库后,就不会再查看这个库了,即使之后的库可能需要这个库的导出符号。链接器只有在处理单个库时,才会可能重新扫描一遍这个库的目标文件。 如果库的 一个目标文件没有提供 未定义符号表中需要的导出符号时,这个目标文件被遗弃掉(注: 源文件的目标文件没有这样的判断条件,源文件所有符号都添加到符号表中),这样做的好处是减少库中不必要的目标文件链接到可执行程序中,减小可执行文件的大小

当链接库完成链接后,会查看符号表,如果未定义符号表中有符号,链接器就会抛出 "undefined reference"错误。例如:可执行文件忘记包含 有main函数的文件,就会得到下列错误:
/usr/lib/x86_64-linux-gnu/crt1.o: In function '_start':
(.text+0x20): undefined reference to 'main'
collect2: ld returned 1 exit status

案例说明

源文件所有目标文件的符号都添加到 符号表中

main.c:
int func(int i);

int main() {

   return func(1);
}

func1.c:
int func(int i) {
    return i + 10;
}

int func1(int i) {
    return i + 20;
}

func2.c: 
int func2(int i) {
   return i + 30;
}

gcc main.c
gcc func1.c
gcc func2.c
gcc main.o func1.o func2.o 
nm a.out
000000000060102c B __bss_start
000000000060102c b completed.6337
0000000000601028 D __data_start
0000000000601028 W data_start
0000000000400430 t deregister_tm_clones
00000000004004a0 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
00000000004005b8 R __dso_handle
0000000000600e28 d _DYNAMIC
000000000060102c D _edata
0000000000601030 B _end
00000000004005a4 T _fini
00000000004004c0 t frame_dummy
0000000000600e10 t __frame_dummy_init_array_entry
0000000000400760 r __FRAME_END__
0000000000400500 T func
000000000040050f T func1
0000000000400520 T func2
0000000000601000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004003a8 T _init
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
00000000004005b0 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
                 w _Jv_RegisterClasses
00000000004005a0 T __libc_csu_fini
0000000000400530 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
00000000004004f0 T main
0000000000400460 t register_tm_clones
0000000000400400 T _start
0000000000601030 D __TMC_END__

func2.c 的函数在其他文件都没有引用到,但是可以看到 源文件的目标文件所有符号都会添加到符号表中,a.out中有 func2符号

库中目标文件 如果任意一符号是 符号表的未定义符号列表中需要的符号,那么这个目标文件的所有符号都加入到符号表中, 如果没有则跳过这个符号文件

main.c:
int func(int i);

int main() {

   return func(1);
}

func1.c:
int func(int i) {
    return i + 10;
}

int func1(int i) {
    return i + 20;
}

func2.c: 
int func2(int i) {
   return i + 30;
}
gcc main.c
gcc func1.c
gcc func2.c
ar r libfunc.a func1.o func2.o
gcc -L. main.o -lfunc

000000000060102c B __bss_start
000000000060102c b completed.6337
0000000000601028 D __data_start
0000000000601028 W data_start
0000000000400430 t deregister_tm_clones
00000000004004a0 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
00000000004005a8 R __dso_handle
0000000000600e28 d _DYNAMIC
000000000060102c D _edata
0000000000601030 B _end
0000000000400594 T _fini
00000000004004c0 t frame_dummy
0000000000600e10 t __frame_dummy_init_array_entry
0000000000400728 r __FRAME_END__
0000000000400500 T func
000000000040050f T func1
0000000000601000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004003a8 T _init
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
00000000004005a0 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
                 w _Jv_RegisterClasses
0000000000400590 T __libc_csu_fini
0000000000400520 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
00000000004004f0 T main
0000000000400460 t register_tm_clones
0000000000400400 T _start
0000000000601030 D __TMC_END__

这里可以看到对应 libfunc.a静态库
  func1.o 的func1符号虽然没有其他 目标文件引用,但是因为 func被引用,func1.o加入到链接库中,func1符号也被加入到符号表中
  func2.o 因为没有符号 在符号表中被需要,func2符号没有添加到 a.out

库中只要有一个目标文件被添加到链接库中,链接器就会重新扫描库中的所有目标文件,查看是否有新的目标文件需要被添加到链接库中

main.c:
int func(int i);

int main() {

   return func(1);
}

func1.c:
int func(int i) {
    return i + 10;
}

int func1(int i) {
    return func2(i) + 20;
}

func2.c:
int func2(int i) {
   return i + 30;
}

gcc main.c
gcc func2.c
gcc func1.c
ar r libfunc.a func2.o func1.o
gcc -L. main.o -lfunc
nm a.out
000000000060102c B __bss_start
000000000060102c b completed.6337
0000000000601028 D __data_start
0000000000601028 W data_start
0000000000400430 t deregister_tm_clones
00000000004004a0 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
00000000004005c8 R __dso_handle
0000000000600e28 d _DYNAMIC
000000000060102c D _edata
0000000000601030 B _end
00000000004005b4 T _fini
00000000004004c0 t frame_dummy
0000000000600e10 t __frame_dummy_init_array_entry
0000000000400770 r __FRAME_END__
0000000000400500 T func
000000000040050f T func1
0000000000400530 T func2
0000000000601000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004003a8 T _init
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
00000000004005c0 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
                 w _Jv_RegisterClasses
00000000004005b0 T __libc_csu_fini
0000000000400540 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
00000000004004f0 T main
0000000000400460 t register_tm_clones
0000000000400400 T _start
0000000000601030 D __TMC_END__

首先构建静态库,顺序是 func2.o func1.o

nm libfunc.a
func2.o:
0000000000000000 T func2

func1.o:
0000000000000000 T func
000000000000000f T func1
                 U func2

那么链接器先链接main.o 当前 导出符号表: main, 未定义符号表: func
在链接 libfunc.a时也是先链接 func2.o 然后是 func1.o, 链接func2.o时,没有符号表需要的符号被跳过, 链接func1.o时, 因为func 被需要,func1.o的所有符号被加入到符号表中, func因为加入到导出符号表,从未定义符号表中删除, func1加入到导出表,func2加入到未定义符号表, 当前导出符号表: main、func、func1,未定义符号表:func2
因为func1.o被加入到链接器中, 链接器重新扫描 libfunc.a的所有目标文件, func2.o的func2被需要,加入到导出列表并从未定义列表中删除。

静态库按照顺序链接,不会再重新扫描,除非在库列表中又重新添加一遍

main.c:
int func(int i);

int main() {

return func(1);
}

func1.c:
int func(int i) {
return i + 10;
}

int func1(int i) {
return func2(i) + 20;
}

func2.c:
int func2(int i) {
return i + 30;
}

gcc main.c
gcc func2.c
gcc func1.c

ar r libfunc1.a func1.o
ar r libfunc2.a func2.o

gcc -L. main.o -lfunc2 -lfunc1
./libfunc1.a(func1.o): In function func1': func1.c:(.text+0x25): undefined reference to func2'
collect2: error: ld returned 1 exit status

先链接main.o,当前 导出符号表: main, 未定义符号表: func
然后链接 libfunc2.a, 目标文件 func2.o因为没有要用到的符号被跳过, libfunc2.a l链接完成
接着链接 libfunc1.a,因为func符号被用到,func1.o的所有符号被加入到符号表中, func因为加入到导出符号表,从未定义符号表中删除, func1加入到导出表,func2加入到未定义符号表, 当前导出符号表: main、func、func1,未定义符号表:func2
链接完成,当前未定义符号表中还有func2,导致报错

解决方法1: 库链接中后面接着加上 libfunc2
gcc -L. main.o -lfunc2 -lfunc1 -lfunc2

解放方法2:调整顺序
gcc -L. main.o -lfunc1 -lfunc2
posted @ 2022-12-14 17:31  蓝天飞翔的白云  阅读(958)  评论(0编辑  收藏  举报