可能的 GCC 链接器错误会导致将弱符号和局部符号链接在一起时出错

Possible GCC linker bug causes error when linking weak and local symbols together

本文关键字:符号 局部 符号链接 出错 在一起 GCC 链接 误会 错误      更新时间:2023-10-16

我正在创建一个库并使用 objcopy 将符号的可见性从全局更改为本地,以避免导出大量内部符号。如果我在链接时使用--undefined标志从库中引入未使用的符号,GCC 会给我以下错误:

`_ZStorSt13_Ios_OpenmodeS_' referenced in section `.text' of ./liblibrary.a(library_stripped.o): defined in discarded section `.text._ZStorSt13_Ios_OpenmodeS_[_ZStorSt13_Ios_OpenmodeS_]' of ./liblibrary.a(library_stripped.o)

以下是重现该问题的两个源文件和生成文件。

字符串流.cpp:

#include <iostream>
#include <sstream>
int main() {
std::stringstream messagebuf;
messagebuf << "Hello world";
std::cout << messagebuf.str();
return 0;
}

图书馆.cpp:

#include <iostream>
#include <sstream>
extern "C" {
void keepme_lib_function() {
std::stringstream messagebuf;
messagebuf << "I'm a library function";
std::cout << messagebuf.str();
}}

制作文件:

CC = g++
all: executable
#build a test program that uses stringstream
stringstream.o : stringstream.cpp
$(CC) -g -O0 -o $@ -c $^
#build a library that also uses stringstream
liblibrary.a : library.cpp
$(CC) -g -O0 -o library.o -c $^
#Set all symbols to local that aren't intended to be exported (keep-global-symbol doesn't discard anything, just changes the binding value to local)
objcopy --keep-global-symbol 'keepme_lib_function' library.o library_stripped.o 
#objcopy --wildcard -W '!keepme_*' library.o library_stripped.o 
rm -f $@
ar crs $@ library_stripped.o
#Link the program with the library, and force keepme_lib_function to be kept in, even though it isn't referenced.
executable : clean liblibrary.a stringstream.o
$(CC) -g -o stringstream stringstream.o -L. -Wl,--undefined=keepme_lib_function,-llibrary # -lgcc_eh -lstdc++ #may need to insert these depending on your environment
clean:
rm -f library_stripped.o
rm -f stringstream.o
rm -f library.o
rm -f liblibrary.a
rm -f stringstream

如果我使用第二个(注释掉的)命令来仅削弱符号,而不是第一个 objcopy 命令,它就可以工作。但我不想削弱符号,我希望它们是本地的,对链接到图书馆的人根本不可见。

对两个对象文件执行 readelf 会给出此交易品种的预期结果。程序中弱(全局),库中本地。据我所知,这应该正确链接吗?

图书馆:

22: 0000000000000000    18 FUNC    LOCAL  DEFAULT    6 _ZStorSt13_Ios_OpenmodeS_

字符串流.o

22: 0000000000000000    18 FUNC    WEAK   DEFAULT    6 _ZStorSt13_Ios_OpenmodeS_

这是 GCC 的一个错误,当我强制从库中引入一个函数时,它已经丢弃了本地符号?通过将库中的符号更改为本地符号,我是否在做正确的事情?

基础

让我们_ZStorSt13_Ios_OpenmodeS_在您的 例。

readelflibrary.ostringstream.o中报告相同:

$ readelf -s main.o | grep Bind
Num:    Value          Size Type    Bind   Vis      Ndx Name
$ readelf -s stringstream.o | grep _ZStorSt13_Ios_OpenmodeS_
25: 0000000000000000    18 FUNC    WEAK   DEFAULT    8 _ZStorSt13_Ios_OpenmodeS_
$ readelf -s library.o | grep _ZStorSt13_Ios_OpenmodeS_
25: 0000000000000000    18 FUNC    WEAK   DEFAULT    8 _ZStorSt13_Ios_OpenmodeS_

因此,它在两个对象文件中都是弱函数符号。对于两个文件中的动态链接(Vis=DEFAULT)都是可见的。它在两个文件的输入链接部分 #8 (Ndx=8) 中定义。 请注意:它在两个目标文件中定义,而不仅仅是在一个目标文件中定义并可能引用 在另一个。

那会是什么东西?全局内联函数。它的内联定义进入 其中一个标头中的两个目标文件。g++发出弱符号 全局内联函数,用于防止来自链接器的多个定义错误: 允许在链接输入中乘以弱符号(与任意数量的其他符号) 弱定义,最多还有一个其他强定义)。

