将弱符号和局部符号链接在一起时,可能的 GCC 链接器错误会导致错误
Posted
技术标签:
【中文标题】将弱符号和局部符号链接在一起时,可能的 GCC 链接器错误会导致错误【英文标题】:Possible GCC linker bug causes error when linking weak and local symbols together 【发布时间】:2017-09-20 23:11:57 【问题描述】:我正在创建一个库并使用 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)
这是重现问题的两个源文件和生成文件。
stringstream.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 可以得到该符号的预期结果。程序中的弱(全局)和库中的局部。据我所知,这应该正确链接?
图书馆.a:
22: 0000000000000000 18 FUNC LOCAL DEFAULT 6 _ZStorSt13_Ios_OpenmodeS_
字符串流.o
22: 0000000000000000 18 FUNC WEAK DEFAULT 6 _ZStorSt13_Ios_OpenmodeS_
这是 GCC 的一个错误,当我强制从库中引入一个函数时,它已经丢弃了本地符号?通过在我的库中将符号更改为本地符号,我是否在做正确的事情?
【问题讨论】:
很有趣,使用-u _GLOBAL__sub_I_keepme_lib_function
或交换stringstream.o
的顺序以在归档后使链接在我的ld (2.26.1) 上工作
使用 -fvisibility 会更容易(并且可能更标准)吗? gcc.gnu.org/wiki/Visibility
谢谢 Neil,-fvisibility 看起来很有趣,我必须检查我所针对的所有 GCC 版本是否都支持它
【参考方案1】:
基础
让我们填写我们对违规符号_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
和library.o
通过<iostream>
。
MVCE
现在让我们为您的链接问题制作一个更简单的样本。
a.cpp
inline unsigned foo()
return 0xf0a;
unsigned keepme_a()
return foo();
b.cpp
inline unsigned foo()
return 0xf0b;
unsigned keepme_b()
return foo();
main.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
使用这个makefile,默认情况下我们将链接一个程序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|bv
之外的所有符号,(已拆解=
keepme_a|b
) 已被强制为 LOCAL
。
此外,如果我们在命令行中定义B_A
,那么链接
a[_stripped].o
和 b[_stripped].o
的顺序将被颠倒。
注意全局内联函数的定义
foo
分别在 a.cpp
和 b.cpp
中:它们是不同的。这
前者返回0xf0a
,后者返回0xf0b
。
这使得我们设法根据 C++ 构建的任何程序非法 标准:One Definition Rule 规定:
对于内联函数...需要定义 在使用 odr 的每个翻译单元中。
和:
每个定义都由相同的标记序列组成(通常出现在同一个头文件中)
这是标准规定的,但编译器当然不能
对不同翻译单元中的定义实施任何约束,
GNU 链接器 ld
不受 C++ 标准或任何语言标准的约束。
那么我们来做一些实验吧。
默认构建:make
$ 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
(demangled = 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
定义
来自b.o
的功能部分.text._Z3foov
。
使 B_A=Yes
这次我们只是颠倒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
解析为
other 一个,keepme_a()
和 keepme_b()
都会调用幸存者
通过_Z3foov
定义 - 正如我们所见。
实验成功就这么多。在实验失败旁边:
使 STRIP=Yes
$ 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
这重现了您的问题。如果我们反转我们也有对称失败 联动顺序:
使 STRIP=Yes B_A=Yes
$ 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
function-sections 之一
对于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
s 的存在。
您无法选择让g++
成为_ZStorSt13_Ios_OpenmodeS_
( = std::operator|(_Ios_Openmode __a, _Ios_Openmode __b
) 本地符号
在你的标准 C++ 库的实现中没有黑客攻击
ios_base.h
,你当然不会。
但你试图做的是破解这个符号的链接 来自标准 C++ 库以使其在一次翻译中成为本地 在你的程序中单元,在另一个程序中弱全局,你盲目 链接器,还有你自己。
那么...
在我的库中将符号更改为本地符号是否正确?
没有。除非它们是你控制其定义的符号, 在您的代码中,然后如果您希望它们本地化,请将它们本地化 在源代码中使用一种语言工具, 并让编译器处理目标代码。
如果您想进一步减少符号膨胀,请参阅How to remove unused C/C++ symbols with GCC and ld? 安全技术允许编译器生成精益目标文件 已链接,和/或允许 链接器 减少脂肪,或至少 对链接的二进制文件进行操作,后链接。
篡改目标文件在编译器和链接器之间 被篡改是你的危险,而且最严重的是如果它被篡改 与外部库符号的链接。
[1]
_ZN12_GLOBAL__N_13fooEv
(解构 = (anonymous namespace)::foo()
)
已经出现,但它是本地的 (t
) 不是全局的 (T
) 并且只是
完全在符号表中,因为我们正在使用-O0
进行编译。
【讨论】:
哇,这可能是我见过的最全面的答案。太感谢了!它真的解释了我需要的一切!以上是关于将弱符号和局部符号链接在一起时,可能的 GCC 链接器错误会导致错误的主要内容,如果未能解决你的问题,请参考以下文章