【C++】静态库和链接选项--whole-archive

欢迎大家关注公众号

最近在迁移服务到Linux,不少人遇到未定义的符号之类的错误无所适从。简单的情况不做介绍,比如库路径不对等,最近几篇文章主要介绍库依赖相关的情况。

预备知识

静态库

静态库实际上是二进制目标文件的集合。生成目标文件,需要用到-c选项;打包静态库用到ar命令。

$ g++ -c a.cpp
$ ar rcs liba.a a.o

第一条命令生成a.o目标文件。

第二条将a.o打包进liba.a静态库,当然目标文件可以有多个。

ar打包常用的几个选项:

  • r:将目标文件打包进库里,如果库里已经有了该模块,则替换(Replace);

  • c:创建(Create)一个库,不管存不存在;

  • s:创建目标文件索引,在创建较大的库时能节省时间。

链接

GCC默认的连接器是ld,一般不直接调用它,而是通过gccg++调用,如:

$ g++ -L. -la -lb main.cpp -o main
  • -L指定库路径,-l指定库名;

  • -la实际指定的库名是liba.a,这是库的默认命名规则,省略掉lib前缀;

  • -l指定的库的链接顺序由右向左,这点不太自然,也就是先尝试连接libb.a,再liba.a;同样,cmake的target_link_libraries指定的库的链接顺序跟GCC保持一致。

问题

示例:主程序main调用库a。

简单起见,代码结构如下:

.
├── a.cpp
├── a.h
├── b.cpp
├── CMakeLists.txt
└── main.cpp

库a

// a.h
void func();

//a.cpp
#include "a.h"
#include 

void func() {
    printf("a-func\n");
}

主程序main

#include "a.h"
#include 

int main() {
    func();
    printf("main\n");
    return 0;
}

使用下面的编译指令会报错:

$ g++ -L. -la main.cpp -o main
/usr/bin/ld: /tmp/ccPkGUcV.o: in function `main':
main.cpp:(.text+0x5): undefined reference to `func()'
collect2: error: ld returned 1 exit status

明明库a里有func,为什么链接器找不到呢?

分析

其实这涉及链接器一条默认行为:如果静态库里的某个目标文件的符号都没被直接或间接使用,链接器就会忽略掉这个文件,用来优化二进制文件的大小。
上面的编译指令,a库在前面,链接器检测到没有被用到,其中的目标文件自然就被忽略掉了,所以后面的main.cpp就找不到func符号了。

最简单的改法是将main.cpp提前,告诉链接器需要用到func符号,从而不会将a.o忽略掉。

$ g++ main.cpp -L. -la -o main
$ ./main
a-func
main

这个例子仅仅只是说明问题,现实中很多情况要复杂得多,不能像上面那样简单地调整顺序解决。所以ld提供了专门的链接选项--whole-archive

$ g++ -L. -Wl,--whole-archive -la -Wl,--no-whole-archive main.cpp -o main
$ ./main
a-func
main

其中,

  • -Wl,[options]用来在编译的时候传递给链接器,否则编译器会不认识这个选项。

  • --whole-archive的作用是将liba.a中的所有目标文件中的符号都链接到可执行文件,不管用不用。

  • --no-whole-archive是它的配对,要一起使用,表示选项的作用到此结束,避免影响其他库的链接规则。

  • --whole-archive--no-whole-archive之间一般是库的列表,本例中只有1个库,可以支持多个,如:

g++ -L. -Wl,--whole-archive -la -la1 -Wl,--no-whole-archive -la2 main.cpp -o main

这种一律链接的方法,比较暴力,虽然省事,也会增加二进制文件的大小,所以仅在必需的时候使用。

cmake使用--whole-archive

cmake中的Generator可以在程序构建的时候执行;cmake 3.24提供了Generator的--whole-archive支持:

cmake_minimum_required (VERSION 3.24.0)
project(main)

add_library(a STATIC a.cpp)

add_executable(${PROJECT_NAME} main.cpp)

target_link_libraries(
    ${PROJECT_NAME}    
    "$"
)

如果你的cmake版本较老,也可以采用下面的方式:

cmake_minimum_required (VERSION 3.24.0)
project(main)

add_library(a STATIC a.cpp)

add_executable(${PROJECT_NAME} main.cpp)

target_link_libraries(
    ${PROJECT_NAME}    
    -Wl,--whole-archive a -Wl,--no-whole-archive
)

推荐第一种方式,这样就不用为每种编译器情况分别处理,cmake已经帮你处理好了;否则,需要你自己处理每种编译器的选项,如MSVC的/WHOLEARCHIVE

应用场景

--whole-archive引入的原因和行为两方面考虑可能的应用场景:

  • 从原因上,主要为了避免静态库中被优化掉目标文件

    凡是目标文件能导致被优化掉的情况,应该都可以使用该选项,如,上面的例子。
    在现实代码环境里,C++的自动注册是一个典型的例子,先挖个坑,下篇文章填。

  • 从行为上,能把静态库里目标文件符号都链接到库或可执行文件

    这给符号的链接提供了一种全新的可能性,还没想到一个好的例子分享给大家,如果大家有好的用例欢迎在留言区分享。我能想到的可能的方式:

    比如,库a强依赖库b的某个版本,非它不可,将b打包到a里,这样使用者就不用自己提供了?

    比如,要更换C/C++库,可以将三方C/C++库打包进程序里,这样就不会使用默认的C/C++库了?

你可能感兴趣的:(c++,开发语言)