目錄
一、靜態鏈接
1、靜態鏈接的基本概念
1. 靜態鏈接實例分析
2. 目標文件分析
3. 關鍵觀察
4. 重定位機制
5. 注意事項
2、靜態鏈接過程詳解
1. 目標文件反匯編分析(上面已分析)
2. 符號表分析
code.o 符號表
hello.o 符號表
3. 鏈接后的可執行文件分析
可執行文件符號表
可執行文件段信息
4. 鏈接地址修正驗證
5. 靜態鏈接過程總結
二、ELF加載與進程地址空間
1、虛擬地址/邏輯地址
核心問題探討
平坦模式 vs. 分段模式(歷史對比)
2、進程虛擬地址空間再認識
ELF入口地址機制
關鍵技術要點
3、使用圖來說明過程
1. ??磁盤存儲階段(右側)??
??2. 加載過程(箭頭)??
??3. 物理內存管理(中部)??
??4. CPU執行機制(左側)??
5. ??地址轉換關鍵路徑??
三、動態鏈接與動態庫加載
1、進程如何訪問動態庫
1. ??進程與內存管理結構??
2. ??虛擬地址到物理內存的映射??
3. ??動態庫的加載過程??
4. ??進程訪問動態庫的流程??
5. ??關鍵特性??
圖示總結
2、進程間如何共享動態庫
基本原理
詳細過程解析
1. 進程結構
2. 動態庫加載過程
3. 內存管理細節
4. 優勢
實際應用示例
3、動態鏈接機制詳解
1. 動態鏈接概述
為什么默認使用動態鏈接而非靜態鏈接?
2、動態鏈接工作原理
3、可執行文件中的動態鏈接痕跡
4、程序啟動流程與動態鏈接
5. 動態庫中的地址編址方案
動態庫反匯編示例
6. 程序與動態庫的映射機制
關鍵實現原理
技術實現要點
??1. 進程虛擬地址空間布局(左側起點)??
??2. 文件系統定位庫文件(右側起點)??
??3. 庫加載與映射流程(核心箭頭方向)??
??4. 關鍵機制與數據結構??
??總結??
7. 庫函數調用機制詳解
調用前提條件
調用原理
調用過程特征
8. 全局偏移量表(GOT)機制詳解
GOT基本概念
動態鏈接的必要條件
地址重定位機制
GOT的創新設計
ELF文件中的GOT
加載特性
9. 全局偏移量表(GOT)與位置無關代碼(PIC)機制詳解
GOT表的進程隔離特性
GOT表的定位機制
動態鏈接調用流程
位置無關代碼(PIC)原理
實例分析
PLT(過程鏈接表)說明
10. 庫間依賴關系
動態鏈接中的庫依賴
動態鏈接的優勢
依賴關系的解析
四、總結:靜態鏈接與動態鏈接對比
1、靜態鏈接
2、動態鏈接
3、核心區別
一、靜態鏈接
1、靜態鏈接的基本概念
????????靜態鏈接的本質是將目標文件(.o)進行連接的過程,無論是用戶自己編譯的.o文件還是靜態庫中的.o文件。因此,研究靜態鏈接的核心就是理解.o文件是如何被鏈接的。
1. 靜態鏈接實例分析
下面通過一個具體示例演示靜態鏈接過程,源代碼如下:
// hello.c
#include<stdio.h>
void run();
int main()
{printf("hello world!\n");run();return 0;
}// code.c
#include<stdio.h>
void run()
{printf("running...\n");
}
-
首先查看源代碼文件:
-
編譯生成目標文件:
gcc -c *.c
-
鏈接生成可執行文件:
gcc *.o -o main.exe
-
查看生成的文件:
2. 目標文件分析
使用objdump工具查看目標文件的反匯編代碼:
-
code.o的反匯編結果:
objdump -d code.o
-
hello.o的反匯編結果:
objdump -d hello.o
3. 關鍵觀察
-
使用
objdump -d
命令可以查看目標文件的代碼段(.text)的反匯編結果。 -
如圖,在hello.o中,main函數調用了printf和run函數,但在編譯階段并不知曉這些函數的具體位置,所以call對應的跳轉地址全都被臨時設置為0。
-
如圖,在code.o中,run函數調用了printf函數,同樣不知道其具體位置,call對應的跳轉地址全都被臨時設置為0。
4. 重定位機制
從反匯編結果可以看到,call指令的跳轉地址都被臨時設置為0。這是因為:
-
在編譯階段,編譯器無法知道外部函數(如printf和run)的內存地址。
-
編譯器會生成重定位表(.rela.text),記錄需要修正的地址位置。
-
鏈接器在鏈接階段會根據重定位表修正這些地址。
5. 注意事項
-
printf函數涉及到動態鏈接庫,其最終解析會在程序加載時完成(動態鏈接)。
-
靜態鏈接主要處理用戶定義的函數和靜態庫中的函數。
2、靜態鏈接過程詳解
1. 目標文件反匯編分析(上面已分析)
關鍵觀察:
-
在編譯階段,編譯器無法確定外部函數(如printf和run)的具體地址
-
call指令的跳轉地址被臨時設置為0
-
編譯器會生成重定位表(.rela.text),記錄需要修正的地址位置
2. 符號表分析
使用readelf
查看符號表信息:
code.o 符號表
readelf -s code.o
hello.o 符號表
符號表關鍵信息:
-
UND (undefine) 表示該符號在當前目標文件中未定義
-
puts是printf的實際實現
-
run函數在hello.o中未定義(定義在code.o中)
3. 鏈接后的可執行文件分析
可執行文件符號表
????????所有符號都有了確定的內存地址,將兩個.o文件合并后,在最終的可執行程序中,可以定位到run函數的內存地址000000000040052d,main函數地址為000000000040053d。這里的"FUNC"標記表明run是一個函數符號。數字13表示run函數所在的section在合并后被分配到的最終section索引。
可執行文件段信息
readelf -S main.exe
關鍵發現:兩個.o文件的.text段被合并到了可執行文件的第16個section中
4. 鏈接地址修正驗證
查看可執行文件反匯編代碼:# 反匯編main.exe只查看代碼段信息,包含源代碼
objdump -d main.exe
關鍵修正:
-
main函數中調用run的地址被修正為40052d
-
run函數中調用puts的地址被修正為400410
-
所有函數調用都有了正確的跳轉地址
5. 靜態鏈接過程總結
-
段合并:鏈接器將多個.o文件的相同段(.text, .data等)合并到一起
-
符號解析:確定所有符號的最終內存地址
-
地址修正:根據重定位表修正所有需要重定位的地址
-
最終布局:生成具有統一地址空間的可執行文件
????????靜態鏈接的核心是將多個獨立編譯的目標文件合并為一個完整的可執行文件,并解決所有跨文件引用的問題。通過符號解析和重定位,鏈接器確保了程序各部分能夠正確協同工作。
在鏈接過程中,程序會對目標文件(.o)中的外部符號進行地址重定位。
二、ELF加載與進程地址空間
1、虛擬地址/邏輯地址
核心問題探討
問題一:一個ELF程序在未被加載到內存時是否具有地址?
深入解析:
-
現代計算機采用"平坦模式"工作,要求ELF文件在編譯時就對自身的代碼和數據進行了統一編址
平坦模式 vs. 分段模式(歷史對比)
特性 平坦模式(現代) 分段模式(傳統 x86 實模式) 地址計算 直接使用線性地址(如? 0x12345678
)段寄存器 << 4 + 偏移
(如?DS:SI
?=?DS×16 + SI
)內存管理 分頁(Paging)為主 分段(Segmentation)為主 寄存器使用 通用寄存器直接尋址(如? MOV RAX, [RDI]
)必須指定段寄存器(如? MOV AX, [ES:DI]
)地址空間 連續、統一(如 64 位程序可訪問 2?? Bytes) 受限(16 位實模式僅 1MB,保護模式分段復雜) 典型應用 現代操作系統(Windows/Linux/macOS) 早期 DOS、16 位實模式程序 -
通過
objdump -S
反匯編可以看到,最左側顯示的地址就是ELF的邏輯地址(嚴格來說是起始地址+偏移量) -
這些地址以0為基準,在程序加載前就已確定,我們稱之為虛擬地址
問題二:進程的mm_struct、vm_area_struct在進程創建時的初始化數據來源?
關鍵發現:
-
初始化數據來源于ELF文件的各個segment
-
每個segment包含自己的起始地址和長度信息
-
內核使用這些信息初始化內存管理結構中的[start, end]范圍數據
-
詳細地址信息最終會填充到頁表中
重要結論:
????????虛擬地址機制不僅需要操作系統支持,編譯器也必須提供相應的支持,兩者協同工作才能實現完整的內存管理功能。
2、進程虛擬地址空間再認識
ELF入口地址機制
ELF文件編譯完成后,會將程序入口地址記錄在ELF header的Entry字段中:
gcc *.o
readelf -h a.out
關鍵技術要點
-
入口地址意義:
-
Entry point address (0x1060) 表示程序執行的起始位置
-
這個地址在鏈接階段由鏈接器確定
-
操作系統加載程序時會從這個地址開始執行
-
-
地址空間建立:
-
程序頭部表(program headers)描述了如何將各個segment映射到進程地址空間
-
每個segment的加載地址在編譯時就已經確定
-
操作系統根據這些信息建立進程的虛擬內存布局
-
-
動態鏈接特性:
-
Type字段顯示為"DYN"表示這是一個動態鏈接的可執行文件
-
動態鏈接會影響最終的內存布局和地址解析方式
-
????????這種設計使得程序可以在編譯時確定邏輯地址布局,同時保持加載時的靈活性,是現代操作系統內存管理的重要基礎。
3、使用圖來說明過程
????????該圖清晰地展示了程序從磁盤加載到內存并最終被CPU執行的全過程,涉及操作系統、內存管理和CPU硬件的關鍵機制。以下是對圖中各環節的深度技術分析:
1. ??磁盤存儲階段(右側)??
- 可執行程序以二進制形式存儲(ELF格式),Entry point address 0x1060指明程序入口虛擬地址
- 匯編代碼(如_start)以高級語言形式展示,實際存儲為機器碼(如0x55對應push ebp)
- 淺紫色圓柱體象征非易失性存儲,需注意此處代碼尚未被操作系統處理
??2. 加載過程(箭頭)??
- 動態鏈接器(ld-linux.so)根據程序頭表(Program Headers)將.text/.data段映射到內存
- 實際發生的是內存映射(mmap)而非物理拷貝,圖中"加載"應理解為虛擬地址空間的建立
- 可能觸發缺頁異常(Page Fault),此時才真正從磁盤讀取數據到物理頁
??3. 物理內存管理(中部)??
- 問題二中說明了對應進程的mm_struct、vm_area_struct在進程創建時的初始化數據來源
- 代碼被加載到0x1060(虛擬地址),經頁表轉換對應物理地址(聯想下面的紅色字)
- 需注意.text段被標記為RX(讀執行),.data段為RW(讀寫)權限
??4. CPU執行機制(左側)??
- EIP(x86)/RIP(x64)寄存器形成指令指針流水線:
- 取指階段:通過MMU查詢頁表(CR3→PML4→PDP→PD→PT)
- 執行階段:ALU運算時會檢查代碼段(CS)權限
- CR3寄存器存儲頂級頁表物理地址,實現進程地址空間隔離(每個進程獨立的CR3)
- CR3寄存器存儲??PML4表的物理基地址?
- 一句話來說就是EIP拿到頁表中的虛擬地址,然后CPU處理后放真實的物理地址在CR3中!!!最后放到頁表中!!!?
5. ??地址轉換關鍵路徑??
虛擬地址0x1060 → CR3定位PML4 → 各級頁表查詢 → 物理地址 → L1/L2緩存 → 執行單元
三、動態鏈接與動態庫加載
1、進程如何訪問動態庫
1. ??進程與內存管理結構??
- ??進程描述符(task_struct)??
每個進程在Linux內核中由一個task_struct
結構體表示,包含進程的所有元信息(如PID、優先級等)。圖中的"進程A"即通過task_struct
標識。 - ??內存描述符(mm_struct)??
mm_struct
是進程虛擬內存的核心管理結構,圖中以淺綠色標注。它定義了進程的虛擬地址空間布局,包括:- ??代碼區??:存放可執行指令(如動態庫的代碼段)。
- ??數據區??:存放全局變量、靜態變量等(如動態庫的數據段)。
- ??共享區??:專門映射共享庫的區域,通過虛擬地址訪問動態庫。
2. ??虛擬地址到物理內存的映射??
- ??頁表機制??
進程通過頁表(Page Table)將虛擬地址轉換為物理地址。圖中"頁表"是連接mm_struct
與物理內存的關鍵:- 當進程訪問共享庫的虛擬地址時,CPU通過頁表查詢對應的物理頁幀。
- 若物理內存中已加載庫(如
XXX.so
),頁表直接指向該位置;若未加載,觸發缺頁異常。
- ??共享區的特殊性??
動態庫的代碼段(.text
)在多個進程間可共享同一物理內存副本,而數據段(.data
)可能因寫時復制(Copy-On-Write)為每個進程創建私有副本。
3. ??動態庫的加載過程??
- ??從磁盤到物理內存??
- ??庫加載請求??:進程首次調用動態庫函數時,通過
ld.so
(動態鏈接器)發起加載請求。 - ??磁盤讀取??:內核從"磁盤"(圖中淺紫色區域)讀取
XXX.so
文件,解析其代碼段和數據段。 - ??物理內存映射??:將庫的代碼段映射到物理內存(淺紅色區域),數據段按需加載。
- ??庫加載請求??:進程首次調用動態庫函數時,通過
- ??內存映射優化??
動態庫通常以mmap
方式映射到進程地址空間,避免完全加載,僅在實際訪問時觸發缺頁異常加載對應頁。
4. ??進程訪問動態庫的流程??
- ??虛擬地址訪問??
進程通過共享區的虛擬地址(如調用XXX.so
中的函數)發起訪問。 - ??頁表查詢??
CPU查詢頁表:
- ??命中??:直接訪問物理內存中的XXX.so
代碼或數據。
- ??未命中??:觸發缺頁異常,內核將庫的對應部分從磁盤加載到物理內存,更新頁表。 - ??執行或讀寫??
- 代碼段:CPU從物理內存讀取指令執行。
- 數據段:讀寫操作可能觸發寫時復制(COW),確保進程間隔離。
5. ??關鍵特性??
- ??共享性??:多個進程的"共享區"可指向同一物理內存中的庫代碼段,節省內存。
- ??延遲加載??:動態庫的物理內存加載是惰性的,減少啟動開銷。
- ??寫時復制??:數據段的修改會為進程創建私有副本,保證安全性。
圖示總結
圖中從左到右的流程清晰展示了:
- 進程通過
mm_struct
管理虛擬地址空間。 - 頁表作為橋梁,將共享區的虛擬地址映射到物理內存中的
XXX.so
。 - 物理內存作為緩存,磁盤作為持久化存儲,共同支撐動態庫的運行時訪問。
這種機制高效平衡了性能(減少拷貝)與隔離性(COW),是現代操作系統的重要設計。
2、進程間如何共享動態庫
基本原理
????????動態庫(共享庫)在內存中只需要加載一次,就可以被多個進程共享使用,這是通過以下機制實現的:
- ??虛擬內存映射??:每個進程都有自己的虛擬地址空間,但可以映射到相同的物理內存區域
- ??寫時復制(Copy-On-Write)??:共享庫的只讀部分(代碼段)可以被多個進程共享,而可寫部分(數據段)在修改時會為每個進程創建副本
詳細過程解析
1. 進程結構
每個進程都有:
task_struct
:內核中表示進程的數據結構mm_struct
:管理進程內存空間的數據結構- 代碼區(text segment):存放進程專有代碼
- 數據區(data segment):存放進程專有數據
- 共享區:用于映射共享庫
2. 動態庫加載過程
-
??首次加載??:
- 當第一個進程(如進程A)需要使用動態庫(XXX.so)時
- 操作系統將磁盤上的XXX.so文件讀入物理內存
- 在進程A的虛擬地址空間中建立映射關系(通過頁表)
-
??后續進程共享??:
- 當另一個進程(如進程B)也需要使用同一個動態庫時
- 操作系統不會再次從磁盤加載庫文件
- 而是讓進程B的虛擬地址空間映射到已經存在于物理內存中的庫代碼
- 通過頁表建立新的虛擬-物理地址映射關系
3. 內存管理細節
- ??頁表作用??:每個進程有自己的頁表,將虛擬地址轉換為物理地址
- ??共享機制??:
- 庫的代碼段(只讀)在物理內存中只有一份副本
- 所有進程的頁表中對應部分都指向相同的物理頁
- ??數據段處理??:
- 庫的全局數據區通常使用寫時復制技術
- 初始時所有進程共享相同的物理頁
- 當有進程嘗試修改數據時,內核會為該進程創建私有副本
4. 優勢
- ??節省內存??:多個進程共享同一份庫代碼,減少物理內存占用
- ??提高性能??:避免重復加載相同的庫文件
- ??簡化更新??:更新庫文件時,只需替換磁盤上的文件,新啟動的進程會自動使用新版本
實際應用示例
當運行多個使用同一動態庫(如libc.so)的程序時:
- 第一個程序啟動時,libc.so被加載到物理內存
- 后續啟動的程序直接共享已加載的libc.so代碼
- 每個程序有自己的數據段副本(如果需要修改全局數據)
這種機制是現代操作系統高效管理內存資源的重要手段之一。
3、動態鏈接機制詳解
1. 動態鏈接概述
????????在現代操作系統中,動態鏈接已成為比靜態鏈接更為常用的鏈接方式。通過分析一個簡單程序a.out的動態庫依賴關系,我們可以看到它主要依賴于C語言運行時庫:
ldd a.out
技術說明:
ldd
命令用于顯示程序或庫文件所依賴的共享庫列表。
libc.so
是C語言的運行時庫,提供了標準輸入輸出、文件操作、字符串處理等基礎功能。
為什么默認使用動態鏈接而非靜態鏈接?
????????靜態鏈接會將所有目標文件和使用的庫合并為一個獨立的可執行文件,雖然具有無需額外依賴的優勢,但存在兩個主要問題:
-
文件體積膨脹:生成的二進制文件體積顯著增大
-
內存資源浪費:相同功能的代碼會在不同進程的內存空間中重復加載
????????隨著軟件復雜度提升,靜態鏈接會導致操作系統臃腫化,不同軟件包含相同功能代碼會造成大量存儲空間浪費。
動態鏈接通過以下方式解決這些問題:
-
將共享代碼提取為獨立的動態鏈接庫
-
在程序運行時才加載所需庫到內存
-
同一庫在內存中僅保留一份副本,供多個進程共享
2、動態鏈接工作原理
動態鏈接的核心機制是將鏈接過程推遲到程序加載時進行。具體流程如下:
-
程序啟動時,操作系統首先加載程序代碼和數據
-
同時加載程序依賴的所有動態庫到內存
-
操作系統根據當前地址空間使用情況,為每個動態庫動態分配內存地址
-
動態庫加載地址確定后,系統會修正庫中所有函數的跳轉地址
3、可執行文件中的動態鏈接痕跡
通過分析常見工具ls
和示例程序main.exe
的依賴關系:
4、程序啟動流程與動態鏈接
在C/C++程序中,執行流程并非直接從main
函數開始,而是遵循以下順序:
-
入口點_start:
-
由C運行時庫(glibc)或鏈接器(ld)提供
-
執行關鍵初始化操作:
-
設置程序堆棧環境
-
初始化數據段(全局/靜態變量)
-
動態鏈接處理:調用動態鏈接器解析和加載依賴庫
-
-
-
動態鏈接器(ld-linux.so):
-
解析程序動態庫依賴關系
-
加載所需庫到內存
-
處理符號解析和地址重定位
-
-
庫搜索機制:
-
通過
LD_LIBRARY_PATH
環境變量指定搜索路徑 -
讀取
/etc/ld.so.conf
配置文件 -
使用
/etc/ld.so.cache
緩存提高加載效率
-
-
移交控制權:
-
調用
__libc_start_main
完成額外初始化-
設置信號處理器
-
初始化線程庫(如使用多線程)
-
-
最終調用用戶
main
函數 -
處理
main
返回值并調用_exit
終止程序
-
開發者須知:雖然這些底層細節對大多數開發者透明(即不可見),了解此流程有助于深入理解程序執行機制和調試復雜問題。
5. 動態庫中的地址編址方案
????????動態庫為了實現靈活加載并映射到任意進程地址空間,對其內部方法采用相對地址編址方案。這種設計使得動態庫可以被加載到進程地址空間的任意位置,同時保持內部引用關系的正確性。
技術說明:實際上可執行文件也采用類似的平坦內存模式(Flat Memory Model),只是可執行文件通常被直接加載到固定地址。
動態庫反匯編示例
在Linux系統中,可以使用objdump
工具查看動態庫的反匯編代碼:
# Ubuntu系統示例
objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less# CentOS系統示例
objdump -S /lib64/libc-2.17.so | less
6. 程序與動態庫的映射機制
關鍵實現原理
-
動態庫的文件本質:
-
動態庫本質上是特殊格式的二進制文件
-
使用前必須像普通文件一樣被加載到內存
-
-
映射關系建立:
-
進程通過文件操作定位并加載動態庫
-
動態鏈接器將庫內容映射到進程的虛擬地址空間
-
函數調用通過虛擬地址跳轉實現
-
-
地址轉換過程:
技術實現要點
-
文件映射:使用
mmap
系統調用將庫文件映射到進程地址空間 -
地址重定位:動態鏈接器在加載時處理所有重定位項
-
延遲綁定:通過PLT(Procedure Linkage Table)和GOT(Global Offset Table)實現高效符號解析
性能提示:現代系統采用地址空間布局隨機化(ASLR)增強安全性,這要求動態庫必須支持位置無關代碼(PIC)才能正常工作。
????????這張圖詳細展示了Linux系統中動態鏈接庫(如libc.so
)從磁盤加載到進程虛擬地址空間的完整流程,涉及內存管理、文件系統和頁表映射等核心機制。
??1. 進程虛擬地址空間布局(左側起點)??
- ??
task_struct
??:代表進程的PCB(進程控制塊),內含內存管理結構體mm_struct
。 - ??
mm_struct
??:管理進程的虛擬內存,核心成員mmap
是一個鏈表,鏈表中每個節點是一個vm_area_struct
(VMA),描述一段連續的虛擬內存區域(如代碼區、數據區、堆、棧、共享庫映射區等)。 - ??VMA劃分??:圖中標注了典型的進程地址空間區域:
- ??代碼區??(
.text
):存放程序指令。 - ??數據區??(
.data
/.bss
):存放全局/靜態變量。 - ??堆區??(向上增長):動態內存分配(
malloc
)。 - ??共享區??:映射共享庫(如
libc.so
)。 - ??棧區??(向下增長):局部變量和函數調用棧。
- ??代碼區??(
??2. 文件系統定位庫文件(右側起點)??
- ??磁盤文件路徑??:如
/lib64/libc.so
,通過文件系統(如ext2)的inode
定位數據塊。- ??
struct file
??:內核中表示打開的文件,包含struct path
。 - ??
struct dentry
??:目錄項,關聯到inode
(如ext2_inode
),通過i_block
數組找到文件數據在磁盤上的物理塊。
- ??
??3. 庫加載與映射流程(核心箭頭方向)??
-
??步驟1:從磁盤讀取庫數據??
- 通過
dentry
→inode
→i_block
鏈,定位磁盤上libc.so
的數據塊。 - 數據首先被讀入內核的??頁緩存??(圖中備注“文件內緩沖區”)。
- 通過
-
??步驟2:物理內存加載??
內核將庫文件的代碼和數據從頁緩存加載到物理內存(圖中“庫的代碼和數據”區域)。 -
??步驟3:頁表映射??
- 內核為進程創建??頁表項??,將物理內存中的庫數據映射到進程虛擬地址空間的??共享區??(VMA的
vm_start
~vm_end
)。 - 映射通過
vm_area_struct
完成:其vm_file
指向struct file
,關聯到庫文件。
- 內核為進程創建??頁表項??,將物理內存中的庫數據映射到進程虛擬地址空間的??共享區??(VMA的
-
??步驟4:返回虛擬地址??
進程通過vm_area_struct
獲取庫映射的起始地址(vm_start
),后續可通過該地址訪問庫函數。
??4. 關鍵機制與數據結構??
- ??共享內存優化??:多個進程映射同一庫文件時,物理內存中只需一份副本,通過不同進程的頁表映射到各自的虛擬地址空間(節省內存)。
- ??延遲加載??:實際代碼/數據可能在首次訪問時通過缺頁異常觸發加載(圖中未明確體現)。
- ??顏色高亮??:紅色標注的
vm_area_struct
和共享區,強調動態庫映射的核心路徑。
??總結??
該圖清晰地串聯了從文件系統到內存管理的完整鏈路:
??????????進程訪問庫函數?? → 通過VMA找到共享區映射 → 頁表指向物理內存 → 若未加載則從磁盤讀取 → 最終執行庫代碼。
這一流程是Linux動態鏈接和共享庫運行的基礎。
7. 庫函數調用機制詳解
調用前提條件
-
庫映射完成:目標庫已被映射到當前進程的地址空間中
-
地址信息已知:
-
已知庫的虛擬起始地址
-
已知庫中每個方法的偏移量地址
-
調用原理
訪問庫中的任意方法只需進行簡單地址計算:方法實際地址 = 庫的起始虛擬地址 + 方法偏移量
調用過程特征
-
地址空間內跳轉:整個調用過程完全在進程地址空間內完成
-
兩階段執行流:
-
從代碼區跳轉到共享區(執行庫函數)
-
執行完畢后返回到代碼區繼續執行
-
這種機制實現了高效的動態庫調用,同時保持了進程地址空間的隔離性和安全性。
8. 全局偏移量表(GOT)機制詳解
GOT基本概念
????????全局偏移量表(Global Offset Table, GOT)是動態鏈接過程中的核心數據結構,用于解決共享庫函數地址的動態解析問題。
動態鏈接的必要條件
-
預先加載:程序運行前,所有依賴的庫必須完成加載和地址空間映射
-
地址預知:所有庫的起始虛擬地址需要在程序運行前確定
地址重定位機制
-
加載時重定位:在內存中對程序的庫函數調用地址進行二次設置
-
技術挑戰:
-
代碼區(.text)通常是只讀的,無法直接修改
-
直接修改代碼段會違反內存保護機制
-
GOT的創新設計
-
專用數據區域:
-
在.data段(或庫自身)中專門預留可讀寫區域
-
用于存儲函數和全局變量的跳轉地址
-
-
關鍵特性:
-
位于可讀寫內存區域(.data)
-
支持運行時動態修改
-
每個表項對應一個需要引用的全局符號地址
-
ELF文件中的GOT
通過工具可以查看GOT相關信息:
readelf -S a.out
readelf -l a.out
加載特性
-
GOT在加載時會與.data段合并為一個可讀寫內存段
-
這種設計既保持了代碼段的只讀屬性,又實現了地址的動態解析
9. 全局偏移量表(GOT)與位置無關代碼(PIC)機制詳解
GOT表的進程隔離特性
-
代碼段共享性:由于代碼段(.text)是只讀的,可以被所有進程共享
-
GOT表私有性:
-
不同進程中動態庫的加載地址各不相同
-
每個進程需要維護獨立的GOT表副本
-
進程間不能共享GOT表,確保地址空間的隔離
-
GOT表的定位機制
相對尋址優勢:
-
在單個共享對象(.so)內部,GOT表與代碼段的相對位置固定
-
可通過CPU的相對尋址指令(如RIP相對尋址)高效定位GOT表
動態鏈接調用流程
-
間接跳轉機制:
call puts@plt // 1. 調用PLT樁代碼 // 在PLT中: jmp *GOT[puts_offset] // 2. 跳轉到GOT表中存儲的地址
-
地址延遲綁定:
-
首次調用時,GOT表項指向動態鏈接器解析例程
-
解析完成后,動態鏈接器將真實函數地址回填到GOT表
-
后續調用直接跳轉到目標函數
-
位置無關代碼(PIC)原理
-
核心思想:
-
代碼不包含絕對地址引用
-
所有外部引用通過GOT表間接訪問
-
使用相對偏移量進行內部訪問
-
-
技術優勢:
-
動態庫可加載到任意內存地址
-
代碼段可被多個進程共享
-
編譯時需指定
-fPIC
選項(PIC = 相對編址 + GOT)
-
實例分析
通過objdump查看PLT/GOT交互:
objdump -S a.out
PLT(過程鏈接表)說明
PLT(Procedure Linkage Table)是:
-
延遲綁定的關鍵組件
-
包含調用外部函數的樁代碼(stub)
-
首次調用時觸發動態鏈接器進行符號解析
-
后續調用直接通過GOT表跳轉
-
與GOT表配合實現"惰性綁定"優化
10. 庫間依賴關系
動態鏈接中的庫依賴
????????動態鏈接不僅涉及可執行程序對庫的調用,還包括庫與庫之間的相互調用。為了實現庫之間的地址無關性(Position-Independent Code, PIC),動態鏈接采用了以下機制:
-
全局偏移表(GOT)
-
每個動態庫和可執行文件都包含自己的GOT(Global Offset Table),用于存儲外部函數和變量的實際地址。
-
GOT在運行時由動態鏈接器填充,確保庫之間的調用能夠正確跳轉到目標地址。
-
可以通過GDB調試觀察GOT表地址的變化。推薦下面的博客:通過GDB學透PLT與GOT_plt got-CSDN博客
-
-
延遲綁定(PLT機制)
-
為了優化性能,動態鏈接采用了延遲綁定(Lazy Binding)技術,通過過程鏈接表(Procedure Linkage Table, PLT)實現。
-
GOT中的跳轉地址默認會指向?段輔助代碼,它也被叫做樁代碼/stup。
-
在函數第一次被調用時,動態鏈接器才會解析其實際地址并更新GOT表,后續調用直接跳轉到目標函數。
-
這種機制避免了程序啟動時對所有函數進行重定位的開銷,尤其適合動態庫中較少被調用的函數。
-
動態鏈接的優勢
????????動態鏈接將符號解析和地址重定位推遲到運行時,雖然犧牲了一定的加載性能,但帶來了以下顯著優勢:
-
節省資源:多個程序可以共享同一動態庫的代碼段,減少磁盤和內存占用。
-
便于維護:庫的更新無需重新編譯可執行文件,只需替換動態庫文件。
-
代碼復用:實現了二進制級別的模塊化,支持靈活的庫依賴關系。
依賴關系的解析
????????動態鏈接器在加載程序時,會遞歸解析所有依賴的庫,并完善各模塊的GOT表,確保庫之間的調用能夠正確跳轉。
四、總結:靜態鏈接與動態鏈接對比
1、靜態鏈接
-
模塊化開發:允許開發者獨立編譯和測試模塊,最終通過靜態鏈接合并為單一可執行文件。
-
靜態重定位:在編譯時修正模塊間的函數和變量地址,生成完全獨立的可執行文件。
2、動態鏈接
-
運行時重定位:將鏈接過程推遲到程序加載時,動態庫的加載地址不固定,但通過GOT實現地址無關調用。
-
性能權衡:雖然增加了加載時間,但顯著提升了資源利用率和維護便利性。
3、核心區別
-
靜態鏈接在編譯時完成所有地址綁定,生成自包含的可執行文件。
-
動態鏈接在運行時通過GOT和PLT機制實現靈活、高效的庫調用,支持代碼共享和動態更新。