指定dynamic linker以使用高版本GCC

今天有人问怎么在没有root的不可更新老旧Linux环境里使用高版本GCC,主要困难在于GCC对glibc版本有一定要求。假设使用现成的GCC二进制包,解压到本地后执行gcc,报错:/lib64/libc.so.6: version `GLIBC_2.14' not found,即系统glibc的libc.so.6中缺乏更高版本的符号(参考info '(ld) VERSION')。使用新符号通常表示API有更新,不过很多时候旧版本的库也能用,可能有两个原因:一是编译机器的库版本可能较新,soname版本号较高,编译出来的可执行文件也会有较高的版本要求;二是源文件因保守指定了较高的版本号,实际上旧版本也能用。

比如最近ncurses主版本号更新到6,/usr/lib/libncursesw.so.5变成了/usr/lib/libncursesw.so.6,导致很多可执行文件没法用了:

1
2
3
4
5
6
7
% ldd /usr/lib/j8/bin/jconsole
linux-vdso.so.1 (0x00007ffcdb9ec000)
libedit.so.2 => /usr/lib/libedit.so.2 (0x00007fd995a09000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007fd995805000)
libncursesw.so.5 => not found
libc.so.6 => /usr/lib/libc.so.6 (0x00007fd9951f4000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd995c45000)

不想重新编译的话,可以软链接做一个libncursesw.so.5,放在ld.so默认会查找的路径下,或者运行时指定环境变量LD_LIBRARY_PATH。另外还有一种修改可执行文件的方法:用十六进制编辑工具把.dynstr section中的字串libncursesw.so.5改成libncursesw.so.6。再次执行时,ld.so将会用新的名字查找依赖的库,就顺利找到了/usr/lib/libncursesw.so.6,但做这样的修改得保证新的路径名长度小于等于原长,否则会覆盖.dynstr中后面的字串。另一种办法是构造一个新的.dynstr放到ELF的其他地方,并变更section header中的.dynstr地址信息,非常麻烦。

回到正题,xptree下载了gcc-c++-4.8.3-9.el7.x86_64.rpmgcc-4.8.3-9.el7.x86_64.rpm,解压后执行,报错:/lib64/libc.so.6: version `GLIBC_2.14' not found。解压glibc-2.17-78.el7.x86_64.rpm,报错:

1
2
3
4
5
6
/usr/bin/python: relocation error: MY_LOCAL/lib64/libc.so.6: symbol
_dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2
with link time reference
g++: relocation error: MY_LOCAL/lib64/libc.so.6: symbol _dl_starting_up,
version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time
reference

看来对ld-linux-x86-64.so.2(dynamic linker,下面简称为ld.so)的版本也有要求。就像修改依赖库的路径那样,修改program header中的PT_INTERP

1
bbe -b '/\x2flib64\x2fld-linux-x86-64.so.2/:27' -e 'L 1' -e 'r 0 /tmp/l/ld.so\0' bin/gcc -o bin/gcc.new

效果是把gcc中第一次出现的/lib64/ld-linux-x86-64.so.2(PT_INTERP项)替换成/tmp/l/ld.so\0,可以观察到:

1
2
3
4
5
% ldd bin/gcc.new
linux-vdso.so.1 (0x00007ffff85f5000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f36788ca000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f3678526000)
/tmp/l/ld.so (0x00007f3678bc8000)

有几个被gcc g++调用的可执行文件如cc1cc1plus等可能也得做同样修改。之后gcc即可执行,但编译生成的可执行文件还使用原先的dynamic linkter,也许会在某些情况下无法执行。一种办法是在用gcc g++链接时指定-Wl,--dynamic-linker,/tmp/l/ld.so,这样产生的可执行文件就会使用/tmp/l/ld.so作为dynamic linker。另一种方法是修改gcc中的built-in specs:

1
2
3
4
5
% gcc -dumpspecs
......
*link:
...%{m16|m32|mx32:;:-dynamic-linker %{muclibc:/lib/ld64-uClibc.so.0;:%{mbionic:/system/bin/linker64;:/lib64/ld-linux-x86-64.so.2}}}...
......

bvibviplus等十六进制编辑器在可执行文件gcc中找这段字串,把/lib64/ld-linux-x86-64.so.2}}}改成/tmp/l/ld.so}}},原串多出来的地方用空格填充。这样得到的gcc产生的可执行文件无需指定-Wl,--dynamic-linker,默认就会使用新的dynamic linker了:

1
2
3
4
5
% bin/gcc.new -xc - -o a <<< 'int main(){}'
% ldd a
linux-vdso.so.1 (0x00007ffeff9b3000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f337fe5b000)
/tmp/l/ld.so (0x00007f33801ff000)

跳出XY problem

如果要装的东西多些,可以考虑Gentoo Prefix、NixOS等,还有unprivileged LXC containers等。轻量省事的方法是userspace chroot PRoot,不需要root,但弊端是运行的进程处于ptrace下,而一个进程不能被ptrace两次,因此gdb、strace等都没法用。