将弱符号和局部符号链接在一起时,可能的 GCC 链接器错误会导致错误
Possible GCC linker bug causes error when linking weak and local symbols together
我正在创建一个库并使用 objcopy 将符号的可见性从全局更改为局部,以避免导出大量内部符号。如果我在 linking 时使用 --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)
这是重现该问题的两个源文件和 makefile。
stringstream.cpp:
#include <iostream>
#include <sstream>
int main() {
std::stringstream messagebuf;
messagebuf << "Hello world";
std::cout << messagebuf.str();
return 0;
}
library.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 命令,而是使用第二个(注释掉的)命令来仅削弱符号,它就可以工作。但我不想削弱这些符号,我希望它们是本地的,根本不会被 link 访问图书馆的人看到。
对两个目标文件进行 readelf 给出了该符号的预期结果。程序中的弱(全局)和库中的本地。据我所知,这应该 link 正确吗?
library.a:
22: 0000000000000000 18 FUNC LOCAL DEFAULT 6 _ZStorSt13_Ios_OpenmodeS_
stringstream.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_
所以它在两个object文件中都是一个弱函数符号。对于 dynamic 可见
linkage (Vis
= DEFAULT
) 在两个文件中。它在两个文件的输入 linkage 部分 #8 (Ndx
= 8
) 中定义。
请注意:它在两个 object 文件中都有定义,而不只是在一个文件中定义并且可能被引用
在另一个。
那是什么东西?全局内联函数。它的内联定义进入
来自您的 headers 之一的两个 object 文件。 g++
发出弱符号
全局内联函数以防止来自 linker 的多个定义错误:
允许在 linkage 输入中多重定义弱符号(与任意数量的其他
弱定义和最多一个其他强定义)。
让我们看看那些link年龄段:
$ 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
它们是相同的模数位置。这里的一个 notable 点是节名本身,
.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-section。这是一个包含 仅 函数 <function_name>
的代码部分。
所以在你的 stringstream.o
和 library.o
中,函数 _ZStorSt13_Ios_OpenmodeS_
得到一个 function-section 给自己。
这与 _ZStorSt13_Ios_OpenmodeS_
是内联全局函数一致,并且
因此弱定义。假设一个弱符号有多个定义
在link时代。 linker 会选择哪个定义?如果任何定义
是强的,linker 最多允许一个强定义并且必须选择那个。
但如果他们都很虚弱怎么办? - 这就是我们用 _ZStorSt13_Ios_OpenmodeS_
得到的结果。
在那种情况下,linker 可以任意选择其中之一。
无论哪种方式,它都必须丢弃所有被拒绝的符号的弱定义
link年龄。这就是通过将内联全局函数的每个弱定义放在 function-section 中来实现的
它自己的。然后 linker 拒绝的任何竞争定义都可以从中删除
linkage 通过丢弃包含它们的 function-sections,没有抵押品
损害。这就是 g++
发出那些 function-section 的原因。
最后我们来识别函数:
$ 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
现在让我们对您的link年龄问题做一个更简单的样本。
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;
}
以及用于加快实验的 makefile:
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,默认情况下我们将从 link 一个程序 prog
untampered-with object 个文件 main.o
、a.o
、b.o
,依此顺序。
如果我们在 make
命令行上定义 STRIP
,我们将替换
a.o
和 b.o
分别与 object 文件 a_stripped.o
和 b_stripped.o
已被篡改:
objcopy --keep-global-symbol '_Z8keepme_$(*)v' $< $@
其中除 _Z8keepme_{a|b}v
以外的所有符号,(demangled =
keepme_{a|b}
) 被强制为 LOCAL
.
此外,如果我们在命令行上定义B_A
,那么linkage
a[_stripped].o
和 b[_stripped].o
的顺序将颠倒。
注意全局内联函数的定义
foo
分别在 a.cpp
和 b.cpp
中:它们是不同的。这
前者 returns 0xf0a
后者 returns 0xf0b
.
这使得我们根据 C++ 设法构建的任何程序非法
标准:One Definition Rule
规定:
For an inline function ... a definition is required
in every translation unit where it is odr-used.
和:
each definition consists of the same sequence of tokens (typically, appears in the same header file)
标准是这么规定的,编译器当然不能
对不同翻译单元中的定义施加任何约束,
GNU linker 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
成功。多亏了 linker 诊断 --trace-symbol='_Z3foov'
,
我们被告知程序定义了 _Z3foov
(demangled = foo
)
在 a.o
中并在 b.o
中引用它。
所以我们在a.o
和b.o
中输入两个不同的foo
定义
在结果 prog
中,我们只有一个。 a.o
中的定义是 c奥森和
b.o
中的那个被抛弃了。
我们可以通过 运行ning 程序来检查,因为它可以(非法)
向我们展示它调用的 foo
的哪个定义:
$ ./prog
f0a
f0a
是的,keepme_a()
(来自a.o
)一个keepme_b()
(来自b.o
)都是
从 a.o
.
调用 foo
我们还要求 linker 生成地图文件 prog.map
,并且
我们在该地图文件的顶部附近找到:
Discarded input sections
...
.text._Z3foov 0x0000000000000000 0xb b.o
...
linker 通过丢弃去掉了 foo
的 b.o
定义
来自 b.o
.
的 function-section .text._Z3foov
制作B_A=是
这次我们只是颠倒 linka.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
...
这次 function-section .text._Z3foov
是从 a.o
这是如何工作的?
好吧,我们可以看到 GNU linker 如何在多个
全局内联函数的弱定义:它只选择它在
link年龄序列 并丢弃其余部分。通过改变 link 年龄顺序
我们可以得到任意一个定义linked.
但是,如果每个翻译中都必须存在内联定义
调用函数的单元,作为标准要求,linker
能够从任意一个翻译单元中删除内联定义,并且
得到一个 object 文件调用其他文件中的内联定义?
编译器使 linker 能够做到这一点。让我们看看装配
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 50, %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
) 被赋予了 function-section
并分类 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 50, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
然后在_Z8keepme_av
(=keepme_a
)中,foo
通过_Z3foov
,
引用
call _Z3foov
不是通过内联定义的局部标签.LFB0
。你会看到
b.cpp
中的模式相同。就这样
function-section 包含可以从中丢弃的内联定义
a.o
或 b.o
,并且 _Z3foov
解析为
其他一个,keepme_a()
和keepme_b()
都会调用幸存者
通过 _Z3foov
定义 - 如我们所见。
实验成功到此为止。实验失败旁边:
制作 STRIP=是
$ 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
这重现了您的问题。如果我们反转,我们也会有对称的失败
link年龄顺序:
制作 STRIP=是 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
干预
为 linker 创建了一个无法解决的问题,你可以在之后观察到
最后一个 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
,无法满足外部需求
来自其他 object 文件的引用。两个定义都在输入部分 #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
在每种情况下仍然是 function-section .text._Z3foov
linker只能保留其中一个输入.text._Z3foov
function-sections
对于 prog
的 .text
部分的输出,必须丢弃其余部分,以
避免 _Z3foov
的多重定义。所以它勾选了那些的 second-comer
输入部分,无论是 a_stripped.o
还是 b_stripped.o
,都将被丢弃。
假设 b_stripped.o
排在第二位。我们的 objcopy
干预使 _Z3foov
本地
在两个 object 文件中。所以在 keepme_b()
中,对 foo()
的调用现在 只能 通过
本地定义 - 在程序集中标签 .LFB0
之后组装的那个 -
在预定的 b_stripped.o
的 .text._Z3foov
function-section 中
被丢弃。因此无法在程序中解析 b_stripped.o
中对 foo()
的引用:
`_Z3foov' referenced in section `.text' of b_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of b_stripped.o
这就是你的问题的解释。
但是...
...你可能会说: linker 不去检查是不是疏忽了,
在决定丢弃 function-section 之前,如果该部分实际上包含
任何可能与其他函数冲突的全局函数定义?
你可以这么说,但不是很有说服力。 Function-sections 是那些东西
只有编译器在现实世界中创建,它们的创建只有两个原因:-
让 linker 丢弃程序未调用的全局函数,而不
附带损害。
为了让 linker 丢弃被拒绝的全局内联函数的多余定义,
没有附带损害。
所以 linker 在 function-section 的假设下操作是合理的
只存在于包含全局函数的定义n.
编译器永远不会用你设计的场景给 linker 带来麻烦,
因为编译器不会发出 linkage 部分包含
只有局部符号。在我们的 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
已从全局符号 tables1 中消失,并且:
$ echo \[$(readelf -t a.o | grep '.text._Z3foov')\]
[]
$ echo \[$(readelf -t b.o | grep '.text._Z3foov')\]
[]
function-section .text._Z3foov
已从两个 object 文件中消失。 linker 永远不知道这些本地 foo
的存在。
您没有选择 g++
来制作 _ZStorSt13_Ios_OpenmodeS_
( = std::operator|(_Ios_Openmode __a, _Ios_Openmode __b
) 局部符号
在你的标准 C++ 库的实现中没有黑客攻击
ios_base.h
,你当然不会。
但是你试图做的是 破解这个符号的 linkage
来自标准 C++ 库,使其在一次翻译中成为本地
单元在你的程序中,而在另一个程序中是弱全局的,而你 blind-sided
link呃,还有你自己。
所以...
Am I doing the right thing by changing symbols to local in my library?
没有。除非它们是定义 you 控制的符号,
在你的代码中,然后如果你想让它们成为本地的,让它们成为本地的
在源代码中为此目的使用一种语言工具,
让编译器处理 object 代码。
如果您想进一步减少符号膨胀,请参阅 How to remove unused C/C++ symbols with GCC and ld?
安全技术允许 编译器 生成精益 object 文件
linked,and/or 允许 linker 减脂,或者至少
在 linked 二进制文件 post linkage 上操作。
篡改 object 文件在编译器和linker之间
正在篡改你的危险,而且永远不会超过如果它被篡改
随着外部库符号的 link 时代。
[1] _ZN12_GLOBAL__N_13fooEv
(已修复 = (anonymous namespace)::foo()
)
已经出现,但它是局部的(t
)而不是全局的(T
)并且只是
在符号 table 中,因为我们正在使用 -O0
. 进行编译
我正在创建一个库并使用 objcopy 将符号的可见性从全局更改为局部,以避免导出大量内部符号。如果我在 linking 时使用 --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)
这是重现该问题的两个源文件和 makefile。
stringstream.cpp:
#include <iostream>
#include <sstream>
int main() {
std::stringstream messagebuf;
messagebuf << "Hello world";
std::cout << messagebuf.str();
return 0;
}
library.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 命令,而是使用第二个(注释掉的)命令来仅削弱符号,它就可以工作。但我不想削弱这些符号,我希望它们是本地的,根本不会被 link 访问图书馆的人看到。
对两个目标文件进行 readelf 给出了该符号的预期结果。程序中的弱(全局)和库中的本地。据我所知,这应该 link 正确吗?
library.a:
22: 0000000000000000 18 FUNC LOCAL DEFAULT 6 _ZStorSt13_Ios_OpenmodeS_
stringstream.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_
所以它在两个object文件中都是一个弱函数符号。对于 dynamic 可见
linkage (Vis
= DEFAULT
) 在两个文件中。它在两个文件的输入 linkage 部分 #8 (Ndx
= 8
) 中定义。
请注意:它在两个 object 文件中都有定义,而不只是在一个文件中定义并且可能被引用
在另一个。
那是什么东西?全局内联函数。它的内联定义进入
来自您的 headers 之一的两个 object 文件。 g++
发出弱符号
全局内联函数以防止来自 linker 的多个定义错误:
允许在 linkage 输入中多重定义弱符号(与任意数量的其他
弱定义和最多一个其他强定义)。
让我们看看那些link年龄段:
$ 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
它们是相同的模数位置。这里的一个 notable 点是节名本身,
.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-section。这是一个包含 仅 函数 <function_name>
的代码部分。
所以在你的 stringstream.o
和 library.o
中,函数 _ZStorSt13_Ios_OpenmodeS_
得到一个 function-section 给自己。
这与 _ZStorSt13_Ios_OpenmodeS_
是内联全局函数一致,并且
因此弱定义。假设一个弱符号有多个定义
在link时代。 linker 会选择哪个定义?如果任何定义
是强的,linker 最多允许一个强定义并且必须选择那个。
但如果他们都很虚弱怎么办? - 这就是我们用 _ZStorSt13_Ios_OpenmodeS_
得到的结果。
在那种情况下,linker 可以任意选择其中之一。
无论哪种方式,它都必须丢弃所有被拒绝的符号的弱定义
link年龄。这就是通过将内联全局函数的每个弱定义放在 function-section 中来实现的
它自己的。然后 linker 拒绝的任何竞争定义都可以从中删除
linkage 通过丢弃包含它们的 function-sections,没有抵押品
损害。这就是 g++
发出那些 function-section 的原因。
最后我们来识别函数:
$ 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
现在让我们对您的link年龄问题做一个更简单的样本。
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;
}
以及用于加快实验的 makefile:
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,默认情况下我们将从 link 一个程序 prog
untampered-with object 个文件 main.o
、a.o
、b.o
,依此顺序。
如果我们在 make
命令行上定义 STRIP
,我们将替换
a.o
和 b.o
分别与 object 文件 a_stripped.o
和 b_stripped.o
已被篡改:
objcopy --keep-global-symbol '_Z8keepme_$(*)v' $< $@
其中除 _Z8keepme_{a|b}v
以外的所有符号,(demangled =
keepme_{a|b}
) 被强制为 LOCAL
.
此外,如果我们在命令行上定义B_A
,那么linkage
a[_stripped].o
和 b[_stripped].o
的顺序将颠倒。
注意全局内联函数的定义
foo
分别在 a.cpp
和 b.cpp
中:它们是不同的。这
前者 returns 0xf0a
后者 returns 0xf0b
.
这使得我们根据 C++ 设法构建的任何程序非法 标准:One Definition Rule 规定:
For an inline function ... a definition is required in every translation unit where it is odr-used.
和:
each definition consists of the same sequence of tokens (typically, appears in the same header file)
标准是这么规定的,编译器当然不能
对不同翻译单元中的定义施加任何约束,
GNU linker 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
成功。多亏了 linker 诊断 --trace-symbol='_Z3foov'
,
我们被告知程序定义了 _Z3foov
(demangled = foo
)
在 a.o
中并在 b.o
中引用它。
所以我们在a.o
和b.o
中输入两个不同的foo
定义
在结果 prog
中,我们只有一个。 a.o
中的定义是 c奥森和
b.o
中的那个被抛弃了。
我们可以通过 运行ning 程序来检查,因为它可以(非法)
向我们展示它调用的 foo
的哪个定义:
$ ./prog
f0a
f0a
是的,keepme_a()
(来自a.o
)一个keepme_b()
(来自b.o
)都是
从 a.o
.
foo
我们还要求 linker 生成地图文件 prog.map
,并且
我们在该地图文件的顶部附近找到:
Discarded input sections
...
.text._Z3foov 0x0000000000000000 0xb b.o
...
linker 通过丢弃去掉了 foo
的 b.o
定义
来自 b.o
.
.text._Z3foov
制作B_A=是
这次我们只是颠倒 linka.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
...
这次 function-section .text._Z3foov
是从 a.o
这是如何工作的?
好吧,我们可以看到 GNU linker 如何在多个 全局内联函数的弱定义:它只选择它在 link年龄序列 并丢弃其余部分。通过改变 link 年龄顺序 我们可以得到任意一个定义linked.
但是,如果每个翻译中都必须存在内联定义 调用函数的单元,作为标准要求,linker 能够从任意一个翻译单元中删除内联定义,并且 得到一个 object 文件调用其他文件中的内联定义?
编译器使 linker 能够做到这一点。让我们看看装配
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 50, %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
) 被赋予了 function-section
并分类 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 50, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
然后在_Z8keepme_av
(=keepme_a
)中,foo
通过_Z3foov
,
call _Z3foov
不是通过内联定义的局部标签.LFB0
。你会看到
b.cpp
中的模式相同。就这样
function-section 包含可以从中丢弃的内联定义
a.o
或 b.o
,并且 _Z3foov
解析为
其他一个,keepme_a()
和keepme_b()
都会调用幸存者
通过 _Z3foov
定义 - 如我们所见。
实验成功到此为止。实验失败旁边:
制作 STRIP=是
$ 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
这重现了您的问题。如果我们反转,我们也会有对称的失败 link年龄顺序:
制作 STRIP=是 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
干预
为 linker 创建了一个无法解决的问题,你可以在之后观察到
最后一个 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
,无法满足外部需求
来自其他 object 文件的引用。两个定义都在输入部分 #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
在每种情况下仍然是 function-section .text._Z3foov
linker只能保留其中一个输入.text._Z3foov
function-sections
对于 prog
的 .text
部分的输出,必须丢弃其余部分,以
避免 _Z3foov
的多重定义。所以它勾选了那些的 second-comer
输入部分,无论是 a_stripped.o
还是 b_stripped.o
,都将被丢弃。
假设 b_stripped.o
排在第二位。我们的 objcopy
干预使 _Z3foov
本地
在两个 object 文件中。所以在 keepme_b()
中,对 foo()
的调用现在 只能 通过
本地定义 - 在程序集中标签 .LFB0
之后组装的那个 -
在预定的 b_stripped.o
的 .text._Z3foov
function-section 中
被丢弃。因此无法在程序中解析 b_stripped.o
中对 foo()
的引用:
`_Z3foov' referenced in section `.text' of b_stripped.o: defined in discarded section `.text._Z3foov[_Z3foov]' of b_stripped.o
这就是你的问题的解释。
但是...
...你可能会说: linker 不去检查是不是疏忽了, 在决定丢弃 function-section 之前,如果该部分实际上包含 任何可能与其他函数冲突的全局函数定义?
你可以这么说,但不是很有说服力。 Function-sections 是那些东西 只有编译器在现实世界中创建,它们的创建只有两个原因:-
让 linker 丢弃程序未调用的全局函数,而不 附带损害。
为了让 linker 丢弃被拒绝的全局内联函数的多余定义, 没有附带损害。
所以 linker 在 function-section 的假设下操作是合理的 只存在于包含全局函数的定义n.
编译器永远不会用你设计的场景给 linker 带来麻烦,
因为编译器不会发出 linkage 部分包含
只有局部符号。在我们的 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
已从全局符号 tables1 中消失,并且:
$ echo \[$(readelf -t a.o | grep '.text._Z3foov')\]
[]
$ echo \[$(readelf -t b.o | grep '.text._Z3foov')\]
[]
function-section .text._Z3foov
已从两个 object 文件中消失。 linker 永远不知道这些本地 foo
的存在。
您没有选择 g++
来制作 _ZStorSt13_Ios_OpenmodeS_
( = std::operator|(_Ios_Openmode __a, _Ios_Openmode __b
) 局部符号
在你的标准 C++ 库的实现中没有黑客攻击
ios_base.h
,你当然不会。
但是你试图做的是 破解这个符号的 linkage 来自标准 C++ 库,使其在一次翻译中成为本地 单元在你的程序中,而在另一个程序中是弱全局的,而你 blind-sided link呃,还有你自己。
所以...
Am I doing the right thing by changing symbols to local in my library?
没有。除非它们是定义 you 控制的符号, 在你的代码中,然后如果你想让它们成为本地的,让它们成为本地的 在源代码中为此目的使用一种语言工具, 让编译器处理 object 代码。
如果您想进一步减少符号膨胀,请参阅 How to remove unused C/C++ symbols with GCC and ld? 安全技术允许 编译器 生成精益 object 文件 linked,and/or 允许 linker 减脂,或者至少 在 linked 二进制文件 post linkage 上操作。
篡改 object 文件在编译器和linker之间 正在篡改你的危险,而且永远不会超过如果它被篡改 随着外部库符号的 link 时代。
[1]
_ZN12_GLOBAL__N_13fooEv
(已修复 = (anonymous namespace)::foo()
)
已经出现,但它是局部的(t
)而不是全局的(T
)并且只是
在符号 table 中,因为我们正在使用 -O0
. 进行编译