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


82

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

C标准的规则放在那些一些限制(安全编译):AC编译器可以看只是一个标识符的前8个字符,所以foobar2k_eggsfoobar2k_spam可能被解释为有效相同标识符-但是每一个现代的编译器允许任意长标识符,因此在我们这个时代(21世纪),我们不必为此烦恼。

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


Answers:


141

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

考虑库foobar的那些头。为了本教程的缘故,我还将为您提供源文件

例子/ex01/foo.h

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 = %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指令,这些指令对库导出的所有符号进行前缀。稍后,我们使用一些不错的舒适包装器标题来执行此操作,但是只是为了演示正在发生的事情,所以要在foob​​ar.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 = %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,您有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将符号前缀应用于静态库的目标文件。

共享库呢?

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

您不会无所事事地编写蹦床包装纸。更糟糕的是,您无法在目标文件级别上链接共享库,而是被迫进行动态加载。但是,这值得拥有自己的文章。

敬请期待,编码愉快。


4
令人印象深刻!没想到如此简单objcopy
科斯,

12
您只是...在询问后1分钟内回答自己的问题吗?
Alex B

18
@Alex B:这是一篇教程文章,我遵循了meta.stackoverflow.com上向我建议的一条路径,即如何放置有关(有趣的)问题及其解决方案的教程。关于冲突库的问题浮出水面,我想“嗯,我知道如何处理这种解决方案”,写了一篇文章,并以问答的形式发布在这里。meta.stackexchange.com/questions/97240/...
datenwolf

4
@datenwolf关于解决iOS库此问题的任何想法。如我所知,objcopy不支持iOS库:/
Ege

6
等等等等objcopy --prefix-symbols ... +1!
本杰克逊

7

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

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

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

然后你就被困住了。向图书馆作者投诉。我曾经遇到过这样的错误,即由于Debian的libSDL链接libsoundfile,我的应用程序的用户无法在Debian上构建它,(至少在当时),链接(例如dsp,我不骗你!)可怕地污染了全局名称空间。我向Debian投诉,他们修复了软件包,并将修复发送到上游(我认为已应用),因为我再也没有听说过该问题了。

我真的认为这是最好的方法,因为它可以解决每个人的问题。您所做的任何本地入侵都会将问题留在库中,让下一个不幸的用户遇到并再次与之战斗。

如果确实需要快速修复,并且您有源码,则可以-Dfoo=crappylib_foo -Dbar=crappylib_bar在makefile中添加一堆等以修复它。如果不是,请使用objcopy找到的解决方案。


您当然是对的,但是有时候您需要一个肮脏的黑客,就像我上面显示的那样。例如,如果您受困于某些遗留库,而供应商或其他类似机构则破产了。我是专门为静态库编写的。
datenwolf 2011年

3

如果您使用的是GCC,则--allow-multiple-definition链接器开关是方便的调试工具。这会限制链接器使用第一个定义(而不是抱怨)。在这里了解更多。

这在开发过程中为我提供了帮助,这时我可以使用供应商提供的库的源,并且由于某种原因需要跟踪到库函数。该开关允许您编译并链接到源文件的本地副本中,并且仍然链接到未修改的静态供应商库。一旦发现之旅完成,别忘了将开关拉出制造符号。带有故意的名称空间冲突的发布发布代码容易陷入陷阱,包括非故意的名称空间冲突。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.