在不使用重新定义的符号时避免多重定义链接器错误

Avoid multiple definition linker error when not using the redefined symbols

我尝试构建一个链接到各种共享库和静态库的可执行文件。事实证明,两个静态库都定义了相同的符号,这导致了多重定义链接器错误。我的可执行文件不使用这个符号,所以这不是一个真正的问题。

我可以通过添加 --allow-multiple-definitions 标志来避免错误,但这似乎是一个核选项。如果我尝试使用多次定义的符号,我希望链接器发出警告。

有没有办法告诉链接器"complain for multiple definitions only if the symbol is used"?或者告诉它,"from lib ABC ignore symbol XYZ"。我正在 linux.

上使用 g++ 进行开发

您可能有问题的一种变体或不同的变体, 取决于您尚未考虑其相关性的事实。或者您可能两者兼而有之,因此我将介绍针对每个变体的解决方案。

您应该熟悉静态库的性质以及它们在链接中的使用方式, as summarised here

Superflous Globals 符号变体

这里有几个源文件和一个 header 文件:

one.cpp

#include <onetwo.h>

int clash = 1;

int get_one()
{
    return clash;
}

two.cpp

#include <onetwo.h>

int get_two()
{
    return 2;
}

onetwo.h

#pragma once

extern int get_one();
extern int get_two();

这些已经内置到静态库中libonetwo.a

$ g++ -Wall -Wextra -pedantic -I. -c one.cpp two.cpp
$ ar rcs libonetwo.a one.o two.o

其预期 API 在 onetwo.h

中定义

同样,一些其他的源文件和一个header已经 内置于静态库 libfourfive.a 中,其预期 API 在 fourfive.h

中定义

four.cpp

#include <fourfive.h>

int clash = 4;

int get_four()
{
    return clash;
}

five.cpp

#include <fourfive.h>

int get_five()
{
    return 5;
}

fourfive.h

#pragma once

extern int get_four();
extern int get_five();

下面是依赖于这两个库的程序的源代码:

prog.cpp

#include <onetwo.h>
#include <fourfive.h>

int main()
{
    return get_one() + get_four();
}

我们尝试这样构建:

$ g++ -Wall -Wextra -pedantic -I. -c prog.cpp
$ g++ -o prog prog.o -L. -lonetwo -lfourfive
/usr/bin/ld: ./libfourfive.a(four.o):(.data+0x0): multiple definition of `clash'; ./libonetwo.a(one.o):(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

遇到符号 clash 的 name-collision,因为它在其中两个全局定义 object 个链接需要的文件,one.ofour.o:

$ readelf -s libonetwo.a libfourfive.a | egrep '(File|Symbol|OBJECT|FUNC)'
File: libonetwo.a(one.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: libonetwo.a(two.o)
Symbol table '.symtab' contains 10 entries:
     9: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 _Z7get_twov
File: libfourfive.a(four.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z8get_fourv
File: libfourfive.a(five.o)
Symbol table '.symtab' contains 10 entries:
     9: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 _Z8get_fivev

我们自己的代码 prog.(cpp|o) 中没有引用问题符号 clash。你想知道:

Is there a way to tell the linker "complain for multiple definitions only if the symbol is used"?

不,没有,但这无关紧要。 one.o 不会从 libonetwo.a 中提取 如果链接器不需要它来解析某些符号,则链接到程序中。它需要它 解决 get_one。同样,它只链接了 four.o,因为它需要解析 get_four。 所以 clash 的冲突定义在链接中。尽管 prog.o 不使用 clash, 它确实使用 get_one,后者使用 clash,而 打算 使用 one.oclash 的定义。 同样 prog.o 使用 get_four,它使用 clash 并打算使用 four.o.

中的不同定义

即使 clash 没有被每个库和程序使用,它被定义的事实 在必须链接到程序中的多个 object 文件中意味着程序将包含 它的多个定义,只有 --allow-multiple-definitions 允许。

在这种情况下,您还会看到:

Or alternatively [is there a way to] tell it, "from lib ABC ignore symbol XYZ".

一般不会飞。如果我们可以告诉链接器忽略(比如说)clash 的定义 在 four.o 中并将符号到处解析为 one.o 中的定义(唯一的其他候选者),然后 get_four() 将 return 1 而不是我们程序中的 4。那 实际上是 --allow-multiple-definitions 的效果,因为它导致了第一个定义 在要使用的链接中。

通过检查 libonetwo.a(或 libfourfive.a)的源代码,我们可以相当自信地找到根 问题的原因。符号 clash 保留了外部链接,其中 它只需要内部链接,因为它没有在关联的 header 文件并且在库中没有被引用,除了 在定义它的文件中。有问题的源文件应该写成:

one_good.cpp

#include <onetwo.h>

namespace {
    int clash = 1;
}

int get_one()
{
    return clash;
}

four_good.cpp

#include <fourfive.h>

namespace {
    int clash = 4;
}

int get_four()
{
    return clash;
}

一切都会好的:

$ g++ -Wall -Wextra -pedantic -I. -c one_good.cpp four_good.cpp
$ readelf -s one_good.o four_good.o | egrep '(File|Symbol|OBJECT|FUNC)'
File: one_good.o
Symbol table '.symtab' contains 11 entries:
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 _ZN12_GLOBAL__N_15clashE
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: four_good.o
Symbol table '.symtab' contains 11 entries:
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 _ZN12_GLOBAL__N_15clashE
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z8get_fourv

$ g++ -o prog prog.o one_good.o four_good.o
$./prog; echo $?
5

由于re-writing这样的源代码不是一个选项,我们必须将object文件修改为 同样的效果。用于此的工具是 objcopy.

$ objcopy --localize-symbol=clash libonetwo.a libonetwo_good.a

此命令与运行:

效果相同
$ objcopy --localize-symbol=clash orig.o fixed.o

在每个 object 文件上 libonetwo(orig.o) 输出一个固定的 object 文件 fixed.o, 并将所有 fixed.o 文件归档到一个新的静态库 libonetwo_good.a 中。和 --localize-symbol=clash 对每个 object 文件的影响是更改链接 符号 clash,如果已定义,则从外部 (GLOBAL) 到内部 (LOCAL):

$ readelf -s libonetwo_good.a | egrep '(File|Symbol|OBJECT|FUNC)'
File: libonetwo_good.a(one.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: libonetwo_good.a(two.o)
Symbol table '.symtab' contains 10 entries:

现在链接器无法在 libonetwo_good.a(one.o) 中看到 clashLOCAL 定义。

这足以避免多重定义错误,但是由于 libfourfive.a 同样的缺陷,我们也会修复它:

$ objcopy --localize-symbol=clash libfourfive.a libfourfive_good.a

然后我们可以使用固定库重新链接 prog 成功。

$ g++ -o prog prog.o -L. -lonetwo_good -lfourfive_good
$ ./prog; echo $?
5

全局符号死锁变体

在这种情况下,libonetwo.a 的来源和 header 是:

one.cpp

#include <onetwo.h>
#include "priv_onetwo.h"

int inc_one()
{
    return inc(clash);
}

two.cpp

#include <onetwo.h>
#include "priv_onetwo.h"

int inc_two()
{
    return inc(clash + 1);
}

priv_onetwo.cpp

#include "priv_onetwo.h"

int clash = 1;

int inc(int i)
{
    return i + 1;
}

priv_onetwo.h

#pragma once

extern int clash;
extern int inc(int);

onetwo.h

#pragma once

extern int inc_one();
extern int inc_two();

libfourfive.a 他们是:

four.cpp

#include <fourfive.h>
#include "priv_fourfive.h"

int dec_four()
{
    return dec(clash);
}

five.cpp

#include <fourfive.h>
#include "priv_fourfive.h"

int dec_five()
{
    return dec(clash + 1);
}

priv_fourfive.cpp

#include "priv_fourfive.h"

int clash = 4;

int dec(int i)
{
    return i - 1;
}

priv_fourfive.h

#pragma once

extern int clash;
extern int dec(int);

fourfive.h

#pragma once

extern int dec_four();
extern int dec_five();

这些库中的每一个都在构建时定义了一些共同的内部结构 在源文件中 - (priv_onetwo.cpp|priv_fourfive.cpp) - 这些内部构件是全局声明的通过私人 header 建立图书馆 - (priv_onetwo.h|priv_fourfive.h) - 不随库分发。 它们是未记录的符号,但仍然暴露给链接器。

现在每个库中有两个文件使未定义的 (UND) 引用 到全局符号 clash,它在另一个文件中定义:

$ readelf -s libonetwo.a libfourfive.a | egrep '(File|Symbol|OBJECT|FUNC|clash)'
File: libonetwo.a(one.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 _Z7inc_onev
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libonetwo.a(two.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 _Z7inc_twov
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libonetwo.a(priv_onetwo.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash
    10: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z3inci
File: libfourfive.a(four.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 _Z8dec_fourv
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libfourfive.a(five.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 _Z8dec_fivev
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libfourfive.a(priv_fourfive.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash
    10: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z3deci

我们这次的节目源是:

prog.cpp

#include <onetwo.h>
#include <fourfive.h>

int main()
{
    return inc_one() + dec_four();
}

和:

$ g++ -Wall -Wextra -pedantic -I. -c prog.cpp
$ g++ -o prog prog.o -L. -lonetwo -lfourfive
/usr/bin/ld: ./libfourfive.a(priv_fourfive.o):(.data+0x0): multiple definition of `clash'; ./libonetwo.a(priv_onetwo.o):(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

