Linux怎么修改ELF解决glibc兼容性问题(elf,glibc,linux,开发技术)

时间:2024-05-03 07:12:32 作者 : 石家庄SEO 分类 : 开发技术
  • TAG :

    Linux%E6%80%8E%E4%B9%88%E4%BF%AE%E6%94%B9ELF%E8%A7%A3%E5%86%B3glibc%E5%85%BC%E5%AE%B9%E6%80%A7%E9%97%AE%E9%A2%98

Linux glibc 问题

相信有不少 Linux 用户都碰到过运行第三方(非系统自带软件源)发布的程序时的 glibc 兼容性问题,这一般是由于当前 Linux 系统上的 GNU C 库(glibc)版本比较老导致的,例如我在 CentOS 6 64 位系统上运行某第三方闭源软件时会报:

CentOS 6 自带的 glibc 还是很老的 2.12 版本,而下载的第三方程序依赖 glibc 2.17 版本,这种情况要么自己重新编译程序,要么只能升级系统的 glibc 版本。由于我使用的程序是第三方编写并且是闭源软件无法自己编译,升级 glibc 固然可能能解决问题,但是 glibc 做为最核心的基础库,在生产环境上直接升级毕竟动作还是太大,因此希望还是能有更好的解决途径。

问题分析

首先我们可以检查一下程序使用了新版本 glibc 的哪些符号,使用 objdump 命令可以查看 ELF 文件的动态符号信息:

从上面的输出可以看到程序使用了 glibc 2.14 版本的 memcpy 函数和 glibc 2.17 版本的 clock_gettime 函数,而这两个常用的函数按说应该是 glibc 很早就已经支持了的,我们可以确认一下当前系统 glibc 提供的符号版本:

这里可以看出 CentOS 6 的 glibc 库提供的 memcpy 实现是 2.2.5 版本的,另外 libc 没有直接实现 clock_gettime 函数,因为老版本 glibc 里 clock_gettime 是由 librt 库提供 clock_gettime 支持的,而且同样也是 2.2.5 版本:

看过这里就基本明白了,第三方程序的开发者是在自带新版本 glibc 的 Linux 系统上编译的,memcpy 和 clock_gettime 的实现默认使用了该系统上 glibc 所提供的最新版本,这样在低版本 glibc 系统中就无法正常运行。

解决方法

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

分析 ELF

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

我们可以在 ELF 的 .dynsym 动态符号表中看到程序用于动态链接的所有导入导出符号,memcpy 和 clock_gettime 后面括号里的数字就是十进制的版本号(分别为 5 和 16),而我们需要格外关注的是下面的 .gnu.version 和 .gnu.version_r 符号版本信息段。

.gnu.version 表包含所有动态符号的版本信息,.dynsym 动态符号表中的每个符号都可以在 .gnu.version 中看到对应的条目(.dynsym 中一共 4583 个符号刚好与 .gnu.version 的结束位置 0x11e7 相等)。

从上面的输出可以看到 .gnu.version 表从 0x05b508 偏移量开始,我们可以看看对应偏移量的十六进制数据:

.gnu.version 中的每个条目占用两个字节,其值为符号的版本,由此可以看到其中第 0x0b 个符号(也就是 .dynsym 表中的 memcpy@GLIBC_2.14 符号)的偏移量即为 0x05b51e(0x05b508 + 0x0b x 2),该偏移量的值 0x0005 也刚好和 .dynsym 表中的值对应,当然 clock_gettime 符号对应的偏移量 0x05b58e 的值 0x0010 同样也是如此。

下面关键的 .gnu.version_r 表示二进制程序实际依赖的库文件版本,从输出中也能看到 .gnu.version_r 表是按照不同的库文件进行分段显示的,每个条目占用 0x10 也就是 16 个字节,该表是从 0x05d8d8 偏移量开始,我们看看 GLIBC_2.17 也就是 0x05d9b8 处的十六进制数据:

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

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

由上面的输出我们可以看到 GLIBC_2.17 对应的 0x05d9b8 处的开始的 4 个字节 vna_hash hash 值为 0x06969197,而 vna_other 的值 0x0010(输出里的 Version: 16)也与 .gnu.version 中 clock_gettime 符号的值一致。同样 GLIBC_2.14 也与 memcpy 符号的值相符。

修改 ELF 符号表

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

首先我们发现 .gnu.version_r 的 libc.so.6 段下面有 10 个条目,最后一个则是我们需要的 GLIBC_2.2.5 版本的符号(从上面的十六进制输出中我们可以看到该符号的偏移量为 0x05da28,vna_hash 值为 0x09691A75,vna_other 版本值为 0x0003,vna_name 字符串名称指向 0003708E 地址),因为这样我们才可以在不修改 ELF 文件大小的前提下直接将 libc.so.6 段下的其它高版本条目指向老版本条目的值。

例如 GLIBC_2.17 对应的 0x05d9b8 偏移量,我们可以直接将 vna_hash 值改为 GLIBC_2.2.5 的 0x09691A75 值,将 vna_other 改为 0003708E 值,为了保持和 .gnu.version 表中的版本值一致,这里我们就不修改 vna_other 值了。

对于 GLIBC_2.14 偏移量我们也修改成同样的值,修改保存之后的 ELF 文件再使用 readelf 命令检查就能看到变化了(只列出了修改的 .gnu.version-r 表):

patchelf 修改 ELF 文件

一般的程序如果只使用了高版本 memcpy 的话,一般这样修改之后程序就可以运行了。但不巧我使用的第三方程序还使用了高版本 glibc 中的 clock_gettime,只是这样修改的话由于 CentOS 6 的 libc 2.12 库并没有提供 clock_gettime,运行时还是会报错。

这个时候我们就需要请出大杀器 PatchELF 了,这个小工具由 NixOS 团队开发,可以直接增加、删除、替换 ELF 文件依赖的库文件,使用起来也非常简单。

检出 PatchELF 的源代码,按照 GitHub 仓库上介绍的步骤编译安装就可以使用了(一般发行版自带的 patchelf 工具版本较老不支持一些新的功能)。

虽然 CentOS 6 的 libc 库没有提供 clock_gettime 实现,但好在 glibc 自带的 librt 库里还是提供了的,因此我们可以使用 patchelf 工具修改原版的程序文件,让程序优先加载 librt 库,这样程序就能正确加载 clock_gettime 符号了:

然后按照上面介绍的方法用十六进制编辑器修改新生成的 ELF 文件的 .gnu.version_r 表(因为 patchelf 运行之后新 ELF 文件的符号表就和之前的不一样了),将 GLIBC_2.17 和 GLIBC_2.14 统一改为 GLIBC_2.2.5 符号,保存 ELF 文件之后就可以看到效果了:

从 ldd 命令的输出中可以看到修改后的程序会加载 librt 库,而且也没有 glibc 版本的报错了,经过测试程序运行起来也没有问题了。

本文:Linux怎么修改ELF解决glibc兼容性问题的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:微服务设计的原则有哪些下一篇:

10 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18