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

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_

所以它在两个目标文件中都是一个弱函数符号。 动态可见 两个文件中的链接 (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.&lt;function_name&gt;, 和表示:一个函数在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.&lt;function_name&gt; 形式的输入部分,例如 .text._ZStorSt13_Ios_OpenmodeS_, 是一个函数部分。这是一个包含函数&lt;function_name&gt; 的代码部分。 所以在你的stringstream.olibrary.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.olibrary.o 通过&lt;iostream&gt;

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();

ma​​in.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.oa.ob.o,按此顺序。

如果我们在make 命令行上定义STRIP,我们将替换 a.ob.o 分别与目标文件 a_stripped.o 和被篡改过的b_stripped.o

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

其中除_Z8keepme_a|bv 之外的所有符号,(已拆解= keepme_a|b) 已被强制为 LOCAL

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

注意全局内联函数的定义 foo 分别在 a.cppb.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.ob.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
...

链接器通过丢弃 foob.o 定义 来自b.o的功能部分.text._Z3foov

使 B_A=Yes

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

再次成功。但是这一次,_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.ob.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.ob_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.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 已从全局符号表中消失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 进行编译。

【讨论】:

哇,这可能是我见过的最全面的答案。太感谢了!它真的解释了我需要的一切!

以上是关于将弱符号和局部符号链接在一起时,可能的 GCC 链接器错误会导致错误的主要内容,如果未能解决你的问题,请参考以下文章

链接时未定义引用符号'socket@GLIBC_2.4'

将 SOIL.lib 与 GCC 一起使用 - 添加符号时出错:无法识别文件格式

GCC中的弱符号与强符号

列出未使用的符号

命名空间中的内联函数在 gcc 上的链接期间生成重复的符号

GCC 优化在运行时导致“未定义的符号”