完全理解連接器:四,庫與可執行文件的生成

咱們繼續來看動態連接。python


咱們知道靜態庫在編譯連接期間就被打包copy到了可執行文件,也就是說靜態庫實際上是在編譯期間(Compile time)連接使用的,那麼動態庫又是在何時才連接使用的呢,動態連接能夠在兩種狀況下被連接使用,分別是load-time dynamic linking(加載時動態連接) 以及 run-time dynamic linking(運行時動態連接),接下來咱們分別講解一下。程序員


  • load-time dynamic linking(加載時動態連接)編程

首先可能有的同窗會問,什麼是load-time呢,load_time翻譯過來也就是加載時,那麼什麼又是加載呢?後端

咱們你們都玩過遊戲,當咱們打開遊戲的時候常常會跳出來一句話:「加載中,請稍後。。。」和這裏的加載意思差很少。這裏的加載指的是程序的加載,而所謂程序的加載就是把可執行文件從磁盤搬到內存的過程,由於程序最終都是在內存中被執行的。至於這個過程的詳解內容我會在接下來的文章《加載器與可執行文件》一文中給你們詳細講解。在這裏咱們只須要簡單的把加載理解爲程序從磁盤複製到內存的過程,加載時動態連接就出如今這個過程。微信


當把可執行文件複製到內存後,且在程序開始運行以前,操做系統會查找可執行文件依賴的動態庫信息(主要是動態庫的名字以及存放路徑),找到該動態庫後就將該動態庫從磁盤搬到內存,並進行符號決議(關於符號決議,參考符號決議一節),若是這個過程沒有問題,那麼一切準備工做就緒,程序就能夠開始執行了,若是找不到相應的動態庫或者符號決議失敗,那麼會有相應的錯誤信息報告爲用戶,程序運行失敗。好比Windows下比較常見的啓動錯誤問題,就是由於沒有找到依賴的動態庫。Linux下一樣會有相似信息提示用戶程序啓動失敗。運維



到這裏,同窗們應該對加載時動態連接應該有一個比較清晰的瞭解了。從整體上看,加載時動態連接能夠分爲兩個階段:階段一,將動態庫信息寫入可執行文件;階段二,加載可執行文件時依據動態庫信息進行動態連接。函數


階段一,將動態庫信息寫入可執行文件性能


在編譯連接生成可執行文件時,須要將使用的動態庫加入到連接選項當中,好比在Linux下引用libMath.so,就須要將libMath.so加入到連接選項當中(好比libMath.so放到了/usr/lib下,那麼使用命令 gcc ... -lMath -L/user/lib ... 進行編譯連接),因此使用這種方式生成的可執行文件中保存了依賴的動態庫信息,在Linux可以使用一個簡單的命令ldd來查看。spa


階段二:加載可執行文件時依據動態庫信息進行動態連接操作系統

因爲在階段一輩子成的可執行文件中保存了動態庫信息,當可執行文件加載完成後,就能夠依據此信息進行中動態庫的查找以及符號決議了。


經過這個過程也能夠清楚的看到靜態庫和動態庫的區別,使用動態庫的可執行文件當中僅僅保留相應信息,動態庫的連接過程被推遲到了程序啓動加載時。


爲加深你對加載時動態連接這個過程的理解,咱們用一個類比來結束本小節,沿用前幾節讀書的例子,咱們正在讀的書中引用了《碼農的荒島求生》以及其它著做,那麼加載時動態連接就比如,讀者開始準備讀這本書的時候(尚未真正的讀)就把全部該書當中引用的資料著做都找齊放到一旁準備查看,當咱們真正看到引用其它文獻的地方時就能夠直接在一旁找到該著做啦。在這個類比當中,開始讀書前的準備工做就比如加載時動態連接。


接下來咱們講解第二種動態連接,run-time dynamic linking(運行時動態連接) 。


  • run-time dynamic linking(運行時動態連接)