再次 clash 被多重定义。要解析 main 中的 inc_one, 链接器需要 one.o,这迫使它解析 inc,这使得它需要 priv_onetwo.o,其中包含 clash 的第一个定义。要解析 main 中的 dec_four, 链接器需要 four.o,这迫使它解析 dec,这使得它需要 priv_fourfive.o,其中包含 clash 的竞争定义。

在这种情况下,clash 具有外部链接并不是两个库中的编码错误。 它需要有外部链接。将 clash 的定义本地化为 objcopy libonetwo.a(priv_onetwo.o)libfourfive.a(priv_fourfive.o) 将不起作用。如果我们这样做 链接将成功但输出错误程序,因为链接器将解析 clash 到另一个 object 文件中幸存的 GLOBAL 定义:然后 dec_four() 将 return 0 而不是程序中的 3,dec_five() 将 return 1 而不是 4;否则 inc_one() 将 return 5 而 inc_two() 将 return 6。 如果我们本地化 两个 定义,那么 clash 的定义将不会在 prog 的链接以满足 one.ofour.o 中的引用,并且对于 clash

的未定义引用将失败

这次objcopy又来救场了,只是有不同的选择1:

$ objcopy --redefine-sym clash=clash_onetwo libonetwo.a libonetwo_good.a

这条命令的作用是新建一个静态库libonetwo_good.a, 包含新的 object 文件,它们与 libonetwo.a 中的文件成对地相同, 除了符号 clash 到处都被替换为 clash_onetwo:

$ readelf -s libonetwo_good.a | egrep '(File|Symbol|clash)'
File: libonetwo_good.a(one.o)
Symbol table '.symtab' contains 13 entries:
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash_onetwo
File: libonetwo_good.a(two.o)
Symbol table '.symtab' contains 13 entries:
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash_onetwo
File: libonetwo_good.a(priv_onetwo.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash_onetwo

我们将用libfourfive.a做相应的事情:

$ objcopy --redefine-sym clash=clash_fourfive libfourfive.a libfourfive_good.a

现在我们可以再次出发了:

$ g++ -o prog prog.o -L. -lonetwo_good -lfourfive_good
$ ./prog; echo $?
5

在两个解决方案中,使用 The Superflous Globals Symbols Variant 的修复程序,如果 superflous globals 是你所拥有的,尽管 The Global Symbols Deadlock Variant 的修复 也会工作。篡改 object 文件是不可取的 在编译和链接之间;它只能是不可避免的或弊端较小的。但 如果你要篡改它们,本地化一个不应该是全局的全局符号 是一种比将符号名称更改为更透明的篡改 在源代码中没有来源。


[1] 不要忘记,如果您想将 objcopy 与任何选项参数一起使用 是 C++ object 文件中的符号,您必须使用 损坏的名称 C++ 标识符比映射到符号。在这个演示代码中,碰巧的是 C++ 标识符 clash 也是 clash。但是,如果,例如完全合格的标识符有 是 onetwo::clash,它的错位名称将是 _ZN6onetwo5clashE,据报道 nmreadelf。当然反过来,如果您希望使用 objcopy 来更改 _ZN6onetwo5clashE 一个 object 文件到一个将 demangle 为 onetwo::klash 的符号,然后该符号将 是 _ZN6onetwo5klashE.