ps:本文轉(zhuǎn)載自(確切來說,是關(guān)于其中4篇鏈接器博文的整理)
https://blog.csdn.net/github_37382319/article/details/82749205?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-4.control
什么是鏈接器(Linker)
??首先是鏈接器的本質(zhì),鏈接器本質(zhì)上也是一個程序,本質(zhì)上和我經(jīng)常使用的普通程序沒什么不同。
?? 最后是鏈接器的輸出,鏈接器在將目標(biāo)文件打包處理后,生成或者可執(zhí)行文件或庫等。
??鏈接器的作用有點類似于我們經(jīng)常使用的壓縮軟WinRAR(Linux下是tar),壓縮軟件將一堆文件打包壓縮成一個壓縮文件,而鏈接器和壓縮軟件的區(qū)別在于鏈接器是將多個目標(biāo)文件打包成一個文件而不進(jìn)行壓縮。
鏈接器的工作過程
??首先,鏈接器對給定的目標(biāo)文件或庫的集合進(jìn)行符號決議以確保模塊間的依賴是正確的。
??其次,鏈接器將給定的目標(biāo)文件集合進(jìn)行拼接打包成需要的庫或最終可執(zhí)行文件。
??最后,鏈接器對鏈接好的庫或可執(zhí)行文件進(jìn)行重定位。
符號決議
??符號決議有時候也被叫做符號綁定,名稱決議;決議更傾向于靜態(tài)鏈接,而綁定更傾向于動態(tài)鏈接。在這個過程當(dāng)中,鏈接器需要做的工作就是確保所有目標(biāo)文件中的符號引用都有唯一的定義。
目標(biāo)文件里有什么
- 代碼部分:指的是計算機可以執(zhí)行的機器指令,也就是源文件中定義的所有函數(shù)。
- 數(shù)據(jù)部分:源文件中定義的全局變量。
那為什么局部變量沒有放到目標(biāo)文件的數(shù)據(jù)段當(dāng)中呢?
??這是因為局部變量是函數(shù)私有的,局部變量只能在該函數(shù)內(nèi)部使用,所以函數(shù)私有的局部變量被放在了代碼段中,作為機器指令的操作數(shù)。
??編譯器在遇到外部定義的全局變量或者函數(shù)時只要能在當(dāng)前文件找到其聲明,編譯器就認(rèn)為編譯正確。而尋找使用變量定義的這項任務(wù)就被留給了鏈接器。鏈接器的其中一項任務(wù)就是要確定所使用的變量要有其唯一的定義。但為了讓鏈接器工作的輕松一點編譯器還是多做了一點工作的,這部分工作就是 符號表(Symbol table)。
符號表(Symbol table)
??編譯器在編譯過程中每次遇到一個全局變量或者函數(shù)名都會在符號表中添加一項,最終編譯器會統(tǒng)計一張符號表。
static用法:如果你認(rèn)為一個變量只應(yīng)該被當(dāng)前文件使用而不暴露給外部,那么你就可以使用static關(guān)鍵字修飾一下。
本質(zhì)上整個符號表只是想表達(dá)兩件事:
- 我能提供給其它文件使用的符號
- 我需要其它文件提供給我使用的符號
符號表存放在哪里
靜態(tài)鏈接下可執(zhí)行文件的生成
??可執(zhí)行文件區(qū)別于目標(biāo)文件的地方在于,可執(zhí)行文件有一個入口函數(shù),這個函數(shù)也就是我們在C語言當(dāng)中定義的main函數(shù),main函數(shù)在執(zhí)行過程中會用到所有可執(zhí)行文件當(dāng)中的代碼和數(shù)據(jù)。main函數(shù)被操作系統(tǒng)調(diào)用執(zhí)行。
??你可以把可執(zhí)行文件生成的過程想象成裝訂一本書,一本書中通常有好多章節(jié),這些章節(jié)是你自己寫的,且一本書不可避免的要引用其它著作。靜態(tài)鏈接這個過程就好比不但要裝訂你自己寫的文章,而且也把你引用的其它人的著作也直接裝訂進(jìn)了你的書里。這些工作完成后,只需要按一下訂書器,一本書就制作完成啦。
??在這個比喻中,你寫的各個章節(jié)就好比你寫的代碼,引用的其它人的著作就好比使用其它人的靜態(tài)庫,裝訂成一本書就好比可執(zhí)行文件的生成。
動態(tài)庫
??將靜態(tài)鏈接生成可執(zhí)行文件的過程比作了裝訂一本書,靜態(tài)鏈接將引用的其它人的著作也裝訂到了書里,而動態(tài)鏈接可以想象成作者僅僅在引用的地方寫了一句話,比如引用了“xxx”,那么作者就在引用的地方寫上“此處參考“xxx”,那么讀者在讀到該處就會自行查找相應(yīng)內(nèi)容,其該過程就是動態(tài)鏈接的基本思想了。
??因此我們就可以知道helloworld程序中的printf函數(shù)到底是在哪里定義的,答案就是該函數(shù)是在libc.so當(dāng)中定義的,Linux下編譯鏈接生成可執(zhí)行文件時會默認(rèn)動態(tài)鏈接libc.so(Windows同理),使用ldd命令可查看可執(zhí)行文件的依賴項(libc.so)。因此雖然你從沒有看到過printf的定義也可以正確的使用這個函數(shù)。
動態(tài)鏈接
??動態(tài)鏈接可以在兩種情況下被鏈接使用,分別是load-time dynamic linking(加載時動態(tài)鏈接) 以及 run-time dynamic linking(運行時動態(tài)鏈接)。
??把加載理解為程序從磁盤復(fù)制到內(nèi)存的過程,加載時動態(tài)鏈接就出現(xiàn)在這個過程Windows下比較常見的啟動錯誤問題,就是因為沒有找到依賴的動態(tài)庫,如下圖:
加載時動態(tài)鏈接
- 階段一,將動態(tài)庫信息寫入可執(zhí)行文件。在編譯鏈接生成可執(zhí)行文件時,需要將使用的動態(tài)庫加入到鏈接選項當(dāng)中;
- 階段二,加載可執(zhí)行文件時依據(jù)動態(tài)庫信息進(jìn)行動態(tài)鏈接。
??為加深對加載時動態(tài)鏈接這個過程的理解,類比一下:沿用前幾節(jié)讀書的例子,我們正在讀的書中引用了“xxx”,那么加載時動態(tài)鏈接就好比讀者開始準(zhǔn)備讀這本書的時候(還沒有真正的讀)就把所有該書當(dāng)中引用的資料著作都找齊放到一旁準(zhǔn)備查看。在這個類比當(dāng)中,開始讀書前的準(zhǔn)備工作就好比加載時動態(tài)鏈接。
運行時動態(tài)鏈接
??不需要在編譯鏈接時提供動態(tài)庫信息,也就是說,在可執(zhí)行文件被啟動運行之前,可執(zhí)行文件對所依賴的動態(tài)庫信息一無所知,只有當(dāng)程序運行到需要調(diào)用動態(tài)庫所提供的代碼時才會啟動動態(tài)鏈接過程。
??運行時動態(tài)鏈接就好比直接拿起一本書開始看,看到有引用的參考文獻(xiàn)時再去找該資料。運行時動態(tài)鏈接更像是我們平時讀書時的樣子。
PS:在編譯鏈接過程中,可以同時使用動態(tài)庫以及靜態(tài)庫。這兩種庫的使用并不沖突,那么在這種情況下生成的可執(zhí)行文件中,可執(zhí)行文件中包含了靜態(tài)庫的數(shù)據(jù)和代碼,以及動態(tài)庫的必要信息。
動態(tài)庫vs靜態(tài)庫
動態(tài)庫優(yōu)點:
??方便了程序升級和bug修復(fù)。如果我們修改了動態(tài)庫的代碼,只需重新編譯動態(tài)庫即可,因為可執(zhí)行文件當(dāng)中僅僅保留了動態(tài)庫的必要信息,重新編譯動態(tài)庫后這些必要都信息不會改變(只要不修改動態(tài)庫的名字和動態(tài)庫導(dǎo)出的供可執(zhí)行文件使用的函數(shù)),編譯好新的動態(tài)庫后只需要簡單的替換原有動態(tài)庫,下一次運行程序時就可以使用新的動態(tài)庫了。我們平時使用都客戶端程序,比如:QQ,輸入法,播放器,都利用了動態(tài)庫的這一優(yōu)點,原因就在于方便升級以bug修復(fù),只需要更新相應(yīng)的動態(tài)庫就可以了。
??插件的實現(xiàn)。我們知道動態(tài)鏈接可以出現(xiàn)在運行時(run-time dynamic link),動態(tài)鏈接的這種特性可以用于擴展程序能力,那么如何擴展呢?你肯定聽說過一樣神器,沒錯,就是插件。你有沒有想過插件是怎么實現(xiàn)的?實現(xiàn)插件時,我們只需要實現(xiàn)幾個規(guī)定好的幾個函數(shù),我們的插件就可以運行了,可這是怎么做到的呢,答案就在于運行時動態(tài)鏈接,可以將插件以動態(tài)的都方式實現(xiàn)。我們知道使用運行時動態(tài)鏈接無需在編譯鏈接期間告訴鏈接器所使用的動態(tài)庫信息,可執(zhí)行文件對此一無所知,只有當(dāng)運行時才知道使用什么動態(tài)庫,以及使用了動態(tài)庫中哪些函數(shù),但是在編譯鏈接可執(zhí)行文件時又怎么知道插件中定義了哪些函數(shù)呢,因此所有的插件實現(xiàn)函數(shù)必須都有一個統(tǒng)一的格式,程序在運行時需要加載所有插件(動態(tài)庫),然后調(diào)用所有插件的入口函數(shù)(統(tǒng)一的格式),這樣我們寫的插件就可以被執(zhí)行起來了。
??多語言編程。我們知道使用Python可以快速進(jìn)行開發(fā),但Python的性能無法同C/C++相比(因為Python是解釋型語言),有沒有辦法可以兼具Python的快速開發(fā)能力以及C/C++的高性能呢,答案是可以的,我們可以將C/C++代碼編譯鏈接成動態(tài)庫,這樣python就可以直接調(diào)用動態(tài)庫中的函數(shù)了。不但Python,Perl以及Java等都可以通過動態(tài)庫的形式調(diào)用C/C++代碼。動態(tài)庫的使用使得同一個項目不同語言混合編程成為可能,而且動態(tài)庫的使用更大限度的實現(xiàn)了代碼復(fù)用。
動態(tài)庫缺點:
??動態(tài)庫中的代碼是地址無關(guān)代碼(Position-Idependent Code,PIC,因此在使用動態(tài)庫中的代碼時程序要多做一些工作。
??動態(tài)鏈接下的可執(zhí)行文件不可以被獨立運行(這里討論的是加載時動態(tài)鏈接,load-time dynamic link),換句話說就是,如果沒有提供所依賴的動態(tài)庫或者所提供的動態(tài)庫版本和可執(zhí)行文件所依賴的不兼容,程序是無法啟動的。動態(tài)庫的依賴問題會給程序的安裝部署帶來麻煩。
靜態(tài)庫優(yōu)點:
??靜態(tài)鏈接下的可執(zhí)行文件由于不依賴任何庫,因為部署非常方便,僅僅用一個新的可執(zhí)行文件進(jìn)行覆蓋就可以了,因此極大的簡化了系統(tǒng)部署以及升級。
靜態(tài)庫缺點:
??會導(dǎo)致可執(zhí)行文件過大,且多個程序靜態(tài)鏈接同一個靜態(tài)庫的話會導(dǎo)致磁盤浪費的問題。