咱們繼續來說解連接器的重定位。程序員
程序的運行過程就是CPU不斷的從內存中取出指令而後執行執行的過程,對於函數調用來講好比咱們在C/C++語言中調用簡單的加法函數add,其對應的彙編指令多是這樣的:微信
call 0x4004fdapp
其中0x4004fd即爲函數add在內存中的地址,當CPU執行這條語句的時候就會跳轉到0x4004fd這個位置開始執行函數add對應的機器指令。函數
再好比咱們在C語言中對一個全局變量g_num不斷加一來進行計數,其對應的彙編指令多是這樣的:ui
mov 0x400fda %eaxspa
add $0x1 %eax操作系統
這裏的意思是把內存中 0x400fda 這個地址的數據放到寄存器當中,而後將寄存器中的數據加一,在這裏g_num這個全局變量的內存地址就是0x400fda。.net
好奇的同窗可能會問,那這些函數以及數據的內存地址是怎麼來的呢?3d
肯定程序運行時的內存地址就是接下來咱們要講解的重點內容,這裏先給出答案,可執行文件中代碼以及數據的運行時內存地址是連接器指定的,也就是上面示例中add的內存地址0x4004fd其是連接器指定的。肯定程序運行時地址的過程就是這裏重定位(Relocation)。blog
爲何這個過程叫作重定位呢,之因此叫作重定位是由於肯定可執行文件中代碼和數據的運行時地址是分爲兩個階段的,在第一個階段中沒法肯定這些地址,只有在第二個階段才能夠肯定,所以就叫作重定位。接下來讓咱們來看看這兩個階段,合併同類型段以及引用符號的重定位。
編譯器的工做
讓咱們回憶一下前幾節的內容,源文件首先被編譯器編譯生成目標文件,目標文件種有三段內容:數據段、代碼段以及符號表,全部的函數定義被放在了代碼段,全局變量的定義放在了數據段,對外部變量的引用放到了符號表。
編譯器在將源文件編譯生成目標文件時能夠肯定一下兩件事:
定義在該源文件中函數的內存地址
定義在該源文件中全局變量的內存地址
注意這裏的內存地址其實只是相對地址,相對於誰的呢,相對於本身的。爲何只是一個相對地址呢?由於在生成一個目標文件時編譯器並不知道這個目標文件要和哪些目標文件進行連接生成最後的可執行文件,而連接器是知道要連接哪些目標文件的。所以編譯器僅僅生成一個相對地址。
而對於引用類的變量,也就是在當前代碼中引用而定義是在其它源文件中的變量,對於這樣的變量編譯器是沒法肯定其內存地址的,這不是編譯器須要關心的,肯定引用類變量的內存地址是連接器的任務,連接器在進行連接時可以肯定這類變量的內存地址。所以當編譯器在遇到這樣的變量時,好比使用了外部定義的函數時,其在目標文件中對應的機器指令多是這樣的:
call 0x000000
也就是說對於編譯器不能肯定的地址都這設置爲空(0x000000),同時編譯器還會生成一條記錄,該記錄告訴連接器在進行連接時要修正這條指令中函數的內存地址,這個記錄就放在了目標文件的.rel.text段中。相應的若是是對外部定義的全局變量的使用,則該記錄放在了目標文件的.rel.data段中。即連接器須要在連接過程當中根據.rel.data以及.rel.text來填好編譯器留下的空白位置
(0x000000)。所以在這裏咱們進一步豐富目標文件中的內容,如圖所示:
生成目標文件後,編譯器完成任務,編譯器肯定了定義在該源文件中函數以及全局變量的相對地址。對於編譯器不能肯定的引用類變量,編譯器在目標文件的.rel.text以及.rel.data段中生成相應的記錄告訴連接器要修正這些變量的地址。
接下來就是連接器的工做了。
連接器的工做
咱們在靜態庫下可執行文件的生成一節中知道,連接器會將全部的目標文件進行合併,全部目標文件的數據段合併到可執行文件的數據段,全部目標文件的代碼段合併到可執行文件的代碼段。當全部合併完成後,各個目標文件中的相對地址也就肯定了。所以在這個階段,連接器須要修正目標文件中的相對地址。
在這裏咱們以合併目標文件中的數據段爲例來講明連接器是如何修正目標文件的相對地址的,合併代碼段時修正相對位置的原理是同樣的。
咱們假設連接器須要連接三個目標文件:
目標文件一:該文件數據段定義了兩個變量apple和banana,apple的長度爲2字節,banana的長度4字節,所以目標文件一的數據段長度爲6字節。從圖中也能夠看出apple的內存地址爲0,也就是相對地址,即apple這個變量在目標文件一的地址是0,banana的地址爲2。
目標文件二:該文件的數據段比較簡單,只定義了一個變量orange,其長度爲2,所以該目標文件的數據段長度爲2。
目標文件三:該文件的數據段定義了三個變量grape、mango以及limo,其長度分別爲4字節、2字節以及2字節,所以該目標文件的數據段長度爲8字節。
連接器在連接三個目標文件時其順序是依次連接的,連接完成後:
目標文件一:該數據段的起始地址爲0,所以該數據段中的變量的最終地址不變。
目標文件二:因爲目標文件一的數據段長度爲6,所以連接完成後該數據段的起始地址爲6(這裏的起始地址其實就是偏移offset),相應的orange的最終內存地址爲0+offset即6。
目標文件三:因爲前兩個數據段的長度爲8,所以該數據段的起始地址爲8(即offset爲8),所以全部該數據段中的變量其地址都要加上該offset,即grape的最終地址爲8,即0+offset,mango的最終地址爲4+offset即12,limo的最終地址爲6+offset即14。
從這個過程當中能夠看到,數據段中的相對地址是經過這個公式來修正的,即:
相對地址 + offset(偏移) = 最終內存地址
而每一個段的偏移只有在連接完成後才能肯定,所以對相對地址的修正只能由連接器來完成,編譯器沒法完成這項任務。
當全部目標文件的同類型段合併完畢後,數據段和代碼段中的相對地址都被連接器修正爲最終的內存位置,這樣全部的變量以及函數都肯定了其各自位置。
至此,重定位的第一階段完成。接下來是重定位的第二階段,即引用符號的重定位。
相對地址是編譯器在編譯過程當中肯定了,在連接器完成後被連接器修正爲最終地址,而對於編譯器沒有肯定的所引用的外部函數以及變量的地址,編譯器將其記錄在了.rel.text和.rel.data中。
因爲在第一階段中,全部函數以及數據都有了最終地址,所以重定位的第二階段就相對簡單了。咱們知道編譯器引用外部變量時將機器指令中的引用地址設置爲空(好比call 0x000000),並將該信息記錄在了目標文件的.rel.text以及.rel.data段中。所以在這個階段連接器依次掃描全部的.rel.text以及.rel.data段並找到相應變量的最終地址(這些位置都已在第一階段肯定),並將機器指令中的0x000000修正爲所引用變量的最終地址就能夠了。
到這裏連接器的重定位就講解的這裏,做爲程序員通常不多會有問題出如今重定位階段,所以這個階段對程序員相對透明。請同窗們注意一點,這裏的分析僅限於目標文件的靜態連接。咱們知道靜態連接下,連接器會將須要的代碼和數據都合併到可執行文件當中,所以須要肯定代碼和數據的最終位置。而對於動態連接庫來講狀況則有所不一樣,動態連接庫能夠同時被多個進程使用,若是動態連接庫的機器指令中不能夠存在引用變量的最終位置,不然在被多個進程使用時會出現一個進程中使用的數據被其它進程修改。所以動態庫下的機器指令都是PIC代碼,即位置無關代碼(Position-Independent Code)。關於PIC的機制原理就不在這裏闡述了,對此感興趣的同窗能夠關注微信公衆號,碼農的荒島求生,我會在那裏來說解。
問題:爲何連接器能肯定運行時地址
咱們知道只有把可執行文件加載到內存當中程序才能夠開始運行。不一樣的程序會被加載到內存的不一樣位置。咱們從前兩節的過程當中能夠看出,連接器徹底沒有考慮不一樣的程序會被加載不一樣的內存位置被執行。好比對於一個可執行文件咱們分別運行兩次,以下圖所示,由於兩個程序數據段變量的地址是同樣的,那麼程序一的數據會不會被程序二修改呢?
若是你去試一試的話就會發現顯然不會有這種問題的。而當可執行文件加載到內存的時候也不會根據程序加載的起始地址再去修改可執行文件中變量的地址(這樣就啓動速度就太慢了),那麼操做系統又是如何能作到基於同一個可執行文件的兩個程序能在各自的內存空間中運行而不相互干擾呢,連接器在可執行文件中肯定的究竟是不是程序最終的運行地址呢,我會在後面的文章當中給出答案,歡迎同窗們關注微信公共帳號碼農的荒島求生獲取更多內容。
《完全理解連接器:六,大型項目是如何被構建(build)出來的》,歡迎關注微信公衆號,碼農的荒島求生,獲取更多內容。
本文分享自微信公衆號 - 碼農的荒島求生(escape-it)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。