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.14
和
Glibc_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/ glibc和Symbol Versioning和如何链接出低版本glibc可运行的程序
5:https://blog.csdn.net/wangmingsuyang/article/details/80089984 高版本glibc环境编译兼容低版本机器的.so文件
【注】:原创不易,转发请注明出处,谢谢!