可能的 GCC 链接器错误会导致将弱符号和局部符号链接在一起时出错
Possible GCC linker bug causes error when linking weak and local symbols together
我正在创建一个库并使用 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_
在您的 例。
readelf
在library.o
和stringstream.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.o
和library.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.o
、a.o
、b.o
。
如果我们在make
命令行上定义STRIP
,我们将替换 分别a.o
和b.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].o
和b[_stripped].o
的顺序将颠倒。
注意全局内联函数的定义foo
分别a.cpp
和b.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.o
和b.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
...
链接器通过丢弃摆脱了foo
的b.o
定义 功能部分.text._Z3foov
来自b.o
。
使B_A=是
这次我们将颠倒a.o
和b.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
再次成功。但这一次,_Z3foov
从b.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.o
或b.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.o
和b_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.o
中foo()
的引用:
`_Z3foov' referenced in section `.text' of b_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of b_stripped.o
这就是对你问题的解释。
但。。。
。你可能会说:不检查不是链接器的疏忽吗, 在决定放弃函数部分之前,如果该部分实际上包含 任何可能与其他函数冲突的全局函数定义?
你可以这么说,但不是很有说服力。功能部分是 只有编译器在现实世界中创建,并且创建它们的原因只有两个:-
让链接器放弃程序未调用的全局函数,无需 附带损害。
要让链接器丢弃被拒绝的全局内联函数的剩余定义, 没有附带损害。
因此,链接器基于以下假设进行操作是合理的:函数部分 仅存在以包含全局函数的定义。
编译器永远不会用你设计的场景给链接器带来麻烦, 因为编译器不会发出包含以下内容的链接部分 仅本地符号。在我们的MCVE中,我们可以选择将foo
设为本地 符号在a.o
或b.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
进行编译。
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 有符号的int和int-有没有一种方法可以在C++中区分它们
- 将无符号char*转换为std::istream*C++
- 如何在C++中将一个无符号的 int 转换为两个无符号的短裤?
- vscode g++链路故障:体系结构x86_64的未定义符号
- 从矢量<无符号字符>转换为字符* 包括垃圾数据
- 如何理解将半精度指针转换为无符号长指针和相关的内存对齐
- Visual studio代码重构似乎不起作用(例如,重命名符号-f2)
- 使用gcc从静态链接的文件中查找可选符号
- C++中无符号字符溢出
- 使用无符号字符数组有效存储内存
- C++:Application.cpp中抛出了未解析的外部符号(解决方案在问题的末尾,供未来的读者参考)
- VC++本机单元测试,找不到调试符号
- 为什么我必须在C++中添加一个赋值符号来声明一个数组
- 检查TCHAR数组输入是否为带符号整数C++
- 用符号版本替换对函数的所有调用
- 未解析的外部符号_MsiLocateComponentW@12.
- 可能的 GCC 链接器错误会导致将弱符号和局部符号链接在一起时出错
- 在没有优化的程序上使用gdb进行调试,但在当前上下文中仍然没有局部变量的符号
- 使局部符号全局化