__attribute__((弱))和静态库

__attribute__((weak)) and static libraries

提问人:AndyB 提问时间:8/2/2018 最后编辑:AndyB 更新时间:2/14/2020 访问量:43846

问:

我想在我的代码中引入一个弱符号,但是,当使用 *.a 文件时,我无法理解它的行为。

这是我最小的例子:

文件A.H:

void foo() __attribute__((weak));

文件A.C:

#include "a.h"
#include <stdio.h>

void foo() { printf("%s\n", __FILE__); }

文件b.c:

#include <stdio.h>

void foo() { printf("%s\n", __FILE__); }

文件主.cpp:

#include "a.h"
#include <stdio.h>

int main() { if (foo) foo(); else printf("no foo\n"); }

现在,根据我使用的是 *.o 文件(和)还是 *.a 文件(和),输出是不同的:make -c a.cmake -c b.car cr a.oar cr b.o

1) 打印 BC
2) 打印 BC
3) 打印无 FOO 4) 打印无 FOO
g++ main.cpp a.o b.og++ main.cpp b.o a.og++ main.cpp a.a b.ag++ main.cpp b.a a.a

1)2) 工作正常,但 3), 4) 的输出似乎有点出乎意料。

我拼命地试图让这个例子与档案一起使用,所以我做了一些改动:

文件A.H:

void foo();

文件A.C:

#include "a.h"
#include <stdio.h>

void __attribute__((weak)) foo() { printf("%s\n", __FILE__); }

修改后:

1) 打印 A.C 2) 打印 B.C
g++ main.cpp a.a b.ag++ main.cpp b.a a.a

所以它的效果会更好一些。运行节目后,没有违反 ODR。但是,我不知道这是否是属性的正确用法。根据 gcc 文档:nm a.aW _Z3foov

weak 属性导致声明作为弱符号而不是全局符号发出。这主要用于定义可在用户代码中重写的库函数,尽管它也可以与非函数声明一起使用。ELF 目标支持弱符号,使用 GNU 汇编器和链接器时也支持 a.out 目标。

然而,我在函数定义上使用属性,而不是声明。

那么问题来了,为什么 weak 不适用于 *.a 文件?在定义上使用属性而不是声明是否正确?

更新

我突然意识到,与 foo() 方法定义一起使用的属性对符号分辨率没有影响。如果没有该属性,最终二进制文件会生成相同的结果:

1) 打印 A.C 2) 打印 B.C
g++ main.cpp a.a b.ag++ main.cpp b.a a.a

因此,只需使用符号的第一个定义,这与默认的 gcc 行为一致。即使显示发出了弱符号,它似乎也不会影响静态链接。nm a.a

是否可以将属性与静态链接一起使用?

我要解决的问题的描述

我有一个被 >20 个客户端使用的库,我们称之为库 A。我还提供了一个库 B,其中包含 A 的测试实用程序。 不知何故,我需要知道库 A 是在测试模式下使用的,因此最简单的解决方案似乎是在与 B 链接期间替换符号(因为客户端已经与 B 链接)。

我知道这个问题有更干净的解决方案,但是我绝对不能影响客户的代码或他们的构建脚本(添加参数来指示测试 A 或某些 DEFINE 进行编译是没有选择的)。

C G++

评论

4赞 SergeyA 8/2/2018
我嗅到了XY问题。你为什么要这么做?
0赞 AndyB 8/2/2018
我想在与测试框架链接时覆盖原始函数实现。以前我使用全局变量处理过类似的问题,但是,由于全局初始化的顺序未定义,这是一个脆弱的解决方案。
0赞 KamilCuk 8/17/2018
为此,请使用 objcopy。从框架中替换或删除原始函数实现,或使用 objcopy 将原始函数设置为弱函数。您不需要重新编译库,从而影响原始框架的编译过程。前任。objcopy -N foo a.o
0赞 KamilCuk 8/17/2018
你是怎么让它输出的?我无法重现它。如果缺少 foo 符号,链接器应该发出错误,foo 符号不能等于 false,除非您在某处定义 .您是否使用带有 C 符号的 C++ 编译器?如果您使用它,通常会运行一个 C 编译器,该编译器创建一个符号,而不是 C++ 符号。no foovoid (*foo)() = 0;make -c ccfoo_Z3foov
0赞 AndyB 8/23/2018
为了生成输出,我使用了以下命令: 正如 Mike Kinghan 已经提到的,一个弱符号可能未定义,然后假设其值为 0。我一直在使用 g++。在写这篇文章时,我错误地混合了 c 和 cpp 扩展名。这在我的原始项目中不是问题。no foog++ -c a.car cr a.a a.og++ -c b.car cr b.a b.og++ main.cpp a.a b.avoid (*foo)() = 0;

