非常感謝作者的這篇文章解決了我的疑問
弱符號與強符號概念
鏈結過程實質上就是把不同目標檔案粘在一起,對不同目標檔案中定義或引用的相同名字進行決議resolve和繫結binding。
符號的分類如下:
鏈結關心的是各種全域性符號。
readelf -s ***.o
所有 bind 這一列為 global 的為 全域性符號。
特殊符號
以上位址均指的是載入後的虛擬位址。
符號修飾和符號簽名
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 節區的。
(這裡有點問題:
在最終輸出檔案這一步並沒有給bss段分配空間,只是在記錄了bss段所需要的大小,也就是說所謂在可執行檔案中,bss段並不佔據目標檔案的任何空間,如果要說有的話,就是儲存bss段大小的儲存空間)【在c專家程式設計中pag117段一節對此有詳細描述】
gcc 可以使用 -fno-common 使得我們可以不以common 塊機制處理未初始化的全域性變數。這時,該符號就相當於乙個強符號。
int global __attribute__((no_common));
當然,如果程式設計師足夠小心,在宣告全域性變數時記住在該加 「extern」 關鍵字時加上它,很多的弱符號型別不一致問題可避免。
__end__
C 和C關於BSS段的區別
當編譯器遇到如下定義 point global 觀念上point的 trivial construction 和 destruction 都會被產生和呼叫,事實上,這些 trival members 要麼沒被產生,要麼沒被呼叫。在c中,global被視為乙個 臨時性的定義 因為他沒有顯示化的初始化操...
1 4 3重定位 使用C語言清除BSS段
使用c語言清除bss段,我們肯定要獲取bss段的起始位址和結束位址,那麼,c語言要怎麼實現使用lds檔案中的變數abc?答 需要以下兩步,這兩步是重點。在函式中宣告該變數為extern型別 型別不重要,因為我們主要是取值,具體原因後面會說 使用時,要取址,比如,int p abc p的值即為lds中...
C語言中為什麼要清除 bss段
bss段裡的內容 顯示初始化為0或者未顯示初始化的全域性變數 顯示初始化為0或者未顯示初始化的static區域性變數。為什麼要清除.bss段 c語言程式在編譯完成後,初始化為非零的全域性變數存放在.data段,而未初始化或初始化為0的全域性變數存放在.bss段中。在生成的可執行檔案中,只有.data...