1. 程式人生 > >編譯、裝載和庫那些事-《程式設計師的自我修養-連結、裝載和庫》總結(讀後感)

編譯、裝載和庫那些事-《程式設計師的自我修養-連結、裝載和庫》總結(讀後感)

         如果你想了解程式如何編譯、連結,動態庫、靜態庫如何載入以及可執行檔案生成過程,推薦俞甲子老師、石凡老師以及 潘愛民老師編著的《程式設計師的自我修養-連結、裝載和庫》。雖然相關知識並不能直觀的提升程式設計技能,但是對於程式編譯、執行過程中遇到的問題,能夠更快、更深入的定位問題的原因。寫這篇文章,一部分是為了和各位分享知識,一部分是為了網路上做備份。正文內容部分來自書籍,部分來自理解,可能有理解錯誤的地方,希望能夠通過私信或評論的方式給我矯正和建議。

       北橋      作用:高速資料交換  涉及硬體:CPU、記憶體...             南橋      作用:低速裝置資料交換    涉及硬體:鍵盤、磁碟...

       記憶體利用從一開始的分段=> 分頁

寫時複製(COW):兩個任務同時自由的讀取記憶體,其中任何一個試圖修改記憶體資料時,就複製一份記憶體單獨使用

        整合開發環境(以下Linux使用gcc,windows使用VS),將編譯、連結稱為構建,整合開發環境將具體細節隱藏,使得我們更專心程式開發。程式從原始碼變為可執行檔案,其實分為四個步驟:

  • 預編譯/預處理

       處理標頭檔案,巨集定義,條件編譯等

       Linux:gcc -E hello.c -o hello.i     Windows:cl /P hello.c

  • 編譯

       詞法分析,語法分析,語義分析。。。

       詞法分析:解析單個詞彙;語法分析:生成語法樹;語義分析:加入變數等型別到語法樹    產生彙編檔案

      Linux: gcc -S hello.i -o hello.s    Windows:cl /c hello.c 

       注:編譯由c語言生成的hello.i時,用這種方式編譯成功,但如果如果檔案是cpp檔案生成的,用gcc -S hello.i -o hello.s出錯(我也不知道出錯原因~。·~),可以使用gcc -S hello.cpp -o hello.s生成彙編檔案

      Windows下好像無法直接生成彙編檔案,再通過彙編生成機器指令檔案。/c直接生成機器指令檔案obj。不過可以通過cl /Fa hello.c生成彙編檔案asm,也可以通過dumpbin /DISASM hello.obj檢視彙編檔案或匯出為彙編檔案。

  • 彙編

      彙編指令與機器指令的對映操作    產生機器指令檔案

     Linux: gcc -c hello.s -o hello.o   Windows:cl /c hello.c 

  • 連結

     目標檔案連結成可執行檔案exe或out

     Linux:ld xxx.o xxxx.so;Windows:link   xxx.obj xxxx.dll

圖1:COFF格式及變種

COFF格式

     我們經常遇見的.exe、.dll、.lib、obj、so等字尾檔案,其實都是COFF格式的變種。Windows下在COFF格式基礎上擴充套件為PF;Linux下在COFF格式基礎上擴充套件為ELF。而COFF格式內容格式如上圖1所示(只列出了主要的格式)。PE和ELF格式檔案內容與COFF格式類似。

     如何檢視以上檔案格式內容呢?Windows平臺提供dumpbin程式,dumpbin應用程式在VS的VC目錄和cl編譯器在同個目錄下;Linux平臺下提供objdump程式,objdump終端即可使用。dumpbin程式想要在終端直接使用,需要在path環境變數中配置。兩個平臺在終端直接鍵入dumpbin或objdump回車,即可提示選項。

圖2:檢視COFF格式檔案

