鏈接器之靜態鏈接與動態鏈接

一、前言

最近在項目中需要用一個公司的CAN卡,然後到官網下載官方提供的庫文件,下載下來的有3個文件.h、.lib、.dll,OK,現在主角出現了,.lib和.dll。

其實一般的庫文件都包含了這三個文件,無奈自己接觸的還是比較少,目前爲止用了三家公司的設備,一家德國、一家日本的公司,還有一家就是CAN卡的這家,前兩家外國大企業都是提供的.h和.lib,並沒有.dll啊,也就是說在二次開發中,只需要將自己編寫的.h、.cpp文件和官方提供的.lib文件一起編譯就行可以運行了,儘管也知道.dll的存在,不就是動態鏈接庫嘛,和.lib一樣,都是別人寫好的實現代碼,但當時還真沒有仔細深究.lib和.dll的不同之處,直到用了這家公司的CAN卡之後,遇到了問題,也就是採用之前的辦法即只引用.lib文件,程序編譯沒有問題,但是卻不能執行,當然這知識一個小問題,就是缺少相應的.dll文件嘛,加進去就可以執行了(爲什麼編譯沒問題,而運行就有問題了?後面有解答)。但作爲一個程序員,就需要有一個毛病,知其然,就要知其所以然(我反正是這樣的,可能有些程序員很反對這個觀點,因爲他們的觀點就是只要會用就行了),然後就各種百度、翻書,就得到了以下的個人見解。

二、鏈接

在《深入理解計算機系統》一書中,P448,鏈接的定義是:將各種代碼和數據部分收集起來並組合爲一個單一文件的過程,這個文件可被加載(或被拷貝)到存儲器並執行。這是定義,那麼實際中呢?鏈接的過程就是將.cpp對應的.obj(Windows下)和.lib(其內部也是一組.obj,即一組可重定位目標文件)合併,生成一個可執行目標文件。這裏首先說明,鏈接分爲靜態鏈接和動態鏈接,靜態鏈接是由靜態鏈接器完成,而動態鏈接是由動態鏈接器完成。

1、靜態鏈接

靜態鏈接分爲符號解析和重定位兩個步驟,其中符號解析主要是將一個模塊內的符號引用與該模塊的符號表中的一個符號定義關聯,這裏的模塊就是單個可重定位目標文件,在Windows裏面是.obj文件,而重定位將多個模塊的符號表中的每一個符號定義以內存地址唯一關聯,經過這兩個步驟,就完成了符號引用到符號定義,再到內存的完整映射,因爲引用一個符號,不就是爲了獲取或改變他在內存中的值嘛。

(1)符號解析

在一個可重定位文件(模塊)中,有三種鏈接器符號:能被其他模塊引用全局符號,引用其他模塊的全局符號,以及本地符號。注意,這裏的本地符號是在當前模塊中所有函數定位外面的全局符號,並且帶有static屬性,並不是函數內部局部變量,鏈接器不關心函數內部的局部變量,這一部分由運行時棧管理,如下:

/*********************m.cpp*****************************/

extern int e;

int a;

static int b;

void fun(int c)

{

int d = c;

return d;

}

上面所示模塊m中,符號a是能被其他模塊引用的全局符號,符號e爲引用其他模塊的全局符號,而符號b則是目標m的本地符號,它只能在模塊m內部引用,其他模塊不可見,另外符號c、d都是模塊m的本地過程變量,由運行時棧管理。

由於鏈接器上下文中有不同的符號類型(上述3種),因此鏈接器的符號解析對象大致分爲兩種情況:引用本地符號和引用全局符號。

a.引用本地符號。這個過程非常簡單,直接對照關聯就行

b.引用全局符號。這個過程就有點複雜了,試想,在兩個模塊中同時兩個名字相同的全局符號,鏈接器該怎麼處理呢?當然是定義一套自己的規則去避開一些麻煩,這些規則是建立在強符號和弱符號定義的基礎之上的,強符號是指函數和已經定義的全局變量,弱符號是指未定義的全局變量,這一套規則就是:如果有多個強符號,就報錯;如果有一個強符號和多個弱符號,就選擇強符號;如果有多個弱符號,就隨機選擇一個弱符號。如下,第一種情況示例會報錯

/fun1.cpp*/

int main()

{

return 0;

}

/fun2.cpp*/

int main()

{

return 0;

}

第二種情況實例不會報錯,但結果與你的預期有差距

/m1.cpp*/

int x=123;

void fun();

int main()

{

fun();

cout<<x<<endl;

return 0;

}

/fun.cpp*/

int x;

void fun()

{

x=234;

}

如果運行程序,則會輸出x=234,因爲在模塊fun.cpp中鏈接的是定義在m模塊中的強符號x。

(2)重定位

