将弱符号和局部符号链接在一起时,可能的 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_ 的了解 例如。

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_

所以它在两个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.olibrary.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.olibrary.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.oa.ob.o,依此顺序。

如果我们在 make 命令行上定义 STRIP,我们将替换 a.ob.o 分别与 object 文件 a_stripped.ob_stripped.o 已被篡改:

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

其中除 _Z8keepme_{a|b}v 以外的所有符号,(demangled = keepme_{a|b}) 被强制为 LOCAL.

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

注意全局内联函数的定义 foo 分别在 a.cppb.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.ob.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 通过丢弃去掉了 foob.o 定义 来自 b.o.

的 function-section .text._Z3foov

制作B_A=是

这次我们只是颠倒 linka.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
...

这次 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.ob.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.ob_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._Z3foovfunction-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.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 已从全局符号 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.

进行编译