Linux部署程序之glibc兼容性问题
在部署程序的时候,一般会遇到glibc不兼容的问题,现象如下:
/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21’ not found
在此之前先要了解一下 gcc/glibc/libc/libstdc++
是什么东东。
gcc/glibc/libc/libstdc++
Linux下开发时经常会遇到 libc、glib、glibc、eglibc、libc++、libstdc++、gcc、g++ 它们都是什么?
gcc/g++
GCC 是 GNU 编译器集合的意思(GNU Compiler Collection), 它可以编译C、C++、JAV、Fortran、Pascal、Object-C、Ada等语言。
- gcc是GCC中的GUN C Compiler(C 编译器)
- g++是GCC中的GUN C++ Compiler(C++编译器)
🗣️就本质而言,gcc和g++并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的GUN编译器而已,比如,用gcc编译一个c文件的话,会有以下几个步骤:
- Step1:Call a preprocessor, like cpp.
- Step2:Call an actual compiler, like cc or cc1.
- Step3:Call an assembler, like as.
- Step4:Call a linker, like ld
由于编译器是可以更换的,所以gcc不仅仅可以编译C文件。
所以,更准确的说法是:gcc调用了C compiler,而g++调用了C++ compiler
具体而言
- g++ 会把 .c 文件当做是 C++ 语言 (在 .c 文件前后分别加上 -xc++ 和 -xnone, 强行变成 C++), 从而调用 cc1plus 进行编译.
- g++ 遇到 .cpp 文件也会当做是 C++, 调用 cc1plus 进行编译.
- g++ 还会默认告诉链接器, 让它链接上 C++ 标准库.
- gcc 会把 .c 文件当做是 C 语言. 从而调用 cc1 进行编译.
- gcc 遇到 .cpp 文件, 会处理成 C++ 语言. 调用 cc1plus 进行编译.
- gcc 默认不会链接上 C++ 标准库.
libc
libc 是 linux 下的旧C ANSI C函数库,也就是 #include < stdio.h>
定义的地方,后来逐渐被glibc取代,也就是 GNU C Library。此外还有klibc、uclibc,但现在用的最多的是 glibc。
主流的一些linux操作系统如 Debian, Ubuntu,Redhat等用的都是glibc。
那 ANSI C 函数库是基本的 C 语言函数库,包含了 C 语言最基本的库函数。这个库可以根据头文件划分为 15 个部分,其中包括:
- <ctype.h>:包含用来测试某个特征字符的函数的函数原型,以及用来转换大小写字母的函数原型;
- <errno.h>:定义用来报告错误条件的宏;
- <float.h>:包含系统的浮点数大小限制;
- <math.h>:包含数学库函数的函数原型;
- <stddef.h>:包含执行某些计算 C 所用的常见的函数定义;
- <stdio.h>:包含标准输入输出库函数的函数原型,以及他们所用的信息;
- <stdlib.h>:包含数字转换到文本,以及文本转换到数字的函数原型,还有内存分配、随机数字以及其他实用函数的函数原型;
- <string.h>:包含字符串处理函数的函数原型;
- <time.h>:包含时间和日期操作的函数原型和类型;
- <stdarg.h>:包含函数原型和宏,用于处理未知数值和类型的函数的参数列表;
- <signal.h>:包含函数原型和宏,用于处理程序执行期间可能出现的各种条件;
- <setjmp.h>:包含可以绕过一般函数调用并返回序列的函数的原型,即非局部跳转;
- <locale.h>:包含函数原型和其他信息,使程序可以针对所运行的地区进行修改。
- 地区的表示方法可以使计算机系统处理不同的数据表达约定,如全世界的日期、时间、美元数和大数字;
- <assert.h>:包含宏和信息,用于进行诊断,帮助程序调试。
由于libc逐渐被glibc取代,所以就不在介绍
glibc
glibc是linux下面c标准库的实现,即GNU C Library。glibc本身是GNU旗下的C标准库,后来逐渐成为了Linux的标准c库,而Linux下原来的标准c库Linux libc逐渐不再被维护。
由于 Linux 是用 C 语言写的,所以 Linux 的一些操作是用 C 语言实现的,因此,GUN 组织开发了一个 C 语言的库 以便让我们更好的利用 C 语言开发基于 Linux 操作系统的程序。 不过现在的不同的 Linux 的发行版本对这两个函数库有不同的处理方法,有的可能已经集成在同一个库里了。
glibc在/lib目录下的.so文件为libc.so.6
那glibc都做了些什么呢?
glibc是Linux系统中最底层的API,几乎其它任何的运行库都要依赖glibc。
glibc最主要的功能就是对系统调用的封装。怎么能在C代码中直接用fopen函数就能打开文件? 打开文件最终还是要触发系统中的sys_open系统调用,而这中间的处理过程都是glibc来完成的。
查看 glibc 的版本
vincent@msi-creator-15:~$
---➤ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31
Copyright (C) 2020 自由软件基金会。
这是一个自由软件;请见源代码的授权条款。本软件不含任何没有担保;甚至不保证适销性
或者适合某些特殊目的。
由 Roland McGrath 和 Ulrich Drepper 编写。
vincent@msi-creator-15:~$
---➤ strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC
GLIBC_2.2.5
GLIBC_2.2.6
GLIBC_2.3
GLIBC_2.3.2
GLIBC_2.3.3
GLIBC_2.3.4
GLIBC_2.4
GLIBC_2.5
GLIBC_2.6
GLIBC_2.7
GLIBC_2.8
GLIBC_2.9
GLIBC_2.10
GLIBC_2.11
GLIBC_2.12
GLIBC_2.13
GLIBC_2.14
GLIBC_2.15
GLIBC_2.16
GLIBC_2.17
GLIBC_2.18
GLIBC_2.22
GLIBC_2.23
GLIBC_2.24
GLIBC_2.25
GLIBC_2.26
GLIBC_2.27
GLIBC_2.28
GLIBC_2.29
GLIBC_2.30
GLIBC_PRIVATE
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
eglibc
这里的e是Embedded的意思,也就是前面说到的变种glibc。
eglibc的主要特性是为了更好的支持嵌入式架构,可以支持不同的shell(包括嵌入式),但它是二进制兼容glibc的,就是说如果你的代码之前依赖eglibc库,那么换成glibc后也不需要重新编译。
曾经 ubuntu 使用过一段时间 eglibc。
从 Debian 8.0 之后 ubuntu 又切换回 glibc 了。
glib
glib也是个c程序库,不过比较轻量级,glib 可以在多个平台下使用,比如 Linux、Unix、Windows 等。
📢 glib 是 Gtk+ 库和 Gnome 的基础
那它跟glibc有什么关系吗?
其实,glib 和 glibc 基本上没有太大联系,可能唯一的共同点就是,其都是 C 编程需要调用的库而已。
libc++
libc++是llvm搞的,是C++标准库的实现,clang++ 默认的 stdlib 链接库。但是一般 Linux 都不会自带它,即使安装了 clang 也不会带,要手动安装。因为 clang 对 libstdc++的支持会更好 😅
libstdc++
libstdc++ 是 gcc 对C++ 标准库的实现,g++ 默认的 stdlib 链接库。
Debian 自带了 libstdc++ 动态库,路径一般是 /lib/x86_64-linux-gnu/libstdc++.so.6
。
libstdc++与gcc是捆绑在一起的,也就是说安装gcc的时候会把libstdc++装上。
那为什么glibc和gcc没有捆绑在一起呢?
相比glibc,libstdc++虽然提供了c++程序的标准库,但它并不与内核打交道。对于系统级别的事件,libstdc++首先是会与glibc交互,才能和内核通信。相比glibc来说,libstdc++就显得没那么基础了。
查看 libstdc++ 支持的 GLIBCXX 版本
GLIBCXX和GLIBC是两个东西,前者针对C++,后者针对C。
vincent@msi-creator-15:~$
---➤ strings /lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX
GLIBCXX_3.4
GLIBCXX_3.4.1
GLIBCXX_3.4.2
GLIBCXX_3.4.3
GLIBCXX_3.4.4
GLIBCXX_3.4.5
GLIBCXX_3.4.6
GLIBCXX_3.4.7
GLIBCXX_3.4.8
GLIBCXX_3.4.9
GLIBCXX_3.4.10
GLIBCXX_3.4.11
GLIBCXX_3.4.12
GLIBCXX_3.4.13
GLIBCXX_3.4.14
GLIBCXX_3.4.15
GLIBCXX_3.4.16
GLIBCXX_3.4.17
GLIBCXX_3.4.18
GLIBCXX_3.4.19
GLIBCXX_3.4.20
GLIBCXX_3.4.21
GLIBCXX_3.4.22
GLIBCXX_3.4.23
GLIBCXX_3.4.24
GLIBCXX_3.4.25
GLIBCXX_3.4.26
GLIBCXX_3.4.27
GLIBCXX_3.4.28
GLIBCXX_DEBUG_MESSAGE_LENGTH
场景
开发环境为gcc 11.1.0,glibc版本是2.31,但是生产环境的 glibc 是2.14。
那么编译好的程序如何在生产环境中运行呢?后续会讲解。
这里要提到一个知识点,比如我们当前的系统是 ubuntu 20.04,系统默认安装的是 gcc 9.3.0,glibc版本是2.31。
vincent@msi-creator-15:~$
---➤ gcc-9 --version
gcc-9 (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
vincent@msi-creator-15:~$
---➤ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31
Copyright (C) 2020 自由软件基金会。
这是一个自由软件;请见源代码的授权条款。本软件不含任何没有担保;甚至不保证适销性
或者适合某些特殊目的。
由 Roland McGrath 和 Ulrich Drepper 编写。
但是我们如何使用更高版本的gcc呢,一般来说有两种方式:
- 通过 apt 来安装
- 通过源码编译安装
有时我们可以通过apt进行安装,但往往版本不会太新,如果想使用比较新的gcc,就要通过源码的方式安装。
那么编译源码使用的是 gcc 9.3.0 版本的编译器去编译 gcc 11.1.0 版本的代码
📢 编译更高版本的gcc可能需要更高版本的编译器
编译好的 gcc 11.1.0 是不带有 glibc 库的,依赖的还是 glibc 2.31。
也就是说通过 gcc 11.1.0 编译的程序最终是依赖 glibc 2.31,除非自行编译了更高版本的 glibc 。
但是 libstd++ 是和gcc版本挂钩的,也就是编译 gcc 的时候就会生成 libstdc++.so 动态库。
案例介绍
当前系统环境:
- gcc 6.3.0
- glibc 2.24
- glibcxx 3.4.22
目标系统环境:
- gcc 4.8.5
- glibc 2.17
- glibcxx 3.4.19
示例程序:
//regex.cpp
#include<iostream>
#include <regex>
using namespace std;
int main()
{
// gcc 4.8.5 run error
std::regex m_regex("[a-z]+");
std::cout << std::regex_match("abc", m_regex) << std::endl;
return 0;
}
编译运行
g++ -o regex regex.cpp
查看 regex 运行所需要的动态库有哪些,通过 ldd 查看。
$ ldd regex
linux-vdso.so.1 (0x00007fff087f9000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdeb0b41000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdeb083d000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdeb0626000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdeb0287000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdeb1103000)
可以看到主要的两个库 libc.so.6
和 libstdc++.so.6
都是使用系统内默认的。
如果此时把 regex 程序拷贝到 gcc 4.8.5 环境中运行将会报错。
$ ./regex
./regex: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by ./regex)
./regex: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./regex)
可以看出 regex 运行最低需要 GLIBCXX_3.4.21 版本。
那在看看 regex 运行所需要的动态库。
$ ldd regex
./regex: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by ./regex)
./regex: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./regex)
linux-vdso.so.1 => (0x00007ffff05d7000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f1d59c9b000)
libm.so.6 => /lib64/libm.so.6 (0x00007f1d59999000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f1d59783000)
libc.so.6 => /lib64/libc.so.6 (0x00007f1d593b6000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1d5a1e2000)
依赖的库是一样的,只是版本不对。
解决兼容性的方法
目前来说有如下几种方式:
- 打包依赖动态库并修改elf(推荐)
- 静态编译
- docker容器
- 升级gcc/g++版本(包含glibc)
打包依赖动态库并修改elf
打包依赖库
在开发环境中,我们把运行所需要的动态库都打包到一个目录下。
可以使用下面的脚本
#!/bin/bash
# copylib.sh
LibDir=$PWD"/lib"
Target=$1
lib_array=($(ldd $Target | grep -o "/.*" | grep -o "/.*/[^[:space:]]*"))
$(mkdir $LibDir)
for Variable in ${lib_array[@]}
do
cp "$Variable" $LibDir
done
执行
./copylib.sh regex
$ tree
.
├── cplib.sh
├── lib
│ ├── ld-linux-x86-64.so.2
│ ├── libc.so.6
│ ├── libgcc_s.so.1
│ ├── libm.so.6
│ └── libstdc++.so.6
└── regex
修改elf的rpath和dynamic loader
这里要解释一下 rpath 和 dynamic loader 是什么?
-
动态库加载器dynamic loader是在程序启动时,操作系统会把控制权转交给ld-linux-x86-64.so.2,而不是交给程序正常的进入地址,ld-linux-x86-64.so.2会寻找并加载所有需要的库文件,然后再将控制权交给应用的起始入口。
它和glibc是关联的,也就是必须要使用正确的动态加载器,当然高版本一般是向下兼容的。
经过查看文件头,可以看出
ld-linux-x86-64.so.2
的位置信息写死在ELF中,并不受rpath和LD_LIBRARY_PATH的影响。$ readelf -l regex Elf 文件类型为 DYN (共享目标文件) 入口点 0x44b0 共有 9 个程序头,开始于偏移量64 程序头: Type Offset VirtAddr PhysAddr ... [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] ...
这个路径有两种方式可以修改,一种是在编译源码的时候指定位置
g++ -o regex regex.cpp -Wl,-dynamic-linker='./lib/ld-linux-x86-64.so.2'
另外一种就是通过 patchelf 工具修改。
动态库加载器也叫ELF interpreter。
-
关于rpath,可以查看 Linux之动态链接库 此篇文章。大致就是指动态库或可执行文件运行时到哪里找依赖的动态库。
接下来通过 patchelf 工具修改 rpath 和 dynamic loader。
首先安装 patchelf
sudo apt install patchelf
查看版本
vincent@msi-creator-15:~$
---➤ patchelf --version
patchelf 0.13
接下来让可执行程序在运行的时候到我们指定的目录下查找所需要的动态库
patchelf --set-rpath `pwd`/lib regex
patchelf --set-interpreter `pwd`/lib/ld-linux-x86-64.so.2 regex
这里的 rpath 也可以通过设置 LD_LIBRARY_PATH 来达到一样的目的。
export LD_LIBRARY_PATH=`pwd`/lib:$LD_LIBRARY_PATH
此时就可以运行了。
静态编译
静态编译就是将运行时所需要的动态库都打包到可执行文件中了,那么就会导致可执行文件程序体积过大,还很多其他缺点,一般不采用这种方式。
g++ -o regex regex.cpp -static-libgcc -static-libstdc++
docker容器
docker容器是一种解决方式,但是由于其需要先安装docker,所以当前应用不太建议使用该方式。
升级gcc/g++版本
这里就需要升级生产环境中的 gcc 和 glibc 版本了,但是升级gcc还好说,升级 glibc 就要慎重了,因为你的系统依赖于既有的 glibc 库,一旦更换几乎系统就不可能重启成功。
所以不推荐使用这种方式。