C語言連結裝載流程全面分析
連結的主要內容是把各個模組之間相互引用的部分處理好,使得各個模組之間能夠正確地銜接。
連結的主要過程包括:地址和空間分配(Address and Storage Allocation),符號決議(Symbol Resolution),重定位(Relocation)等。
連結分為靜態連結和動態連結。
靜態連結是指在編譯階段直接把靜態庫加入到可執行檔案中去,這樣可執行檔案會比較大。
而動態連結則是指連結階段僅僅只加入一些描述資訊,而程式執行時再從系統中把相應動態庫載入到記憶體中去。
靜態連結的大致過程如下圖所示:
static linking
二、目標檔案的樣子(以linux下的elf檔案格式為例)
夾在ELF頭和節頭部表之間的都是節。一個典型的ELF可重定位目標檔案包含下面幾個節:
- .text:已編譯程式的機器程式碼。
- .rodata:只讀資料,比如printf語句中的格式串和開關(switch)語句的跳轉表。
- .data:已初始化的全域性C變數。區域性C變數在執行時被儲存在棧中,既不出現在.data中,也不出現在.bss節中。
- .bss:未初始化的全域性C變數。在目標檔案中這個節不佔據實際的空間,它僅僅是一個佔位符。目標檔案格式區分初始化和未初始化變數是為了空間效率在:在目標檔案中,未初始化變數不需要佔據任何實際的磁碟空間。
- .symtab:一個符號表(symbol table),它存放在程式中被定義和引用的函式和全域性變數(包括引用到的外部變數和函式,不含有區域性變數)的資訊。一些程式設計師錯誤地認為必須通過-g選項來編譯一個程式,得到符號表資訊。實際上,每個可重定位目標檔案在.symtab中都有一張符號表。然而,和編譯器中的符號表不同,.symtab符號表不包含區域性變數的表目。
- .rel.text:當連結噐把這個目標檔案和其他檔案結合時,.text節中的許多位置都需要修改。一般而言,任何呼叫外部函式或者引用全域性變數(包括本目標檔案內的全域性變數,因為在連結時要多個目標檔案的相同段合併,這樣資料的地址就會改變,所以要重定位)的指令都需要修改。另一方面呼叫本地函式的指令則不需要修改。注意,可執行目標檔案中並不需要重定位資訊,因此通常省略,除非使用者顯式地指示連結器包含這些資訊。
- .rel.data:被模組定義或引用的任何全域性變數的資訊。一般而言,任何已初始化全域性變數的初始值是全域性變數或者外部定義函式的地址都需要被修改。
- .debug:一個除錯符號表,其有些表目是程式中定義的區域性變數和型別定義,有些表目是程式中定義和引用的全域性變數,有些是原始的C原始檔。只有以-g選項呼叫編譯驅動程式時,才會得到這張表。
- .line:原始C源程式中的行號和.text節中機器指令之間的對映。只有以-g選項呼叫編譯驅動程式時,才會得到這張表。
- .strtab:一個字串表,其內容包括.symtab和.debug節中的符號表,以及節頭部中的節名字。字串表就是以null結尾的字串序列。
旁註:為什麼未初始化的資料稱為.bss?
用術語.bss來表示未初始化的資料是很普遍的。它起始於IBM 704組合語言(大約在1957年)中”塊儲存開始(Block Storage Start)“指令的首字母縮寫,並沿用至今。一個記住區分.data和.bss節的簡單方法是把“bss”看成是“更好地節省空間(Better Save Space)!“的縮寫。
連結就是將不同部分的程式碼和資料收集和組合成一個單一檔案的過程,也就是把不同目標檔案合併成最終可執行檔案的過程。當然,務必知道:這個過程不涉及記憶體。連結可以分為三種情形:
1,編譯時連結,也就是我們常說的靜態連結;
2,裝載時連結;
3,執行時連結。裝載時連結和執行時連結合稱為動態連結
1、什麼是靜態連結?
靜態連結就是將多個目標檔案組合在一起形成一個可執行檔案,如將a.o 和 b.o 連結在一起形成 可執行檔案ab。
2、靜態連結的過程包括哪幾個部分?
靜態連結包括兩個大部分:一是空間和地址的分配;二是符號解析和重定位
(1)空間和地址的分配
編譯器在將a.o 和 b.o 是如何合併在一起的??
第二種方法就是 相似段合併:顧名思義 就是把不同目標檔案的相同名字的段合併成一個段,如下圖:
圖1
靜態連結的整個過程分為兩步:
第一步:空間和地址分配。
掃描所有的輸入目標檔案,獲得他們的各個段的長度、屬性和位置,並且將輸入目標檔案中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全域性符號表。這樣,聯結器將能夠獲得所有輸入目標檔案的段長度,並且將它們合併,計算出輸出檔案中各個段合併後的長度與位置,並建立對映關係。
這裡可能會有一個問題:建立了什麼樣的對映關係。如上面的圖1,你可能就會有所瞭解。對映關係就是指可執行檔案與程序虛擬地址空間之間的對映。那麼,這裡程式還沒有執行,更不會出現程序,哪裡來的程序地址空間呢?此時虛擬儲存器便發揮了很大的作用:雖然此時沒有程序,但是每個程序的虛擬地址空間的格式都是一致的。所以,為可執行檔案的每個段甚至每個符號符號分配地址也就不會有什麼錯了。注意:在連結之前,目標檔案中的所有段的虛擬地址都是0,因為虛擬空間還沒有被分配,預設都為0.等到連結之後,可執行檔案中的各個段已經都被分配到了相應的虛擬地址
第二步:符號解析與重定位
首先,符號解析。解析符號就是將每個符號引用與它輸入的可重定位目標檔案中的符號表中的一個確定的符號定義聯絡起來。若找不到,則出現編譯時錯誤。
第三步:重定位;
不同的處理器指令對於地址的格式和方式都不一樣。我們這裡採用的是32位的x86處理器,介紹兩種定址方式。
X86基本重定位型別
巨集定義
值
重定位修正方法
R_386_32
1
絕對定址修正S + A
R_386_PC32
2
相對定址修正S + A - P
這裡,我們就要提及“靜態庫”的概念。其實一個靜態庫可以簡單地看成一組目標檔案的集合,即很多目標檔案經過壓縮打包後形成的一個檔案。與靜態庫連結的過程是這樣的:ld連結器自動查詢全域性符號表,找到那些為決議的符號,然後查出它們所在的目標檔案,將這些目標檔案從靜態庫中“解壓”出來,最終將它們連結在一起成為一個可執行檔案。也就是說只有少數幾個庫和目標檔案被連結入了最終的可執行檔案,而非所有的庫一股腦地被連結進了可執行檔案。
裝載:
以Linux核心裝載ELF為例簡述一下裝載過程。當我們在Linux系統的bash下輸入一個命令執行某個ELF程式時,在使用者層面,bash程序會呼叫fork()系統呼叫建立一個新的程序,然後新的程序呼叫execve()來執行指定的ELF檔案,原先的bash程序繼續返回等待剛才啟動時新程序結束,然後繼續等待使用者輸入命令。這裡需注意,隨著一個新程序的出現,作業系統會為它建立一個獨立的虛擬地址空間。
【建立虛擬地址空間】我們知道一個虛擬空間由一組對映函式將虛擬空間的各個頁對映到相應的物理空間,那麼建立一個虛擬空間實際上並不是建立空間而是建立對映函式所需要的資料結構。舉例來說,在x86的Linux下建立虛擬地址空間實際上只是分配一個頁目錄(頁表)就可以了,甚至不設定頁對映關係,這些對映關係等到後面程式發生“缺頁”時在進行設定。
在進入execve()系統呼叫之後,Linux核心就開始進行真正的裝載工作。在核心中,execve()系統呼叫相應的入口是sys_execve(),作用:引數的檢查複製;呼叫do_execve(),流程:查詢被執行的檔案,讀取檔案的前128個位元組以判斷檔案的格式是elf還是其它;呼叫search_binary_handle(),流程:通過判斷檔案頭部的魔數確定檔案的格式,並且呼叫相應的裝載處理程式。ELF可執行檔案的裝載處理過程叫load_elf_binary(),它的主要步驟如下:
1,檢查ELF可執行檔案格式的有效性,比如魔數、程式頭表中段的數量。
2,尋找動態連結的“.interp”段,找到動態連結器的路徑,以便於後面動態連結時會用上。
3,讀取可執行檔案的程式頭,並且建立虛擬空間與可執行檔案的對映關係。
【讀取可執行檔案的程式頭(儲存了哪些部分被對映),並且建立虛擬空間與可執行檔案的對映關係】建立虛擬空間時的頁對映關係函式是虛擬空間到實體記憶體的對映關係,而這一步所做的事虛擬空間與可執行檔案的對映關係。我們知道,當程式發生缺頁是,作業系統會為實體記憶體分配一個物理頁,然後將該缺頁從磁碟中讀取到記憶體,在設定缺頁的虛擬頁與物理頁之間的對映關係,這樣程式才可以得以正常執行。但是明顯的一點是,當作業系統捕獲到缺頁錯誤時,他應當知道程式當前需要的頁在可執行檔案中的哪一個位置。而這就是虛擬儲存與可執行檔案之間的對映關係。實際上,這種對映關係僅僅是儲存在作業系統內部的一個數據結構。當發生缺頁錯誤是,CPU將控制權交給作業系統,作業系統利用專門的缺頁處理例程來查詢這個資料結構(對映關係),然後找到所需頁所在的虛擬記憶體區域,以及在可執行檔案的偏移,然後把該頁載入進實體記憶體,同時將該虛擬頁與物理頁之間建立對映關係,最後把控制權還給程序,程序從剛才缺頁位置重新開始執行。
4,初始化ELF程序環境。
5,將系統呼叫的返回地址修改成ELF可執行檔案的入口點,這個入口點取決於程式的連結方式,對於靜態連結的ELF可執行檔案,它就是ELF檔案的檔案頭中e_entry所指的地址;對於動態連結的ELF可執行檔案,程式入口點就是動態連結器。
【將CPU指令暫存器設定成可執行檔案的入口,啟動執行】對動態連結來講,此時就啟動了動態連結器。
當load_elf_binary()執行完畢,返回至do_execve()在返回至sys_execve()時,系統呼叫的返回地址已經被改寫成了被裝載的ELF程式的入口地址了。所以,當sys_execve()系統呼叫從核心態返回到使用者態時,EIP暫存器直接跳轉到ELF程式的入口地址。此時,ELF可執行檔案裝載完成。接下來就是動態連結器對程式進行動態連結了。
五、動態連結
動態連結ELF檔案的生成過程:
主要原因有兩個:
第一,考慮記憶體和磁碟空間。靜態連結極大地浪費記憶體空間。因為在靜態連結的情況下,假設有兩個程式共享一個模組,那麼在靜態連結後輸出的兩個可執行檔案中各有一個共享模組的副本。如果同時執行這兩個可執行檔案,那麼這個共享模組將在磁碟和記憶體中都有兩個副本,對磁碟和記憶體造成極大地浪費;
第二,程式的更新。一旦程式中的一個模組被修改,那麼整個程式都要重新連結、釋出給使用者。如果這個程式相當的大,那麼後果就會更加嚴重!
對於一個共享物件(linux下共享的模組),要實現被其他程式之間的共享,就要使其程式碼和資料分開,每個程式都會有該模組的資料部分的副本,程式碼部分是共享的。
共享模組被對映的虛擬地址空間就在上面程序虛擬空間中的 (Memery Mapping部分)
共享模組被對映的模樣是
動態連結做了什麼?
務必知道,動態連結是相對於共享物件而言的。動態連結器將程式所需要的所有共享庫裝載到程序的地址空間,並且將程式彙總所有為決議的符號繫結到相應的動態連結庫(共享庫)中,並進行重定位工作。
對於共享模組來說,要實現共享,那麼其程式碼對資料的訪問必須是地址無關(就是程式碼中的地址是固定的,當然這是用的相對地址嘍)的,如何做到地址無關,編譯器是這麼幹的,每一個共享模組,都會在其程式碼段有一個GOT(global offset table)段,如上圖所示,Got是一個指標陣列,用來儲存外部變數的地址,而程式碼相對於Got的距離是固定的,當對外部模組變數資料和函式進行訪問時,就去訪問變數在GOT中的位置。
共享模組對於資料的訪問方式:
本模組的全域性變數和函式------相對地址
外模組的全域性變數和函式-------GOT段
動態連結重定位時修改GOT中的值就實現了對變數的正確訪問。
動態連結的ELF檔案啟動過程
動態連結基本分為三步:先是啟動動態連結器本身,然後裝載所有需要的共享物件,最後重定位和初始化。
1,動態連結器自舉
就我們所知道的,對普通的共享物件檔案來說,它的重定位工作是由動態連結器來完成;它也可以依賴於其他共享物件,其中被依賴的共享物件由動態連結器負責連結和裝載。那麼,對於動態連結器本身呢,它也是一個共享物件,它的重定位工作由誰完成?它是否可以依賴於其他的共享物件檔案?
動態連結器有其自身的特殊性:首先,動態連結器本身不可以依賴其他任何共享物件(人為控制);其次動態連結器本身所需要的全域性和靜態變數的重定位工作由它自身完成(自舉程式碼)。
我們知道,在Linux下,動態連結器ld.so實際上也是一個共享物件,作業系統同樣通過對映的方式將它載入到程序的地址空間中。作業系統在載入完動態連結器之後,就將控制權交給動態連結器。動態連結器入口地址即是自舉程式碼的入口。動態連結器啟動後,它的自舉程式碼即開始執行。自舉程式碼首先會找到它自己的GOT(全域性偏移表,記錄每個段的偏移位置)。而GOT的第一個入口儲存的就是“.dynamic”段的偏移地址,由此找到動態連結器本身的“.dynamic”段。通過“.dynamic”段中的資訊,自舉程式碼便可以獲得動態連結器本身的重定位表和符號表等,從而得到動態連結器本身的重定位入口,然後將它們重定位。完成自舉後,就可以自由地呼叫各種函式和全域性變數。
2,裝載共享物件
完成自舉後,動態連結器將可執行檔案和連結器本身的符號表都合併到一個符號表當中,稱之為“全域性符號表”。然後連結器開始尋找可執行檔案所依賴的共享物件:從“.dynamic”段中找到DT_NEEDED型別,它所指出的就是可執行檔案所依賴的共享物件。由此,動態連結器可以列出可執行檔案所依賴的所有共享物件,並將這些共享物件的名字放入到一個裝載集合中。然後連結器開始從集合中取出一個所需要的共享物件的名字,找到相應的檔案後開啟該檔案,讀取相應的ELF檔案頭和“.dynamic”,然後將它相應的程式碼段和資料段對映到程序空間中。如果這個ELF共享物件還依賴於其他共享物件,那麼將依賴的共享物件的名字放到裝載集合中。如此迴圈,直到所有依賴的共享物件都被裝載完成為止。
當一個新的共享物件被裝載進來的時候,它的符號表會被合併到全域性符號表中。所以當所有的共享物件都被裝載進來的時候,全域性符號表裡面將包含動態連結器所需要的所有符號。
3,重定位和初始化
當上述兩步完成以後,動態連結器開始重新遍歷可執行檔案和每個共享物件的重定位表,將表中每個需要重定位的位置進行修正,原理同前。
重定位完成以後,如果某個共享物件有“.init”段,那麼動態連結器會執行“.init”段中的程式碼,用以實現共享物件特有的初始化過程。
此時,所有的共享物件都已經裝載並連結完成了,動態連結器的任務也到此結束。同時裝載連結部分也將告一段落!接下來便是程式的執行了。。。
更多關於 動態庫和靜態庫請前往:https://blog.csdn.net/yexiangCSDN/article/details/83827724