如何处理静态链接库之间的符号冲突



在编写库时,最重要的规则和最佳实践之一是将库到库特定的名称空间。由于namespace关键字,c++使这变得很容易。在在C语言中,通常的方法是给标识符加上一些库特有的前缀。

C标准规则对这些设置了一些限制(为了安全编译):C编译器可能只查看第一个8个字符的标识符,所以foobar2k_eggsfoobar2k_spam可以被解释为相同的标识符有效——然而每个现代编译器都允许任意长的标识符,所以在我们这个时代(21世纪)我们不应该为此烦恼。

但是如果你面对的是一些你不能改变符号名/标识符的库呢?也许你得到了只有静态二进制文件和头文件或者不愿意,或者不允许自己调整和重新编译。

至少在静态库的情况下,您可以非常方便地绕过它。

考虑库foobar的头文件。为了本教程的目的,我还将提供源文件

例子/ex01/foo。

int spam(void);
double eggs(void);

examples/ex01/foo.c(可能不透明/不可用)

int the_spams;
double the_eggs;
int spam()
{
    return the_spams++;
}
double eggs()
{
    return the_eggs--;
}

例子/ex01 bar.h

int spam(int new_spams);
double eggs(double new_eggs);

examples/ex01/bar.c(可能不透明/不可用)

int the_spams;
double the_eggs;
int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}
double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

我们想在程序中使用它们foobar

例子/ex01 foobar.c

#include <stdio.h>
#include "foo.h"
#include "bar.h"
int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;
    printf("foo: spam = %d, eggs = %fn", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %fn", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );
    return 0;
}

一个问题马上就显现出来了:C语言不知道重载。我们有2乘以2个函数姓名相同,但签名不同。所以我们需要一些方法来区分它们。不管怎样,让我们看看编译器必须说:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

好吧,这并不奇怪,它只是告诉了我们,我们已经知道的,或者至少是怀疑的。

那么我们能在不修改原始库的情况下解决标识符冲突吗?源代码还是头文件?事实上我们可以。

首先让我们解决编译时间问题。为此,我们用a包围标题include一堆预处理器#define指令,前缀库导出的所有符号。稍后我们将使用一些非常舒适的wrapper-header来完成此操作,但这只是为了演示我们在foobar.c源文件

中逐字逐句地执行它:

例子/ex02 foobar.c

#include <stdio.h>
#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs
#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs
int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;
    printf("foo: spam = %d, eggs = %fn", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %fn", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );
    return 0;
}

现在如果我们编译这个…

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

…一开始情况看起来更糟了。但仔细看:实际上是编译阶段一切都很好。只是链接器在抱怨有符号碰撞它告诉我们发生这种情况的位置(源文件和行)。我们可以看到这些符号没有前缀。

让我们看一下使用nm实用程序的符号表:
example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams
example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

所以现在我们面临的挑战是在一些不透明的二进制中为这些符号加上前缀。是的,我知道在这个例子中,我们有源,可以改变这个。但现在,只是假设你只有那些。O 文件,或一个。A (实际上就是一堆.o)。

objcopy to rescue

有一个对我们来说特别有趣的工具:objcopy

objcopy适用于临时文件,所以我们可以像在原地操作一样使用它。有一个选项/操作名为——prefix-symbols,你有3次猜测它的作用。

让我们把这个家伙放到顽固库中:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

nm向我们展示了这似乎是有效的:

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams
example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

让我们试着链接这整个东西:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

确实有效:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

现在我把实现一个工具/脚本来自动提取库的符号使用nm,写一个结构

的包装头文件
/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

并使用objcopy将符号前缀应用于静态库的对象文件。

共享库呢?

原则上,共享库也可以这样做。然而共享库,顾名思义,在多个程序之间共享,因此以这种方式扰乱共享库并不是一个好主意。

您将无法绕过编写蹦床包装。更糟糕的是,您无法链接到共享库在对象文件级别,但强制执行动态加载。但这值得专门写一篇文章。

继续关注,祝大家编码愉快。

C标准规则对这些进行了一些限制(为了安全编译):C编译器可能只查看标识符的前8个字符,因此foobar2k_eggs和foobar2k_spam可能被有效地解释为相同的标识符-然而每个现代编译器都允许任意长的标识符,所以在我们的时代(21世纪)我们不应该为此而烦恼。

这不仅仅是现代编译器的扩展;当前的C标准还要求编译器支持合理的长外部名称。我忘记了确切的长度,但如果我没记错的话,现在大概有31个字符。

但是如果你面对的是一些你不能改变符号名/标识符的库呢?也许你只有一个静态二进制文件和头文件,或者不想这样做,或者不允许自己调整和重新编译。

那么你就卡住了。向图书馆的作者投诉。我曾经遇到过这样一个错误,我的应用程序的用户无法在Debian上构建它,因为Debian的libSDL链接了libsoundfile,这(至少在当时)用dsp这样的变量严重污染了全局命名空间(我不骗你!)。我向Debian投诉,他们修复了他们的软件包,并将修复发送到上游,我认为它被应用了,因为我再也没有听说过这个问题。

我真的认为这是最好的方法,因为它解决了每个人的问题。你所做的任何局部攻击都会将问题留在库中,让下一个不幸的用户再次遇到并与之斗争。

如果你真的需要一个快速修复,你有源代码,你可以添加一堆-Dfoo=crappylib_foo -Dbar=crappylib_bar等的makefile来修复它。如果没有,请使用您找到的objcopy解决方案。

如果您正在使用GCC,那么——allow-multiple-definition链接器开关是一个方便的调试工具。这就迫使链接器使用第一个定义(而不是抱怨它)。

在开发过程中,当我有供应商提供的库的源代码可用并且由于某种原因需要跟踪库函数时,这对我很有帮助。该开关允许您编译和链接源文件的本地副本,并且仍然链接到未修改的静态供应商库。一旦发现之旅结束,别忘了把开关从make符号上拉回来。发布带有故意的名称空间冲突的发布代码容易出现陷阱,包括无意的名称空间冲突。

最新更新