解决glibc版本兼容问题

如何在Linux上运行一个C语言程序?C语言程序要在操作系统上运行,还需要调用各种系统功能,因此还需要C支持库才能运行。Linux上的C支持库之一,也是使用最广泛的支持库,就是glibc。但是使用过程中,可能因为支持库版本不同而造成兼容性问题,造成软件无法运行。今天我就遇到了这个问题,在网上搜索了一圈后,终于找到了有效的解决办法。

复现

写的程序非常简单,就是输出一句话:

main.c代码:

#include "libmewwoof_hello.h"

int main() {
    mewwoof_hello_print();
    return 0;
}

libmewwoof_hello.h代码:

#ifndef _LIB_MEWWOOF_HELLO_H_

#define _LIB_MEWWOOF_HELLO_H_

void mewwoof_hello_print();

#endif

libmewwoof_hello.c代码:

#include <stdio.h>
#include "libmewwoof_hello.h"

void mewwoof_hello_print() {
    printf("Hello, mewwoof!\n");
}

然后分别编译成动态库和主程序:

gcc libmewwoof_hello.c -fPIC -shared -o libmewwoof_hello.so
gcc main.c -L. -lmewwoof_hello -o mewwoof-hello

得到可执行文件mewwoof-hello和动态库文件libmewwoof_hello.so。在编译机上执行:

LD_LIBRARY_PATH=. ./mewwoof-hello

能正常运行。

但是当我将这两个文件复制到另外一个系统运行时,却报错了:

test@test-server:~# mewwoof-hello 
mewwoof-hello: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by mewwoof-hello)

环境

看起来完全没有头绪,但是这提到了glibc,众所周知,这是C程序的Linux支持库,由GNU编写。C支持库就是用来告诉操作系统如何解析并执行C可执行文件的,并且是C程序和操作系统的桥梁。Linux下常用glibc,Windows下常用Visual C/C++库。

但是一个操作系统是不可能没有装glibc的,是版本问题吗?那就来看看两台设备的C环境。

编译机环境:

  • 操作系统 Archlinux,安装了最新更新
  • 64位
  • gcc 版本 11.2.0 (GCC)

运行机环境:

  • 操作系统 Ubuntu Focal (20.04 LTS),安装了最新更新
  • 64位
  • gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)

可以看到,两台设备的编译器版本差别有点大。但怎么说这Ubuntu Focal也仍然是最新的LTS版本系统了,也就是说仍然是主流,软件在新编译器下编译,在旧设备上运行不了,何况是这么简单的程序,实在是让人接受不了。经过网上一番搜索,算是弄懂了一些原理,并解决了问题,且看下文。

问题原理

gcc版本差异过大?

有人可能会问:gcc版本差异过大,不是只会影响编译么?对软件能不能正常运行有什么影响呢?

我以前也是这么想的,但是今天被推翻了。gcc只是GNU工具链的一个软件,狭义地说,仅仅负责将C语言代码翻译成机器语言(事实上也可以调用做连接工作)。而LD负责连接,将每个由C翻译过来的文件连接在一起,如果提示使用系统的动态库,就留个函数名称(符号),等运行的时候再从内存找。

很明显,编译时采用的方法和运行时查找连接不能割裂。编译时我使用了一个库中含有的函数,而运行时因为库版本不一样,这个函数被删除了,或者返回参数不一样了,显然程序就乱套了。所以你可以这样理解:gcc版本决定了glib版本,也决定LD、LDD版本。

除了使用gcc -v查看gcc版本之外,还可以直接运行库文件查看版本信息:

test@test-server:~# /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

可以看到,运行时libc版本是2.31。

compile@compile-server:$ /usr/lib/libc.so.6
GNU C Library (GNU libc) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.archlinux.org/>.

编译使用的库是2.35版本。

符号版本(Compact Symbols)

那么当两个版本中,一些函数名没变(能找到动态链接入口),但是函数参数或者返回值变了怎么办呢?如果强行运行,也会发生错误。如何区分这个函数的不同版本呢?于是符号版本就被发明了出来。

可以使用objdump查看可执行文件中的所有符号,同时列出了版本信息。我们可以尝试看看glibc库中printf系列符号的信息:

compiler@compile-server:$ objdump -T /usr/lib/libc.so.6 | grep printf
0000000000000000      DF *UND*  0000000000000000 (GLIBC_PRIVATE) _dl_fatal_printf
...
000000000005d700 g    DF .text  00000000000000b8  GLIBC_2.2.5 fprintf
...
0000000000057920 g    DF .text  000000000000000b  GLIBC_2.2.5 vfprintf
000000000005a8f0 g    DF .text  000000000000001d  GLIBC_2.2.5 __printf_fp
000000000005d950 g    DF .text  00000000000000c6  GLIBC_2.2.5 _IO_sprintf
0000000000121e20 g    DF .text  00000000000000c1  GLIBC_2.8   __asprintf_chk
000000000005d950 g    DF .text  00000000000000c6  GLIBC_2.2.5 sprintf
000000000007d0f0  w   DF .text  00000000000000b8  GLIBC_2.2.5 fwprintf

可以看到,每个符号/函数名对应的符号版本还不一样。

编译生成的可执行文件包含的版本信息,在运行过程中,必须要在动态库中找到相同版本的符号,才能成功运行,否则就会报我们今天的主题——version `GLIBC_2.34' not found错误。

我们继续来看看之前编译的两个文件的符号信息:

compiler@compile-server:$ objdump -T ./mewwoof-hello                             

./mewwoof-hello:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize

compiler@compile-server:$ objdump -T ./libmewwoof_hello.so             

./libmewwoof_hello.so:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) puts
0000000000000000  w   D  *UND*  0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000001109 g    DF .text  0000000000000016  Base        mewwoof_hello_print

显然,主程序的__libc_start_main的符号版本是运行机glibc库无法提供的:

test@test-server:~# objdump -T /lib/x86_64-linux-gnu/libc.so.6 | grep __libc_start_main
0000000000026fc0 g    DF .text  00000000000001e3  GLIBC_2.2.5 __libc_start_main

而编译机则能提供:

compiler@compile-server:$ objdump -T /usr/lib/libc.so.6| grep __libc_start_main
000000000002d340 g    DF .text  0000000000000148  GLIBC_2.34  __libc_start_main
000000000002d340 g    DF .text  0000000000000148 (GLIBC_2.2.5) __libc_start_main

我们又可以看出,这个符号不得了,是main函数的入口。这个符号都没法匹配,程序根本跑不起来啊。怎么办呢?下面来解决这个问题。

解决方法

首先我们可以看到,新的glibc还是保留了旧版本的符号,说明我们只需修改一下自己程序的符号。我们要从代码和编译上双管齐下,同时下手,替换新符号,绑定旧符号。

修改后的main.c

#include "libmewwoof_hello.h"

void *__libc_start_main_glibc_2_2_5();
asm(".symver __libc_start_main_glibc_2_2_5, __libc_start_main@GLIBC_2.2.5");
void *__wrap___libc_start_main() {
    return __libc_start_main_glibc_2_2_5();
}

int main() {
    mewwoof_hello_print();
    return 0;
}

然后加wrap参数编译:

gcc main.c -L. -lmewwoof_hello -Wl,--wrap=__libc_start_main -o mewwoof-hello

再查看符号,发现已经变成2.2.5版本的了:

test@test-server:~# objdump -T /usr/bin/mewwoof-hello | grep GLIBC_
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __libc_start_main
0000000000000000  w   DF *UND*  0000000000000000  GLIBC_2.2.5 __cxa_finalize

再运行,成功输出。

参考资料

[1] glibc版本查看_glibc做版本兼容的原理介绍 https://blog.csdn.net/weixin_39924674/article/details/110586758

[2] linux glibc 版本查看,三种方法查看glibc的版本号 https://blog.csdn.net/weixin_34108829/article/details/116642964

[3] https://stackoverflow.com/questions/41145062/wrapping-a-glibc-function-using-the-dynamic-linker

[4] https://stackoverflow.com/questions/4032373/linking-against-an-old-version-of-libc-to-provide-greater-application-coverage

[5] One way to solve the glibc compatibility problem https://gist.github.com/nicky-zs/7541169

[6] libc-wrappers: Added wrappers for __libc_start_main, pthread_attr_get… https://github.com/olivergs/toolbox/commit/27605ebc47e46b92f10b3bbd035d7d66979d4623

《解决glibc版本兼容问题》有1条评论

发表评论