在.so文件中链接旧符号版本

52
在x86_64 Linux上使用gcc和ld,我需要链接到一个新版本的库(glibc 2.14),但可执行文件需要在旧版本系统(2.5)上运行。由于唯一不兼容的符号是memcpy(需要memcpy@GLIBC_2.2.5,但库提供的是memcpy@GLIBC_2.14),因此我想告诉链接器,它应该使用我指定的旧版本,而不是默认版本。我找到了一个非常笨拙的方法:只需在链接器命令行中指定旧的.so文件的副本即可。这很好用,但我不喜欢有多个.so文件的想法(我只能通过指定所有也具有对memcpy引用的我链接到的旧库来使其起作用)。我正在寻找一种告诉链接器使用旧版本符号的方法。
以下替代方案对我来说并不是很好:
- 使用asm .symver(如Trevor Pounds博客的Web Archive所示),因为这要求我确保symver位于所有使用memcpy的代码之前,这将非常困难(具有第三方代码的复杂代码库) - 维护具有旧库的构建环境;只是因为我想在桌面系统上开发,并且在我们的网络中同步东西会很麻烦。
当考虑到链接器执行的所有任务时,它似乎不是一件难事,毕竟它也有一些代码来确定符号的默认版本。
任何其他想法都欢迎,只要它们与简单链接器命令行(如创建简单链接器脚本等)具有相同的复杂性级别,同时它们不是像编辑结果二进制文件之类的奇怪黑客。为了让未来的读者更好地阅读,除了下面的建议外,我还发现链接器选项--wrap可能有时也很有用。


4
你知道,memcpy() 在过去三十年里没有改变过。你可能需要说明为什么需要它。(我很抱歉问这个问题;我不喜欢别人这样问我。但是,考虑到memcpy()已经如此稳定了这么长时间,你一定有一个真正紧急的原因需要知道。)谢谢! - Pete Wilson
7
@PeteWilson:之所以如此,是因为当我使用glibc 2.14编译我的程序时,它将无法在使用较旧版本glibc的系统上运行,因为这些版本不提供带有版本号的memcpy符号@GLIBC_2.14。 - PlasmaHH
1
@PeteWilson 我的回答链接了一个关于memcpy问题的bug报告,从实际变化的角度解释了问题 - glibc做出了一项更改,打破了依赖传统unix memcpy实现中(在重叠情况下技术上未定义的)未记录的“总是从左到右迭代”的行为的代码。然而,更相关于这个问题的是,旧版本显然是早期glibc中唯一提供的版本,而他必须支持旧版本。 - Random832
另请参见:https://dev59.com/VW865IYBdhLWcg3wA5yc#39537664 - patstew
这个看起来很合理:https://gcc.gnu.org/ml/gcc-help/2008-11/msg00303.html - Johannes Schaub - litb
@JohannesSchaub-litb 怎么了?符号版本控制就是为此而存在的。因此,如果我决定旧版本及其旧语义适合我,为什么不能链接到该版本?在glibc的众多符号中,只有realpathmemcpy曾经引起注意并需要更正。而且,在变更日志中阅读、比较源代码并找到旧版本符合预期的内容并没有真正的问题... - 0xC0000022L
12个回答

52

我找到了以下可行的解决方案。首先创建文件 memcpy.c:

#include <string.h>

/* some systems do not have newest memcpy@@GLIBC_2.14 - stay with old good one */
asm (".symver memcpy, memcpy@GLIBC_2.2.5");

void *__wrap_memcpy(void *dest, const void *src, size_t n)
{
    return memcpy(dest, src, n);
}

编译此文件无需额外的CFLAGS。然后使用-Wl,--wrap = memcpy链接您的程序。