答:

4赞 KamilCuk 8/17/2018 #1

我发现这是最好的解释:

如果链接器在搜索所有输入对象后无法解析该引用,则链接器将仅搜索库以解析该引用。如果需要,将根据库在链接器命令行上的位置从左到右搜索库。库中的对象将按其存档顺序进行搜索。一旦 armlink 找到引用的符号匹配项,搜索就完成了,即使它与弱定义匹配也是如此。
ELF ABI 第 4.6.1.2 节说:“
弱定义不会改变从库中选择目标文件的规则。但是,如果链接集同时包含弱定义和非弱定义,则将始终使用非弱定义。
“链接集”是链接器加载的对象集。它不包括库中不需要的对象。
因此,不建议将两个对象存档到库或单独的库中,其中一个对象包含给定符号的弱定义,另一个对象包含该符号的非弱定义。

请注意以下事项。基本上重命名了 .mv a.c definition.cmv b.c noweak.cmv second_a.c declaration.c

> for i in Makefile *.c; do echo "cat $i <<EOF"; cat $i; echo EOF; done
cat Makefile <<EOF
tgt=
tgt+=only_weak_1.out only_weak_2.out
tgt+=definition.out declaration.out noweak.out
tgt+=definition_static.out declaration_static.out noweak_static.out
tgt+=1.out 2.out 3.out 4.out
tgt+=5.out 6.out 7.out 8.out
tgt+=10.out 11.out 12.out
tgt+=13.out
tgt+=14.out

only_weak_1_obj= definition.o declaration.o
only_weak_2_obj= declaration.o definition.o
definition_obj= definition.o
declaration_obj= declaration.o
noweak_obj= noweak.o
definition_static_obj= definition.a
declaration_static_obj= declaration.a
noweak_static_obj= noweak.a
1_obj= declaration.o noweak.o
2_obj= noweak.o declaration.o
3_obj= declaration.a noweak.a
4_obj= noweak.a declaration.a
5_obj= definition.o noweak.o
6_obj= noweak.o definition.o
7_obj= definition.a noweak.a
8_obj= noweak.a definition.a
10_obj= noweak.a definition.a declaration.a
11_obj= definition.a declaration.a noweak.a
12_obj= declaration.a definition.a noweak.a
13_obj= all.a
14_obj= all.o


.PRECIOUS: % %.o %.c %.a
def: run
.PHONY: run
run: $(tgt)
    { $(foreach a,$^,echo "$($(a:.out=)_obj)#->#$(a)#:#$$(./$(a))";) } | { echo; column -t -s'#' -N 'objects, ,executable, ,output' -o' '; echo; }
.SECONDEXPANSION:
%.out: main.o $$(%_obj) 
    $(CC) -o $@ $^
%.o: %.c
    $(CC) -c -o $@ $^
%.a: %.o
    ar cr $@ $^
all.a: declaration.o definition.o noweak.o
    ar cr $@ $^
all.o: declaration.o definition.o noweak.o
    $(LD) -i -o $@ $^
clean:
    rm -fv *.o *.a *.out
EOF

cat declaration.c <<EOF
#include <stdio.h>
__attribute__((__weak__)) void foo();
void foo() { printf("%s\n", __FILE__); }
EOF
cat definition.c <<EOF
#include <stdio.h>
__attribute__((__weak__)) void foo() { printf("%s\n", __FILE__); }
EOF
cat main.c <<EOF
#include <stdio.h>
void foo();
int main() {
    if (foo) foo(); else printf("no foo\n");
    return 0;
}
EOF
cat noweak.c <<EOF
#include <stdio.h>
void foo() { printf("%s\n", __FILE__); }
EOF