让我们看一下这些链接部分:

$ readelf -t stringstream.o
There are 31 section headers, starting at offset 0x130c0:
Section Headers:
[Nr] Name
Type              Address          Offset            Link
Size              EntSize          Info              Align
Flags
...
...
[ 8] .text._ZStorSt13_Ios_OpenmodeS_
PROGBITS               PROGBITS         0000000000000000  00000000000001b7  0
0000000000000012 0000000000000000  0                 1
[0000000000000206]: ALLOC, EXEC, GROUP

和:

$ readelf -t library.o 
There are 31 section headers, starting at offset 0x130d0:
Section Headers:
[Nr] Name
Type              Address          Offset            Link
Size              EntSize          Info              Align
Flags
...
...
[ 8] .text._ZStorSt13_Ios_OpenmodeS_
PROGBITS               PROGBITS         0000000000000000  00000000000001bc  0
0000000000000012 0000000000000000  0                 1
[0000000000000206]: ALLOC, EXEC, GROUP

它们是相同的模位置。这里值得注意的一点是部分名称本身,.text._ZStorSt13_Ios_OpenmodeS_,其形式为:.text.<function_name>, 并表示:text(即程序代码)区域中的函数

我们希望程序代码中有一个函数,但将其与例如您的 其他函数keepme_lib_function,其中

$ readelf -s library.o | grep keepme_lib_function
26: 0000000000000000   246 FUNC    GLOBAL DEFAULT    3 keepme_lib_function

告诉我们在library.o的#3部分。和部分 #3

$ readelf -t library.o
...
...
[ 3] .text
PROGBITS               PROGBITS         0000000000000000  0000000000000050  0
0000000000000154 0000000000000000  0

只是.text部分。不.text.keepme_lib_function.

表单.text.<function_name>的输入部分,如.text._ZStorSt13_Ios_OpenmodeS_, 是一个功能部分。这是一个代码部分,包含函数<function_name>。 所以在你的stringstream.olibrary.o,函数_ZStorSt13_Ios_OpenmodeS_获取自身的函数部分。

这与_ZStorSt13_Ios_OpenmodeS_是一个内联全局函数一致,并且 因此定义较弱。假设一个弱符号有多个定义 在联动中。链接器将选择哪个定义?如果有任何定义 是强的,链接器最多可以允许一个强定义,并且必须选择那个。 但是,如果他们都很弱呢?- 这就是我们在这里得到的_ZStorSt13_Ios_OpenmodeS_. 在这种情况下,链接器可以任意选择其中任何一个

无论哪种方式,它都必须丢弃所有被拒绝的符号弱定义 联动。这就是通过将内联全局函数的每个弱定义放在函数部分中来实现的 它自己的。然后,可以从中删除链接器拒绝的任何竞争定义 通过丢弃包含它们的功能部分来实现链接,没有抵押品 损伤。这就是g++发出这些功能部分的原因。

最后,让我们确定函数:

$ c++filt _ZStorSt13_Ios_OpenmodeS_
std::operator|(std::_Ios_Openmode, std::_Ios_Openmode)

我们可以在/usr/include/c++下侦探此签名,并找到它(对我来说) 在/usr/include/c++/6.3.0/bits/ios_base.h

inline _GLIBCXX_CONSTEXPR _Ios_Openmode
operator|(_Ios_Openmode __a, _Ios_Openmode __b)
{ return _Ios_Openmode(static_cast<int>(__a) | static_cast<int>(__b)); }

它确实是一个内联全局函数,并且它的定义从何而入 您的stringstream.o并通过<iostream>library.o

MVCE

现在让我们为您的链接问题做一个更简单的标本。

答.cpp

inline unsigned foo()
{
return 0xf0a;
}
unsigned keepme_a() {
return foo();
}

乙.cpp

inline unsigned foo()
{
return 0xf0b;
}
unsigned keepme_b() {
return foo();
}

主.cpp

extern unsigned keepme_a();
extern unsigned keepme_b();
#include <iostream>
int main() {
std::cout << std::hex << keepme_a() << std::endl;
std::cout << std::hex << keepme_b() << std::endl;
return 0;
}

以及一个用于加快实验的制作文件:

CXX := g++
CXXFLAGS := -g -O0
LDFLAGS := -g -L. -Wl,--trace-symbol='_Z3foov',-M=prog.map,--cref
ifdef STRIP
A_OBJ := a_stripped.o
B_OBJ := b_stripped.o
else
A_OBJ := a.o
B_OBJ := b.o
endif
ifdef B_A
OBJS := main.o $(B_OBJ) $(A_OBJ)
else
OBJS := main.o $(A_OBJ) $(B_OBJ)
endif

.PHONY: all clean
all: prog
%_stripped.o: %.o
objcopy --keep-global-symbol '_Z8keepme_$(*)v' $< $@
prog : $(OBJS) 
$(CXX) $(LDFLAGS) -o $@ $^
clean:
rm -f *.o *.map prog

使用此生成文件,默认情况下,我们将链接一个程序prog未篡改的对象文件按该顺序main.oa.ob.o

如果我们在make命令行上定义STRIP,我们将替换 分别a.ob.o对象文件a_stripped.o以及被篡改的b_stripped.o

objcopy --keep-global-symbol '_Z8keepme_$(*)v' $< $@

其中除_Z8keepme_{a|b}v以外的所有符号 , (拆解 =keepme_{a|b})被迫LOCAL

此外,如果我们在命令行上定义B_A,那么链接a[_stripped].ob[_stripped].o的顺序将颠倒。

注意全局内联函数的定义foo分别a.cppb.cpp:它们是不同的。这 前者返回0xf0a,后者返回0xf0b

这使得我们设法构建的任何程序都是非法的C++ 标准:一个定义规则 规定:

对于内联函数...定义是必需的 在每个使用 ODR 的翻译单元中。

和:

每个定义由相同的标记序列组成(通常出现在同一个头文件中)

这是标准规定的,但编译器当然不能 对不同翻译单元中的定义强制执行任何约束, GNU 链接器ld不受C++标准或任何语言标准的约束。

那我们来做一些实验吧。

默认版本:使

$ make
g++ -g -O0   -c -o main.o main.cpp
g++ -g -O0   -c -o a.o a.cpp
g++ -g -O0   -c -o b.o b.cpp
g++ -g -L. -Wl,--trace-symbol='_Z3foov' -o prog main.o a.o b.o
a.o: definition of _Z3foov
b.o: reference to _Z3foov

成功。并感谢接头诊断--trace-symbol='_Z3foov', 我们被告知该程序定义了_Z3foov(拆解 =foo) 在a.o中并在b.o中引用它。

因此,我们在a.ob.o中输入了两种不同的foo定义 而在由此产生的prog中,我们只有一个。选择了a.o中的定义,并且b.o的那个被抛弃了。

我们可以通过运行程序进行检查,因为它可以(非法) 向我们展示它所称foo的定义:

$ ./prog
f0a
f0a

是的,keepme_a()(从a.o)一个keepme_b()(从b.o)都是 从a.o打电话给foo

我们还要求链接器生成映射文件prog.map,并且 在该地图文件的顶部附近,我们发现:

Discarded input sections
...
.text._Z3foov  0x0000000000000000        0xb b.o
...

链接器通过丢弃摆脱了foob.o定义 功能部分.text._Z3foov来自b.o

使B_A=是

这次我们将颠倒a.ob.o的链接顺序:

$ make clean
rm -f *.o *.map prog 
$ make B_A=Yes
g++ -g -O0   -c -o main.o main.cpp
g++ -g -O0   -c -o b.o b.cpp
g++ -g -O0   -c -o a.o a.cpp
g++ -g -L. -Wl,--trace-symbol='_Z3foov',-M=prog.map,--cref -o prog main.o b.o a.o
b.o: definition of _Z3foov
a.o: reference to _Z3foov

再次成功。但这一次,_Z3foovb.o并且仅在a.o中引用。看看:

$ ./prog
f0b
f0b

现在地图文件包含:

Discarded input sections
...
.text._Z3foov  0x0000000000000000        0xb a.o
...

功能部分.text._Z3foov这次从a.o中删除

这是怎么回事?

好吧,我们可以看到 GNU 链接器如何在多个之间做出任意选择 全局内联函数的弱定义:它只是选择它在 链接序列并删除其余部分。通过改变联动顺序 我们可以得到一个任意一个要链接的定义。

但是,如果每个翻译中都必须存在内联定义 调用函数的单元,按照标准要求,链接器如何 能够从任意一个翻译单元中删除内联定义,并且 获取一个对象文件,该文件调用在其他某个对象文件中内联的定义?

编译器使链接器能够执行此操作。让我们看一下a.cpp

