欢迎大家关注公众号
最近在迁移服务到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
,一般不直接调用它,而是通过gcc
或g++
调用,如:
$ 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中的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++库了?