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

96

在编写库时,最重要的规则和最佳实践之一是将所有符号放入特定于该库的命名空间中。由于C++具有namespace关键字,因此这变得很容易。在C语言中,通常的方法是给标识符添加一些特定于库的前缀。

C标准的规则对它们施加了一些限制(以进行安全编译):C编译器只能查看标识符的前8个字符,因此foobar2k_eggsfoobar2k_spam可以被有效地解释为相同的标识符。然而,每个现代编译器都允许任意长的标识符,因此在我们这个时代(21世纪),我们不必再担心这个问题。

但是,如果您面对的是一些库,您无法更改其符号名称/标识符怎么办?也许您只获得了一个静态二进制文件和头文件,或者不想、不能调整并重新编译自己。


参见:https://dev59.com/mmw15IYBdhLWcg3wi8dv - ninjalj
3个回答

169

至少在静态库的情况下,你可以很方便地解决它。

考虑库foobar的头文件。为了本教程的方便,我也会给你源文件。

examples/ex01/foo.h

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

示例/ex01/foo.c(可能是不透明/不可用的)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

example/ex01/bar.h

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

示例/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 = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

一个问题立即显现:C语言不支持重载。因此,我们有两个相同名称但签名不同的函数。所以我们需要一些方法来区分它们。无论如何,让我们看看编译器对此的反应:

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

好的,这并不意外,它只是告诉我们我们已经知道或者至少怀疑的内容。

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

首先让我们解决编译时问题。为此,我们使用一堆预处理器#define指令来给库导出的所有符号添加前缀。稍后我们会使用一些漂亮舒适的包装头文件来完成这个操作,但是为了演示正在进行的工作,我们将在foobar.c源文件中直接进行操作:

example/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 = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           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

有一个特别适合我们的工具:objcopy

objcopy在临时文件上运行,因此我们可以将其用作就地操作。有一个选项/操作称为--prefix-symbols,您猜它是做什么的。

因此,让我们将这个家伙扔到我们的固执库中:

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将符号前缀应用于静态库的对象文件。

那么共享库呢?

原则上,共享库也可以这样做。但是,共享库(名称就是这个意思)被多个程序共享,因此以这种方式搞乱共享库并不是一个好主意。

您无法绕过编写跳板包装器。更糟糕的是,您不能在对象文件级别上链接共享库,而是被迫进行动态加载。但是,这值得一篇专门的文章。

敬请关注,愉快编码。


5
太棒了!没想到使用 objcopy 这么容易。 - Kos
12
你刚才是不是在一分钟内自问自答了? - Alex B
20
@Alex B:这是一篇教程文章,我按照meta.stackoverflow.com上建议的路径,介绍了如何编写有关(有趣的?)问题和解决方案的教程。有人提出了有关冲突库的问题,我想:“嗯,我知道如何处理这种解决方案”,于是写了一篇文章,并以问答形式发布在这里。http://meta.stackexchange.com/questions/97240/asking-for-requests-for-comments/97242#97242 - datenwolf
5
@datenwolf,您对解决iOS库的问题有什么想法?据我所知,objcopy不支持iOS库 :/ - Ege Akpinar
8
好的,我会尽力进行翻译。以下是需要翻译的内容:Blah blah blah objcopy --prefix-symbols ... +1!咕噜咕噜咕噜... objcopy --prefix-symbols ... 加1! - Ben Jackson
显示剩余8条评论

8
C标准的规则对于安全编译设置了一些限制:C编译器只能查看标识符的前8个字符,因此foobar2k_eggs和foobar2k_spam可能被正确解释为相同的标识符。但是现代编译器允许任意长度的标识符,所以在21世纪,我们不必再担心这个问题。
这不仅仅是现代编译器的扩展;当前的C标准要求编译器支持合理长的外部名称。我忘记确切的长度,但如果我没记错的话,它现在大约是31个字符。
但是,如果您面对的是一些库,您无法更改符号名称/标识符怎么办?也许你只有一个静态二进制文件和头文件,或者不想或不允许自己进行调整和重新编译。那么你就陷入了困境。向库的作者投诉。我曾经遇到过这样一个错误,我的应用程序的用户由于Debian链接libSDL时使用了libsoundfile(至少当时如此),导致全局名称空间中出现了诸如dsp之类的变量(我没有开玩笑!)。我向Debian投诉,他们修复了他们的软件包并将修复发送到上游,我认为它已经应用了,因为我再也没有听说过这个问题。
我真的认为这是最好的方法,因为它为每个人解决了问题。你所做的任何本地hack都将使问题留在库中,以便下一个不幸的用户再次遇到并与之抗争。
如果您确实需要快速修复,并且具有源代码,则可以向makefile添加一堆-Dfoo=crappylib_foo -Dbar=crappylib_bar等来解决它。如果没有,则使用您发现的objcopy解决方案。

当然,你是正确的,但有时候你需要一个“脏”技巧,就像我上面展示的那个。例如,如果你被一些遗留库卡住了,而供应商已经停业或类似情况。我特别为静态库编写了这篇文章。 - datenwolf

4
如果您正在使用GCC,--allow-multiple-definition链接器开关是一个方便的调试工具。它将链接器绑定到使用第一个定义(而不抱怨)的操作。更多信息请参见此处
在开发过程中,当我有供应商提供的库源代码并且因某种原因需要跟踪库函数时,这对我很有帮助。该开关允许您编译和链接本地源文件的副本,同时仍然链接到未修改的静态供应商库。完成探索之旅后,请不要忘记从制作符号中撤回开关。故意名称空间冲突的发货版本代码容易出现问题,包括无意的名称空间冲突。

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