> make
cc -c -o definition.o definition.c
cc -c -o declaration.o declaration.c
cc -c -o main.o main.c
cc -o only_weak_1.out main.o definition.o declaration.o
cc -o only_weak_2.out main.o declaration.o definition.o
cc -o definition.out main.o definition.o
cc -o declaration.out main.o declaration.o
cc -c -o noweak.o noweak.c
cc -o noweak.out main.o noweak.o
ar cr definition.a definition.o
cc -o definition_static.out main.o definition.a
ar cr declaration.a declaration.o
cc -o declaration_static.out main.o declaration.a
ar cr noweak.a noweak.o
cc -o noweak_static.out main.o noweak.a
cc -o 1.out main.o declaration.o noweak.o
cc -o 2.out main.o noweak.o declaration.o
cc -o 3.out main.o declaration.a noweak.a
cc -o 4.out main.o noweak.a declaration.a
cc -o 5.out main.o definition.o noweak.o
cc -o 6.out main.o noweak.o definition.o
cc -o 7.out main.o definition.a noweak.a
cc -o 8.out main.o noweak.a definition.a
cc -o 10.out main.o noweak.a definition.a declaration.a
cc -o 11.out main.o definition.a declaration.a noweak.a
cc -o 12.out main.o declaration.a definition.a noweak.a
ar cr all.a declaration.o definition.o noweak.o
cc -o 13.out main.o all.a
ld -i -o all.o declaration.o definition.o noweak.o
cc -o 14.out main.o all.o
{ echo "definition.o declaration.o#->#only_weak_1.out#:#$(./only_weak_1.out)"; echo "declaration.o definition.o#->#only_weak_2.out#:#$(./only_weak_2.out)"; echo "definition.o#->#definition.out#:#$(./definition.out)"; echo "declaration.o#->#declaration.out#:#$(./declaration.out)"; echo "noweak.o#->#noweak.out#:#$(./noweak.out)"; echo "definition.a#->#definition_static.out#:#$(./definition_static.out)"; echo "declaration.a#->#declaration_static.out#:#$(./declaration_static.out)"; echo "noweak.a#->#noweak_static.out#:#$(./noweak_static.out)"; echo "declaration.o noweak.o#->#1.out#:#$(./1.out)"; echo "noweak.o declaration.o#->#2.out#:#$(./2.out)"; echo "declaration.a noweak.a#->#3.out#:#$(./3.out)"; echo "noweak.a declaration.a#->#4.out#:#$(./4.out)"; echo "definition.o noweak.o#->#5.out#:#$(./5.out)"; echo "noweak.o definition.o#->#6.out#:#$(./6.out)"; echo "definition.a noweak.a#->#7.out#:#$(./7.out)"; echo "noweak.a definition.a#->#8.out#:#$(./8.out)"; echo "noweak.a definition.a declaration.a#->#10.out#:#$(./10.out)"; echo "definition.a declaration.a noweak.a#->#11.out#:#$(./11.out)"; echo "declaration.a definition.a noweak.a#->#12.out#:#$(./12.out)"; echo "all.a#->#13.out#:#$(./13.out)"; echo "all.o#->#14.out#:#$(./14.out)"; } | { echo; column -t -s'#' -N 'objects, ,executable, ,output' -o' '; echo; }

objects                                executable               output
definition.o declaration.o          -> only_weak_1.out        : definition.c
declaration.o definition.o          -> only_weak_2.out        : declaration.c
definition.o                        -> definition.out         : definition.c
declaration.o                       -> declaration.out        : declaration.c
noweak.o                            -> noweak.out             : noweak.c
definition.a                        -> definition_static.out  : definition.c
declaration.a                       -> declaration_static.out : declaration.c
noweak.a                            -> noweak_static.out      : noweak.c
declaration.o noweak.o              -> 1.out                  : noweak.c
noweak.o declaration.o              -> 2.out                  : noweak.c
declaration.a noweak.a              -> 3.out                  : declaration.c
noweak.a declaration.a              -> 4.out                  : noweak.c
definition.o noweak.o               -> 5.out                  : noweak.c
noweak.o definition.o               -> 6.out                  : noweak.c
definition.a noweak.a               -> 7.out                  : definition.c
noweak.a definition.a               -> 8.out                  : noweak.c
noweak.a definition.a declaration.a -> 10.out                 : noweak.c
definition.a declaration.a noweak.a -> 11.out                 : definition.c
declaration.a definition.a noweak.a -> 12.out                 : declaration.c
all.a                               -> 13.out                 : declaration.c
all.o                               -> 14.out                 : noweak.c

