解决 a.out 文件在低版本 glibc 运行问题

本篇文档将详细记录一个在低版本glibc机器上运行由a.cpp文件编译之后的a文件,由最初的缺少GLIBC_2.14错误提示到最终成功运行的一系列步骤。

1:我们在centos7.6和centos6.9机器上查看GLIBC版本,如下:

[root@localhost ~]# getconf GNU_LIBC_VERSION
glibc 2.17

[root@oracle ~]# getconf GNU_LIBC_VERSION
glibc 2.12

这是a.c文件:

#include <stdio.h>

#include <string.h>

int main()
{
char a[] = “hi”;
char b[] = “ho”;
memcpy(a, b, 2);
printf(“test\n”);
return 0;
}

在centos7上使用gcc a.c命令编译之后生成了a.out文件,我们在本机运行a.out文件,结果如下:

root@uos-PC:~# ./a.out
test

可以发现a.out文件可以成功运行。

我们在centos6.9机器上运行此文件,运行结果如下:

[root@oracle ~]# ./a.out
./a.out: /lib64/libc.so.6: version `GLIBC_2.14′ not found (required by ./a.out)

可以发现,此时由于centos6.9机器上没有所需的GLIBC_2.14,因此程序执行报错了,那么我们首先需要确定这个a.out程序引用了GLIBC_2.14的哪个函数,再查看GLIBC_2.12中有没有这个函数,如果有,那么只需要将该引用修改为引用低版本的GLIBC即可。

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

[root@oracle ~]# objdump -T a.out |grep “GLIBC_2.14”
0000000000000000 DF UND 0000000000000000 GLIBC_2.14 memcpy

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

[root@oracle ~]# objdump -T /lib64/libc.so.6 |grep “memcpy”
00000034ae091450 w DF .text 0000000000000009 GLIBC_2.2.5 wmemcpy
00000034ae100f50 g DF .text 000000000000001b GLIBC_2.4 __wmemcpy_chk
00000034ae089800 g DF .text 0000000000000465 GLIBC_2.2.5 memcpy
00000034ae0897f0 g DF .text 0000000000000009 GLIBC_2.3.4 __memcpy_chk

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

解决办法:

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

分析 ELF

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

[root@oracle ~]# readelf -sV a.out

Symbol table ‘.dynsym’ contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND libc_start_main@GLIBC_2.2.5 (2) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.14 (3)

Symbol table ‘.symtab’ contains 64 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400238 0 SECTION LOCAL DEFAULT 1
2: 0000000000400254 0 SECTION LOCAL DEFAULT 2
3: 0000000000400274 0 SECTION LOCAL DEFAULT 3
4: 0000000000400298 0 SECTION LOCAL DEFAULT 4
5: 00000000004002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000400330 0 SECTION LOCAL DEFAULT 6
7: 0000000000400380 0 SECTION LOCAL DEFAULT 7
8: 0000000000400390 0 SECTION LOCAL DEFAULT 8
9: 00000000004003c0 0 SECTION LOCAL DEFAULT 9
10: 00000000004003d8 0 SECTION LOCAL DEFAULT 10
11: 0000000000400438 0 SECTION LOCAL DEFAULT 11
12: 0000000000400460 0 SECTION LOCAL DEFAULT 12
13: 00000000004004b0 0 SECTION LOCAL DEFAULT 13
14: 0000000000400664 0 SECTION LOCAL DEFAULT 14
15: 0000000000400670 0 SECTION LOCAL DEFAULT 15
16: 0000000000400688 0 SECTION LOCAL DEFAULT 16
17: 00000000004006c0 0 SECTION LOCAL DEFAULT 17
18: 0000000000600e10 0 SECTION LOCAL DEFAULT 18
19: 0000000000600e18 0 SECTION LOCAL DEFAULT 19
20: 0000000000600e20 0 SECTION LOCAL DEFAULT 20
21: 0000000000600e28 0 SECTION LOCAL DEFAULT 21
22: 0000000000600ff8 0 SECTION LOCAL DEFAULT 22
23: 0000000000601000 0 SECTION LOCAL DEFAULT 23
24: 0000000000601038 0 SECTION LOCAL DEFAULT 24
25: 000000000060103c 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
28: 0000000000600e20 0 OBJECT LOCAL DEFAULT 20 JCR_LIST
29: 00000000004004e0 0 FUNC LOCAL DEFAULT 13 deregister_tm_clones
30: 0000000000400510 0 FUNC LOCAL DEFAULT 13 register_tm_clones
31: 0000000000400550 0 FUNC LOCAL DEFAULT 13 do_global_dtors_aux 32: 000000000060103c 1 OBJECT LOCAL DEFAULT 25 completed.6355 33: 0000000000600e18 0 OBJECT LOCAL DEFAULT 19 _do_global_dtors_aux_fin 34: 0000000000400570 0 FUNC LOCAL DEFAULT 13 frame_dummy 35: 0000000000600e10 0 OBJECT LOCAL DEFAULT 18 __frame_dummy_init_array 36: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c 37: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c 38: 00000000004007b0 0 OBJECT LOCAL DEFAULT 17 _FRAME_END
39: 0000000000600e20 0 OBJECT LOCAL DEFAULT 20 JCR_END
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS
41: 0000000000600e18 0 NOTYPE LOCAL DEFAULT 18 init_array_end 42: 0000000000600e28 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC 43: 0000000000600e10 0 NOTYPE LOCAL DEFAULT 18 __init_array_start 44: 0000000000400688 0 NOTYPE LOCAL DEFAULT 16 __GNU_EH_FRAME_HDR 45: 0000000000601000 0 OBJECT LOCAL DEFAULT 23 _GLOBAL_OFFSET_TABLE 46: 0000000000400660 2 FUNC GLOBAL DEFAULT 13 _libc_csu_fini 47: 0000000000601038 0 NOTYPE WEAK DEFAULT 24 data_start 48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5 49: 000000000060103c 0 NOTYPE GLOBAL DEFAULT 24 _edata 50: 0000000000400664 0 FUNC GLOBAL DEFAULT 14 _fini 51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC 52: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 24 __data_start 53: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start
54: 0000000000400678 0 OBJECT GLOBAL HIDDEN 15 dso_handle 55: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@@GLIBC_2.14 56: 0000000000400670 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used 57: 00000000004005f0 101 FUNC GLOBAL DEFAULT 13 __libc_csu_init 58: 0000000000601040 0 NOTYPE GLOBAL DEFAULT 25 _end 59: 00000000004004b0 0 FUNC GLOBAL DEFAULT 13 _start 60: 000000000060103c 0 NOTYPE GLOBAL DEFAULT 25 __bss_start 61: 000000000040059d 69 FUNC GLOBAL DEFAULT 13 main 62: 0000000000601040 0 OBJECT GLOBAL HIDDEN 24 __TMC_END
63: 0000000000400438 0 FUNC GLOBAL DEFAULT 11 _init

Version symbols section ‘.gnu.version’ contains 5 entries:
Addr: 0000000000400380 Offset: 0x000380 Link: 5 (.dynsym)
000: 0 (local) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 0 (local)
004: 3 (GLIBC_2.14)

Version needs section ‘.gnu.version_r’ contains 1 entries:
Addr: 0x0000000000400390 Offset: 0x000390 Link: 6 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 2
0x0010: Name: GLIBC_2.14 Flags: none Version: 3
0x0020: Name: GLIBC_2.2.5 Flags: none Version: 2

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

.gnu.version 表包含所有动态符号的版本信息,.dynsym 动态符号表中的每个符号都可以在 .gnu.version 中看到对应的条目。

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

[root@oracle ~]# hexdump -C -s 0x00390 -n 64 a.out
00000390 01 00 02 00 01 00 00 00 10 00 00 00 00 00 00 00 |…………….|
000003a0 94 91 96 06 00 00 03 00 38 00 00 00 10 00 00 00 |……..8…….|
000003b0 75 1a 69 09 00 00 02 00 43 00 00 00 00 00 00 00 |u.i…..C…….|
000003c0 f8 0f 60 00 00 00 00 00 06 00 00 00 03 00 00 00 |..`………….|
000003d0

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

