本文将讲给您:
1.怎样把libc静态链接到程序中。
2.怎样把程序打包成"不调用"glibc动态库的模块。
3.怎样把c++11/c++17的程序嫁接到gcc4.8.5甚至更低版本编译器生成的程序上。
我们用c++11(c++17)实现了一个动态库(libA.so)并交给客户,客户要将这个动态库在10年前的老系统中进行链接。
老系统使用gcc-4.1.2,glibc-2.5,不支持c++11标准。
用gcc-9编译好libA.so,拿给老系统去链接,会直接报出类似下边的错误
ld:对符号'clock_gettime@@GLIBC_2.31'的未定义引用
下图可表示当前遇到的问题:
1)针对以上问题,有一种便捷的处理方式是,升级目标机的glibc到高版本。因为glibc是严格向下兼容的,如果可以获得目标机的升级权限,这种方法简单易行,到官网下载稳定源码包,编译安装即可。必要时可直接安装二进制包,或者交叉编译源码。
2)考虑到方法1存在线上升级失败的可能性,我们还需要探索其他方法:是否可以安装glibc-2.31到线上机器,并且不覆盖glibc-2.5版本,让主程序去动态链接glibc-2.5,而让libA.so去动态链接glibc-2.31?
首先他们的库名称SONAME都是libc.so.6,程序运行时只会动态链接一个,修改其中一个的SONAME,可以使主程序同时链接到libc高版本和低版本的两个库。于是遇到新的问题:两个版本的glibc都拥有glibc-2.5版本以下的符号,同时链接它们,会出现符号重定义的错误。
初步尝试将2.31源码分离,剥离掉2.5以下版本的符号,后来还是选择了放弃。
3)重定义的符号,是否有并存的可能性?
就像这段代码
//func.c
void func() { printf("func from file 1"); }
//main.c
void func() { printf("func from file 2"); }
void main() {
func();
}
> gcc func.c main.c ↙
func.c:(.text+0x0): multiple definition of `func'; /tmp/cctl1JQy.o:main.c:(.text+0x0): first defined here
很明显,编译不通过
我们换一个工程结构,如下图:
这一次,main.o通过test.o的跳转,链接到了func.o;在同一程序中,实现了同时对第一个func()和对第二个func()的调用
这里我们先说用到的技术点,这也是本文解决的第一个困难:
gcc传给链接器的 -Wl,–version-script=./test.map 指令,可指定链接目标中符号的可见性 ,public属性的符号可外部链接,private属性的符号,外部不可链接。
//test.map
shared.so
{
global:
test;
local:
*;
};
> ar -rc test.a test.o
> ar -rc func.a func.o
> gcc test.a func.a -Wl,–version-script=./test.map -o libshared.so ↙
检查一下程序运行的结果
> ./run ↙
func() from ..//main.c
func() from ..//func.c
现在,在同一个程序中,我们调用到了两个func()函数,
完整示例源码和makefile下载地址:
https://github.com/sunxiao2010n/statically-link-glibc/blob/master/link-the-same-symbols/makefile
有了这个前提,我们回到开头的问题上:
现在,我们尝试把功能模块libA.so编译成刚刚libshared.so的样子,同时,把高版本的glibc打包到libA.so当中,除功能模块接口外,隐藏一切其他符号。
4)静态链接libc.a
linux默认提供的libc.a对于静态链接非常不友好,因为它早期是不支持静态链接的,2.2以下版本的源程序,社区自己也找不到了。
静态链接中存在的一些问题,比如,libc.a会再去动态链接libnss,libnss又属于libc,这会让好不容易去掉的动态库,又被链接回来。虽然能传给编译器-static-libnss选项来避免这个问题,iconv、getaddrinfo、stdio.h中的函数,也都会触发类似的机制,这让libc.so不是那么容易被静态链接。
https://stackoverflow.com/questions/57476533/why-is-statically-linking-glibc-discouraged
5)可否考虑换一个开源的libc
我们选取了两个开源项目,来尝试替换glibc,这是本文尝试解决的第二个困难。
1.musl
musl库对glibc有较好的兼容性,编译选项简单,容易上手使用,对c程序能有良好的兼容,而对c++的兼容则需要改动源码来解决。因为musl维护较慢,(如对gcc-6.0以后的-fstack-protector机制支持并不完善),只好用低版本gcc来编译/链接musl。可是低版本的gcc又蕴含着丰富的bug,足以让实验进行不下去,这是没有继续使用musl的原因。
2.uclibc-ng
它是uclibc的next generation,这个项目期初是给嵌入式用的,现在x86-64上也能用。uclibc-ng需要手动导入目标平台的kernel头文件,其他的配置项也比较多,上手方面不大友好。在进行了细致的研究,找到了哪些配置项必选、哪些项不能选以后,我们编译了特定linux-2.6.16.700平台使用的静态库,生成了能更好的适配目标运行环境的libc.a
接下来的实验,我们用的就是这个libc.a
http://t.zoukankan.com/merlindu-p-6370825.html
6)继续尝试,换成开源libc
6.1 如果执行一条最简单的编译指令,发生什么?
gcc -shared func.c -o libshared.so
这时系统会输出:
/usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/cchlPq3Y.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/9/…/…/…/x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/…/…/…/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/…/…/…/x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/…/…/…/…/lib -L/lib/x86_64-linux-gnu -L/lib/…/lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/…/lib -L/usr/lib/gcc/x86_64-linux-gnu/9/…/…/… /tmp/ccaUWSnX.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/…/…/…/x86_64-linux-gnu/crtn.o
先研究一下这几个编译选项(略):
Scrt1.o
crti.o
crtbeginS.o
-lgcc_s
-lgcc
crtendS.o
crtn.o
https://blog.csdn.net/farmwang/article/details/73195951
6.2 接下来的就是链接开源的libc,链接的目标是:
1.生成的功能模块不依赖于其他动态库,statically linked。
2.链接好的功能模块不暴露所依赖的其他库的符号。
我们在6.1链接libshared.so的这条命令基础上,编写一条编译命令
gcc -ggdb3 -v -shared -nostdlib -static-libgcc -fPIC -Wl,--whole-archive test.a -Wl,--no-whole-archive func.a ${HIGH_LIB_DIR}/libc.a -Wl,--version-script=./test.map -o libshared.so
其中
-nostdlib:不使用默认库
--whole-archive:主调文件
--no-whole-archive:依赖文件
--version-script:指定符号在链接时的可见性
${HIGH_LIB_DIR}/libc.a:开源的libc
详细的源程序请参考:
https://github.com/sunxiao2010n/statically-link-glibc/blob/master/link-c-programme/makefile
我们在ubuntu-20.04/gcc-9环境下编译好run、libshared.so,并将这两个文件拷贝到centos5/gcc-4.1.2/glibc-2.5环境下运行。运行通过。
6.3 最后我们再编写一个c++11标准的程序。
gcc -mabi=sysv -nostdlib -v -ggdb3 -shared -static-libgcc -static-libstdc++ -fPIC -Wl,--whole-archive test.a -Wl,--no-whole-archive ${LINK_LIBS_BEGIN} func.a ${LINK_STDCPP} ${LINK_LIBS_END} -Wl,--version-script=./test.map -o libshared.so
其中
-mabi=sysv:使用sysv版本的abi,这是为了适配足够老的目标机,倘若生成gnu类型的abi,程序是不能加载的。
-static-libstdc++:指定静态链接stdc++库,libstdc++依赖于libgcc、libc,libstdc++属于编译器的一部分,一般可以通过yum/apt源来安装。
详细的源程序请参考:
https://github.com/sunxiao2010n/statically-link-glibc/blob/master/makefile
链接uclibc过程中会报出__preinit_array_end未定义的问题,这是由于这个符号是由gcc的编译脚本提供,并且静态链接时,gcc不会提供它的实现。所以报错。
在 __uClibc_main.c 中加入下面的代码,跳过c++静态对象的析构
#undef __UCLIBC_CTOR_DTOR__
再次编译安装uclibc,重新编译我们的程序,就得到了无依赖so文件的功能模块
同样,在ubuntu-20.04/gcc-9环境下编译好run、libshared.so,并将这两个文件拷贝到centos5/gcc-4.1.2/glibc-2.5环境下运行。运行通过。
到这里,我们分别实现了高版本c程序,c++程序的abi和libc降级。
苦于客户环境限制过多、线上机器系统过老的小伙伴们,不妨拿来一试验,希望这篇文章能帮助到你。
链接低版本的libstdc++.a容易导致程序运行失败。这里有编译器对c++版本支持的列表
https://gcc.gnu.org/projects/cxx-status.html#cxx17
这里有从gcc13中编译好的libstdc++.a,方便下载使用。
https://download.csdn.net/download/sxncusc/86019835