弱符號與強符號概念

 

鏈接過程實質上就是把不同目標文件粘在一起,對不同目標文件中定義或引用的相同名字進行決議resolve和綁定binding。

符號的分類如下:

  • 定義在本目標文件中的全局符號,可以被其它文件引用。
  • 在本目標文件中引用的全局符號,卻沒有定義在本目標文件,這一般叫做外部符號(External Symbol), 也就是我們前所謂符號引用。
  • 段名,這種符號通常由編譯器產生,它的值就是該段的起始地址。
  • 局部符號,這類符號只在當前編譯單元內部可見。局部符號對於鏈接過程沒有作用,鏈接器往往忽略它們。
  • 行號信息,即目標指令與源代碼中代碼行的對應關係,它是可選的。

鏈接關心的是各種全局符號。

readelf -s xxx.o

所有 bind 這一列爲 GLOBAL 的爲 全局符號。

 

 

特殊符號

  • __executable_start   該符號爲程序起始地址,不是入口地址,是程序最開始的地址。
  • __etext, _etext, etext   該符號爲代碼段結束地址
  • _edata edata   該符號爲數據段結束地址
  • _end end   該符號爲程序結束地址

以上地址均指的是載入後的虛擬地址。

 

符號修飾和符號簽名

Name Decoration   Name Mangling

gcc 編譯選項 "-fleading-underscore" 或 "-fno-leading-underscore" 可以打開或者關閉在編譯時 C 語言符號前加上下劃線。

 

C++符號修飾因編譯器不同而區別很大。比如下面一段代碼:

int func(int);

float func(float);

class C {

    int func(int);

    class C2 {

        int func(int);

    };

};

 

namespace N {

    int func(int);

    class C {

        int func(int);

    };

}

 

 

在gcc下編譯,其得到的修飾後的符號名稱爲:

函數簽名                              修飾後符號名

int func(int)                          _Z4funci

float func(float)                    _Z4funcf

int C::func(int)                     _ZN1C4funcEi

int C::C2::func(int)               _ZN1C2C24funcEi

int N::func(int)                     _ZN1N4funcEi

int N::C::func(int)                _ZN1N1C4funcEi

 

binutils 工具集中的c++filt 可以用於解析被修飾過的名稱

如: c++filt _ZN1N1C4funcEi 輸出爲 N::C::func(int)

 

如果是VC編譯上面這段代碼得到的名稱修飾結果爲

函數簽名                              修飾後符號名

int func(int)                          ?func@@YAHH@Z

float func(float)                    ?func@@YAMM@Z

int C::func(int)                     ?func@C@@AAEHH@Z

int C::C2::func(int)              ?func@C2@C@@AAEHH@Z

int N::func(int)                    ?func@N@@YAHH@Z

int N::C::func(int)               ?func@C@N@@AAEHH@Z

 

微軟提供了一個api將修飾後的名稱轉換爲函數簽名,UnDecorateSymbolName().

 

在linux平臺上, extern “C” 的作用就是讓 gcc 編譯 C++文件時,對C++函數或變量不採用C++的方式來進行名稱修飾。

 

 

 

弱符號與強符號

對於C++來說,弱符號通常來源於未初始化的全局變量。而默認情況下,編譯器將函數和初始化了的全局變量作爲強符號。

可以通過gcc的 __attribute__((weak)) 來定義任何一個強符號爲弱符號。

不同的目標文件中不能有同名的強符號,否則不能鏈接在一起。

如果一個符號在某個目標文件中是強符號,在其它文件中都是弱符號,那麼該名稱在鏈接時選擇強符號。

如果一個符號在所有的目標文件中都是弱符號,則選擇佔用空間(字節數)最大的一個。

 

相應的有 弱引用與強引用的概念。

可以將一個外部函數申明爲弱引用,比如下面的做法:

__attribute__((weakref)) void foo();

int main()

{

    if(foo)  foo();

}

 

多個符號定義類型不一致及其處理

不一致有三種情況:

  • 兩個或兩個以上強符號類型不一致;
  • 有一個強符號,其他都是弱符號,出現類型不一致;
  • 兩個或者兩個以上弱符號類型不一致。

第一種情況,在編譯的時候會提示多重定義錯誤,因爲多個同名強符號定義本身就是非法的。

後面兩種情況需要鏈接器(ld)來處理。

編譯器把未初始化的全局變量作爲弱符號處理。比如在某個.o中定義了一個未初始化的全局變量 global_uninit_var。此時用 readelf -s查看該變量會看到:

st_name = "global_uninit_var"

st_value = 4

st_size = 4

st_info = 0x11 STB_GLOBAL STT_OBJECT

st_other = 0

st_shndx = 0xfff2 SHN_COMMON

發現這個變量是一個 SHN_COMMON類型。這裏使用的是一種成爲 Common Block的機制,是一種事先聲明臨時使用空間的機制。

如果在另一個.o文件也定義了相同名字的 global_uninit_var 變量,且未初始化,類型爲佔8個字節的double,則按照common block的鏈接規則,在最終鏈接後的輸出文件中,global_uninit_var的大小會以輸入文件中佔用空間最大的那個爲準。在上面這個例子中,global_uninit_var最終所佔的空間是8個字節。

COMMON類型的鏈接規則是針對符號都是弱符號的情況,如果其中有個符號是強符號,其他都是弱符號,則最終輸出結果中的符號所佔空間與強符號相同。如果鏈接過程中有弱符號大於強符號,那麼ld鏈接器會報如下警告:

ld: warning: alignment 4 of symbol `global' in a.o is smaller than 8 in b.o

 

正是由於未初始化的全局變量(弱符號)其大小在編譯某個目標文件時未可知,所以那時無法爲其在 .bss 節區分配空間。在鏈接過程中,任何一個弱符號的最終大小都可以確定了,所以它可以在最終輸出文件的bss段爲其分配空間。所以,從最終的輸出可執行文件來看,未初始化的全局變量是放在 .bss 節區的。

 

GCC 可以使用 -fno-common 使得我們可以不以COMMON 塊機制處理未初始化的全局變量。這時,該符號就相當於一個強符號。

int global __attribute__((no_common));

 

當然,如果程序員足夠小心,在聲明全局變量時記住在該加 “extern” 關鍵字時加上它,很多的弱符號類型不一致問題可避免。

 

 

 

__END__

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章