2
由于某些原因,这对我没有起作用。仍然在寻找旧的memcpy。 - hookenz
Ortwin Angermeier的答案对我没有用,但令我惊讶的是 - 这个方法完美地解决了问题,并且glibc 2.14的依赖关系被轻松解决。谢谢! - Kaa
这是因为memcpy的接口没有改变。在一般情况下,我们不仅需要旧符号,还需要旧头文件。例如结构成员发生变化的情况,因此旧版本的函数期望旧结构。 - Kaz
1
如果您在一个不是在2002年添加x86-64的架构上编译,那么这将无法工作 - 您将收到一个错误,指出所请求的版本化符号不可用。您需要像#if defined(__x86_64__) && !defined(__ILP32__) && defined(__GNU_LIBRARY__)这样的东西来防范这种情况(__ILP32__是为了x32 ABI而引入的,该ABI于2012年推出)。 - GreenReaper

21

只需静态链接memcpy - 从libc.a中提取memcpy.o ar x /path/to/libc.a memcpy.o (无论哪个版本 - memcpy基本上是一个独立的函数),并将其包含在最终链接中。请注意,如果您的项目分发给公众而不是开源,则静态链接可能会使许可问题复杂化。

或者,您可以自己实现memcpy,尽管glibc中手动调整的汇编版本可能更有效率。

请注意,memcpy@GLIBC_2.2.5被映射到memmove(旧版本的memcpy始终按可预测的方向复制,这导致它有时在应该使用memmove的情况下被误用),这是版本升级的唯一原因 - 对于此特定情况,您可以简单地在代码中用memmove替换memcpy。

或者,您可以进行静态链接,或者确保网络上的所有系统都具有与构建机器相同或更好的版本。


在长期来看,最好的解决方案是确保构建机器(即您的桌面机器)具有最低公共库版本 - 通过维护单独的构建环境或升级其他所有人来实现。 - Random832
2
+1 如果提到了 memmove();在我看来,使用 memcpy() 的愚蠢重叠字符串限制没有好的理由。 - Pete Wilson
1
@Random832:这是管理和开发之间的常见斗争。我想使用最新版本,无法使用服务器上提供的古老工具,而他们想要保持十年的稳定性 :/ - PlasmaHH
3
只有在 x86_64 上才应该将 memcpy() 替换为 memmove(),而不是其他没有版本化 memcpy() 的体系结构,因此不会增加依赖关系。不要不必要地牺牲性能。(并确保使用 memcpy() 进行测试) - user72421
@PeteWilson:memmove在重叠检查时有两个额外的潜在流水线气泡,我认为对于已知大小(如memcpy),它无法被内联化。 - Zan Lynx
显示剩余3条评论

9

我曾经遇到过类似的问题。我们使用的第三方库需要旧版的memcpy@GLIBC_2.2.5。我的解决方法是在 @anight 提供的方法之上进行扩展。

我也封装了memcpy命令,但是我不得不使用略微不同的方法,因为 @anight 提供的解决方案对我不起作用。

memcpy_wrap.c:

#include <stddef.h>
#include <string.h>

asm (".symver wrap_memcpy, memcpy@GLIBC_2.2.5");
void *wrap_memcpy(void *dest, const void *src, size_t n) {
  return memcpy(dest, src, n);
}

memcpy_wrap.map:

GLIBC_2.2.5 {
   memcpy;
};

构建包装器:

gcc -c memcpy_wrap.c -o memcpy_wrap.o

现在,在链接程序时最后添加:

  • -Wl,--version-script memcpy_wrap.map
  • memcpy_wrap.o

这样,你最终会得到类似以下的结果:

g++ <some flags> -Wl,--version-script memcpy_wrap.map <some .o files> memcpy_wrap.o <some libs>

8

我有一个类似的问题。在RHEL 7.1上尝试安装一些oracle组件时,遇到了这个错误:

$ gcc -o /some/oracle/bin/foo .... -L/some/oracle/lib ... 
/some/oracle/lib/libfoo.so: undefined reference to `memcpy@GLIBC_2.14'

看起来我的RHEL的glibc只定义了memcpy@GLIBC_2.2.5:

$ readelf -Ws /usr/lib/x86_64-redhat-linux6E/lib64/libc_real.so | fgrep memcpy@
   367: 000000000001bfe0    16 FUNC    GLOBAL DEFAULT    8 memcpy@@GLIBC_2.2.5
  1166: 0000000000019250    16 FUNC    WEAK   DEFAULT    8 wmemcpy@@GLIBC_2.2.5

因此,我成功绕过了这个问题,首先创建一个没有包装的memcpy.c文件,如下所示:

#include <string.h>
asm (".symver old_memcpy, memcpy@GLIBC_2.2.5");       // hook old_memcpy as memcpy@2.2.5
void *old_memcpy(void *, const void *, size_t );
void *memcpy(void *dest, const void *src, size_t n)   // then export memcpy
{
    return old_memcpy(dest, src, n);
}

我们有一个memcpy.map文件,将我们的memcpy导出为memcpy@GLIBC_2.14:

GLIBC_2.14 {
   memcpy;
};

我随后将自己的memcpy.c编译成了一个共享库,步骤如下:
$ gcc -shared -fPIC -c memcpy.c
$ gcc -shared -fPIC -Wl,--version-script memcpy.map -o libmemcpy-2.14.so memcpy.o -lc

我将libmemcpy-2.14.so移动到/some/oracle/lib目录下(由我的链接参数-L指定),然后重新链接。

$ gcc -o /some/oracle/bin/foo .... -L/some/oracle/lib ... /some/oracle/lib/libmemcpy-2.14.so -lfoo ...

(没有错误编译)并通过以下方式进行验证:
$ ldd /some/oracle/bin/foo
    linux-vdso.so.1 =>  (0x00007fff9f3fe000)
    /some/oracle/lib/libmemcpy-2.14.so (0x00007f963a63e000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007f963a428000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f963a20c000)
    librt.so.1 => /lib64/librt.so.1 (0x00007f963a003000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f9639c42000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f963aa5b000)

这对我起作用了,希望对你也有用。


5
我很抱歉回复晚了,但最近我升级了我的Linux操作系统到XUbuntu 14.04(更多理由不要升级),这个版本带有新的libc。在我的机器上编译了一个共享库,这个库被客户端使用,因为某些合法的原因,他们的环境没有从10.04升级。由于gcc对memcpy glibc v. 2.14(或更高版本)有依赖性,我编译的共享库在他们的环境中无法加载。让我们暂时放下这种荒谬的情况。整个项目的解决方法如下:
  1. 在我的gcc cflags中添加:-include glibc_version_nightmare.h
  2. 创建glibc_version_nightmare.h文件
  3. 创建一个perl脚本来验证.so文件中的符号

glibc_version_nightmare.h:

#if defined(__GNUC__) && defined(__LP64__)  /* only under 64 bit gcc */
#include <features.h>       /* for glibc version */
#if defined(__GLIBC__) && (__GLIBC__ == 2) && (__GLIBC_MINOR__ >= 14)
/* force mempcy to be from earlier compatible system */
__asm__(".symver memcpy,memcpy@GLIBC_2.2.5");
#endif
#undef _FEATURES_H      /* so gets reloaded if necessary */
#endif

Perl脚本片段:

...
open SYMS, "nm $flags $libname |";

my $status = 0;

sub complain {
my ($symbol, $verstr) = @_;
print STDERR "ERROR: $libname $symbol requires $verstr\n";
$status = 1;
}

while (<SYMS>) {
next unless /\@\@GLIBC/;
chomp;
my ($symbol, $verstr) = (m/^\s+.\s(.*)\@\@GLIBC_(.*)/);
die "unable to parse version from $libname in $_\n"
    unless $verstr;
my @ver = split(/\./, $verstr);
complain $symbol, $verstr
    if ($ver[0] > 2 || $ver[1] > 10);
}
close SYMS;

exit $status;

4

最小可运行自包含示例

GitHub 上游.

main.c

#include <assert.h>
#include <stdlib.h>

#include "a.h"

#if defined(V1)
__asm__(".symver a,a@LIBA_1");
#elif defined(V2)
__asm__(".symver a,a@LIBA_2");
#endif

int main(void) {
#if defined(V1)
    assert(a() == 1);
#else
    assert(a() == 2);
#endif
    return EXIT_SUCCESS;
}

a.c

#include "a.h"

__asm__(".symver a1,a@LIBA_1");
int a1(void) {
    return 1;
}

/* @@ means "default version". */
__asm__(".symver a2,a@@LIBA_2");
int a2(void) {
    return 2;
}

a.h

#ifndef A_H
#define A_H

int a(void);

#endif

a.map

LIBA_1{
    global:
    a;
    local:
    *;
};

LIBA_2{
    global:
    a;
    local:
    *;
};

Makefile

CC := gcc -pedantic-errors -std=c89 -Wall -Wextra

.PHONY: all clean run

all: main.out main1.out main2.out

run: all
    LD_LIBRARY_PATH=. ./main.out
    LD_LIBRARY_PATH=. ./main1.out
    LD_LIBRARY_PATH=. ./main2.out

main.out: main.c libcirosantilli_a.so
    $(CC) -L'.' main.c -o '$@' -lcirosantilli_a

main1.out: main.c libcirosantilli_a.so
    $(CC) -DV1 -L'.' main.c -o '$@' -lcirosantilli_a

main2.out: main.c libcirosantilli_a.so
    $(CC) -DV2 -L'.' main.c -o '$@' -lcirosantilli_a

a.o: a.c
    $(CC) -fPIC -c '$<' -o '$@'

libcirosantilli_a.so: a.o
    $(CC) -Wl,--version-script,a.map -L'.' -shared a.o -o '$@'

libcirosantilli_a.o: a.c
    $(CC) -fPIC -c '$<' -o '$@'

clean:
    rm -rf *.o *.a *.so *.out

在Ubuntu 16.04上进行了测试。


3

这个解决方法似乎与-flto编译选项不兼容。

我的解决方案是调用memmove。 memmove执行的任务与memcpy完全相同。 唯一的区别是当源和目标区域重叠时,memmove是安全的,而memcpy是不可预测的。因此,我们可以始终安全地调用memmove而不是memcpy。

#include <string.h>

#ifdef __cplusplus
extern "C" {
#endif

    void *__wrap_memcpy(void *dest, const void *src, size_t n)
    {
        return memmove(dest, src, n);
    }

#ifdef __cplusplus
}
#endif

3
对于 nim-lang,我详细阐述了使用 C 编译器的 --include= 标志找到的解决方案,步骤如下:
1. 创建名为 symver.h 的文件:
__asm__(".symver fcntl,fcntl@GLIBC_2.4");

使用 nim c ---passC:--include=symver.h 构建你的程序。

对于我而言,我也在进行交叉编译。我使用 nim c --cpu:arm --os:linux --passC:--include=symver.h ... 进行编译,并且可以使用 arm-linux-gnueabihf-objdump -T ../arm-libc.so.6 | grep fcntl 获取符号版本。

我曾经不得不删除过 ~/.cache/nim。然后它似乎就工作了。


这个方法是可行的,但缺点是如果你已经有一个现有的共享对象(比如libstdc++.so),它链接了更新的符号,那么链接器仍然会在这些符号上报错。这个技巧只对你真正自己编译的代码有效。是的,这意味着你可以使用类似上面的头文件从源代码重新构建libstdc++ - 0xC0000022L

2

我认为你可以通过创建一个包含symver语句和可能调用memcpy的虚拟函数的简单C文件来达到目的。然后,您只需要确保生成的目标文件是链接器给出的第一个文件。


3
那会很好,但不幸的是事实并非如此。使用nm检查文件时,似乎对于每个“memcpy”的提到(作为U),它都会采用“memcpy@GLIBC_2.14”,只有在显式提及memcpy@GLIBC_2.2.5时才采用后者。最终的可执行文件有两个未定义的条目,一个为2.14,另一个为2.2.5。当我将虚拟文件放置在链接器行中的不同位置时,没有任何变化。 - PlasmaHH

1

我建议您要么静态链接memcpy(),要么找到memcpy()的源代码并将其编译为自己的库。


2
这可能存在许可问题,因为glibc是在GPL下发布的。 - scrutari
在这种情况下,请使用musl-libc的版本,可以消除许可问题(因为它是根据非常自由的许可证进行授权的,并且具有公共领域的部分),而且非常便携。 - 0xC0000022L

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接