簡體   English   中英

將弱和本地符號鏈接在一起時,可能的GCC鏈接器錯誤會導致錯誤

[英]Possible GCC linker bug causes error when linking weak and local symbols together

我正在創建一個庫並使用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)

以下是重現該問題的兩個源文件和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();
}}

Makefile文件:

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可以獲得此符號的預期結果。 程序中的弱(全局),以及庫中的本地。 據我所知,這應該正確鏈接?

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報告它:

$ 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 )都Vis 它在兩個文件的輸入鏈接部分#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.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)); }

確實它是一個內聯全局函數,並且它的定義通過<iostream>進入你的stringstream.olibrary.o

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;
}

以及加速實驗的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,默認情況下,我們將按順序將程序progmain.o鏈接到目標文件main.oaobo

如果我們在make命令行上定義STRIP ,我們將分別用已經過修改的目標文件a_stripped.ob_stripped.o替換aobo

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

其中_Z8keepme_{a|b}v以外的所有符號(demangled = keepme_{a|b} )都被強制為LOCAL

此外,如果我們在命令行上定義B_A ,那么a[_stripped].ob[_stripped].o的鏈接順序將被顛倒。

請注意a.cppb.cpp全局內聯函數foo的定義:它們是不同的。 前者返回0xf0a ,后者返回0xf0b

這使得我們根據C ++標准管理的任何程序都是非法的: One Definition Rule規定:

對於內聯函數......在每個使用它的翻譯單元中都需要定義。

和:

每個定義由相同的令牌序列組成(通常出現在同一個頭文件中)

這就是標准規定的內容,但編譯器當然不能對不同翻譯單元中的定義強制執行任何約束,而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' ,我們被告知程序在ao定義_Z3foov (demangled = foo )並在bo引用它。

所以我們在aobo輸入兩個不同foo定義,在得到的prog ,我們只有一個。 選擇了ao中的定義,並拋棄了bo中的定義。

我們可以通過運行程序來檢查,因為它可以(非法)向我們顯示它調用的foo定義:

$ ./prog
f0a
f0a

是的, keepme_a() (來自ao )一個keepme_b() (來自bo )都是從ao調用foo

我們還要求鏈接器生成映射文件prog.map ,並且在我們找到的該映射文件的頂部附近:

Discarded input sections

...
 .text._Z3foov  0x0000000000000000        0xb b.o
...

鏈接器通過丟棄來自bo的函數部分.text._Z3foov來擺脫foobo定義。

使B_A =是

這次我們只是顛倒aobo的聯系順序:

$ 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

再次成功。 但這一次, _Z3foovbo得到它的定義,並且只在ao引用。 檢查出:

$ ./prog
f0b
f0b

現在地圖文件包含:

Discarded input sections

...
 .text._Z3foov  0x0000000000000000        0xb a.o
...

功能部分.text._Z3foov是從ao刪除的

這是如何運作的?

好吧,我們可以看到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的程序b.cpp看到相同的模式。 因此,包含該內聯定義的函數段可以從aobo丟棄, _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

這再現了你的問題。 如果我們反轉連接順序,我們也會出現對稱失敗:

制作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干預為鏈接器創建了一個不可解決的問題,因為您可以在最后一個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函數部分中的一個,用於在prog.text部分輸出,並且必須丟棄其余部分,以避免_Z3foov多個定義。 因此,無論是在a_stripped.o還是b_stripped.o ,它都會丟棄這些輸入節的第二個角落。

說它是b_stripped.o ,排在第二位。 我們的objcopy干預使_Z3foov在兩個目標文件中都是本地的 因此,在keepme_b()調用foo()是的標簽后,組裝一個-現在只能由本地定義解決.LFB0在組裝-這是在.text._Z3foov的功能部分b_stripped.o是預定被丟棄。 因此,無法在程序中解析對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中,我們可以選擇在aobo或兩者ao foo成為本地符號而不會落后於編譯器。 我們可以使它成為一個static函數,或者更多的C ++ - 我們可以把它放在一個匿名的命名空間中。 對於最后的實驗,讓我們這樣做:

a.cpp(reprise)

namespace {

inline unsigned foo()
{
    return 0xf0a;
}

}

unsigned keepme_a() {
    return foo();
}

b.cpp(reprise)

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的存在。

你沒有選擇讓g++在你的標准C ++庫的實現std::operator|(_Ios_Openmode __a, _Ios_Openmode __b _ZStorSt13_Ios_OpenmodeS_ (= std::operator|(_Ios_Openmode __a, _Ios_Openmode __b )成為本地符號,而不是黑客攻擊ios_base.h ,當然你不會“T。

但是你要做的就是破解這個符號與標准C ++庫的聯系,使其在程序中的一個翻譯單元中本地化,而在另一個翻譯單元中弱化,並且你盲目地將鏈接器和你自己置於一邊。

所以...

我通過在我的庫中將符號更改為本地來做正確的事嗎?

不是。除非它們是控制的符號,在您的代碼中,然后如果您希望它們是本地的,在源代碼中使用一個語言工具將它們作為本地的符號,並讓編譯器處理對象碼。

如果要進一步最小化符號膨脹,請參閱如何使用GCC和ld刪除未使用的C / C ++符號? 安全技術允許編譯器生成鏈接的精簡對象文件,和/或允許鏈接器削減脂肪,或者至少操作鏈接二進制文件后鏈接。

篡改編譯器和鏈接器之間的目標文件會篡改您的危險,並且永遠不會篡改外部庫符號的鏈接。


[1] _ZN12_GLOBAL__N_13fooEv (demangled = (anonymous namespace)::foo() )已經出現,但它本地( t )不是全局( T ),而且只在符號表中,因為我們正在使用-O0編譯。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM