之前一篇文章讲了C++编译过程,经过前面的分析,我们知道C++代码走完编译全流程,将汇编文件*.o通过ld指令链接运行库CRT,就可以生成可执行程序,当然这有一个前提是源码中有默认入口函数main或者ld -E指定一个入口函数。
与生成可执行文件类似,静态库与动态库也要经过编译的前三步,将源码翻译成汇编文件*.o,只是不执行最后一步的ld链接指令。
静态库生成
静态库生成就像压缩打包,用ar工具来生成:
ar cr libhelloworld.a [xxx.o]
# -c 如果存档文件不存在,则创建。
# -r 向存档文件中插入.o文件,替换已有的同名文件,新成员添加到文档末尾。
# 可以通过ar t xxx.a查看已有的归档内容。
ar工具有一个妙用:当你依赖的第三方库比较多,比如grpc工程、boost库、abseil等,可以在生成静态库的时候,把所有*.o中间文件全部打包到一个libxxx.a静态库中,这样编译的时候就不需要每一个静态库都在编译脚本中写一遍。打包的参考命令:
find ./ -name "*.o" | xargs ar cr libxxx.a
静态库生成的二进制部署非常方便。在实际工作中,我参与过的几个业务,也是都使用静态库,所有代码在一个大仓下面,通过bazel的BUILD来处理编译依赖。
静态链接的坑
静态库需要静态链接,静态链接通常的指令:
g++ main.cpp -s -static ./libxxx.a
在实际工程中使用静态链接要小心,openssl的Configure选项对-static有专门的描述,总结起来就是因为存在getaddrinfo和gethostbyname的调用,建议在生产环境不要用-static来消除glibc动态库的依赖。为了让大家注意到这个点,编译器会产生如下告警:
../libopenssl/3rdlib/openssl/lib/libcrypto.a(b_sock.o): In function `BIO_gethostbyname':
b_sock.c:(.text+0x71): warning: Using 'gethostbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
这个告警是说gethostbyname静态链接时一定会链接glibc的动态库版本,类似的函数还有dlopen、getaddrinfo、getpwnam、getpwuid、endpwent、getservbyname。
为什么会有告警呢,因为上面的函数即使采用-static静态链接,运行时也需要dlopen glibc。这种行为是未知的,可能因为glibc环境不同导致崩溃。
gethostbyname是万恶之源,静态链接在多个线程中都调用到gethostbyname函数,程序就必然崩溃,也无法捕获异常,加锁也不行,多线程中静态链接gethostbyname和getaddrinfo也会crash。
可以通过以下参数检查是否依赖glibc,非静态链接就会有依赖的符号,而静态链接则没有:
# nm a.out |grep GLIBC_
nm: a.out: no symbols 表示没有glibc的依赖
那么既然只有几个函数有这样的问题,能不能找出那几个函数对应的glibc库,不对其进行静态链接呢?ld的确提供了这样的方法,比如:
g++ main.cpp -Bstatic -lm -Bdynamic -ldl
只不过这样做太麻烦了。实际上glibc在每台机器上都必须有,所以只需要静态链接c++的库就可以满足很多跨机拷贝执行的需求:
g++ main.cpp -s -static-libstdc++ -static-libgcc
不过glibc版本只向下兼容,如果在高版本的机器上编译,就不能在低版本的机器上运行:
/lib64/libc.so.6: version `GLIBC_2.25' not found (required by ./a.out)
此时就需要自己来判断是否使用-static选项了,建议不要在生产环境使用。生产环境也可以选择:
1、容器发布
2、打包SO文件
3、glibc多版本共存
动态库生成
尽管我不使用动态库,但是据我所知也有不少团队使用动态库插件,以便于热更新。比如国内某头部云厂商的主机安全agent就是动态库方式。相信还有很多C端的程序,也偏向于使用动态库。
动态库使用g++指令生成:
g++ -shared -fPIC -o libhelloworld.so [xxx.o]
# -share该选项指定生成动态连接库。
# -fPIC表示编译为位置独立的代码,不用此选项的话,编译后的代码是位置相关的,所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
生成动态库时链接静态库要小心,尤其是当出现如下告警时:
relocation R_X86_64_PC32 against symbol 'ares_free' can not be used when making a shared object; recompile with -fPIC
这个时候如果根据错误提示,贸然的将链接的静态库加上-fPIC,可能会导致段错误。推荐的做法是链接的库也改成动态库。
其实,动态库和静态库,没有绝对的优劣势,重要的是要看是否适合自己业务场景。