上一小節中咱們看到若是咱們想使用加載時動態連接,那麼在編譯連接生成可執行文件階段時須要告訴編譯器所依賴的動態庫信息,而run-time dynamic linking 運行時動態連接則不須要在編譯連接時提供動態庫信息,也就是說,在可執行文件被啓動運行以前,可執行文件對所依賴的動態庫信息一無所知,只有當程序運行到須要調用動態庫所提供的代碼時纔會啓動動態連接過程。


咱們在上一節中介紹了load-time,也就是程序加載時,那麼程序加載完成後就開始程序執行了,那麼所謂run-time(運行時)指的就是從程序開始被CPU執行到程序執行完成退出的這段時間。


因此運行時動態連接這種方式對於「動態連接」闡釋的更加淋漓盡致,由於可執行文件在啓動運行以前都不知道須要依賴哪些動態庫,只在運行時根據代碼的須要再進行動態連接。同加載時動態連接相比,運行時動態連接將連接這個過程再次推遲日後推遲,推遲到了程序運行時。


因爲在編譯連接生成可執行文件的過程當中沒有提供所依賴的動態庫信息,所以這項任務就留給了程序員,在代碼當中若是須要使用某個動態庫所提供的函數,咱們可使用特定的API來運行時加載動態庫,在Windows下經過LoadLibrary或者LoadLibraryEx,在Linux下經過使用dlopen、dlsym、dlclose這樣一組函數在運行時連接動態庫。當這些API被調用後,一樣是首先去找這些動態庫,將其從磁盤copy到內存,而後查找程序依賴的函數是否在動態庫中定義。這些過程完成後動態庫中的代碼就能夠被正常使用了。


相對於加載時動態連接,運行時動態連接更加靈活,同時將動態連接過程推遲到運行時能夠加快程序的啓動速度。


爲了和加載時動態連接做比對,咱們繼續使用上一小節當中讀書的例子,加載時動態連接就比如在開始準備讀一本書以前,將該書中全部引用到的資料文獻找齊全,而運行時動態連接則不須要這個過程,運行時動態連接就比如直接拿起一本書開始看,看到有引用的參考文獻時再去找該資料,找到後查看該文獻而後繼續讀咱們的書。從這個例子當中運行時動態連接更像是咱們平時讀書時的樣子。


至此,兩種動態連接的形式咱們就都已經清楚了,接下來咱們看一下動態連接下生成的可執行文件。


動態連接下可執行文件的生成


在靜態連接下,連接器經過將各個目標文件的代碼段和數據段合併拷貝到可執行文件,所以靜態連接下可執行文件當中包含了所依賴的全部代碼和數據,而與之對比的動態連接下可執行文件又是什麼樣的呢?


其實咱們在動態庫這一節中已經瞭解了動態連接下可執行文件的生成,即,在動態連接下,連接器並非將動態庫中的代碼和數據拷貝到可執行文件中,而是將動態庫的必要信息寫入了可執行文件,這樣當可執行文件在加載時就能夠根據此信息進行動態連接了。爲方便理解,咱們將該信息僅僅認爲是動態庫都名字,真實狀況固然要更復雜一點,這裏咱們以Linux下可執行文件即ELF文件爲例(這一系列的文章重點關注最本質的原理思想,因此這裏討論的一樣適合Windows下的可執行文件即exe文件)。


在前幾節中咱們將可執行文件簡單的劃分爲了兩段,數據段和代碼段,在這裏咱們繼續豐富可執行文件中的內容,如圖所示,在動態連接下,可執行文件當中會新增兩段,即dynamic段以及GOT(Global offset table)段,這兩段內容就是是咱們以前所說的必要信息。



dynamic段中保存了可執行文件依賴哪些動態庫,動態連接符號表的位置以及重定位表的位置等信息。關於dynamic以及GOT段的做用限於篇幅就不重點闡述了。若是你對GOT段的具體做用很好奇的話,歡迎關注微信公共帳號,碼農的荒島求生。

當加載可執行文件時,操做系統根據dynamic段中的信息便可找到使用的動態庫,從而完成動態連接。