typedef struct {

Elfxx_Word    vna_hash;

    Elfxx_Half    vna_flags;

    Elfxx_Half    vna_other;

    Elfxx_Word    vna_name;

    Elfxx_Word    vna_next;
}Elfxx_Vernaux;

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

由上面的输出我们可以看到 GLIBC_2.14 对应的 0x0003a0 处的开始的 4 个字节 vna_hash hash 值为 94919606,而 vna_other 的值 0300(输出里的 Version: 3)也与 .gnu.version 符号的值一致。

修改 ELF 符号表

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

vim打开文件,:%!xxd转换为16进制编辑,修改对应的数值,改完后,:%!xxd -r转回二进制,再:wq保存。

我们现在需要修改0x0003a0 这行数字中的vna_hash; vna_other; vna_name;这三项和下面的0x0003b0保持一致, 0x000410是对GLIBC_2.12的引用。

这三项所在的字节分别是1~4字节,7~8字节,9~12字节。

修改之后内容如下:

0000390: 0100 0200 0100 0000 1000 0000 0000 0000 …………….
00003a0: 751a 6909 0000 0200 4300 0000 1000 0000 ……..8…….
00003b0: 751a 6909 0000 0200 4300 0000 0000 0000 u.i…..C…….

此时,修改保存之后的 ELF 文件再使用 readelf 命令检查就能看到变化了(只列出了修改的 .gnu.version-r 表):

Version needs section ‘.gnu.version_r’ contains 1 entries:
Addr: 0x0000000000400390 Offset: 0x000390 Link: 6 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 2
0x0010: Name: GLIBC_2.2.5 Flags: none Version: 2
0x0020: Name: GLIBC_2.2.5 Flags: none Version: 2

可以发现,原来上面那行引用的GLIBC_2.14变成了现在的GLIBC_2.2.5,此时再使用./a.out命令执行得到正确结果:

[root@oracle ~]# ./a.out
test