只需要理解一點,在鏈接過程中,當所有符號引用與符號定位關聯之後,我們仍然無法知道引用該符號所得到的內容,因爲內容是存儲在內存中的,這就需要重定位來完成,重定位包括重定位節和符號定義、重定位節中的符號引用兩個步驟,完成重定位之後,就知道了該引用所引用的內存值。

(3)與靜態庫鏈接

靜態庫就是一開始提到的.lib文件,它是一個目標模塊的集合,其包含一組目標模塊(可重定位目標文件),當我們將自己的cpp文件與lib庫文件鏈接生成可執行文件時,只是將靜態庫中被應用程序引用的模塊拷貝出來,其符號解析過程還是和上述一樣。比如我們自己編寫了一個m.cpp文件,需要用到別人提供給的.lib中的一些函數,比如fun1.cpp,fun2.cpp,在.lib庫文件中,就應該是有fun1、和fun2模塊對應的可重定位模塊,假設名爲fun1.obj、fun2.obj,而我們自己編寫的m模塊被編譯器和彙編器生成爲m.obj的目標文件,鏈接過程就是合併m.obj、fun1.obj、fun2.obj的過程,即它並不是拷貝整個.lib文件,只是拷貝了fun1和fun2兩個文件,這也是爲了減少存儲器資源消耗的方法,那麼鏈接器是如何實現這一過程的呢?

它通過3個集合:E、U、D來完成。其中集合E表示鏈接器維護的一個可重定位目標文件的集合,這個集合剛開始爲空,鏈接完成之後,鏈接器就合併這個集合中的目標文件,生成可執行目標文件;集合U表示爲解析的符號引用,即在我們編寫的.cpp文件中引用了但尚未定義的符號,剛開始也爲空;集合D表示已定義的符號集合,初始時也爲空。具體過程可參考《深入瞭解計算機系統》(原書第二版)p460內容。

總之,與靜態庫鏈接(即引用靜態庫)需要明白兩個概念:(1)靜態庫就是一組可重定位目標文件集合(在Windows下是.obj文件);(2)鏈接過程中,應用程序只拷貝在自己源程序中引用的目標文件,並不是拷貝整個.lib庫文件。

(4)動態鏈接共享庫

靜態庫有自己的優點,也有缺點,缺點就是需要定期維護更新,然後引用該靜態庫文件的應用程序也要重新與該庫文件鏈接才能正確運行,最主要的問題是與靜態庫鏈接需要直接拷貝庫文件中的目標模塊,儘管是選擇性拷貝,但是對於一個小的程序來說,拷貝一些.lib文件還是會造成一些內存資源浪費。爲了解決這些問題,聰明的人就創造了動態鏈接共享庫,動態庫在Windows中是用.dll擴展名的文件(好,現在就到了解釋前言中提高的問題了),記住,動態庫鏈接是由動態鏈接器完成,但是他也需要靜態鏈接器(爲什麼呢?看後面解釋)。

這裏要說明,繼續前言的敘述,一個動態鏈接庫包含.h、.lib、.dll文件,而真正的可重定位目標模塊是放在.dll中的。咦?剛剛講到靜態鏈接庫的時候,可重定位目標模塊是放在.lib中,這裏在動態庫中既然是放在.dll裏面的,那麼爲什麼還需要一個.lib文件呢?這個.lib文件裏面到底放的是什麼呢?這就是動態鏈接器的設計需要。

動態鏈接器的鏈接過程主要分爲兩步:生成部分鏈接的可執行目標文件、生成完全鏈接的可執行文件,這兩個步驟分別有靜態鏈接器和動態鏈接器完成(這個過程可參考《深入瞭解計算機系統》P468圖7-15)。

這裏生成部分鏈接的可執行目標文件有靜態鏈接器完成,需要使用.lib文件。這裏.lib文件中並不包含任何關於動態庫文件的代碼和數據內容,只是包含一些重定位和符號表信息,這使得運行時可以解析對.dll庫文件中的代碼和數據的引用。所以這就解釋了我在前言中說明的在鏈接了.lib文件之後,程序編譯時沒有問題的。

完成部分鏈接之後,就已經生成了一個可執行目標文件,在Windows中是.exe文件,但是這只是一個部分鏈接的可執行目標文件,它沒有包含任何與庫有關的數據和代碼內容,直接運行當然會報錯,而報錯信息正是缺少相應的.dll文件,這裏,只要把相應的.dll文件與生成的部分鏈接的可執行目標文件放在同一個目錄下,程序就可以運行了,完美。咦?整個動態鏈接的過程還是沒有引用或者拷貝庫文件(.dll)中的代碼和數據內容啊,程序怎麼又能運行了呢?那是因爲你在執行.exe文件的時候,程序加載器動態的加載了.dll動態庫文件,然後與.exe文件一起運行的,這是加載器幫你完成的,所以你看不到咯。


發佈了40 篇原創文章 · 獲贊 127 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章