這裏須要強調一點,在編譯連接過程當中,能夠同時使用動態庫以及靜態庫。這兩種庫的使用並不衝突,那麼在這種狀況下生成的可執行文件中,可執行文件中包含了靜態庫的數據和代碼,以及動態庫的必要信息。


至此,關於靜態庫,靜態連接,動態庫,動態連接就講述到這,那麼接下來的問題就是靜態庫和動態庫都有什麼樣的優缺點。


動態庫vs靜態庫


在計算機的歷史當中,最開始程序只能靜態連接,可是人們很快發現,靜態連接生成的可執行文件存在磁盤空間浪費問題,由於對於每一個程序都須要依賴的libc庫,在靜態連接下每一個可執行文件當中都有一份libc代碼和數據的拷貝,爲解決該問題才提出動態庫。


在前幾節咱們知道,動態連接下可執行文件當中僅僅保留動態庫的必要信息,所以解決了靜態連接下磁盤浪費問題。動態庫的強大之處不只僅於此,咱們知道對於現代計算機系統,好比PC,一般會運行成百上千個程序(進程),且程序只有被加載到內存中才可使用,若是使用靜態連接那麼在內存中就會有成百上千份一樣的libc代碼,這對於寶貴的內存資源一樣是極大的浪費,而使用動態連接,內存中只須要有一份libc代碼,全部的程序(進程)共享這一份代碼,所以極大的節省了內存資源,這也是爲何動態庫又叫共享庫。


動態庫還有另一個強大之處,那就是若是咱們修改了動態庫的代碼,咱們只須要從新編譯動態庫就能夠了而無需從新新編譯咱們本身的程序,由於可執行文件當中僅僅保留了動態庫的必要信息,從新編譯動態庫後這些必要都信息是不會改變的(只要不修改動態庫的名字和動態庫導出的供可執行文件使用的函數),編譯好新的動態庫後只須要簡單的替換原有動態庫,下一次運行程序時就可使用新的動態庫了,所以動態庫的這種特性極大的方便了程序升級和bug修復。咱們平時使用都客戶端程序,好比咱們經常使用QQ,輸入法,播放器,都利用了動態庫的這一優勢,緣由就在於方便升級以bug修復,只須要更新相應的動態庫就能夠了。


動態庫的優勢不止於此,咱們知道動態連接能夠出如今運行時(run-time dynamic link),動態連接的這種特性能夠用於擴展程序能力,那麼如何擴展呢?你確定據說過同樣神器,沒錯,就是插件。你有沒有想過插件是怎麼實現的?實現插件時,咱們只須要實現幾個規定好的幾個函數,咱們的插件就能夠運行了,可這是怎麼作到的呢,答案就在於運行時動態連接,能夠將插件以動態的都方式實現。咱們知道使用運行時動態連接無需在編譯連接期間告訴連接器所使用的動態庫信息,可執行文件對此一無所知,只有當運行時才知道使用什麼動態庫,以及使用了動態庫中哪些函數,可是在編譯連接可執行文件時又怎麼知道插件中定義了哪些函數呢,所以全部的插件實現函數必須都有一個統一的格式,程序在運行時須要加載全部插件(動態庫),而後調用全部插件的入口函數(統一的格式),這樣咱們寫的插件就能夠被執行起來了。


動態庫都強大優點還體如今多語言編程上。咱們知道使用Python能夠快速進行開發,但Python的性能沒法同C/C++相比(由於Python是解釋型語言,至於什麼是解釋型語言我會在後面碼農的荒島求生系列文章當中給你們詳細講解),有沒有辦法能夠兼具Python的快速開發能力以及C/C++的高性能呢,答案是能夠的,咱們能夠將C/C++代碼編譯連接成動態庫,這樣python就能夠直接調用動態庫中的函數了。不但Python,Perl以及Java等均可以經過動態庫的形式調用C/C++代碼。動態庫的使用使得同一個項目不一樣語言混合編程成爲可能,並且動態庫的使用更大限度的實現了代碼複用。


瞭解了動態庫的這麼多優勢,那麼動態庫就沒有缺點嗎,固然是有的。


首先因爲動態庫是程序加載時或運行是才進行連接的,所以同靜態連接相比,使用動態連接的程序在性能上要稍弱於靜態連接,這時由於對於加載時動態連接,這無疑會減慢程序都啓動速度,而對於運行時連接,當首次調用到動態庫的函數時,程序會被暫停,當連接過程結束後才能夠繼續進行。且動態庫中的代碼是地址無關代碼(Position-Idependent Code,PIC),之因此動態庫中的代碼是地址無關代碼是由於動態庫又被成爲共享庫,全部的程序均可以調用動態庫中的代碼,所以在使用動態庫中的代碼時程序要多作一些工做,這裏咱們再也不具體展開講解到底程序多作了哪些工做,對此感興趣當同窗能夠參考CSAPP(深刻理解計算機系統)。這裏咱們說動態連接的程序性能相比靜態連接稍弱,可是這裏的性能損失是微乎其微的,同動態庫能夠帶來的好處相比,咱們能夠徹底忽略這裏的性能損失,同窗們能夠放心的使用動態庫。


動態庫的一個優勢其實也是它的缺點,即動態連接下的可執行文件不能夠被獨立運行(這裏討論的是加載時動態連接,load-time dynamic link),換句話說就是,若是沒有提供所依賴的動態庫或者所提供的動態庫版本和可執行文件所依賴的不兼容,程序是沒法啓動的。動態庫的依賴問題會給程序的安裝部署帶來麻煩,在Linux環境下尤爲嚴重,以筆者曾參與開發維護的一個虛擬桌面系統爲例,咱們在開發過程當中依賴的一些比較有名的第三方庫默認不會隨着安裝包發佈,這就會致使用戶在較低版本Linux中安裝時常常會出現程序沒法啓動的問題,緣由就在於咱們編譯連接使用都動態庫和用戶Linux系統中都動態庫不兼容。解決這個問題的方法一般有兩種,一個是用戶升級系統中都動態庫,另外一個是咱們講須要都第三方庫隨安裝包一塊兒發佈,固然這是在取得許可的狀況下。


在瞭解了動態庫的優缺點後,接下來咱們來看一下靜態庫。


靜態連接是最古老也是最簡單的連接技術。靜態連接都最大優勢就是使用簡單,編譯好的可執行文件是完備的,即靜態連接下的可執行文件不須要依賴任何其它的庫,由於靜態連接下,連接器將全部依賴的代碼和數據都寫入到了最終的可執行文件當中,這就消除了動態連接下的庫依賴問題,沒有了庫都依賴問題就意味着程序都安裝部署都獲得了極大都簡化。請你們不要小看這一點,這對當今那些擁有海量用戶的後端系統來講相當重要,好比相似微信這種量級的系統,其後端會部署在成千上萬臺機器上,這麼多的機器其系統的安裝部署以及升級會給運維帶來極大挑戰,而靜態連接下的可執行文件因爲不依賴任何庫,由於部署很是方便,僅僅用一個新的可執行文件進行覆蓋就能夠了,所以極大的簡化了系統部署以及升級。筆者以前所在的某電商廣告後端系統就徹底使用靜態連接來簡化部署升級。


而靜態庫的缺點相信你們都已經清楚了,那就是靜態連接會致使可執行文件過大,且多個程序靜態連接同一個靜態庫的話會致使磁盤浪費的問題。


到這裏關於靜態庫和動態庫的討論就告一段落了,相信你們對於這兩種連接類型都有了清晰都認知。接下來讓咱們稍做休息,開始連接器的下一個重要功能,重定位。


《完全理解連接器:五,重定位》,歡迎關注微信公衆號,碼農的荒島求生,獲取更多內容。



本文分享自微信公衆號 - 碼農的荒島求生(escape-it)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索