$ g++ -O0 -S a.cpp && cat a.s 
.file   "a.cpp"
.section    .text._Z3foov,"axG",@progbits,_Z3foov,comdat
.weak   _Z3foov
.type   _Z3foov, @function
_Z3foov:
.LFB0:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
movl    $3850, %eax
popq    %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size   _Z3foov, .-_Z3foov
.text
.globl  _Z8keepme_av
.type   _Z8keepme_av, @function
_Z8keepme_av:
.LFB1:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
call    _Z3foov
popq    %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size   _Z8keepme_av, .-_Z8keepme_av
.ident  "GCC: (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406"
.section    .note.GNU-stack,"",@progbits    

在那里,你可以看到符号_Z3foov( =foo)被赋予了它的函数部分 和分类weak

.section    .text._Z3foov,"axG",@progbits,_Z3foov,comdat
.weak   _Z3foov

该符号立即与内联定义组装在一起 以后:

_Z3foov:
.LFB0:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
movl    $3850, %eax
popq    %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

然后在_Z8keepme_av( =keepme_a) 中,foo通过_Z3foov引用,

call    _Z3foov

不是通过内联定义的本地标签.LFB0。你会看到b.cpp的组装中的模式相同。因此, 包含该内联定义的函数部分可以从中丢弃a.ob.o_Z3foov解析为另一个keepme_a()keepme_b()都会打电话给幸存的人 通过_Z3foov定义 - 正如我们所看到的。

实验成功就这么多。在实验失败旁边:

使条带=是

$ make clean
rm -f *.o *.map prog
$ make STRIP=Yes
g++ -g -O0   -c -o main.o main.cpp
g++ -g -O0   -c -o a.o a.cpp
objcopy --keep-global-symbol '_Z8keepme_av' a.o a_stripped.o
g++ -g -O0   -c -o b.o b.cpp
objcopy --keep-global-symbol '_Z8keepme_bv' b.o b_stripped.o
g++ -g -L. -Wl,--trace-symbol='_Z3foov',-M=prog.map,--cref -o prog main.o a_stripped.o b_stripped.o
`_Z3foov' referenced in section `.text' of b_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of b_stripped.o
collect2: error: ld returned 1 exit status
Makefile:28: recipe for target 'prog' failed
make: *** [prog] Error 1

这重现了您的问题。如果我们逆转 联动顺序:

使条带=是 B_A=是

$ make clean
rm -f *.o *.map prog 
$ make STRIP=Yes B_A=Yes
g++ -g -O0   -c -o main.o main.cpp
g++ -g -O0   -c -o b.o b.cpp
objcopy --keep-global-symbol '_Z8keepme_bv' b.o b_stripped.o
g++ -g -O0   -c -o a.o a.cpp
objcopy --keep-global-symbol '_Z8keepme_av' a.o a_stripped.o
g++ -g -L. -Wl,--trace-symbol='_Z3foov',-M=prog.map,--cref -o prog main.o b_stripped.o a_stripped.o
`_Z3foov' referenced in section `.text' of a_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of a_stripped.o
collect2: error: ld returned 1 exit status
Makefile:28: recipe for target 'prog' failed
make: *** [prog] Error 1

为什么?

正如您现在可能已经看到的那样,这是因为objcopy干预 为链接器创建一个无法解决的问题,正如您可以在之后观察到的那样 最后make

$ readelf -s a_stripped.o | grep _Z3foov
16: 0000000000000000    11 FUNC    LOCAL  DEFAULT    6 _Z3foov
$ readelf -s b_stripped.o | grep _Z3foov
16: 0000000000000000    11 FUNC    LOCAL  DEFAULT    6 _Z3foov

该符号在a_stripped.ob_stripped.o中仍然有定义, 但是定义现在LOCAL,无法满足外部 来自其他对象文件的引用。这两个定义都在输入部分#6

$ readelf -t a_stripped.o
...
...
[ 6] .text._Z3foov
PROGBITS               PROGBITS         0000000000000000  0000000000000053  0
000000000000000b 0000000000000000  0                 1
[0000000000000206]: ALLOC, EXEC, GROUP

$ readelf -t b_stripped.o
...
...
[ 6] .text._Z3foov
PROGBITS               PROGBITS         0000000000000000  0000000000000053  0
000000000000000b 0000000000000000  0                 1
[0000000000000206]: ALLOC, EXEC, GROUP

在每种情况下都保持功能部分.text._Z3foov

