为了解释这里发生了什么,让我们先谈谈你的原始源文件,
a.h (1):
void foo() __attribute__((weak));
和:
交流 (1):
#include "a.h"
#include <stdio.h>
void foo() { printf("%s\n", __FILE__); }
示例代码中.c 和.cpp 文件的混合与
问题,所有代码都是 C,所以我们会说 main.cpp 是 main.c 和
使用gcc进行所有编译和链接:
$ gcc -Wall -c main.c a.c b.c
ar rcs a.a a.o
ar rcs b.a b.o
首先让我们回顾一下弱声明符号之间的区别,例如
你的:
void foo() __attribute__((weak));
和一个强烈声明的符号,比如
void foo();
这是默认的:
如果在程序中链接了对 foo 的弱引用(即对弱声明的 foo 的引用),则
链接器不需要在链接中的任何地方找到foo 的定义:它可能仍然存在
不明确的。如果在程序中链接了对foo 的强引用,
链接器需要找到foo 的定义。
链接最多可以包含一个强定义 foo(即定义
foo 强烈声明)。否则会导致多重定义错误。
但它可能包含foo 的多个弱定义而不会出错。
如果链接包含foo 的一个或多个弱定义,并且还包含一个强
定义,则链接器选择强定义而忽略弱定义
那些。
如果链接只包含foo 的一个弱定义并且没有强
定义,链接器不可避免地使用一个弱定义。
如果链接包含foo 的多个弱定义并且没有强
定义,然后链接器选择一个弱定义任意。
接下来,让我们回顾一下在链接中输入目标文件的区别
并输入一个静态库。
静态库只是我们可能提供给目标文件的ar 存档
从中选择需要进行链接的链接器。
当一个目标文件被输入到一个链接时,链接器无条件地链接它
进入输出文件。
当静态库输入到链接时,链接器会检查存档以
在其中找到为未解析的符号引用提供定义它需要的任何目标文件
从已经链接的输入文件中产生的。如果它找到任何这样的目标文件
在存档中,它提取它们并将它们链接到输出文件中,就像
如果它们是单独命名的输入文件并且根本没有提到静态库。
考虑到这些观察结果,考虑编译和链接命令:
gcc main.c a.o b.o
幕后gcc 将其分解为编译步骤和链接
步骤,就像你已经运行一样:
gcc -c main.c # compile
gcc main.o a.o b.o # link
所有三个目标文件都无条件地链接到(默认)程序./a.out。 a.o 包含一个
foo 的弱定义,我们可以看到:
$ nm --defined a.o
0000000000000000 W foo
而b.o 包含一个强定义:
$ nm --defined b.o
0000000000000000 T foo
链接器将找到这两个定义并从b.o 中选择强定义,因为我们可以
另见:
$ gcc main.o a.o b.o -Wl,-trace-symbol=foo
main.o: reference to foo
a.o: definition of foo
b.o: definition of foo
$ ./a.out
b.c
颠倒a.o和b.o的链接顺序不会有任何区别:有
仍然是foo 的一个强定义,b.o 中的一个。
相比之下,考虑编译和链接命令:
gcc main.cpp a.a b.a
分解为:
gcc -c main.cpp # compile
gcc main.o a.a b.a # link
这里,只有main.o 是无条件链接的。这会产生一个未定义的弱引用
到foo进入链接:
$ nm --undefined main.o
w foo
U _GLOBAL_OFFSET_TABLE_
U puts
对foo 的弱引用不需要定义。所以链接器将
不要尝试在a.a 或b.a 的任何目标文件中找到解析它的定义,并且
如我们所见,它将在程序中未定义:
$ gcc main.o a.a b.a -Wl,-trace-symbol=foo
main.o: reference to foo
$ nm --undefined a.out
w __cxa_finalize@@GLIBC_2.2.5
w foo
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U __libc_start_main@@GLIBC_2.2.5
U puts@@GLIBC_2.2.5
因此:
$ ./a.out
no foo
同样,如果你颠倒a.a和b.a的链接顺序也没关系,
但这次是因为它们都没有对链接做出任何贡献。
现在让我们转向您通过更改 a.h 和 a.c 发现的不同行为
到:
a.h (2):
void foo();
a.c (2):
#include "a.h"
#include <stdio.h>
void __attribute__((weak)) foo() { printf("%s\n", __FILE__); }
再一次:
$ gcc -Wall -c main.c a.c b.c
main.c: In function ‘main’:
main.c:4:18: warning: the address of ‘foo’ will always evaluate as ‘true’ [-Waddress]
int main() { if (foo) foo(); else printf("no foo\n"); }
看到那个警告了吗? main.o 现在包含一个强烈 声明的对foo 的引用:
$ nm --undefined main.o
U foo
U _GLOBAL_OFFSET_TABLE_
所以代码(当链接时)必须有foo 的非空地址。进行中:
$ ar rcs a.a a.o
$ ar rcs b.a b.o
那就试试联动吧:
$ gcc main.o a.o b.o
$ ./a.out
b.c
并且目标文件颠倒:
$ gcc main.o b.o a.o
$ ./a.out
b.c
和以前一样,顺序没有区别。所有的目标文件都是链接的。 b.o 提供
foo、a.o 的强定义提供了弱定义,因此 b.o 获胜。
接下来尝试联动:
$ gcc main.o a.a b.a
$ ./a.out
a.c
并且库的顺序颠倒了:
$ gcc main.o b.a a.a
$ ./a.out
b.c
这确实有所作为。为什么?让我们重做与诊断的联系:
$ gcc main.o a.a b.a -Wl,-trace,-trace-symbol=foo
/usr/bin/x86_64-linux-gnu-ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
main.o
(a.a)a.o
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
main.o: reference to foo
a.a(a.o): definition of foo
忽略默认库,ours 的唯一目标文件
链接是:
main.o
(a.a)a.o
而foo的定义取自a.a的归档成员a.o:
a.a(a.o): definition of foo
反转库顺序:
$ gcc main.o b.a a.a -Wl,-trace,-trace-symbol=foo
/usr/bin/x86_64-linux-gnu-ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
main.o
(b.a)b.o
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
main.o: reference to foo
b.a(b.o): definition of foo
这次链接的目标文件是:
main.o
(b.a)b.o
而foo的定义取自b.a中的b.o:
b.a(b.o): definition of foo
在第一个链接中,链接器有一个未解决的强引用
foo 在到达 a.a 时需要对其进行定义。所以
在档案中查找提供定义的目标文件,
并找到a.o。这个定义很弱,但这并不重要。不
已经看到了强烈的定义。 a.o 是从 a.a 中提取并链接的,
因此解决了对foo 的引用。接下来到达b.a,其中
foo 的强定义 已在 b.o 中找到,如果链接器仍需要一个
并寻找它。但它不再需要一个,也不看。联动:
gcc main.o a.a b.a
完全一样如下:
gcc main.o a.o
还有链接:
$ gcc main.o b.a a.a
完全一样:
$ gcc main.o b.o
你真正的问题...
...出现在您的帖子中的一个 cmets 中:
我想在与测试框架链接时覆盖 [the] 原始函数实现。
你想链接一个程序输入一些静态库lib1.a
其中有一些成员file1.o 定义了一个符号foo,并且你想淘汰
foo 的定义并链接在其他对象中定义的另一个
文件file2.o。
__attribute__((weak)) 不适用于该问题。解决方案更多
初级。 您只需确保在输入之前输入 file2.o 到链接
lib1.a(在任何其他提供foo 定义的输入之前)。
然后链接器将使用file2.o 中提供的定义解析对foo 的引用,并且不会尝试找到任何其他
到达lib1.a 时的定义。链接器根本不会消耗lib1.a(file1.o)。它也可能不存在。
如果您将file2.o 放入另一个静态库lib2.a 会怎样?然后输入
lib2.a before lib1.a 将完成链接lib2.a(file2.o) before
达到lib1.a 并将foo 解析为file2.o 中的定义。
同样,当然,lib2.a 成员提供的每个定义都将链接到
优先于lib1.a 中提供的相同符号的定义。如果那不是什么
你想要,然后不喜欢lib2.a:链接file2.o 本身。
终于
是否可以将 [the] 弱属性与静态链接一起使用?
当然。这是一个第一原则用例:
foo.h (1)
#ifndef FOO_H
#define FOO_H
int __attribute__((weak)) foo(int i)
{
return i != 0;
}
#endif
aa.c
#include "foo.h"
int a(void)
{
return foo(0);
}
bb.c
#include "foo.h"
int b(void)
{
return foo(42);
}
prog.c
#include <stdio.h>
extern int a(void);
extern int b(void);
int main(void)
{
puts(a() ? "true" : "false");
puts(b() ? "true" : "false");
return 0;
}
编译所有源文件,为每个函数请求单独的 ELF 部分:
$ gcc -Wall -ffunction-sections -c prog.c aa.c bb.c
注意foo 的weak 定义是通过foo.h 编译成both
aa.o 和bb.o,我们可以看到:
$ nm --defined aa.o
0000000000000000 T a
0000000000000000 W foo
$ nm --defined bb.o
0000000000000000 T b
0000000000000000 W foo
现在从所有目标文件链接一个程序,请求链接器
丢弃未使用的部分(并给我们地图文件和一些诊断):
$ gcc prog.o aa.o bb.o -Wl,--gc-sections,-Map=mapfile,-trace,-trace-symbol=foo
/usr/bin/x86_64-linux-gnu-ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
prog.o
aa.o
bb.o
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
aa.o: definition of foo
此链接与:
$ ar rcs libaabb.a aa.o bb.o
$ gcc prog.o libaabb.a
尽管 aa.o 和 bb.o 都已加载,并且每个都包含
foo的一个定义,没有多重定义的错误结果,因为每个定义
弱。 aa.o 在 bb.o 之前加载,foo 的定义是从 aa.o 链接的。
那么bb.o 中foo 的定义发生了什么变化?地图文件向我们展示了:
地图文件 (1)
...
...
Discarded input sections
...
...
.text.foo 0x0000000000000000 0x13 bb.o
...
...
链接器丢弃了包含定义的函数部分
在bb.o
让我们颠倒aa.o和bb.o的链接顺序:
$ gcc prog.o bb.o aa.o -Wl,--gc-sections,-Map=mapfile,-trace,-trace-symbol=foo
...
prog.o
bb.o
aa.o
...
bb.o: definition of foo
现在相反的事情发生了。 bb.o 在 aa.o 之前加载。这
foo 的定义是从 bb.o 链接的,并且:
地图文件 (2)
...
...
Discarded input sections
...
...
.text.foo 0x0000000000000000 0x13 aa.o
...
...
aa.o 的定义被丢弃了。
你会看到链接器如何任意选择多个中的一个
在没有强定义的情况下,符号的弱定义。它简直
选择你给它的第一个并忽略其余的。
我们刚才所做的实际上就是 GCC C++ 编译器为我们所做的,当我们
定义一个全局内联函数。重写:
foo.h (2)
#ifndef FOO_H
#define FOO_H
inline int foo(int i)
{
return i != 0;
}
#endif
重命名我们的源文件*.c -> *.cpp;编译链接:
$ g++ -Wall -c prog.cpp aa.cpp bb.cpp
现在在aa.o 和bb.o 中都有foo 的弱定义(C++ 损坏):
$ nm --defined aa.o bb.o
aa.o:
0000000000000000 T _Z1av
0000000000000000 W _Z3fooi
bb.o:
0000000000000000 T _Z1bv
0000000000000000 W _Z3fooi
链接使用它找到的第一个定义:
$ g++ prog.o aa.o bb.o -Wl,-Map=mapfile,-trace,-trace-symbol=_Z3fooi
...
prog.o
aa.o
bb.o
...
aa.o: definition of _Z3fooi
bb.o: reference to _Z3fooi
然后扔掉另一个:
地图文件 (3)
...
...
Discarded input sections
...
...
.text._Z3fooi 0x0000000000000000 0x13 bb.o
...
...
您可能知道,C++ 函数模板的每个实例化
全局范围(或类模板成员函数的实例化)是
内联全局函数。再次重写:
#ifndef FOO_H
#define FOO_H
template<typename T>
T foo(T i)
{
return i != 0;
}
#endif
重新编译:
$ g++ -Wall -c prog.cpp aa.cpp bb.cpp
再次:
$ nm --defined aa.o bb.o
aa.o:
0000000000000000 T _Z1av
0000000000000000 W _Z3fooIiET_S0_
bb.o:
0000000000000000 T _Z1bv
0000000000000000 W _Z3fooIiET_S0_
aa.o 和 bb.o 中的每一个都有以下弱定义:
$ c++filt _Z3fooIiET_S0_
int foo<int>(int)
并且链接行为现在很熟悉了。一种方式:
$ g++ prog.o aa.o bb.o -Wl,-Map=mapfile,-trace,-trace-symbol=_Z3fooIiET_S0_
...
prog.o
aa.o
bb.o
...
aa.o: definition of _Z3fooIiET_S0_
bb.o: reference to _Z3fooIiET_S0_
反之:
$ g++ prog.o bb.o aa.o -Wl,-Map=mapfile,-trace,-trace-symbol=_Z3fooIiET_S0_
...
prog.o
bb.o
aa.o
...
bb.o: definition of _Z3fooIiET_S0_
aa.o: reference to _Z3fooIiET_S0_
重写后我们的程序的行为没有改变:
$ ./a.out
false
true
所以weak属性在ELF对象链接中的符号应用——
无论是静态的还是动态的 - 启用 C++ 模板的 GCC 实现
对于 GNU 链接器。你可以说它支持现代 C++ 的 GCC 实现。