編譯與鏈接詳解

原文鏈接:https://blog.csdn.net/gamebot/article/details/78301714

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/Gamebot/article/details/78301714
前文:

我們知道一個.c/.cpp源程序文件要最後變成我們的.exe(windows)或者.out(Linux)可執行文件,要經過我們的編譯和鏈接。瞭解這個過程對程序員來說是最基本的素質(因爲寫了這麼久的代碼都不知道它最後爲啥能執行,那不是搞笑嗎?)。

注:文章中紅色字體是重要關鍵部分,藍色字體是後面我的博客會詳細講解的內容,如果有興趣可以關注,一起學習,並且揪出我的錯誤,甚是感激。

正文:

首先總體分爲了編譯和鏈接兩大步。

   編譯又包含了:預處理,編譯,彙編,生成二進制可重定位文件

           鏈接又包含了:

鏈接第一步:1.合併所有需要鏈接的.obj(.o)文件 的段,並且調整段的偏移和段長度,合併.o文件的符號表。2.符號的解析。3。分配符號的虛擬內存地址。

鏈接第二步:符號的重定位

在linux底下,可以通過下面的命令來獲得,編譯鏈接的每個階段文件。


(1) 預編譯   gcc -E a.c -o a.i  

(2) 編譯     gcc -S a.i-o a.s   

(3) 彙編     gcc -c a.s -o a.o  

(4) 鏈接     gcc -o a.o -o a.out

預處理階段:它爲我們把源代碼中展開了所有以“#”開頭的宏定義,並且刪除了所有的註釋。預處理後生成我們的.i文件

編譯階段:

在這個過程中,源代碼會被放入到一個掃描器中,掃描器會利用一種叫有限狀態機的算法將裏面的代碼劃分成單個記號,這些記號一般就是關鍵字,標識符類等。然後會進行語法分析,語法分析的過程中,會將掃描器中的記號用樹狀結構連接起來。然後進行一些運算符的優先級判斷,以及表達式的錯誤判斷,然後進行語義分析,語義分析就是對樹狀結構的記號加上類型,從而讓編譯器去執行一些隱示類型轉換,以及類型的錯誤判斷。接下來編譯器會將源代碼優化成我們的彙編語言

   彙編階段:通過彙編器將彙編語言轉換成我們的計算機可以執行的二進制可重定位文件。爲何是可重定位?

因爲我們任何一個源文件在進行編譯階段的時候會去產生我們的符號表,符號表中存放的就是我們程序所產生的符號(例如:函數名,變量名等),我們的編譯階段是不會去給我們的符號分配正確的地址,因此當我們查看.obj(.o)文件的符號表信息時就會出現下面這種情況:

我們定義一個main函數:                                                                             同時定義一個add函數:

                             

這時我們去編譯main.c生成我們的.o文件,並且去查看.o文件的符號表:

我們可以發現我們符號的地址都是0x00000000,這些都是錯誤的地址,因此我們的計算機無法通過正確的地址去尋找到它的指令,那麼計算機就無法去執行,這也就是我們.obj(.o)文件沒有鏈接爲何不能執行的根本原因.

在這裏簡單的介紹一下.o(.obj)文件組成:

首先在.o文件組成開頭是一個ELF頭部信息。它的作用是告知該文件的類型,版本號,程序的入口地址等基本信息,並且它還保留section table段的地址(可以使用readelf -h 命令來查看)。

Section table段:裏面保存着.o文件其他段的屬性,偏移量,段長度等信息。

.text段是指令段,可讀可執行

.rodata段是隻讀數據段

.data是已經完成初始化和初始化不爲0的數據段,可讀可寫

.bss未初始化,並且初始化爲0的數據段,可讀可寫

還有很多段信息,在這不一一詳解


鏈接第一步:

掃描所有的輸入的.o文件,去獲得它們各個段的長度,屬性和位置,然後按照段的屬性和段的長度合併,並且去把每個.o文件的符號全部收集起來放入同一個符號表中。在這裏要注意的是我們在鏈接的時候不光是要去鏈接我們用戶自己所寫編譯後的.o文件還有一些庫函數中的(例如:printf.o),同時還會去鏈接我們的glibc的輔助庫函數。

例如crt1.o,crti.o,crtbegin.o,crtend.o,crtn.o等

前面這5個目標文件的作用分別是啓動、初始化、構造、析構和結束,它們通常會被自動鏈接到應用程序中

一般的鏈接順序就是:ld ctr1.o ctri.o crtbegin.o用戶的.o  系統庫函數.o  ctrend.o  crtn.o
crt1.o中包含程序的入口函數_start以及兩個未定義的符號__libc_start_main和main,由_start負責調用 __libc_start_main初始化libc,然後調用我們源代碼中定義的main函數;

另外,由於類似於全局靜態對象這樣的代碼需要在main函 數之前執行,crti.o和crtn.o負責輔助啓動這些代碼。
另外,Gcc中同樣也有crtbegin.o和crtend.o兩個文件,這兩個目標文件 用於配合glibc來實現C++的全局構造和析構。

crt1.o是crt0.o的後續演進版本,crt1.o中會非常重要的.init段和.fini段以及_start函數的入口..init段和.fini段實際上是靠crti.o以及crtn.o來實現的.

 init段是main函數之前的初始化工作代碼, 比如全局變量的構造. 
fini段則負責main函數之後的清理工作.crti.o crtn.o是負責C的初始化,而C++則必須依賴crtbegin.o和crtend.o來幫助實現.

然後是我們的符號解析,符號解析的過程就是一些符號還處於UND狀態,說明我們只是在引用它們,並未找到它們定義的地方,這時鏈接器就會去找它們正確定義的地方,如果沒找到那麼就是鏈接報錯,在找的過程中,可能涉及到強符號和弱符號,一般編譯器會以強符號定義爲準,找到之後我們就要給這些符號分配正確的虛擬地址。

鏈接的第二步:符號的重定位,經歷過鏈接第一步,我們的符號的地址已經被正確的分配,因此我們需要回到我們的.o文件的符號表中,去給這些符號修改成正確的地址,這樣我們的計算機就能夠正確的尋址,從而執行指令,達到我們的可執行文件。


        看似簡單的編譯和鏈接,其實底層也發生了很多事情,虛心學習,其實你懂的只是滄海一粟。只有不斷的學習積累,纔能有所成就。

更多的內容可以閱讀:《程序員的自我修養》:第1,2,3,4,7章
————————————————
版權聲明:本文爲CSDN博主「Gamebot」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/gamebot/article/details/78301714

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