链接器只能保留输入.text._Z3foov功能部分之一 对于prog.text部分中的输出,并且必须丢弃其余部分,以 避免_Z3foov的多个定义。所以它勾选了那些的第二个来者 输入部分,无论是a_stripped.o还是b_stripped.o,都要丢弃。

b_stripped.o排在第二位。我们的objcopy干预使_Z3foov本地化在两个目标文件中。因此,在keepme_b(),对foo()的调用现在只能通过 本地定义 - 在程序集中的标签.LFB0后组装的定义 - 位于计划的b_stripped.o.text._Z3foov功能部分中 要丢弃。因此,无法在程序中解析对b_stripped.ofoo()的引用:

`_Z3foov' referenced in section `.text' of b_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of b_stripped.o

这就是对你问题的解释。

但。。。

。你可能会说:不检查不是链接器的疏忽吗, 在决定放弃函数部分之前,如果该部分实际上包含 任何可能与其他函数冲突的全局函数定义?

你可以这么说,但不是很有说服力。功能部分是 只有编译器在现实世界中创建,并且创建它们的原因只有两个:-

  • 让链接器放弃程序未调用的全局函数,无需 附带损害。

  • 要让链接器丢弃被拒绝的全局内联函数的剩余定义, 没有附带损害。

因此,链接器基于以下假设进行操作是合理的:函数部分 仅存在以包含全局函数的定义。

编译器永远不会用你设计的场景给链接器带来麻烦, 因为编译器不会发出包含以下内容的链接部分 仅本地符号。在我们的MCVE中,我们可以选择将foo设为本地 符号在a.ob.o或两者之间,而不落后于编译器的 返回。我们可以让它成为一个static函数,或者更C++地, 我们可以把它放在一个匿名命名空间中。对于最后的实验,让我们做 那:

A.cpp(重播)

namespace {
inline unsigned foo()
{
return 0xf0a;
}
}
unsigned keepme_a() {
return foo();
}

b.cpp (重播)

namespace {
inline unsigned foo()
{
return 0xf0b;
}
}
unsigned keepme_b() {
return foo();
}

构建并运行:

$ make && ./prog
g++ -g -O0   -c -o a.o a.cpp
g++ -g -O0   -c -o b.o b.cpp
g++ -g -L. -Wl,--trace-symbol='_Z3foov',-M=prog.map,--cref -o prog main.o a.o b.o
f0a
f0b

现在自然而然地,keepme_a()keepme_b()各自调用其本地定义foo,以及:

$ nm -s a.o
000000000000000b T _Z8keepme_av
0000000000000000 t _ZN12_GLOBAL__N_13fooEv
$ nm -s b.o
000000000000000b T _Z8keepme_bv
0000000000000000 t _ZN12_GLOBAL__N_13fooEv

_Z3foov从全局符号表1中消失,并且:

$ echo [$(readelf -t a.o | grep '.text._Z3foov')]
[]
$ echo [$(readelf -t b.o | grep '.text._Z3foov')]
[]

两个对象文件中的函数节.text._Z3foov都消失了。链接器永远不会知道这些本地foo的存在。

您没有选择让g++制作_ZStorSt13_Ios_OpenmodeS_( =std::operator|(_Ios_Openmode __a, _Ios_Openmode __b) 局部符号 在标准C++库的实现中,没有黑客攻击ios_base.h,你当然不会。

但是你想做的是破解这个符号的链接 从标准C++库中,通过一次翻译将其本地化 在你的程序中单位,在另一个程序中弱全局,你盲目 链接器,以及您自己。

所以。。。

通过将库中的符号更改为本地符号,我是否在做正确的事情?

不。除非它们是控制其定义的符号, 在你的代码中,然后如果你想把它们变成本地的,就把它们变成本地的 在源代码中使用一种语言工具为目的, 并让编译器处理目标代码。

如果要进一步减少符号膨胀,请参阅如何使用 GCC 和 ld 删除未使用的 C/C++ 符号? 安全技术允许编译器生成精益对象文件 链接,和/或允许链接器减少脂肪,或至少 对链接的二进制文件进行操作,后链接。

篡改编译器和链接器之间的目标文件 篡改是你的危险,从来没有比它的篡改更是如此 与外部库符号的链接。


[1]_ZN12_GLOBAL__N_13fooEv(拆解 =(anonymous namespace)::foo()) 已经出现,但它是本地的(t)而不是全局的(T),并且只是 在符号表中,因为我们使用-O0进行编译。