從靜態鏈結的角度看global static與宣告

2021-06-28 14:24:16 字數 2760 閱讀 7560

就像搭積木,鏈結將不同的軟體模組組裝成乙個完整的可執行的程式。對於聯結器來說,軟體模組的基本單位是目標檔案,來自於某個原始檔。a.c要想引用乙個外部的函式或者變數,只需進行相應的宣告就可以。目標檔案以段(section)為單位將程式組織起來。鏈結大體上可以分為兩步:

1. 同名段合併。典型的合併就是兩個.text段(**段)的合併。

2. 符號重定位。符號是個很寬泛的概念,目標檔案中的符號包括變數名,函式名,段名(如.text, .data),檔名等等。目標檔案利用符號表儲存所有符號,符號表項含有很多內容,包括名稱,值,大小,型別等等。函式符號和變數符號的值就是函式的入口位址或變數的虛擬位址。靜態鏈結最關鍵的,最核心的操作就是重定位符號的位置,然後根據新的符號位置修正**中對符號的引用。

從鏈結的角度來看,所謂全域性變數指那些所有目標檔案可視的變數。譬如,原始檔a.c宣告了乙個全域性變數global_n,那麼利用extern關鍵字,原始檔b.c就可以引用global_n。原始檔b.c對global_n的修改可以被a.c察覺,反之亦然。所謂靜態變數指那些只能在本目標檔案中可視的變數。譬如,原始檔a.c和原始檔b.c都宣告了乙個靜態變數static_m,那麼它們可以引用各自的static_m,互不影響,即使這個靜態變數的名字是一樣的。如果考慮到變數的儲存空間分配,global和static關鍵字的作用就能更加清晰。由global和static關鍵字修飾的變數都會在elf/pe檔案中分配儲存空間。儲存空間分配的細節很囉嗦,因為未初始化的變數會被分配到.bss段,而初始化的變數會被分配到.data段,而且未初始化的變數空間分配問題還牽扯到common段,強弱符號,實在是瑣碎!!!無論如何,只看最後可執行elf/pe的話,由global和static關鍵字修飾的變數都會在硬碟中被分配空間。而兩個.c檔案中同名的static變數,比如static_m,在鏈結過程中會被修飾為不同的符號名,譬如static_m.1和static_m.2。因此,雖然從原始碼上看,兩個.c檔案都引用了static_m,但是從elf/pe上看,它們引用的是不同的符號。總之,global有兩個作用,一是將乙個變數的可(被)視範圍拓展到所有參與鏈結的目標檔案,二是在elf/pe的資料段為該變數分配儲存空間。static有兩個作用,一是將乙個變數的可(被)視範圍限定在本目標檔案內部,二是在...(同global)分配儲存空間。

靜態鏈結也解釋了函式宣告(function declaration)的必要性。如前所述,我們在某乙個原始檔中引用的函式並不一定在本檔案中定義。所謂定義,指的是給出該函式的輸入引數,輸出引數和函式主體。試想,我們在a.c中使用如下語句引用func:

result = func(1,2);

而func()的定義在b.c中,那麼問題來了: 編譯器應該如何傳遞引數1和2呢?大部分高階程式語言採用壓棧方式傳遞引數,那麼為了傳遞1和2,編譯器大體上會將這個語句翻譯成兩個push和乙個call,但是壓棧的運算元卻取決於常量1和2占用的位元組數。大部分情況下,乙個int型別的資料占用4位元組,而long long型別(如果編譯器支援的話)占用8個位元組。所以,當面對上述語句的時候,編譯器無法判斷應該push多少資料到棧裡面。函式宣告的存在是為了告知編譯器乙個函式的引數傳遞細節,這些細節將參與到編譯過程中。在ubuntu 14.04.1下,使用gcc version 4.8.2編譯下面兩個原始檔,得到兩個目標檔案,通過比較這兩個目標檔案,可以看到宣告是如何影響編譯的。彙編中沒有顯式的使用push,而隱式的利用esp完成壓棧操作。

--source1.c-------------------------------------------

int func(long long , long long);

void main()

--source2.c-------------------------------------------

int func(int, int);

void main()

--source1.o中func的呼叫部分---------------------------

9:   c7 44 24 08 02 00 00

movl   $0x2,0x8(%esp)

10:00 

11:c7 44 24 0c 00 00 00

movl   $0x0,0xc(%esp)

18:00 

19:c7 04 24 01 00 00 00

movl   $0x1,(%esp)

20:c7 44 24 04 00 00 00

movl   $0x0,0x4(%esp)

27:00 

28:e8 fc ff ff ff      

call   29

--source2.o中func的呼叫部分---------------------------

9:c7 44 24 04 02 00 00

movl   $0x2,0x4(%esp)

10:00 

11:c7 04 24 01 00 00 00

movl   $0x1,(%esp)

18:e8 fc ff ff ff      

call   19

--我是分界線------------------------------------------

從編譯結果也可以看到,宣告和函式定義的不一致並不會導致編譯的失敗,甚至不會導致鏈結的失敗,只會導致程式執行時的異常。總之,宣告(declaration)真正宣告(delcare)的是介面,是函式(function),也就是386手冊中所謂的過程(procedure)的介面。這也解釋了為什麼宣告可以只指出輸入,輸出引數的型別(int func(int, int);),而沒必要指明形參名(int func(int a, int b);)。

從彙編角度看引用

引用型別到底是什麼?它和指標有什麼關係?它本身占用記憶體空間嗎?帶著這些疑問,我們來進行分析。先看 include include using namespace std void main 通過彙編檢視 如下 9 int x 1 00401048 mov dword ptr ebp 4 1 10 ...

從辯證的角度看產品

從辯證的角度看產品 然而,當我們用我們自身的思維角度去看待一款產品時,往往可能由於對產品接觸的時間太少,或者是使用到功能的不全面,導致我們對一款產品的認識只能達到乙個有限的程度,這往往是不可避免的。同樣的,當我們要去開發一款產品,往往可能由於對產品真正需求的不確定,或者是考慮的不夠周全,導致我們希望...

從彙編的角度看棧

大家都知道,棧區是儲存函式,區域性變數的一塊記憶體區域。那麼讓我們從彙編的角度,來看看函式的執行過程。首先,當我們使用pushl將資料入棧時,棧頂會移動,以容納新增加的值。實際上,我們能不斷將值入棧,棧會在記憶體中保持向下增長,知道存放 或資料的地方。那麼,我們如何知道棧頂位址呢?棧暫存器 esp總...