更改引用高版本glibc的程序到引用低版本的glibc

1:问题背景描述

在拥有高版本glibc的机器上编译分布式xgboost程序,结果在拥有低版本glibc版本的集群机器上无法运行,总是报错,显示缺少glibc_2.14,为了解决整个问题,google查阅了很多资料,大体给出了两种方案:

方案一:升级集群所有机器的glibc版本以满足程序运行要求,但是升级glibc是有很大风险的,尤其是在生产环境,风险最大,所以放弃这个方法!

方案二:在低版本的glibc版本的机器上重新编译我们的分布式xgboost,由于报错需要高版本的glibc的文件只有少数,所以重新编译整个xgboost不划算,比较麻烦,而且还有一种情况,这种办法是无法解决的,那就是某个程序引用了高版本glibc才有的某个接口或函数,这个时候用低版本的glibc编译显然不能提供这个函数,故这种方法也不可取。

2:解决办法

后来偶然在谷歌上看到了一片文章,链接为:

https://zohead.com/archives/mod-elf-glibc/

按照文章中的方法,首先需要查看我们的xgboost是哪个文件引用了高版本的glibc,在运行xgboost之后,从运行日志中看到下面内容:

可以看到该xgboost文件引用了高版本的glibc_2.14,我们接着可以检查一下该程序使用了新版本 glibc_2.14 的哪些符号,使用 objdump 命令可以查看 ELF 文件的动态符号信息,使用命令objdump –T xgboost |grep “GLIBC_2.14”,显示内容如下:

可以看到,该xgboost文件只引用了glibc_2.14中的memcpy符号。

接下来我们需要查看我们系统【也就是集群中拥有较低版本glibc的那些机器】含有的glibc中是否含有这个memcpy符号,如果有,那就最好,如果没有,那就麻烦了。切换到/lib64下,使用命令:objdump -T libc.so.6 |grep "memcpy",可以看到,低版本的glibc也提供了memcpy符号。

那么现在整理一下头绪,目前到此为止,我们找到了分布式xgboost不能在所有集群机器上运行的原因,原因是我们编译的分布式xgboost中的xgboost文件【注意,这只是一个文件,和我说的分布式xgboost不要搞混了,分布式xgboost是一个大工程,而现在说的只是这个大工程中的一个文件而已】,这个文件引用了高版本的glibc_2.14中的memcpy函数,而刚好,我们发现低版本的glibc_2.2.5中也有这个函数,所以现在最直接的方法就是通过修改该xgboost文件的ELF内容,强制它去调用glibc_2.2.5中的memcpy函数,而不是去调用glibc_2.14中的memcpy,不然它找不到就会报错。

 

接下来我们修改xgboost文件的ELF内容,【注意,这次修改是修改16进制数字,所以为了保险起见,先备份xgboost文件】,通过我上面那个博客里面的讲解,我们需要查看该xgboost文件。

 

虽然我们无法重新编译第三方程序,但如果可以修改 ELF 文件强制让 LD 库加载程序时使用老版本的 memcpy,应该就可以避免升级 glibc

分析 ELF

首先用 readelf 命令查看 ELF 的符号表,由于该命令输出非常多,这里只贴出我们关心的信息:

使用命令:readelf -sV xgboost

显示内容如下:

在表中,可以看到Glibc_2.14Glibc_2.2.5以及它后面的数字19和3,也就是他们两个的十进制的版本号。其次我们可以看到.gnu.veersion_r文件的起始偏移量是0x003d00.

.gnu.version_r 表示二进制程序实际依赖的库文件版本,从输出中也能看到 .gnu.version_r 表是按照不同的库文件进行分段显示的,每个条目占用 0x10 也就是 16 个字节,该表是从 0x003d00偏移量开始,我们打开xgboost文件找到该起始位置:使用vim xgboost命令内容如下:

看不懂,我们切换到16进制查看,使用命令:%!xxd打开文件,找到起始位置,内容如下:

红线标注位置即为.gnu.version_r 的起始位置,下面我们需要根据偏移量找到glibc_2.14和glibc_2.2.5的位置,修改内容,这里起始位置为0x003d00,十进制是15616,而通过readelf –sV xgboost命令查看到的glibc_2.14偏移量是:0x00c0十进制是192,glibc_2.2.5偏移量是0x00d0,十进制是208,现在我们给.gnu.version_r 的初始位置加上偏移量,分别得到glibc_2.14的位置是15616+192=15808,转换成16进制是0x003dc0,glibc_2.2.5加上偏移量之后的位置是15616+208=15824,转换为16进制就是0x003dd0,现在我们找到这两个位置,如下:

可以看到,有红线的上面那行是3dc0也就是glibc_2.14, 下面3dd0是glibc_2.2.5,

那么现在该怎么改,改什么?

修改 ELF 符号表

由于 Linux 系统中的 LD 库(也就是 /lib64/ld-linux-x86-64.so.2 库)加载 ELF 时检查 .gnu.version_r 表中的符号,我们可以来修改 .gnu.version_r 表中的符号值来强制使用老版本的函数实现。

 

.gnu.version_r 表中每个条目是 16 个字节的 Elfxx_Vernaux 结构体,其声明如下(Elfxx_Half 占用 2 个字节,Elfxx_Word 占用 4 个字节):

 

typedef struct {

    Elfxx_Word    vna_hash;   //占4字节

    Elfxx_Half    vna_flags;  //占2字节

    Elfxx_Half    vna_other;  //占2字节

    Elfxx_Word    vna_name;   //占4字节

    Elfxx_Word    vna_next;   //占4字节

} Elfxx_Vernaux;

vna_hash 为 4 个字节的库名称(也就是上面的 GLIBC_2.14 字符串)的 hash 值,vna_other 为对应的 .gnu.version 表中符号的版本值,vna_name 指向库名称字符串的偏移量(也可以在 ELF 头中找到),vna_next 为下一个条目的位置(一般固定为 0x00000010)。

因此我们需要修改glibc那行内容的vna_hash, vna_other, vna_name 这三部分内同容和glibc_2.2.5相同即可。也就是前四个字节内容,7~8字节内容和9~12字节内容。

使用命令vim xgboost打开xgboost文件,再使用命令:%!xxd切换到16进制显示,找到0x003dc0,显示如下:

 

现在我们下面那行标红线的内容复制到上面同等位置,复制完后的内容如下:

再使用:%!xxd –r命令退出16进制显示,再使用:wq保存文件退出,修改保存之后的 ELF 文件再使用 readelf 命令检查就能看到变化了(只列出了修改的 .gnu.version-r 表):

而修改ELF内容之前是这样子的:

对比即可发现,该xgboost文件已经从引用glibc_2.14改为引用glibc_2.2.5了,到此,就可以在低版本的机器上运行分布式xgboost了,再次运行并查看日志如下:

所有机器都没有了本文档开头显示缺少GLIBC_2.14的错误提示了,程序成功运行!

我的总结:

解决该问题的办法比较通用,以后只要是在linux系统上出现了glibc版本不一致问题,都可以使用该方法解决。

注意

在有些文件使用readref –sV切换到16进制查看glibc_2.14起始位置的时候,并不一定是从每一行的第一个字节开始的,如下面这个例子:

上面这个文件的起始偏移量是0x011f88,打开该文件并使用十六进制查看之后,如下:

原始偏移量是011f88,加上0090得到glibc_2.14最终的16进制位置是12018,找到该位置,如下:

红线所示就是glibc_2.14的位置,蓝线所示就是glibc_2.2.5的位置,剩下的怎么修改,看上面的内容即可。

参考文档:

1:https://www.jianshu.com/p/7a75324e98ab  

程序破解及ELF文件格式分析

2:https://zohead.com/archives/mod-elf-glibc/   

Linux修改ELF解决glibc兼容性问题

3:http://kinva.cc/2017/03/20/GLIBC-2-14-not-found/ 

错误:/lib64/libc.so.6 version `GLIBC_2.14` not found解决办法

4:https://blog.blahgeek.com/glibc-and-symbol-versioning/              glibcSymbol Versioning和如何链接出低版本glibc可运行的程序

5:https://blog.csdn.net/wangmingsuyang/article/details/80089984          高版本glibc环境编译兼容低版本机器的.so文件

【注】:原创不易,转发请注明出处,谢谢!