在連接器可操做的元素這一節中咱們提到,連接器能夠操做的最小單元爲目標文件,也就是說咱們見到的不管是靜態庫、動態庫、可執行文件,都是基於目標文件構建出來的。目標文件就比如樂高積木中最小的零部件。python
給定目標文件以及連接選項,連接器能夠生成兩種庫,分別是靜態庫以及動態庫,如圖所示,給定一樣的目標文件,連接器能夠生成兩種不一樣類型的庫,接下來咱們分別介紹。程序員
假設這樣一個應用場景,基礎設計團隊設計了好多實用而且功能強大的工具函數,業務團隊須要用到裏面的各類函數。每次新添加其中一個函數,業務團隊都要去找相應的實現文件並修改連接選項。使用靜態庫就能夠解決這個問題。靜態庫在Windows下是以.lib爲後綴的文件,Linux下是以.a爲後綴的文件。編程
爲解決上述問題,基礎設計團隊能夠提早將工具函數集合打包編譯連接成爲靜態庫提供給業務團隊使用,業務團隊在使用時只要連接該靜態庫就能夠了,每次新使用一個工具函數的時候,只要該函數在此靜態庫中就無需進行任何修改。segmentfault
你能夠簡單的將靜態庫理解爲由一堆目標文件打包而成, 使用者只須要使用其中的函數而無需關注該函數來自哪一個目標文件(找到函數實現所在的目標文件是連接器來完成的,從這裏也能夠看出,不是全部靜態庫中的目標文件都會用到,而是用到哪一個連接器就連接哪一個)。靜態庫極大方便了對其它團隊所寫代碼的使用。後端
靜態庫是連接器經過靜態連接將其和其它目標文件合併生成可執行文件的,以下圖一所示,而靜態庫只不過是將多個目標文件進行了打包,在連接時只取靜態庫中所用到的目標文件,所以,你能夠將靜態連接想象成以下圖2所示的過程。
微信
靜態庫是使用庫的最簡單的方法,若是你想使用別人的代碼,找到這些代碼的靜態庫並簡單的和你的程序連接就能夠了。靜態連接生成的可執行文件在運行時不依賴任何其它代碼,要理解這句話,咱們須要知道靜態連接下,可執行文件是如何生成的。運維
在上一節中咱們知道,能夠將靜態連接簡單的理解爲連接器將使用到的目標文件集合進行拼裝,拼裝以後就生成了可執行文件,同時咱們在目標文件裏有什麼這一節中知道,目標文件分紅了三段,代碼段,數據段,符號表,那麼在靜態連接下可執行文件的生成過程如圖所示:函數
從上圖中咱們能夠看到可執行文件的特色:工具
可執行文件和目標文件沒有什麼本質的不一樣,可執行文件區別於目標文件的地方在於,可執行文件有一個入口函數,這個函數也就是咱們在C語言當中定義的main函數,main函數在執行過程當中會用到全部可執行文件當中的代碼和數據。而這個main函數是被誰調用執行的呢,答案就是操做系統(Operating System),這也是後面文章當中要重點介紹的內容。性能
如今你應該對可執行文件有一個比較形象的認知了吧。你能夠把可執行文件生成的過程想象成裝訂一本書,一本書中一般有好多章節,這些章節是你本身寫的,且一本書不可避免的要引用其它著做。靜態連接這個過程就比如不但要裝訂你本身寫的文章,並且也把你引用的其它人的著做也直接裝訂進了你的書裏,這裏不考慮版權問題 :),這些工做完成後,只須要按一下訂書器,一本書就製做完成啦。
在這個比喻中,你寫的各個章節就比如你寫的代碼,引用的其它人的著做就比如使用其它人的靜態庫,裝訂成一本書就比如可執行文件的生成。
靜態連接是使用庫的最簡單最直觀的形式, 從靜態連接生成可執行文件的過程當中能夠看到,靜態連接會將用到的目標文件直接合併到可執行文件當中,想象一下,若是有這樣的一種靜態庫,幾乎全部的程序都要使用到,也就是說,生成的全部可執行文件當中都有一份如出一轍的代碼和數據,這將是對硬盤和內存的極大浪費,假設一個靜態庫爲2M,那麼500個可執行文件就有1G的數據是重複的。如何解決這個問題呢,答案就是使用動態庫。
在前三小節中咱們瞭解了靜態庫、靜態連接以及使用靜態連接下可執行文件是如何生成的。接下里咱們講解一下動態庫,那麼什麼是動態庫?
動態庫(Dynamic Library),又叫共享庫(Shared Library),動態連接庫等,在Windows下就是咱們常見的大名鼎鼎的DLL文件了,Windows系統下大量使用了動態庫。在Linux下動態庫是以.so爲後綴的文件,同時以lib爲前綴,好比進行數字計算的動態庫Math,編譯連接後產生的動態庫就叫作libMath.so。從名字中咱們知道動態庫也是庫,本質上動態庫一樣包含咱們已經熟悉的代碼段、數據段、符號表。只不過動態庫的使用方式以及使用時間和靜態庫不太同樣。
在前面幾個小節中咱們知道,使用靜態庫時,靜態庫的代碼段和數據段都會直接打包copy到可執行文件當中,使用靜態庫無疑會增大可執行文件的大小,同時若是程序都須要某種類型的靜態庫,好比libc,使用靜態連接的話,每一個可執行文件當中都會有一份一樣的libc代碼和數據的拷貝,如圖所示,動態庫的出現解決了此類問題。
動態庫容許使用該庫的可執行文件僅僅包含對動態庫的引用而無需將該庫拷貝到可執行文件當中。也就是說,同靜態庫進行總體拷貝的方式不一樣,對於動態庫的使用僅僅須要可執行文件當中包含必要的信息便可,爲了方便理解,你能夠將可執行文件當中保存的必要信息僅僅理解爲須要記錄動態庫的名字就能夠了,如圖所示,同靜態庫相比,動態庫的使用減小了可執行文件的大小。
從上面這張圖中能夠看出,動態庫的使用解決了靜態連接當中可執行文件過大的問題。咱們在前幾節中將靜態連接生成可執行文件的過程比做了裝訂一本書,靜態連接將引用的其它人的著做也裝訂到了書裏,而動態連接能夠想象成做者僅僅在引用的地方寫了一句話,好比引用了《碼農的荒島求生》,那麼做者就在引用的地方寫上「此處參考《碼農的荒島求生》」,那麼讀者在讀到這裏的時候會本身去找到碼農的荒島求生這本書並查找相應的內容,其實這個過程就是動態連接的基本思想了。
到這裏咱們就能夠回答以前提到過的問題了,helloworld程序中的printf函數究竟是在哪裏定義的,答案就是該函數是在libc.so當中定義的,Linux下編譯連接生成可執行文件時會默認動態連接libc.so(Windows下也是一樣的道理),使用ldd命令就會發現每一個可執行文件都依賴libc.so。所以雖然你從沒有看到過printf的定義也能夠正確的使用這個函數。
接下來咱們講解一下動態連接
咱們知道靜態庫在編譯連接期間就被打包copy到了可執行文件,也就是說靜態庫實際上是在編譯期間(Compile time)連接使用的,那麼動態庫又是在何時才連接使用的呢,動態連接能夠在兩種狀況下被連接使用,分別是load-time dynamic linking(加載時動態連接) 以及 run-time dynamic linking(運行時動態連接),接下來咱們分別講解一下。
1,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來查看。
階段二:加載可執行文件時依據動態庫信息進行動態連接
因爲在階段一輩子成的可執行文件中保存了動態庫信息,當可執行文件加載完成後,就能夠依據此信息進行中動態庫的查找以及符號決議了。
經過這個過程也能夠清楚的看到靜態庫和動態庫的區別,使用動態庫的可執行文件當中僅僅保留相應信息,動態庫的連接過程被推遲到了程序啓動加載時。
爲加深你對加載時動態連接這個過程的理解,咱們用一個類比來結束本小節,沿用前幾節讀書的例子,咱們正在讀的書中引用了《碼農的荒島求生》以及其它著做,那麼加載時動態連接就比如,讀者開始準備讀這本書的時候(尚未真正的讀)就把全部該書當中引用的資料著做都找齊放到一旁準備查看,當咱們真正看到引用其它文獻的地方時就能夠直接在一旁找到該著做啦。在這個類比當中,開始讀書前的準備工做就比如加載時動態連接。
2, 接下來咱們講解第二種動態連接,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段中的信息便可找到使用的動態庫,從而完成動態連接。
這裏須要強調一點,在編譯連接過程當中,能夠同時使用動態庫以及靜態庫。這兩種庫的使用並不衝突,那麼在這種狀況下生成的可執行文件中,可執行文件中包含了靜態庫的數據和代碼,以及動態庫的必要信息。
至此,關於靜態庫,靜態連接,動態庫,動態連接就講述到這,那麼接下來的問題就是靜態庫和動態庫都有什麼樣的優缺點。
在計算機的歷史當中,最開始程序只能靜態連接,可是人們很快發現,靜態連接生成的可執行文件存在磁盤空間浪費問題,由於對於每一個程序都須要依賴的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系統中都動態庫不兼容。解決這個問題的方法一般有兩種,一個是用戶升級系統中都動態庫,另外一個是咱們講須要都第三方庫隨安裝包一塊兒發佈,固然這是在取得許可的狀況下。
在瞭解了動態庫的優缺點後,接下來咱們來看一下靜態庫。
靜態連接是最古老也是最簡單的連接技術。靜態連接都最大優勢就是使用簡單,編譯好的可執行文件是完備的,即靜態連接下的可執行文件不須要依賴任何其它的庫,由於靜態連接下,連接器將全部依賴的代碼和數據都寫入到了最終的可執行文件當中,這就消除了動態連接下的庫依賴問題,沒有了庫都依賴問題就意味着程序都安裝部署都獲得了極大都簡化。請你們不要小看這一點,這對當今那些擁有海量用戶的後端系統來講相當重要,好比相似微信這種量級的系統,其後端會部署在成千上萬臺機器上,這麼多的機器其系統的安裝部署以及升級會給運維帶來極大挑戰,而靜態連接下的可執行文件因爲不依賴任何庫,由於部署很是方便,僅僅用一個新的可執行文件進行覆蓋就能夠了,所以極大的簡化了系統部署以及升級。筆者以前所在的某電商廣告後端系統就徹底使用靜態連接來簡化部署升級。
而靜態庫的缺點相信你們都已經清楚了,那就是靜態連接會致使可執行文件過大,且多個程序靜態連接同一個靜態庫的話會致使磁盤浪費的問題。
到這裏關於靜態庫和動態庫的討論就告一段落了,相信你們對於這兩種連接類型都有了清晰都認知。接下來讓咱們稍做休息,開始連接器的下一個重要功能,重定位。
接下來的內容我會在如下篇文章當中介紹:
若是你喜歡這一系列的文章,也歡迎關注個人微信公共帳號,碼農的荒島求生,獲取更多內容。
這個系列完整的文章目錄: