??所屬專欄:Linux??
??作者主頁:嶔某??
ELF文件
什么是編譯?編譯就是將程序源代碼編譯成能讓CPU直接執行的機器代碼
如果我們要編譯一個 .c
文件,使用gcc -c
將.c文件編譯為二進制文件.o
,如果一個項目有多個.c
文件,會生成多個 .o
文件,如果我們修改一個 .c
文件,那么只需要將這一個 .c
文件重新編譯即可。不需要浪費時間去重新編譯整個工程文件。目標文件就是一種二進制文件,ELF
是二進制文件的格式,也是對二進制文件的封裝
ubuntu@VM-4-4-ubuntu:~/Code/25/2_13$ file test.o
test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
要理解編譯的細節,就先要了解ELF
文件,以下四種都是ELF
文件:
- 可重定位?件(Relocatable File) :即
xxx.o
文件。包含適合于與其他目標文件鏈接來創建可執行文件或者共享目標文件的代碼和數據。 - 可執??件(Executable File) :即可執行程序。
- 共享?標?件(Shared Object File) :即
xxx.so
文件。 - 內核轉儲(core dumps) ,存放當前進程的執?上下?,?于
dump
信號觸發。
每個ELF
文件都由以下部分組成:
- ELF頭(ELF header) :描述文件的主要特性。其位于文件的開始位置,它的主要目的是定位文件的其他部分。
- 程序頭表(Program header table) :列舉了所有有效的段(
segments
)和他們的屬性。表?記著每個段的開始的位置和位移(offset
)、長度,畢竟這些段,都是緊密的放在二進制?件中,需要段表的描述信息,才能把他們每個段分割開。 - 節頭表(Section header table) :包含對節(
sections
)的描述。 - 節(Section ):
ELF
文件中的基本組成單位,包含了特定類型的數據。ELF
文件的各種信息和數據都存儲在不同的節中,如代碼節存儲了可執行代碼,數據節存儲了全局變量和靜態數據等。
最常見的節有代碼節.text
和數據節.data
,代碼節用于保存機器指令,是程序的主要執行部分。數據節保存已經初始化的全局變量和局部靜態變量。
ELF形成到加載的大概輪廓
ELF形成可執行文件
- 將多個
C/C++
源代碼翻譯為目標.o
文件 - 將多個
.o
文件的section
合并
具體的合并方式會比這復雜,是在鏈接時合并的,并且還涉及到對庫的合并
ELF可執行文件加載
- 在一個
ELF
文件中也會有許多不同的Section
,在加載到內存的時候,也會進行Section
的合并,形成Segment
- 合并原則:相同屬性(可讀,可寫,可執行,加載時需要申請內存空間)
- 即使時不同的
Section
,加載到內存之后,可能都會在一個Segment
里 - 很顯然,這種合并方法已經在形成ELF時就已經確定了,并且記錄在了
ELF
的程序頭表Program header table
中
# 查看可執行程序的Section
ubuntu@VM-4-4-ubuntu:~/Code/25/2_13$ readelf -S a.out
There are 31 section headers, starting at offset 0x37b8:Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .interp PROGBITS 0000000000000318 00000318000000000000001c 0000000000000000 A 0 0 1[ 2] .note.gnu.pr[...] NOTE 0000000000000338 000003380000000000000030 0000000000000000 A 0 0 8[ 3] .note.gnu.bu[...] NOTE 0000000000000368 000003680000000000000024 0000000000000000 A 0 0 4[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c0000000000000020 0000000000000000 A 0 0 4[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b00000000000000024 0000000000000000 A 6 0 8[ 6] .dynsym DYNSYM 00000000000003d8 000003d800000000000000c0 0000000000000018 A 7 1 8[ 7] .dynstr STRTAB 0000000000000498 0000049800000000000000c8 0000000000000000 A 0 0 1[ 8] .gnu.version VERSYM 0000000000000560 000005600000000000000010 0000000000000002 A 6 0 2[ 9] .gnu.version_r VERNEED 0000000000000570 000005700000000000000050 0000000000000000 A 7 2 8[10] .rela.dyn RELA 00000000000005c0 000005c000000000000000c0 0000000000000018 A 6 0 8[11] .rela.plt RELA 0000000000000680 000006800000000000000018 0000000000000018 AI 6 24 8[12] .init PROGBITS 0000000000001000 00001000000000000000001b 0000000000000000 AX 0 0 4[13] .plt PROGBITS 0000000000001020 000010200000000000000020 0000000000000010 AX 0 0 16[14] .plt.got PROGBITS 0000000000001040 000010400000000000000010 0000000000000010 AX 0 0 16[15] .plt.sec PROGBITS 0000000000001050 000010500000000000000010 0000000000000010 AX 0 0 16[16] .text PROGBITS 0000000000001060 000010600000000000000107 0000000000000000 AX 0 0 16[17] .fini PROGBITS 0000000000001168 00001168000000000000000d 0000000000000000 AX 0 0 4[18] .rodata PROGBITS 0000000000002000 000020000000000000000014 0000000000000000 A 0 0 4[19] .eh_frame_hdr PROGBITS 0000000000002014 000020140000000000000034 0000000000000000 A 0 0 4[20] .eh_frame PROGBITS 0000000000002048 0000204800000000000000ac 0000000000000000 A 0 0 8[21] .init_array INIT_ARRAY 0000000000003da8 00002da80000000000000008 0000000000000008 WA 0 0 8[22] .fini_array FINI_ARRAY 0000000000003db0 00002db00000000000000008 0000000000000008 WA 0 0 8[23] .dynamic DYNAMIC 0000000000003db8 00002db80000000000000200 0000000000000010 WA 7 0 8[24] .got PROGBITS 0000000000003fb8 00002fb80000000000000048 0000000000000008 WA 0 0 8[25] .data PROGBITS 0000000000004000 000030000000000000000010 0000000000000000 WA 0 0 8[26] .bss NOBITS 0000000000004010 000030100000000000000008 0000000000000000 WA 0 0 1[27] .comment PROGBITS 0000000000000000 000030100000000000000026 0000000000000001 MS 0 0 1[28] .symtab SYMTAB 0000000000000000 0000303800000000000003c0 0000000000000018 29 21 8[29] .strtab STRTAB 0000000000000000 000033f800000000000002a0 0000000000000000 0 0 1[30] .shstrtab STRTAB 0000000000000000 00003698000000000000011a 0000000000000000 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),D (mbind), l (large), p (processor specific)# 查看Section合并的Segment
readelf -l a.outElf file type is DYN (Position-Independent Executable file)
Entry point 0x10a0
There are 13 program headers, starting at offset 64Program Headers:Type Offset VirtAddr PhysAddrFileSiz MemSiz Flags AlignPHDR 0x0000000000000040 0x0000000000000040 0x00000000000000400x00000000000002d8 0x00000000000002d8 R 0x8INTERP 0x0000000000000318 0x0000000000000318 0x00000000000003180x000000000000001c 0x000000000000001c R 0x1[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]LOAD 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000698 0x0000000000000698 R 0x1000LOAD 0x0000000000001000 0x0000000000001000 0x00000000000010000x00000000000001ed 0x00000000000001ed R E 0x1000LOAD 0x0000000000002000 0x0000000000002000 0x00000000000020000x00000000000000fc 0x00000000000000fc R 0x1000LOAD 0x0000000000002da8 0x0000000000003da8 0x0000000000003da80x0000000000000268 0x0000000000000270 RW 0x1000DYNAMIC 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x00000000000001f0 0x00000000000001f0 RW 0x8NOTE 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030 R 0x8NOTE 0x0000000000000368 0x0000000000000368 0x00000000000003680x0000000000000044 0x0000000000000044 R 0x4GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030 R 0x8GNU_EH_FRAME 0x000000000000201c 0x000000000000201c 0x000000000000201c0x0000000000000034 0x0000000000000034 R 0x4GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 0x10GNU_RELRO 0x0000000000002da8 0x0000000000003da8 0x0000000000003da80x0000000000000258 0x0000000000000258 R 0x1Section to Segment mapping:Segment Sections...00 01 .interp 02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .plt.got .plt.sec .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .dynamic .got .data .bss 06 .dynamic 07 .note.gnu.property 08 .note.gnu.build-id .note.ABI-tag 09 .note.gnu.property 10 .eh_frame_hdr 11 12 .init_array .fini_array .dynamic .got
為什么要將
Section
合并成為Segment
?
Section
合并的主要原因是為了減少??碎?,提?內存使?效率。如果不進?合并,假設????為4096
字節(內存塊基本??,加載,管理的基本單位),如果.text
部分為4097
字節,.init
部分為512
字節,那么它們將占?3個??,?合并后,它們只需2個??。- 此外,操作系統在加載程序時,會將具有相同屬性的
section
合并成?個?的segment
,這樣就可以實現不同的訪問權限,從?優化內存管理和權限訪問控制
對于程序頭表和節頭表?有什么?呢,其實 ELF
?件提供 2 個不同的視圖/視?來讓我們理解這兩個部分:
- 鏈接視圖
Linking view
對應節頭表Section header table
- 文件結構的粒度更細,將文件按功能模塊的差異進行劃分,靜態鏈接分析的時候一般關注的是鏈接視圖,能夠理解ELF文件中包含的各個部分的信息。
- 為了空間布局上的效率,將來在鏈接目標文件時,鏈接器會把很多節
section
合并規整成可執行的段segment
、可讀寫的段、只讀段等。合并之后,空間利用率提高了。否則很小很小的一段,會浪費很多物理內存(物理內存頁分配一般都是4k的整數倍一起給你),所以,鏈接器趁著鏈接就把小塊們都合并了。
- 執行視圖
execution view
對應程序頭表Program header table
- 告訴操作系統,如何加載可執行文件,完成進程內存的初始化。一個可執行程序的格式中,一定有
program header table
。
說白了,一個在鏈接時作用,一個在運行加載中作用。
從鏈接視圖來看:
- 命令
readelf -S hello.o
可以幫助查看ELF
文件的節頭表 .text
節:是保存了程序代碼指令的代碼節。.data
節:保存了初始化的全局變量和局部靜態變量等數據。.rodata
節:保存了只讀的數據如一行C語言代碼中的字符串。由于.rodata
節是只讀的,所以只能存在于一個可執行文件的只讀段中。因此,只能是在text
段(不是data
段)中找到.rodata
節.BSS
節:為未初始化的全局變量和局部靜態變量預留位置.symtab
節:Symbol Table
符號表,就是源碼中的函數名,變量名和代碼的對應關系。.got.plt
節(全局偏移表-過程鏈接表):.got
節保存了全局偏移表。.got
節和.plt
節一起提供了對導入的共享函數的訪問入口,由動態鏈接器在運行時進行修改。
- 使用
readelf
命令查看.so
文件可以看到該節
從執行視圖來看:
- 告訴操作系統哪些模塊可以被加載進內存。
- 加載進內存之后哪些分段是可讀可寫,哪些分段是只讀,哪些分段是可執行的
我們可以在ELF
頭中找到文件的基本信息,以及可以看到ELF
頭是如何定位程序頭表和節頭表的。例如:
// 查看目標文件
$ readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64 # 文件類型
Data: 2's complement, little endian # 指定的編碼方式
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file) # 指出ELF文件的類型
Machine: Advanced Micro Devices X86-64 # 該程序需要的體系結構
Version: 0x1
Entry point address: 0x0 # 系統第?個傳輸控制的虛擬地址,在那啟動進程。假如文件沒有如何關聯的?口點,該成員就保持為0。
Start of program headers: 0 (bytes into file)
Start of section headers: 728 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes) # 保存著ELF頭大小(以字節計數)
Size of program headers: 0 (bytes) # 保存著在文件的程序頭表(program header table)中一個入口的大小
Number of program headers: 0 # 保存著在程序頭表中入口的個數。因此,e_phentsize和e_phnum的乘積就是表的大小(以字節計數).假如沒有程序頭表,變量為0。
Size of section headers: 64 (bytes) # 保存著section頭的大小(以字節計數)。一個section頭是在section頭表的一個入口
Number of section headers: 13 # 保存著在section headertable中的入口數目。因此,e_shentsize和e_shnum的乘積就是section頭表的??(以字節計數)。假如?件沒有section頭表,值為0。
Section header string table index: 12 # 保存著跟section名字字符表相關入口的section頭表(section header table)索引。// 查看可執?程序
$ gcc *.o
$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060
Start of program headers: 64 (bytes into file)
Start of section headers: 14768 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
理解連接與加載
靜態連接
無論是自己的.o
還是靜態庫中的.o
,其本質都是將.o
文件與本地源文件進行連接,所以研究靜態鏈接,本質就是研究這個。
使用objdump -d
將.o
文件反匯編,查看code.o
和hello.o
ubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ objdump -d code.ocode.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <run>:0: f3 0f 1e fa endbr644: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <run+0xf>f: 48 89 c7 mov %rax,%rdi12: e8 00 00 00 00 call 17 <run+0x17>17: 90 nop18: 5d pop %rbp19: c3 retubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ objdump -d hello.ohello.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: f3 0f 1e fa endbr644: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>f: 48 89 c7 mov %rax,%rdi12: e8 00 00 00 00 call 17 <main+0x17>17: b8 00 00 00 00 mov $0x0,%eax1c: e8 00 00 00 00 call 21 <main+0x21>21: b8 00 00 00 00 mov $0x0,%eax26: 5d pop %rbp27: c3 ret
可以很清楚的看到在code.o
中call
中的地址全是0,這說明在code.c
中根本不認識printf
函數,而在hello.o中也不認識printf
和run
。因為它們的跳轉地址都變成了全零。
因為在編譯的時候,編譯器是完全不知道其他函數是什么,位于內存的哪個區塊,代碼長什么樣,所以編譯器只能將這些函數的跳轉地址都先設為0
。這個地址會在鏈接的時候被修正,為了讓鏈接器在鏈接時能正確的重定位到這些被修正的地址,在代碼塊.data
中存在一張重定位表,在鏈接的時候就會根據這張表對地址進行修正。
靜態鏈接就是將本地的.o
文件和庫里的.a
文件進行鏈接,鏈接其實就是將編譯之后的所有?標文件連同?到的?些靜態庫運?時庫組合,拼裝成?個獨?的可執?文件。其中就包括我們之前提到的地址修正,當所有模塊組合在?起之后,鏈接器會根據我們的.o
?件或者靜態庫中的重定位表找到那些需要被重定位的函數全局變量,從?修正它們的地址。這其實就是靜態鏈接的過程。
所以,鏈接過程中會涉及到對.o
中外部符號進行地址重定位。
ELF加載與進程地址空間
虛擬地址/邏輯地址
- 一個ELF程序,在沒有加載到內存的時候,有沒有地址?
- 進程mm_struct、vm_area_struct在進程剛剛創建的時候,初始化數據從哪來的?
ans:
一個ELF程序,在沒有加載到內存的時候,是有地址的,當代計算機工作的時候都采用平坦模式進行工作。所以也要求ELF對自己的代碼和數據進行統一編址,我們再看hello.o
的反匯編,最左側的就是ELF的虛擬地址,嚴格來說應該叫做邏輯地址(起始地址+偏移量)但是我們認為起始地址就是0。
也就是說其實虛擬地址在程序還沒有加載到內存的時候,就已經把可執行程序進行統一編址了。
ubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ objdump -S hello.ohello.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: f3 0f 1e fa endbr644: 55 push %rbp5: 48 89 e5 mov %rsp,%rbp8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>f: 48 89 c7 mov %rax,%rdi12: e8 00 00 00 00 call 17 <main+0x17>17: b8 00 00 00 00 mov $0x0,%eax1c: e8 00 00 00 00 call 21 <main+0x21>21: b8 00 00 00 00 mov $0x0,%eax26: 5d pop %rbp27: c3 ret
進程mm_struct
、vm_area_struct
在進程創建的時候,從ELF文件的各個segment
獲取,每一個segment
有自己的其實地址和長度,就可以填充內核中的strat
、end
、頁表等數據。
所以操作系統和編譯器都支持虛擬地址機制。
所以ELF文件在被編譯好之后,會在ELF header
的Entry
字段中記錄程序的入口地址,運行時,這個地址最先被加載到CPU
寄存器里面,然后跟著運行后面的代碼。
ubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ gcc -o hello hello.c
ubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ readelf -h hello
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: DYN (Position-Independent Executable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060Start of program headers: 64 (bytes into file)Start of section headers: 13968 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 56 (bytes)Number of program headers: 13Size of section headers: 64 (bytes)Number of section headers: 31Section header string table index: 30
所以靜態鏈接就是將本地ELF
文件.o
和庫中的ELF
文件.a
鏈接,通過program header table
的信息將他們的segment
合并,統一進行平坦編址、這個是邏輯地址。然后程序運行的時候,內核生成對應的task_struct
,ELF
可執行文件加載到內存中,擁有了對應的物理地址,將虛擬地址和物理地址分別填入頁表,對應的mm_struct
等信息一填,然后將程序的Entry point address
加載到CPU
中,程序開始執行。
動態鏈接與動態庫加載
首先,動態鏈接比靜態鏈接要常用的多,大部分官方庫都是默認采用動態庫。使用ldd
命令查看一個可執行程序依賴的各種庫
ubuntu@VM-4-4-ubuntu:~/Code/25/2_24$ ldd hellolinux-vdso.so.1 (0x00007ffddc939000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5174a00000)/lib64/ld-linux-x86-64.so.2 (0x00007f5174e10000)
這里的libc.so
是C語言
的運行時庫里面提供了常用的標準輸入輸出文件字符串處理等功能。
這里要說的是,靜態鏈接會將編譯產生的所有目標文件,連同到用到的各種的庫一起合并為一個獨立的可執行文件,雖然它不需要任何額外依賴就可以運行。但是其最大的問題就是生成的文件體積太大了,相當耗費內存資源。隨著軟件復雜度的提升,操作系統也越來越臃腫,不同的軟件可能都包含了相同的部分功能和代碼,這顯然不利于計算機的發展。
所以,動態鏈接的優勢就體現在這里,我們將共享的代碼提取出來,封裝為一個單獨的動態鏈接庫。等到程序運行的時候加載到內存,不但可以節省空間,而且同一個模塊只需要在內存中加載一次,就可以被不同的進程所共享。
那么,動態鏈接是如何工作的?
首先,結論、動態鏈接將鏈接的整個過程推遲到了程序加載的時候。我們運行一個程序,操作系統首先將程序的數據代碼連同它用到的一系列動態庫先加載到內存,其中每個動態庫的加載地址都是不固定的從,操作系統會根據當前的內存使用情況為它們動態分配一段內存。當動態庫被加載到內存后,一旦物理地址(內存里的地址)確定,我們就可以去修正動態庫中的那些函數的跳轉地址了。
要徹底理清楚,先要知道我們的程序是怎么開始啟動的
在C/C++程序中,當程序開始運行時,它不是從main
函數開始執行的。實際上,程序的入口點是_start
這是一個由C
運行時庫glibc
或鏈接器ld
提供的特殊函數。在_start
函數中,會執行一系列初始化操作,這些操作包括:
- 設置堆棧,為程序創建一個初始堆棧環境。
- 初始化數據段,將程序的數據段(如全局和靜態變量)從初始化數據段復制到對應的內存位置,并清零未初始化的數據段
- 動態鏈接,
_start
函數會調用動態鏈接器的代碼來解析和加載程序所依賴的動態庫shareed libraries
。動態鏈接器會處理所有的符號解析和重定位,確保程序中的函數調用和變量訪問能夠正確的映射到動態庫的實際地址。
動態鏈接器:(以下簡稱
Dynamic Linker
)
- 動態鏈接器(如
ld-linux.so
)負責在程序運行時加載動態庫- 當程序啟動時,
Dy Linker
會解析程序中的動態庫依賴,并加載這些庫到內存中。環境變量和配置文件:
Linux
系統通過環境變量和配置文件來指定動態庫的默認搜索路徑。- 這些路徑會被動態鏈接器在加載動態庫時搜索。
緩存文件:
- 為了提高動態庫的加載效率,
Linux
系統會維護一個名為/etc/ld.so.cache
的緩存文件。- 該文件包含了系統中所有已知的動態庫路徑和相關信息,動態鏈接器在加載動態庫時會首先搜索這個緩存文件。
- 調用
__libc_start_main
,一旦動態鏈接完成,_start
函數就會被調用。__libc_start_main
這個函數又glibc
提供,負責執行一些額外的初始化工作,比如設置信號處理函數,初始化線程庫(如果使用了線程)等。 - 調用
main
函數,最后__libc_start_main
會調用mian
函數,此時,程序的控制權才正式的交給了用戶編寫的代碼。 - 處理
main
函數的返回值,main
函數返回時__libc_start_main
負責處理這個返回值,并最終調用_exit
函數來終止程序。
上述過程描述的時C/C++
程序在執行main
函數之前的一系列操作,這部分被編譯器“優化”掉的操作,對于大多數程序員來說時透明的。程序員通常只需要關注main
函數中的代碼,而不需要關心程序底層的初始化過程。了解這些過程可以使我們更好的理解程序的執行流程和Dbug
。
動態庫中的相對地址
動態庫為了能隨時加載,支持并映射到任意進程的任意位置,對動態庫中的方法統一編制,采用相對編址(平坦模式)的方案。(可執行程序,靜態庫,動態庫都采用從全零開始的相對編址)
動態庫本質也是一個磁盤上的文件,要加載也是要被打開的,要讓進程看到動態庫,不僅要將動態庫加載到內存中,進程要訪問動態庫,還要讓進程看到動態庫,也就是將動態庫的虛擬地址和物理地址分別填入進程的頁表中
通過上面這張圖,動態庫的加載過程我們清楚了,那么進程是怎么進行庫調用的呢?
- 庫已經被我們映射到了當前進程的地址空間中
- 庫的起始地址我們知道了,庫中每一個方法的偏移量我們也知道。
- 所以庫的起始地址+方法偏移量就可以定位庫中的任意方法。
- 整個調用過程,從代碼區跳轉到共享區,調用完后返回代碼區,整個過程都是在進程地址空間完成的。
- 也就是說程序運行之前,先把所有庫加載并映射,所有庫的起始虛擬地址我們都提前知道了。
- 然后對我們加載到內存中的程序的庫函數調用進行地址修改,在內存中二次完成地址設置(加載地址重定位)
- Wait!!!剛剛我們是不是修改了什么,修改了call后面的地址是吧?這不是代碼段嗎?代碼段不是只讀的不能修改嗎?
全局偏移量表GOT
(global offset table
)
所以,動態鏈接的做法是在.data
(或者庫里面)預留一片區域用來存放函數的跳轉地址,也被叫做全局偏移量表GOT
,表中每一項都是本運行模塊也要引用的一個全局變量或函數的地址。
.data
區是可讀寫的,支持動態修改。
$ readelf -S a.out
...
[24] .got PROGBITS 0000000000003fb8 00002fb80000000000000048 0000000000000008 WA 0 0 8
...
$ readelf -l a.out # .got在加載的時候,會和.data合并成為?個segment,然后加載在?起
- 由于代碼段只讀,我們不能直接修改代碼段。有了
GOT
表,代碼可以被所有進程共享。但在不同進程的地址空間中,各個動態庫的絕對地址、相對位置都不同。反應到GOT
表上,就是每個進程的每個動態庫都有獨立的GOT
表,所以進程間不能共享GOT
表 - 在單個
.so
下,由于GOT
表與.text
的相對位置都是固定的,我們完全可以利用CPU
的相對尋址找到GOT
表 - 在調用函數的時候會首先查表,然后根據表中的地址來進行跳轉,這些地址在動態庫加載時會被修改為真正的地址
- 這種方式實現的動態鏈接就被叫做
PIC 地址無關代碼
。也就是說,動態庫不需要做任何修改,被加載到任意內存地址都能正常運行,并且被所有進程共享,這也是為什么在制作動態庫時給編譯器指定-fPIC
參數的原因,PIC = 相對編址 + GOT
PLT 的作用和原理
由于動態鏈接在程序加載的時候需要對大量函數進?重定位,這?步顯然是非常耗時的。為了進一步降低開銷,我們的操作系統還做了?些其他的優化,比如延遲綁定Lazy Binding
,或者也叫PLT
(過程連接表Procedure Linkage Table
)。與其在程序?開始就對所有函數進行重定位,不如將這個過程推遲到函數第?次被調?的時候,因為絕大多數動態庫中的函數可能在程序運行期間?次都不會被使?到。
思路是:GOT
中的跳轉地址默認會指向?段輔助代碼,它也被叫做樁代碼/stup
。在我們第?次調?函數的時候,這段代碼會負責查詢真正函數的跳轉地址,并且去更新GOT
表。于是我們再次調?函數的時候,就會直接跳轉到動態庫中真正的函數實現。
總而言之,動態鏈接實際上將鏈接的整個過程,比如符號查詢、地址的重定位從編譯時推遲到了程序的運行時,它雖然犧牲了?定的性能和程序加載時間,但絕對是物有所值的。因為動態鏈接能夠更有效的利?磁盤空間和內存資源,以極大方便了代碼的更新和維護,更關鍵的是,它實現了?進制級別的代碼復用。
庫間依賴
不僅可執行程序會調用庫,庫也會調用其他庫,庫之間是有依賴的,那么如何做到庫和庫之間調用也是地址無關的呢?
因為庫中也有GOT
表,這就是為什么它們都是ELF
格式文件。
總結
靜態鏈接的出現,提?了程序的模塊化?平。對于一個大的項?,不同的人可以獨立地測試和開發自己的模塊。通過靜態鏈接,生成最終的可執行文件。
我們知道靜態鏈接會將編譯產生的所有目標文件,和用到的各種庫合并成?個獨立的可執行文件,其中我們會去修正模塊間函數的跳轉地址,也被叫做編譯重定位(也叫做靜態重定位)。
?動態鏈接實際上將鏈接的整個過程推遲到了程序加載的時候。比如我們去運??個程序,操作系統會?先將程序的數據代碼連同它?到的?系列動態庫先加載到內存,其中每個動態庫的加載地址都是不固定的,但是?論加載到什么地?,都要映射到進程對應的地址空間,然后通過.GOT
?式進?調?(運行重定位,也叫做動態地址重定位)。