编译的库能否跨Linux发行版运行,本质是二进制兼容性问题。
glibc版本差异只是冰山一角,坑还有很多。
总结起来,大概有五点。
1)系统基础库版本差异
glibc(C标准库)版本必须 ≥ 编译时版本。
libstdc++(C++标准库)要匹配GCC版本。
比如,用Ubuntu 22.04(glibc 2.35)编译的库,在CentOS 7(glibc 2.17)上直接跑会炸。
2)编译器优化参数有影响
若编译时用了-march=native
这类CPU专属优化时,二进制文件可能依赖特定CPU指令集(可能包含AVX2等扩展指令)。
解决方法:编译时加-march=x86-64 -mtune=generic
普适参数。
3)动态库路径
不同发行版动态库安装路径可能不同(/lib vs /lib64),有的版本把ld-linux.so藏在/lib目录下,有的放在/lib64目录,甚至像Arch Linux这种极简主义系统,连目录结构都重新设计了。
编译时加-Wl,-rpath='$ORIGIN'
让程序优先从当前目录找库。
4)ABI兼容性
C++库容易中招,比如,GCC5之后改了C++11的ABI。
编译时加-D_GLIBCXX_USE_CXX11_ABI=0
强制旧版兼容模式。
5)内核特性依赖
直接调用新内核系统调用(如io_uring)的库在旧内核上会段错误
查看/proc/kallsyms
确认目标系统内核支持的功能
有三种比较好的解决办法,可以三选一。
1)静态编译,-static
打包所有依赖(但glibc不能静态链接)。
2)容器化,直接打包成Docker镜像。
3)降级到最低兼容环境编译,用老系统或交叉编译工具链
为什么在宿主机环境的Linux编译的库在别的Linux用不了?
即使都是Linux系统,并不意味着用A系统编译的库就能在B系统“无缝”运行。主要原因是:Linux的内核、底层库和软件环境不是完全一致的。
主要影响因素有哪些?
1. glibc的版本
glibc(GNU C Library)是Linux系统非常核心的库,绝大多数程序和库都依赖它。
如果我们的库是在glbic版本较高的系统上编译的,而运行目标系统的glibc版本较低,就有“找不到符号”或“版本太低”等错误。glibc向前兼容但不向后兼容。
2. 其它依赖库的版本
除了glibc,还有诸如libstdc++
(C++标准库)、libpthread
(多线程支持)、librt
(实时扩展)等依赖库。
如果这些库的版本在目标系统和编译系统之间有差异,也可能导致运行时出错。
3. 系统架构和内核接口
虽然说不考虑CPU架构,但还要注意不同Linux发行版对某些函数、系统调用的支持可能不一样,尤其是新内核才有的新特性。
4. 编译选项和工具链
比如我们在gcc/g++编译时用了 -march=native
之类的参数,生成的代码可能只适合当前机器,发到别处跑就会报不支持的指令错误。
选用比较新的编译器工具链,也可能导致生成的二进制用不了。
5. 运行时路径与依赖找不到
有些库依赖运行时的配置,比如 /etc/ld.so.conf
或 LD_LIBRARY_PATH。如果库找不到依赖,也会失败。
要怎么提高通用性/兼容性?
1. 选择更老的Linux系统来编译
优点
- 通常用 CentOS 7、Ubuntu 16.04 等发行版打包,可以保证在多数主流Linux上用。兼容大部分市场上的Linux发行版。
- 向前兼容性好,低版本glibc编译的程序在高版本glibc系统上通常能正常运行
- 不增加程序体积,不需要额外打包依赖
- 实现简单,不需要特殊技术,只需要较老的编译环境
缺点
- 功能受限,无法使用新版glibc和编译器提供的新特性和优化
- 性能可能受影响,老编译器可能缺少某些优化选项
- 增加环境维护成本,需要维护老旧的编译环境(虚拟机或容器)
- 最重要的是不能解决所有问题,仍可能有其他系统依赖不兼容
2. 静态链接尽量多的依赖
优点
- 把依赖库直接打包进我们的库文件,即“全家桶”,减少外部对环境的依赖。不过
glibc
不建议静态链接。 - 能够减少外部依赖,不受目标系统库版本影响
缺点
- 体积膨胀:可执行文件会显著增大
- 最重要的是glibc不建议静态链接。glibc静态链接可能会出现问题,比如NSS(名称解析)功能可能失效,动态加载模块功能受限,内存分配和线程可能有异常。
3. 使用容器、AppImage、Flatpak等技术
优点
- 使用Docker、AppImage等,把所有依赖和运行环境都“封装”进去,能最大限度减少环境适配难度。
- 环境隔离,可以完全控制运行环境,几乎消除兼容性问题
- 版本并存,同一系统可运行不同版本的应用
- 全栈打包,从库到配置文件全部打包
- 升级方便,整体更新,不破坏依赖关系
缺点
- 资源消耗,磁盘空间和内存占用较大
- 影响运行效率,可能有轻微性能损失(Docker尤其如此)
- 复杂性增加,需要了解容器技术和维护容器镜像
- 最重要的是对目标系统有要求,目标系统必须支持相应容器技术。有些旧的linux不支持容器,比如ubuntu14.04 32位。。
4. 编译时避免使用平台相关/新特性
优点
- 不要用太“新潮”的编译参数或函数特性。保持环境、编译参数为广泛兼容配置。最大限度提高跨平台兼容性
- 减少平台锁定,避免对特定平台特性的依赖
缺点
- 无法利用新平台特性带来的便利和性能优势
- 可能需要编写更多兼容性代码,限制开发时使用现代工具和库,可能会导致为了兼容性而放弃更优解决方案
转自:https://www.zhihu.com/question/11649951049