如果仅使用弱符号(大小写 only_weak_1 和 only_weak_2),则使用第一个定义。
如果仅是静态库(情况 3、4、7、8、10、11、12、13),则使用第一个定义。
如果仅使用目标文件(情况 1、2、5、6、14),则省略弱符号,仅使用 noweak 中的符号。
从我提供的链接:

有多种方法可以保证 armlink 选择给定符号的非弱版本:
- 不要存档此类对象 - 在存档之前,确保弱符号和非弱符号包含在同一个对象

- 使用部分链接作为替代方法。

评论

1赞 AndyB 8/23/2018
感谢您的详细回复。所以现在很清楚,我不能在静态库中使用弱符号。不幸的是,我似乎没有在不影响客户端构建系统的情况下实现预期的结果。
2赞 KamilCuk 8/23/2018
您可以使用 objcopy 删除符号,然后链接到您的 foo 实现。使用我的代码:将从 noweak.a 中删除不弱的符号,然后从 declaration.a ALWAYS 中使用 foo 符号。objcopy -Nfoo noweak.a; gcc -o main.out noweak.a declaration.a main.c
0赞 Tomas 1/23/2020
谢谢你的热烈回应:-)
0赞 smwikipedia 8/7/2021
感谢您的精彩回答。但是 ARM 链接已断开。
0赞 Dino Saric 12/3/2021
原始响应中的链接已损坏,我找不到新链接。有人设法找到它吗?
80赞 Mike Kinghan 8/22/2018 #2

为了解释这里发生了什么,让我们先谈谈你的原始源文件,用

A.H (1)

void foo() __attribute__((weak));

和:

交流 (1)

#include "a.h"
#include <stdio.h>

void foo() { printf("%s\n", __FILE__); }

示例代码中 和 文件的混合与 问题,并且所有代码都是 C,所以我们会说是 使用以下方法进行所有编译和链接:.c.cppmain.cppmain.cgcc

$ 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();

这是默认值:

  • 当在程序中链接对 的弱引用(即对弱声明的引用)时, 链接器不需要在链接中找到任何位置的定义:它可能保留 定义。如果在程序中链接了强引用, 链接器需要找到 的定义。foofoofoofoofoo

  • 一个联系最多可以包含一个强有力的定义(即一个定义 强烈地宣布了这一点)。否则,将导致多重定义错误。 但它可能包含多个无误的弱定义。foofoofoo

  • 如果链接包含一个或多个弱定义和强定义 定义,则链接器选择强定义并忽略弱定义 的。foo

  • 如果一个链接只包含一个弱定义,而没有强定义 定义,链接器不可避免地使用一个弱定义。foo

  • 如果链接包含多个弱定义和无强定义 definition,则链接器任意选择一个弱定义。foo

接下来,让我们回顾一下在链接中输入对象文件之间的区别 并输入静态库。

静态库只是我们可以提供给的目标文件的存档 从中选择进行链接所需的链接器。ar

当将对象文件输入到链接时,链接器会无条件地链接它 添加到输出文件中。

当静态库输入到链接时,链接器会检查存档 在其中查找任何对象文件,这些文件为未解析的符号引用提供所需的定义 从已链接的输入文件中累积的。如果找到任何此类对象文件 在存档中,它提取它们并将它们链接到输出文件中,就像 如果它们是单独命名的输入文件,并且根本没有提及静态库。

考虑到这些观察结果,请考虑 compile-and-link 命令:

gcc main.c a.o b.o

在幕后,它必须分解为编译步骤和链接 步骤,就像你跑了:gcc

gcc -c main.c     # compile
gcc main.o a.o b.o  # link

所有三个对象文件都无条件地链接到(默认)程序中。 包含一个 的弱定义,正如我们所看到的:./a.outa.ofoo

$ 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.ob.ofoob.o

相比之下,请考虑 compile-and-link 命令:

gcc main.cpp a.a b.a

具体分解为:

gcc -c main.cpp     # compile
gcc main.o a.a b.a  # link                   

在这里,只有无条件链接。这放置了一个未定义的弱引用 要进入联动:main.ofoo

$ nm --undefined main.o
                 w foo
                 U _GLOBAL_OFFSET_TABLE_
                 U puts

这种薄弱的提及不需要定义。所以链接器将 不要尝试在 OR 和 将在程序中将其保留为未定义状态,正如我们所看到的:fooa.ab.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.ab.a

现在让我们转向您通过更改和以下方式发现的不同行为:a.ha.c

