Assembler, Linker & Loader
本文為下面兩本書的讀書筆記。
Assemblers And Loaders.pdf - Free download books
Linkers and Loaders Mirror (wh0rd.org)
一、概述
1、程序”一生“
從用編程語言(C、匯編等)完成程序,到機器執行程序的過程一般是:編譯、鏈接、加載。編譯生成目標文件、和其他目標文件或庫進行鏈接生成可執行文件,最后被加載到內存中執行。
這些過程可以大體分成兩步:
編譯就是“翻譯":輸入一個源文件,輸出一個目標文件。它將編程語言這種能被人類理解的語言,翻譯成目標代碼這種能被機器理解的”語言“。目標代碼是機器碼的一部分,它就是01串,但不能直接在機器上實際運行,因為還不夠完整。
鏈接、加載則是“修繕”目標文件:鏈接輸入目標文件(可能包含庫),輸出可執行文件(把輸入的目標文件修改打包);加載輸入可執行文件,輸出被加載的程序。本質上鏈接和加載都在對目標文件進行修改、補充使其能夠運行。

2、再說編譯
關於編譯器的開發邏輯,一般是“迭代增量開發”,從這個角度可以看到程序語言的發展歷史。
機器能執行的“語言”是機器碼,所以直接書寫機器碼可以在機器上運行。
機器碼太過具體,限制了人類編寫更加復雜的例程,因為涉及的細節太過龐大。這時需要一個抽象一些的語言,既能夠容易地翻譯成機器碼,又具備抽象性隱藏部分細節,它就是匯編語言。使用匯編語言來編程意味着需要能夠翻譯自身的翻譯器,而這個翻譯器只能使用機器碼完成(現在咱只有機器碼)。通過這個翻譯器,匯編語言程序能夠經過翻譯得到機器碼,在機器上加載運行。
匯編語言雖然具備一定抽象性,但隨着操作系統等復雜軟件系統的實現需求,用這種低級語言仍然很困難,因此出現了更加抽象的高級語言,如Fortran語言、C語言等。同樣的,使用高級語言編程需要能夠翻譯自己的翻譯器,同理,使用低級語言實現這樣的翻譯器。這個翻譯器可以將高級語言程序轉換成低級語言程序 ,再轉換成機器碼分兩步走(第二步已經實現),也可以一步到位直接生成機器碼。
總結來看,使用存在翻譯器的程序語言來實現新語言的翻譯器,使得新語言程序能夠得以翻譯運行。特別的,機器碼不需要翻譯器,所以理論上可以用機器碼實現一切,當然只是理論上。
二、編譯
編譯包含五個階段:詞法分析、語法分析、語義分析、中間代碼優化、目標代碼生成。其中,前三者稱為前端,中間代碼優化稱為中端,目標代碼生成稱為后端。前端的過程和算法十分成熟,大同小異;中端的中間代碼優化涉及多種算法,中間代碼結構也可分層,充滿技巧;后端的目標代碼生成和運行機器息息相關,因為目標代碼就是機器碼的一部分。
階段 | input | output |
---|---|---|
詞法分析 | 高級語言編寫的源程序 | 單詞序列 |
語法分析 | 單詞序列 | AST/語法序列 |
語義分析 | AST | 中間代碼 |
中間代碼優化 | 中間代碼 | 中間代碼 |
目標代碼生成 | 中間代碼 | 目標代碼 |
三、匯編
1、匯編語言
匯編語言程序一般包含:指令(instr)、偽指令(directive)、宏(macro)。指令在匯編后翻譯成機器碼,最終會執行的。偽指令則是用來指導匯編過程、鏈接過程、加載過程進行的。宏則是以符號定義指令序列,並且可以帶參數。
1)指令
匯編指令一般包含4個域(field):符號label,操作符mnemonic(operation),操作數oprand,注釋comment。
loop: add $1, $8, $9 #loop is the label, add is the mnemonic, 1/8/9 is the oprand, now is the comment
integer: .word 4 #.word is mnemonic, the line is a directive
j loop #j is the mnemonic, loop is the operand, no label here
2)偽指令
偽指令用來指示匯編過程、加載過程,種類繁多,用途廣泛。詳細偽指令可參考Assemblers And Loaders中DIRECTIVE一章。
3)宏
宏是用符號來代替字符串,一般包括宏定義和宏擴展,且支持嵌套定義和嵌套擴展(通過棧,擴展中遇到新的擴展則擴展新的后繼續原擴展)。
宏最關鍵的性質是支持參數,可以像函數一樣傳遞參數,宏定義時的形式參數(parameter),宏擴展時的實際參數(arguement)。
宏的定義放在MDT中(macro definition table),一般通過pass 0(第0遍掃描)來收集所有的宏定義(嵌套宏定義只收集最外層名字/定義,內部當作字符串存儲在MDT中),並處理所有的宏擴展(擴展過程中若有新的定義則加入MDT后繼續原擴展)。實現算法可參考Assemblers And Loaders中MACRO一章。
2、符號
匯編語言能夠被人類所理解,主要原因是包含符號。
計算機執行一般程序主要操作的就是地址和數據,因此機器碼的含義包含操作和操作數(數值、地址)。數值和地址處理直接出現在匯編語言中,更靈活的通過符號出現。比如定義一個符號(比如指令的label域),其余指令可以引用該符號來使用它代表的數值或地址。
符號映射到高級語言就是變量、過程、函數。
符號的定義可以通過指令,可以通過偽指令。
LC #location counter
0 loop: add $1, $8, $9 #the value of loop is 0
4 integer: .word 4 #the value of the integer is 4
8 j loop
上面的LC是位置計數器,用來計算每個指令的相對主LC位置的地址。
LC可以有多個,每個LC獨立計數,比如text的LC和data的LC分別對應目標文件的text節,data節。LC的切換通過在匯編文件中USE偽指令。
3、符號解析(resolve)和重定位(relocation)
解析符號:匯編語言中的符號是機器不能理解的,因此通過匯編將符號引用翻譯成符號被定義的地址和數值。符號表用來存儲符號的定義。符號表項一般包含符號名name,符號值value,符號是否定義type。
重定位:並非所有符號都在匯編過程被解析(符號定義可能來自其他目標文件或庫,導致解析可能放到鏈接過程),且程序運行地址一般目前無法確定(可能需要加載時重定位),因此目標代碼涉及到的地址、數值可能需要修改,這個過程就是重定位。
鏈接、加載、運行時進行的重定位,需要回答兩個問題:哪條指令需要修改?修改成什么?如何修改?
回答由匯編過程產生的重定位表給出。重定位項一般包含對應的目標代碼指針,所引用的符號/段指針,重定位類型。目標代碼指針(where)給出要修改的目標代碼,引用符號/段指針(what)告知該代碼涉及到的符號/段從而提供修改的“材料”(由於符號/段的值更新導致重定位),重定位類型(how)指導如何進行修改。
重定位類型取決於重定位發生的時間和目標代碼的尋址方式。
可能發生的重定位場景:
- 未解析的符號找到了定義
- 鏈接時節的合並,各個目標文件中的節的初始地址改變(不再為0)。
- 程序運行地址加載時才能確定,因此加載時可能重定位。
- 。。。。
可能的代碼的尋址方式:
- 直接尋址:指令包含符號的全部或部分值,符號值改變需重定位。
- pc相對尋址:同節的引用不需重定位(鏈接等操作不“拆節”),跨節的引用需要重定位(由於節的間距改變導致引用和自己的相對距離改變)。
- 基地址尋址:一般無需處理,寄存器和offset都是確定的數,無從重定位(除非offset由於某些原因可能改變)。
- 。。。。。
4、匯編過程
匯編輸入匯編程序文件,主要輸出目標文件(目標代碼、重定位表、符號表等),根據不同要求生成額外內容(比如listing file),指令到目標代碼通常是一對一的。
匯編過程分為多種,這里主要介紹一遍式匯編和兩遍式匯編。但不管方式如何,匯編過程需要生成符號表管理符號信息,用於解析符號。
1)一遍式
掃描指令(LC自增)時,既添加符號的定義到符號表(if存在符號定義),也把指令翻譯成機器碼加載到內存。這里需要注意的是先引用符號后定義符號的情況,引用時就添加該符號,但其value指向該指令以便找到定義之后修改,其type為U(未定義,找到定義后改為D)。多個引用在前一個定義在后的情況可采用鏈表結構。
一遍式主要優勢在於輕便快速且生成目標代碼直接加載到內存,不會進行鏈接等操作,一般也不支持宏和很多和鏈接加載相關的偽指令。
2)兩遍式
第一遍掃描主要對於所有符號定義構建符號表,第二表掃描主要解析符號生成目標代碼到目標文件。不同於一遍式,兩遍式可以支持更多功能(宏,更多偽指令),且一般需要鏈接加載到內存執行。
兩遍掃描可以提供更大的自由度以支持更強大的功能,下面以多LC的實現為例介紹偽指令在兩遍之間的協調合作。
第一遍掃描遇到USE時,會將其定義的LC符號加入符號表(name,value,type);第二遍掃描將所有的USE換成相應的加載偽指令來指導鏈接加載過程的進行,並切換LC繼續翻譯。
上圖在匯編文件中被分成了7部分,第二遍結束后鏈接加載偽指令展示了每個部分的所屬節及其地址。加載時可按照main、data、beta、gamma的順序或1、3、6、2、5、4、7的順序。
以上只是一個邏輯過程,真實細節取決於具體數據結構,比如ELF的可重定位文件其實完成了上述的“加載”,將節規整好。
四、鏈接和加載
鏈接和加載是關系較緊密的兩個過程,因此放在一起討論。
鏈接把輸入的目標文件中相同的節合並成段,進行符號解析和重定位,生成一個可執行文件用於加載運行。
鏈接過程通常是兩遍。
Pass 1
收集每一個目標文件的引入符號(引用外部定義的符號)、導出符號(全局可見的符號)(匯編程序通過偽指令EXTRN、ENTRY聲明),構建全局的符號表來進行全局符號的解析。當然各自的符號表也可能用於重定位。
各目標文件符號表:
全局符號表:
同時,鏈接過程收集目標文件的所有節信息來進行段布局用於重定位以及生成可執行文件。
Pass 2
掃描所有目標代碼依據其重定位項和第一遍收集的符號、段信息進行重定位。
鏈接過程可以假設生成的可執行文件加載的地址:
對程序運行地址固定的程序(存在虛擬內存),使用該固定地址對鏈接時的進行重定位,如此加載時不再需要重定位。
對程序運行地址無法確定的情況,假設從零開始,並生成重定位信息用於加載時根據實際運行地址來修改代碼,運行。
五、庫
很多功能函數定義了好的輸入輸出接口並生成目標文件被打包成庫,可以直接用來使用,也就不用重復造輪子。
1、靜態庫
靜態庫就是很多目標文件的集合。在自己的程序中引用對應模塊,程序鏈接的過程就會在所有庫中搜尋直至找到其所在庫並鏈接該模塊。鏈接該模具時還可能遇到其依賴的別的模塊,重復上述。
2、靜態共享庫
詳情參見Linkers and Loaders中SHARED LIBRARIES一章
1)動機
靜態庫的模塊是被鏈接進目標代碼中生成可執行文件,但當相同的模塊參與了多個目標文件的鏈接,該模塊就在內存中重復多次,造成浪費。由此出現了共享庫。
2)共享原理(虛擬內存)
每個程序有自己的獨立地址空間,由OS負責為地址空間分配物理頁面,也就是地址映射。
對於庫模塊的text段頁面,因為其只讀,所以可以將其物理頁面映射到多個地址空間。
但是對於data段等可寫的頁面,不能簡單的映射,而采用COW(寫時復制)機制。COW,即在程序沒有試圖修改data段之前,data頁面都被映射進程序地址空間,當程序試圖寫data段頁面(比如虛頁A,對應實頁PA)時,OS分配新的物理頁面(比如PB)來映射該將要被寫的data段頁面(A),從此data段頁面(A)可以自由讀寫。
該原理本質在於:程序執行依靠虛擬地址,頁面復制前后,data段頁面的虛擬地址並沒有改變。
3)共享庫的鏈接和運行
首先明確靜態共享庫的加載地址是固定的,放在程序地址空間的最上方,同用戶程序空間分開。因此靜態共享庫是不需要加載重定位的,由於其地址確定,鏈接時也不需要對靜態共享庫模塊進行重定位。
共享庫包含一個存根庫(stub library),它包含共享庫中導入和導出符號的定義,同鏈接時目標文件提供的符號表和導入導出符號類似,不同的是這里不需要其本地的符號表。這些導入導出符號的定義會在引用該庫的程序進行鏈接時參與程序的符號解析和綁定(全局符號表的構建)。通俗的說,存根庫參與程序的鏈接的pass 1。
程序執行時需要做共享庫的地址映射,這個工作不同系統實現不同,可以通過在引用共享庫的程序中添加一段代碼執行,也可以由OS執行這個工作。地址映射完成后,可能需要做一些庫模塊的初始化工作,取決於具體庫模塊。
4)共享庫的不足
共享庫的更新是一大問題,共享庫的更新如果引起引用他的程序失效,則只能重新編譯鏈接加載這些程序,但找出所有引用該庫的程序是很困難的,所以這是個有嚴重后果的假設。
理想情況共享庫的更新對引用它的程序透明,這需要兩個約束:庫在內存所占空間能滿足更新,庫導出符號的地址不變。前者很矛盾,一方面大一點可以保證由空間更新,另一方面不希望占用太多內存。后者包含導出的text符號值和data符號值都盡量不變。
對於導出的text符號,將他們的符號值統一放在一張表格(jump表,每個表項都是一個jump語句),並將該表項的地址作為其對應的導出符號值。這樣所有引用導出符號的指令都會跳轉到對應的jump語句,之后jump跳到目的地址。這樣做的好處是任何導出text符號的實際值可以改變,但不會影響其被其他程序“看見”的值(stub lib中展現的)。
對於導出的data符號,由編程者保證導出的符號值不變:確保導出符號大小固定且符號值很少改變。有一個技巧是把導出的符號放在data段前面,這樣后面的局部data符號改變不會影響前面的導出符號。
5)共享庫的編寫(這里直接看原文吧)
Determine at what address the library’s code and data will be loaded.
Scan through the input library to find all of the exported code symbols. (One of the control files may be a list of some of symbols not to export, if they’re just used for inter-routine communication within the library.
Make up the jump table with an entry for each exported code symbol.
If there’s an initialization or loader routine at the beginning of the library, compile or assemble that.
Create the shared library: Run the linker and link everything together into one big executable format file.
Create the stub library: Extract the necessary symbols from the newly created shared library, reconcile those symbols with the symbols from the input library, create a stub routine for each library routine, then compile or assemble the stubs and combine them into the stub library. In COFF libraries, there’s also a little initialization code placed in the stub library to be linked into each executable.
六、位置無關代碼(PIC)
詳情可參考Linkers and Loaders中Position-Independent Code一章
1、原理
PIC去除了text段中所有的直接地址尋址的指令,包括局部符號的實際地址和全局符號的實際地址。以ELF文件為例,每個可執行文件的text段和data段距離固定(每個程序的該距離都不一定一樣,但鏈接之后該距離確定下來),因此可以通過pc加偏移的方式(pc就是運行時指令地址)確定data段位置,從而確定data中符號的位置訪問這些符號值。
2、具體實現
PIC把程序中的所有全局符號的實際地址存儲在GOT(global offset table)中,並把text段中所有的直接地址指令根據引用的符號是全局還是局部,分別換成通過GOT間接尋址和GOT基地址尋址。
1)GOT的運行地址
這里涉及到一個問題GOT的地址如何獲得?根據開始說的依靠text段和data段距離固定來通過pc(運行時指令的地址)加上這段距離獲得,具體的,
call .L2 ;; push PC in on the stack
.L2:
popl %ebx ;; PC into register EBX
addl $_GLOBAL_OFFSET_TABLE_+[.-.L2],%ebx;; adjust ebx to GOT address
上面的代碼在運行時獲得了所在可執行文件的GOT表的運行時地址。其中$_GLOBAL_OFFSET_TABLE_是GOT的符號,在編譯(包括匯編)后上面的addl指令對應一個類型為R_386_GOTPC的重定位項,該重定位項在鏈接時將該符號解析到GOT的鏈接后地址(可執行文件中的地址,此后指令和GOT的相對距離固定),從而該addl指令將該可執行文件的GOT的運行時地址放到寄存器ebx中。
存在GOT的可執行文件中的代碼在調用函數時,目前存儲GOT的運行時地址的約定寄存器需要保護,因為每個可執行文件的GOT是獨立的,當調用其他的文件(也存在GOT)的函數時,該約定寄存器的值可能會被覆蓋。
2)處理“直接地址”
現在我們得到了GOT的運行時地址,它保存在約定的寄存器中。下面來處理程序中剩余段中的直接地址:
text段中的引用局部符號和全局符號的直接尋址指令,通過兩種重定位類型在鏈接時處理:R_386_GOT32、R_386_GOTOFF。(這兩個重定位類型都是構造PIC的鏈接過程使用的)
data段中的直接地址,通過R_386_RELATIVE來指導進行加載時重定位。
假設有如下C代碼:
static int a; /* static variable */
extern int b; /* global variable */
...
a=1; b= 2;
a=1被編譯成如下指令:
movl $1,a@GOTOFF(%ebx);; R_386_GOTOFF reference to variable "a"
movl的重定位類型為GOTOFF,因此符號a被重定位為data段符號a和GOT之間的距離。如此movl通過基地址尋址完成對a的訪問。
b=2被編譯成如下指令:
movl b@GOT(%ebx),%eax;; R_386_GOT32 ref to address of variable "b"
movl $2,(%eax)
movl的重定位類型為GOT,因此符號b被重定位其在GOT中的相對地址(下標),通過第一個movl來從b的GOT項取出b的運行地址(這里暗示GOT需要加載時重定位、解析),再對該地址指向的內存進行存取。
PIC只保證text段可以無需加載時重定位,但data中的GOT和其他直接地址仍然需要加載重定位,后者就通過類型為R_386_RELATIVE的重定位項來標記。
2、優劣
1)優勢
PIC的text段不需要加載重定位。
位置無關代碼解決了目標文件(運行地址不確定)需要大量加載時重定位的問題,主要用於動態共享庫,解除了靜態共享庫對目標文件確定加載地址的限制。
2)劣勢
需要占用一個寄存器保存GOT運行時地址。
調用函數時開銷增加,需要維護GOT的運行時地址。
PIC在加載之后仍然需要進行data段的重定位,主要集中在GOT中和data段的直接地址。
七、動態鏈接
詳情可參考Linkers and Loaders中DYNAMIC LINKING AND LOADING一章
動態鏈接指把符號的綁定和庫對可執行文件的綁定推遲到運行時。這里的動態共享庫都是PIC。
常規情況在鏈接時完成這些工作,雖然時機不同,但要做的仍然是對程序和動態共享庫進行“鏈接”:動態共享庫的加載(也就是PIC的重定位),程序和動態共享庫的鏈接(程序的動態段提供程序連接所需信息)。這里程序不需要加載重定位,因為存在虛擬內存,地址空間獨立,加載地址早已確定。
0、ELF動態共享模塊邏輯結構
程序頭是整個文件的目錄,記載“主干”的位置信息。
hash、dysym,dynstr三個段是動態符號表,hash用於快速查找,dysym為符號項(不包含名字),dynstr則為名字。
text段中的函數調用都指向plt段。
dynamic段提供動態鏈接器需要的所有信息。
1、動態鏈接
”Pass 1“
OS加載可執行文件時發現INTERPRETER段,這就是動態鏈接器ld.so。OS首先加載這個動態鏈接器,並將程序運行前需要的信息傳遞給鏈接器。這些信息包括:程序頭(該可執行文件的“目錄”),程序的啟動地址(第一個執行的指令地址),鏈接器自身的加載地址。
動態鏈接器執行自己的啟動代碼,找到自己的GOT(第一項為其動態段),接着找到動態段,進行自身的重定位和符號解析,並開始構建全局符號表(和常規鏈接時的全局符號表一樣的作用)。
之后ld按照程序的動態段提供的需要模塊及庫的信息,依次加載它們,將其符號表添加進全局符號表。
此時程序全部依賴的動態庫完成加載和地址映射,全局符號表構建完成。到這里其實完成了常規鏈接過程的pass 1。
后面對程序和共享庫進行符號解析和重定位。也就是進行pass 2。
“Pass 2”
此時共享庫的重定位有四種類型:
R_386_GLOB_DAT, used to initialize a GOT entry to the address of a symbol defined in another library.來自外部的全局符號解析
R_386_32, a non-GOT reference to a symbol defined in another library, generally a pointer in static data.來自外部的局部符號解析
R_386_RELATIVE, for relocatable data references, typically a pointer to a string or other locally defined static data.自身的局部符號地址重定位
R_386_JMP_SLOT, used to initialize GOT entries for the PLT, described later.指向對應PLT項的指針。
完成對共享庫的重定位、解析之后,再利用程序的動態段和全局符號表對程序進行重定位和解析。由此完成pass 2。
回顧一下,GOT是程序運行時依賴的數據結構,而全局符號表和動態段是鏈接過程進行重定位和符號解析依賴的數據結構。
2、延遲綁定
1)背景
PIC中外部函數的地址存放在GOT中,而其定義的查找可能會很慢(大量的函數定義),這進一步拖慢了共享模塊解析符號,重定位的速度。為了改善這一點,將外部函數的地址解析推遲到其被調用時進行,也就是延遲綁定。那么現在對外部函數的調用通過什么呢?通過跳轉到模塊中對應的的PLT項執行。PLT(procedure linkage table)本身是PIC,包含在動態模塊的text段中。(如果是本地全局函數,加載時就能重定位)
2)具體實現
ELF將GOT拆分成兩個表".got"和"".got.plt"。其中"".got"用來保存全局變量的引用地址。".got.plt"用來保存函數引用的地址,也就是說,所有對於外部函數的引用全部被分離出來放到了 ".got.plt"中。另外 ".got.plt"還有一個特殊的地方就是它的前三項是有特殊意義的,分別含義如下:
- 第一項保存的是 ".dynamic" 段的地址
- 第二項保存的是本模塊的ID
- 第三項保存的是_dl_runtime_resolve()的地址,用於解析下面的外部函數的符號(進行綁定)。
其中第二項和第三項由動態鏈接器在加載共享模塊的時候負責將它們初始化。".got.plt"的其余項分別對應每一個外部函數的引用。