函式和變數修飾符       

       隨著專案工程程式碼量越來越大,為防止函式重定義的衝突,各編譯器生成目標檔案obj對原始碼中函式和變數進行修飾。各編譯器修飾規則不一樣,導致各編譯器生成的目標檔案obj,沒法混合連結,形成了二進位制級別ABI的不相容。Windows平臺下VS編譯C語言32位程式,新增修飾符下劃線;編譯C語言64位程式,不新增下劃線。Linux平臺下gcc編譯C語言,不新增下劃線。對於C++的修飾方式更加複雜,但修飾後的函式和變數32位程式也會有下劃線,64位程式則沒有。

例子一:oracle的oci庫分為32位和64位,如果你在VS的32位平臺,使用64位的OCI庫,連結提示未定義錯誤。因為VS32位程式編譯後有修飾符下劃線,而64位的oci動態庫的匯入庫,沒有下劃線,連結器不識別,所以未定義錯誤。這就是32位應用程式必須使用oci32位的庫。檢視匯入庫內容和目標檔案內容可以使用檢視COFF格式檔案命令。

圖3:OCI庫與應用程式

例子二:在閱讀原始碼時,經常能夠看見extern "C"{}這種寫法,主要是為了C++程式能夠使用C庫。下面從修飾符的角度描述,在cpp檔案中使用到C庫的A函式,cpp生成的中間檔案obj將採用C++修飾符修飾,而C庫的A函式匯入庫lib中採用C修飾符修飾,導致連結失敗。將使用的函式使用extern寫法包括,將使編譯器知道,中間的函式使用C修飾符修飾,連結器連結可識別,連結成功。同理,也可以通過這種方式實現C++實現的庫被C語言使用。

靜態連結和動態連結

       靜態連結庫和動態連結庫可以理解為中間檔案obj的集合。在單獨編譯檔案時,生成中間檔案,而原始檔中有些內容引用的是其餘檔案,這時編譯器就會將外部引用的內容對應的地址做個標記,標識是外部引用的內容,地址設定位0x000000。

        對於靜態連結,生成可執行檔案,在連結的過程中,連結器起到重定位的作用,即將值為0x000000的地址,根據其餘連結過來的檔案重新設定地址,如果這個連結過程中,內容無法重定位,則連結出錯,未定義。所有的中間檔案,庫檔案等全部被寫入到可執行檔案中,所以靜態連結生成的庫較於動態連結來說大。

       對於動態連結,連結生成exe,生成可執行檔案必須確定引用函式性質(猜測此時用到匯入庫.lib),連結器將引用函式標記為動態連結符號,但此時的可執行檔案並沒有包括依賴的庫檔案,即可執行檔案地址還存在0x000000,未重定位,需要在執行的時候載入依賴的庫dll,即重定位的操作放在了執行時。

      靜態連結較於動態連結的缺點是1)可執行檔案大 2)依賴庫一旦更新,所有用到的專案工程需要重新編譯

Linux共享庫版本和查詢過程

圖4:Linux下共享庫路徑

Windows下動態連結建立

     建立動態庫dll    cl /lDd xxx 建立除錯版本的動態庫

     被外部使用的匯出函式使用__declspec(dllexport) 修飾

     使用外部引用的函式__declspec(dllimport)修飾

     建立動態庫dll時,會產生字尾名exp結尾的檔案,該檔案是連結器建立dll時的臨時檔案(包含匯出表的資訊)

函式呼叫慣例

圖5:常用的函式呼叫方式

程式執行過程

1)執行庫入口函式_start.XXX

2)環境初始化:堆、IO、執行緒、全域性變數構造、環境變數填充argc,argv...

      執行這一步之後,才能在函式體中呼叫malloc,new等

3)main函式體執行

4)清理工作:堆、IO。。。

圖6:程式呼叫路徑

小結

gcc   GCC編譯器                                             cl    msvc編譯器

ld      GNU連結器                                             link msvc連結器

objdump GNU檔案檢視器                                dumpbin PE檔案檢視器