A.H (2)

void foo();

交流 (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.ofoo

$ nm --undefined main.o
                 U foo
                 U _GLOBAL_OFFSET_TABLE_

因此,代码(链接时)必须具有 的非 null 地址。进行中: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.ofooa.ob.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

忽略默认库,我们唯一得到的对象文件 链接有:

main.o
(a.a)a.o

而 的定义取自 的档案成员:fooa.oa.a

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

的定义取自:foob.ob.a

b.a(b.o): definition of foo

在第一个链接中,链接器有一个未解析的强引用,当它到达时,它需要对该引用进行定义。所以它 在存档中查找提供定义的对象文件, 并发现.这个定义很弱,但这并不重要。不 已经看到了强有力的定义。 摘自并链接, 因此,对 的提及得到了解决。接下来到达了哪里,哪里 如果链接器仍然需要,则会在 中找到强有力的定义 并寻找它。但它不再需要一个,也没有看。联动:fooa.aa.oa.oa.afoob.afoob.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

你真正的问题...

...出现在您对帖子的评论之一中:

我想在与测试框架链接时覆盖原始函数实现。

你想链接一个程序,输入一些静态库,该库有一些定义符号的成员,你想敲掉 该定义并链接了在其他对象中定义的不同定义 文件。lib1.afile1.ofoofoofile2.o

__attribute__((weak))不适用于该问题。解决方案是更多 初等。您只需确保在输入之前(以及在提供 的 定义的任何其他输入之前)输入到链接。 然后,链接器将解析对 的引用,并不会尝试查找任何其他定义 定义,当它达到 .链接器根本不会消耗。它可能不存在。file2.olib1.afoofoofile2.olib1.alib1.a(file1.o)

如果你放了另一个静态库怎么办?然后输入 before 将完成链接 before 的工作,并解析为 中的定义。file2.olib2.alib2.alib1.alib2.a(file2.o)lib1.afoofile2.o

同样,当然,成员提供的每个定义都将链接在 优先于 中提供的相同符号的定义。如果那不是 你想要,然后不喜欢:链接本身。lib2.alib1.alib2.afile2.o

最后

是否可以将弱属性与静态链接一起使用?

当然。下面是一个第一性原理用例:

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

请注意,的定义通过 和 编译为 ,正如我们所看到的:foofoo.haa.obb.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.obb.ofooaa.obb.ofooaa.o

那么in的定义怎么了呢?地图文件显示:foobb.o

地图文件 (1)

...
...
Discarded input sections
...
...
 .text.foo      0x0000000000000000       0x13 bb.o
...
...

链接器放弃了包含定义的函数部分 在bb.o

让我们反转 和 的链接顺序:aa.obb.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.oaa.ofoobb.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

现在,每个 和 中都有一个弱定义(C++ mangled):fooaa.obb.o

$ 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.obb.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

那么在ELF对象的联动中,属性对符号的应用—— 无论是静态还是动态 - 启用 C++ 模板的 GCC 实现 用于 GNU 链接器。你可以公平地说,它支持现代 C++ 的 GCC 实现。

评论

1赞 Paul Stelian 8/25/2019
赞成,但是我不喜欢关于它支持现代 C++ 的 GCC 实现的最后一句话。它只允许内联函数执行多个定义,并且内联函数存在于 C 中并具有此行为。只有模板(默认情况下是内联的)是特定于 C++ 的。但是,他们通过内联获得这种行为,并且只是通过弱链接间接获得这种行为。
0赞 Peter Cordes 8/3/2023
@PaulStelian:GCC 在 C 中处理函数的方式与在 C++ 中实现函数的方式略有不同。在 C 语言中,您确实需要在一个文件中实例化非内联定义,例如 .请参阅在 C99 中没有“静态”或“外部”的“内联”有用吗?否则,如果函数未在每个调用站点内联,则会出现链接错误。inlinegcc.cextern inline void f();
0赞 Peter Cordes 8/3/2023
(相关:最近更改为多个非暂定定义,因为全局变量是一个错误:如果具有相同名称的全局变量在 C/C++ 文件中声明两次,但不是静态的、外部的或易失性的,如何抛出 GCC 错误?我以为 C 中的函数有这样的东西,但显然我错了,因为人们在 GCC10 之前在没有实例化的情况下遇到链接器错误。gcc-fno-commoninlineextern inline