基础
让我们填写我们对违规符号_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|b}v 之外的所有符号,(已拆解=
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 已从两个目标文件中消失。链接器永远不知道这些本地foos 的存在。
您无法选择让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 进行编译。