鏈接過程實質上就是把不同目標文件粘在一起,對不同目標文件中定義或引用的相同名字進行決議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__