PLT的內容如下,
Special first entry #特別的第一項,壓入自己的模塊名,跳轉到函數引用解析例程
PLT0: pushl GOT+4 #GOT中第二項,就是caller所在模塊名。
jmp *GOT+8 #跳轉到解析器
Regular entries, non-PIC code: #非PIC版本
PLTn: jmp *GOT+m
push #reloc_offset
jmp PLT0
Regular entries, PIC code: #PIC版本
PLTn: jmp *GOT+m(%ebx) #第一次執行等價於nop,之后跳向函數實際地址
push #reloc_offset #壓入函數名
jmp PLT0
3)整個過程
第一次調用外部函數時,跳轉到相應的PLTn項,執行其代碼段:
PLTn:jmp *GOT+m(%ebx) 被第一次調用時不會產生結果(跳到相鄰指令),因為其目標地址存在函數對應的GOT項中,而該GOT項初始化為jmp(本指令)的下一條指令:push #reloc_offset。
push #reloc_offset 被執行,壓入函數符號。這里對應一個類型為R_386_JMP_SLOT的重定位項,加載時重定位為函數符號項(符號表中)。
jmp PLT0 被執行,跳轉到PLT0。
PLT0: pushl GOT+4 被執行,壓入函數所在模塊。作為參數傳遞給解析例程,方便找到定義后定位該模塊的GOT,進行函數符號綁定。
jmp *GOT+8 被執行跳轉到解析器。此時棧中從頂開始,依次是:模塊名,函數名,調用者的返回地址。
解析例程保存所有寄存器,並在符號表中查找函數定義,依據傳遞的模塊對其GOT相應的項填充函數實際地址。
之后恢復寄存器,彈出PLT壓入的兩個參數,跳轉到相應函數例程。
之后的該函數調用可以直接通過PLT的jmp跳轉進入。
3、隨用隨加載
程序和動態庫的結構基本一樣,意味着程序可以在運行時才加載新的動態庫並與之鏈接,並調用庫的函數和數據。能這樣做的原因在於延遲綁定。
八、回看
編譯針對單個源文件進行翻譯,生成目標代碼文件。
鏈接、加載過程是程序設計(使得程序可以拆成多個源文件)、運行的手段,貫穿始終的就是由於代碼序列的分散、重組和加載地址的不確定導致的符號解析和重定位。
庫本質還是一堆目標模塊,只是因為不同的用途有着不同的設計。
常規鏈接且只使用靜態庫的程序,符號被綁定到一個地址以及庫模塊被綁定到可執行文件中(打包在一起)都發生在鏈接過程,也因此鏈接過程可能有大量的重定位發生。
靜態共享庫的符號綁定仍然發生在鏈接時(使用庫的存根stub lib來進行符號解析),但庫模塊和可執行文件的綁定在程序運行時才發生(COW機制)。
動態共享庫的符號綁定以及庫模塊被綁定到可執行文件都在程序運行時發生運行時,甚至延遲綁定(lazy binding將函數調用的地址綁定推遲到調用)。
雖然分出來三種類型的庫,但實際上蘊含的思想只有兩個:共享和動態(運行時)。將二者結合在一起就是動態共享庫,在linux中叫共享庫,windows中叫動態鏈接庫。