目錄
靜態鏈接
ELF加載與進程地址空間(靜態鏈接)
動態鏈接與動態庫加載
GOT表
靜態鏈接
對于多個.o文件在沒有鏈接之前互相是不知到對方存在的,也就是說這個.o文件中調用函數的的跳轉地址都會被設定為0(當然這個函數是在其他.o文件中定義的)這個地址會在哪個時候被修正?鏈接的時候!為了讓鏈接器將來在鏈接時能夠正確定位到這些被修正 的地址,在代碼塊(.data)中還存在?個重定位表,這張表將來在鏈接的時候,就會根據表?記錄的地址將其修正。這也就是為什么.o文件叫做可重定位文件。
ELF加載與進程地址空間(靜態鏈接)
從上面的連接過程可以看到,在我們鏈接完成的之后形成的可執行程序中是有地址的,這個時候程序顯然沒有加載到內存中那這個地址就不可能是內存中的物理地址。事實上這個地址是一種邏輯地址,其思想與虛擬地址類似,也與虛擬地址對應,也就是說磁盤上的邏輯地址就是以后運行可執行程序時的虛擬地址。在當代計算機內部,這個邏輯地址采用平坦模式進行編址(也就是從0開始編址)。所以也要求ELF文件對自己的代碼和數據進行統一編址。
簡直巧妙,原來虛擬地址跟磁盤中可執行文件的邏輯地址是對應的。我們知道可執行程序的執行需要os創建子進程來執行,那么mm_struct、vm_area_struct在進程剛剛創建的時候,初始化數據從哪?來?就從邏輯地址來。從ELF各個segment來,每個segment有??的起始地址和??的?度,?來初始化內核結構中的[start, end] 等范圍數據,另外再?詳細地址,填充?表。
mm_struct
? 描述進程的整個虛擬地址空間,包含所有?vm_area_struct
?的鏈表或紅黑樹。例如:
struct mm_struct {struct vm_area_struct *mmap; // 虛擬內存區域鏈表unsigned long start_code; // 代碼段起始地址(ELF的 .text)unsigned long end_code;unsigned long start_data; // 數據段起始地址(ELF的 .data)unsigned long end_data;// ...
};
vm_area_struct
? ?描述一個連續的虛擬內存區域(如一個ELF段),包括權限、文件映射信息等。
struct vm_area_struct {unsigned long vm_start; // 起始虛擬地址(ELF的 p_vaddr)unsigned long vm_end; // 結束虛擬地址struct file *vm_file; // 關聯的ELF文件unsigned long vm_pgoff; // 文件中的偏移(對應ELF段在文件中的位置)pgprot_t vm_page_prot; // 訪問權限(如可讀、可執行)// ...
};
示例:ELF加載到虛擬地址空間
假設一個ELF文件有兩個可加載段:
-
代碼段:
.text
,p_vaddr = 0x400000
,?p_memsz = 0x1000
-
數據段:
.data
,p_vaddr = 0x401000
,?p_memsz = 0x2000
進程創建時,內核會:
-
創建兩個?
vm_area_struct
:-
代碼段:
vm_start=0x400000
,?vm_end=0x401000
, 權限為?RX
(讀+執行)。 -
數據段:
vm_start=0x401000
,?vm_end=0x403000
, 權限為?RW
(讀+寫)。
-
-
通過?
mmap
?將這兩個段映射到虛擬地址空間,但物理內存尚未分配。 -
程序先加載到內存,用虛擬地址初始化了mm_struct,當進程首次執行?
0x400000
?處的指令時,觸發缺頁中斷,內核將?.text
?段的內容從磁盤加載到物理內存,并更新頁表。
問題是cpu怎么知道從哪里開始執行呢?ELF文件的LEF Header中有一個Entry point address 這個就是程序的入口地址。cpu中有一個寄存器EIP其中存放的是當前執行指令的下一條指令的地址,CR3寄存器執行頁表。所以當程序開始執行的就時候就將Entry point address中的地址load到cpu中的EIP寄存器中,然后程序從入口開始執行。
動態鏈接與動態庫加載
我們知道動態庫跟我們編譯鏈接好的可執行和程序之間是獨立的存在于磁盤的。
我們的所有依賴于動態庫的可執行文件都依賴于一個這個庫:/lib64/ld-linux-x86-64.so.2,lib64/ld-linux-x86-64.so.2 是 Linux 系統中的一個動態鏈接器庫文件,主要用于在程序運行時動態加載和鏈接共享庫(.so 文件)
在我們要運行可執行程序時,我們先是跟靜態庫一樣的過程,先通過Entry point address找到程序的入口,事實上程序的入口就是_start函數,這是一個由C運?時庫(通常是glibc)或鏈接器(如ld)提供的特殊函數。在_start函數中會執行一下一系列操作:
1.設置堆棧:為程序設置一個初始的堆棧環境
2.初始化數據段:將程序的數據段(全局變量和靜態變量)從初始化數據段復制到相應的內存位置,并清零未初始化的數據段。
3.動態鏈接:_start函數會調用動態鏈接器的代碼來解析和加載程序運行所需要的動態庫,動態連接器會處理所有的符號解析和重定位,確保程序中的調用函數和變量訪問能夠正確的映射到動態庫中的實際地址。(動態鏈接實際上將鏈接的整個過程推遲到了程序加載的時候)
動態連接的優點:可以看到對于不同的進程如果需要同一個庫中的函數,我們只需要在內存中加載一份動態庫,分配一份物理地址即可,但是對于靜態庫來說,其可執行文件就是已經包含靜態庫中的函數的了,所以其磁盤空間和內存空間都是會產生浪費的。
動態鏈接器? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 動態鏈接器(如ld-linux.so)負責在程序運?時加載動態庫。
但是我們的程序具體是怎么和庫映射起來的?
首先可執行程序中存有依賴的動態庫的路徑,通過這個路徑可以將動態庫加載到物理內存。動態庫也是采用了平坦模式進行編址,我們叫做庫中方法的偏移量。然后通過創建新的mm_area_struct用庫的大小開辟一段新的進程地址空間,就能得到庫的虛擬地址,并建立頁表映射關系。通過庫的虛擬地址和庫中的偏移量就能找到對應的方法。
?所以庫函數的調用機制如下:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?庫已經被我們映射到了當前進程的地址空間中 庫的虛擬起始地址我們也已經知道了,庫中每?個? ??法的偏移量地址我們也知道
?所有:訪問庫中任意?法,只需要知道庫的起始虛擬地址+?法偏移量即可定位庫中的?法
GOT表
那GOT具體是怎么工作的呢?? ? ?比如,程序在編譯時,對于外部函數比如printf,編譯器并不知道它運行時的具體地址,所以會在GOT中生成一個條目。當程序第一次調用printf時,動態鏈接器(如ld-linux.so)會找到printf的實際地址并填入GOT中,之后的調用就直接使用這個地址了。這樣可以實現延遲綁定,也就是PLT(Procedure Linkage Table)和GOT配合使用。PLT負責跳轉到GOT中的地址,而GOT存儲實際的地址。第一次調用時,GOT中的地址可能指向PLT中的解析代碼,由動態鏈接器完成地址解析后,GOT中的條目會被更新為正確的地址。另外,GOT還可能用于全局變量的訪問,因為動態庫中的全局變量地址在加載時確定,也需要通過GOT來間接訪問。