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

编写一个库时最重要的规则和最佳实践之一是将库的所有符号放入一个特定于库的名称空间中。 由于namespace关键字,C ++使这个变得简单。 在C中,通常的做法是在标识符的前面添加一些库特定的前缀。

C标准的规则对这些进行了一些约束(为了安全编译):AC编译器可能只查看标识符的前8个字符,所以foobar2k_eggsfoobar2k_spam可以被有效地解释为相同的标识符 – 但是每个现代编译器都允许任意很长的标识符,所以在我们这个时代(21世纪),我们不应该为此烦恼。

但是如果你正面临一些你不能更改符号名称/标识符的库呢? 也许你只有一个静态的二进制和头或不想,或不允许调整和重新编译自己。

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

考虑图书馆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 

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

那么我们可以在不修改原始库的源代码或头文件的情况下以某种方式解决这个标识冲突吗? 其实我们可以。

首先解决编译时问题。 为此,我们在头文件中包含一系列预处理程序#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在临时文件上工作,所以我们可以像使用它一样使用它。 有一个选项/操作称为 – 前缀 – 符号 ,你有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将符号前缀应用于静态库的对象文件。

那共享库呢?

原则上,共享库也可以做到这一点。 然而,共享库,名称告诉它,在多个程序之间共享,所以用这种方式混淆共享库不是一个好主意。

你不会在写一个蹦床包装。 更糟糕的是,你不能链接到目标文件级的共享库,而是被迫做dynamic加载。 但这是值得自己的文章。

请继续关注,快乐的编码。

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

这不仅仅是现代编译器的延伸, 目前的C标准还要求编译器支持相当长的外部名称。 我忘记了确切的长度,但现在如果我记得正确的话,它现在是31个字符。

但是如果你正面临一些你不能更改符号名称/标识符的库呢? 也许你只有一个静态的二进制和头或不想,或不允许调整和重新编译自己。

那么你卡住了。 抱怨图书馆的作者。 由于Debian的libSDL链接libsoundfile (至less在那个时候)全球性的命名空间被像dsp这样的variables(我不懂你的孩子!)污染了全局命名空间,我曾经遇到过这样一个错误,那就是我的应用程序用户无法在Debian上build立它。 我向Debian抱怨,他们固定了他们的软件包,并将修复程序发送到了上游,我假定它已经被应用,因为我再也没有听说过这个问题。

我真的认为这是最好的方法,因为它解决了每个人的问题。 你所做的任何本地黑客都会将问题留在图书馆,以便下一位不幸的用户再次遇到并与之战斗。

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

如果使用GCC,–allow-multiple-definition链接器开关是一个方便的debugging工具。 这使得链接器使用第一个定义(而不是抱怨)。 更多关于这里 。

这有助于我在开发过程中获得供应商提供的库的源代码,并且由于某种原因需要追踪库函数。 该开关允许您编译和链接源文件的本地副本,并仍然链接到未经修改的静态供应商库。 一旦发现的航程完成,不要忘记将切换开关从符号中抽出。 具有故意名称空间冲突的发行版本代码容易出现陷入,包括无意的名称空间冲突。