Linux:ELF文件-靜動態庫原理

??所屬專欄: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

1740388705932

要理解編譯的細節,就先要了解ELF文件,以下四種都是ELF文件:

  1. 可重定位?件(Relocatable File) :即 xxx.o 文件。包含適合于與其他目標文件鏈接來創建可執行文件或者共享目標文件的代碼和數據。
  2. 可執??件(Executable File) :即可執行程序。
  3. 共享?標?件(Shared Object File) :即 xxx.so文件。
  4. 內核轉儲(core dumps) ,存放當前進程的執?上下?,?于dump信號觸發。

每個ELF文件都由以下部分組成:

  1. ELF頭(ELF header) :描述文件的主要特性。其位于文件的開始位置,它的主要目的是定位文件的其他部分。
  2. 程序頭表(Program header table) :列舉了所有有效的段(segments)和他們的屬性。表?記著每個段的開始的位置和位移(offset)、長度,畢竟這些段,都是緊密的放在二進制?件中,需要段表的描述信息,才能把他們每個段分割開。
  3. 節頭表(Section header table) :包含對節(sections)的描述。
  4. 節(Section ):ELF文件中的基本組成單位,包含了特定類型的數據。ELF文件的各種信息和數據都存儲在不同的節中,如代碼節存儲了可執行代碼,數據節存儲了全局變量和靜態數據等。

最常見的節有代碼節.text和數據節.data,代碼節用于保存機器指令,是程序的主要執行部分。數據節保存已經初始化的全局變量和局部靜態變量。

7f4d909663164a7f8439356d841446c2

ELF形成到加載的大概輪廓

ELF形成可執行文件

  1. 將多個C/C++源代碼翻譯為目標 .o 文件
  2. 將多個.o文件的section合并

627945ff2f40451085f649cf08c51bd3

具體的合并方式會比這復雜,是在鏈接時合并的,并且還涉及到對庫的合并

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

說白了,一個在鏈接時作用,一個在運行加載中作用。

2b29a68abd4a4073ad929508727071c3

從鏈接視圖來看:

  • 命令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.ohello.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.ocall中的地址全是0,這說明在code.c中根本不認識printf函數,而在hello.o中也不認識printfrun。因為它們的跳轉地址都變成了全零。

因為在編譯的時候,編譯器是完全不知道其他函數是什么,位于內存的哪個區塊,代碼長什么樣,所以編譯器只能將這些函數的跳轉地址都先設為0。這個地址會在鏈接的時候被修正,為了讓鏈接器在鏈接時能正確的重定位到這些被修正的地址,在代碼塊.data中存在一張重定位表,在鏈接的時候就會根據這張表對地址進行修正。

靜態鏈接就是將本地的.o文件和庫里的.a文件進行鏈接,鏈接其實就是將編譯之后的所有?標文件連同?到的?些靜態庫運?時庫組合,拼裝成?個獨?的可執?文件。其中就包括我們之前提到的地址修正,當所有模塊組合在?起之后,鏈接器會根據我們的.o?件或者靜態庫中的重定位表找到那些需要被重定位的函數全局變量,從?修正它們的地址。這其實就是靜態鏈接的過程。

image-20250226131742897

所以,鏈接過程中會涉及到對.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_structvm_area_struct在進程創建的時候,從ELF文件的各個segment獲取,每一個segment有自己的其實地址和長度,就可以填充內核中的stratend、頁表等數據。

所以操作系統和編譯器都支持虛擬地址機制。

所以ELF文件在被編譯好之后,會在ELF headerEntry字段中記錄程序的入口地址,運行時,這個地址最先被加載到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_structELF可執行文件加載到內存中,擁有了對應的物理地址,將虛擬地址和物理地址分別填入頁表,對應的mm_struct等信息一填,然后將程序的Entry point address加載到CPU中,程序開始執行。

image-20250226203350306

動態鏈接與動態庫加載

首先,動態鏈接比靜態鏈接要常用的多,大部分官方庫都是默認采用動態庫。使用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.soC語言的運行時庫里面提供了常用的標準輸入輸出文件字符串處理等功能。

這里要說的是,靜態鏈接會將編譯產生的所有目標文件,連同到用到的各種的庫一起合并為一個獨立的可執行文件,雖然它不需要任何額外依賴就可以運行。但是其最大的問題就是生成的文件體積太大了,相當耗費內存資源。隨著軟件復雜度的提升,操作系統也越來越臃腫,不同的軟件可能都包含了相同的部分功能和代碼,這顯然不利于計算機的發展。

所以,動態鏈接的優勢就體現在這里,我們將共享的代碼提取出來,封裝為一個單獨的動態鏈接庫。等到程序運行的時候加載到內存,不但可以節省空間,而且同一個模塊只需要在內存中加載一次,就可以被不同的進程所共享。

那么,動態鏈接是如何工作的?

首先,結論、動態鏈接將鏈接的整個過程推遲到了程序加載的時候。我們運行一個程序,操作系統首先將程序的數據代碼連同它用到的一系列動態庫先加載到內存,其中每個動態庫的加載地址都是不固定的從,操作系統會根據當前的內存使用情況為它們動態分配一段內存。當動態庫被加載到內存后,一旦物理地址(內存里的地址)確定,我們就可以去修正動態庫中的那些函數的跳轉地址了。

要徹底理清楚,先要知道我們的程序是怎么開始啟動的

在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

動態庫中的相對地址

動態庫為了能隨時加載,支持并映射到任意進程的任意位置,對動態庫中的方法統一編制,采用相對編址(平坦模式)的方案。(可執行程序,靜態庫,動態庫都采用從全零開始的相對編址)

動態庫本質也是一個磁盤上的文件,要加載也是要被打開的,要讓進程看到動態庫,不僅要將動態庫加載到內存中,進程要訪問動態庫,還要讓進程看到動態庫,也就是將動態庫的虛擬地址和物理地址分別填入進程的頁表中

image-20250227143654849

通過上面這張圖,動態庫的加載過程我們清楚了,那么進程是怎么進行庫調用的呢?

  • 庫已經被我們映射到了當前進程的地址空間中
  • 庫的起始地址我們知道了,庫中每一個方法的偏移量我們也知道。
  • 所以庫的起始地址+方法偏移量就可以定位庫中的任意方法。
  • 整個調用過程,從代碼區跳轉到共享區,調用完后返回代碼區,整個過程都是在進程地址空間完成的。

image-20250227145350745

  • 也就是說程序運行之前,先把所有庫加載并映射,所有庫的起始虛擬地址我們都提前知道了。
  • 然后對我們加載到內存中的程序的庫函數調用進行地址修改,在內存中二次完成地址設置(加載地址重定位)
  • Wait!!!剛剛我們是不是修改了什么,修改了call后面的地址是吧?這不是代碼段嗎?代碼段不是只讀的不能修改嗎?

全局偏移量表GOTglobal 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,然后加載在?起

image-20250227150827780

  1. 由于代碼段只讀,我們不能直接修改代碼段。有了GOT表,代碼可以被所有進程共享。但在不同進程的地址空間中,各個動態庫的絕對地址、相對位置都不同。反應到GOT表上,就是每個進程的每個動態庫都有獨立的GOT表,所以進程間不能共享GOT
  2. 在單個.so下,由于GOT表與.text的相對位置都是固定的,我們完全可以利用CPU的相對尋址找到GOT
  3. 在調用函數的時候會首先查表,然后根據表中的地址來進行跳轉,這些地址在動態庫加載時會被修改為真正的地址
  4. 這種方式實現的動態鏈接就被叫做PIC 地址無關代碼。也就是說,動態庫不需要做任何修改,被加載到任意內存地址都能正常運行,并且被所有進程共享,這也是為什么在制作動態庫時給編譯器指定 -fPIC參數的原因,PIC = 相對編址 + GOT

PLT 的作用和原理

由于動態鏈接在程序加載的時候需要對大量函數進?重定位,這?步顯然是非常耗時的。為了進一步降低開銷,我們的操作系統還做了?些其他的優化,比如延遲綁定Lazy Binding,或者也叫PLT(過程連接表Procedure Linkage Table)。與其在程序?開始就對所有函數進行重定位,不如將這個過程推遲到函數第?次被調?的時候,因為絕大多數動態庫中的函數可能在程序運行期間?次都不會被使?到。

思路是:GOT中的跳轉地址默認會指向?段輔助代碼,它也被叫做樁代碼/stup。在我們第?次調?函數的時候,這段代碼會負責查詢真正函數的跳轉地址,并且去更新GOT表。于是我們再次調?函數的時候,就會直接跳轉到動態庫中真正的函數實現。

總而言之,動態鏈接實際上將鏈接的整個過程,比如符號查詢、地址的重定位從編譯時推遲到了程序的運行時,它雖然犧牲了?定的性能和程序加載時間,但絕對是物有所值的。因為動態鏈接能夠更有效的利?磁盤空間和內存資源,以極大方便了代碼的更新和維護,更關鍵的是,它實現了?進制級別的代碼復用。

庫間依賴

不僅可執行程序會調用庫,庫也會調用其他庫,庫之間是有依賴的,那么如何做到庫和庫之間調用也是地址無關的呢?

因為庫中也有GOT表,這就是為什么它們都是ELF格式文件。

總結

靜態鏈接的出現,提?了程序的模塊化?平。對于一個大的項?,不同的人可以獨立地測試和開發自己的模塊。通過靜態鏈接,生成最終的可執行文件。

我們知道靜態鏈接會將編譯產生的所有目標文件,和用到的各種庫合并成?個獨立的可執行文件,其中我們會去修正模塊間函數的跳轉地址,也被叫做編譯重定位(也叫做靜態重定位)。

?動態鏈接實際上將鏈接的整個過程推遲到了程序加載的時候。比如我們去運??個程序,操作系統會?先將程序的數據代碼連同它?到的?系列動態庫先加載到內存,其中每個動態庫的加載地址都是不固定的,但是?論加載到什么地?,都要映射到進程對應的地址空間,然后通過.GOT?式進?調?(運行重定位,也叫做動態地址重定位)。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/72241.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/72241.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/72241.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++性能優化常用技巧

一. 選擇合適的數據結構 1.1 map與unordered_map的選擇 如果僅僅只需要使用到快速查找的特性&#xff0c;那么unordered_map更加合適&#xff0c;他的復雜度是O(1)。如果還需要排序以及范圍查找的能力&#xff0c;那么就選擇map。 1.2 vector與list的選擇 通常情況下&#…

Towards Graph Foundation Models: A Survey and Beyond

Towards Graph Foundation Models: A Survey and Beyond WWW24 ?#paper/???#? #paper/&#x1f4a1;#? 背景和動機 背景與意義 隨著基礎模型&#xff08;如大語言模型&#xff09;在NLP等領域的突破&#xff0c;圖機器學習正經歷從淺層方法向深度學習的范式轉變。GFM…

基于 Python 深度學習的電影評論情感分析可視化系統(2.0 全新升級)

基于 Python 深度學習的電影評論情感分析可視化系統&#xff0c;基于 Flask 深度學習&#xff0c;構建了一個 影評情感分析系統&#xff0c;能夠 自動分析影評、計算情感趨勢 并 可視化展示&#xff0c;對于電影行業具有重要參考價值&#xff01; 基于 Python 深度學習的電影評…

Cargo, the Rust package manager, is not installed or is not on PATH.

今天在Windows操作系統上通過pip 安裝jupyter的時候遇到這個報錯&#xff0c;Cargo, the Rust package manager, is not installed or is not on PATH.。 解決辦法 官網&#xff1a;https://rustup.rs/# 下載&#xff1a;https://win.rustup.rs/x86_64 安裝完成之后&#xff0c…

CSS—text文本、font字體、列表list、表格table、表單input、下拉菜單select

目錄 1.文本 2.字體 3.列表list a.無序列表 b.有序列表 c.定義列表 4.表格table a.內容 b.合并單元格 3.表單input a.input標簽 b.單選框 c.上傳文件 4.下拉菜單 1.文本 屬性描述color設置文本顏色。direction指定文本的方向 / 書寫方向。letter-spacing設置字符…

開啟AI短劇新紀元!SkyReels-V1/A1雙劍合璧!昆侖萬維開源首個面向AI短劇的視頻生成模型

論文鏈接&#xff1a;https://arxiv.org/abs/2502.10841 項目鏈接&#xff1a;https://skyworkai.github.io/skyreels-a1.github.io/ Demo鏈接&#xff1a;https://www.skyreels.ai/ 開源地址&#xff1a;https://github.com/SkyworkAI/SkyReels-A1 https://github.com/Skywork…

數學建模:MATLAB極限學習機解決回歸問題

一、簡述 極限學習機是一種用于訓練單隱層前饋神經網絡的算法&#xff0c;由輸入層、隱藏層、輸出層組成。 基本原理&#xff1a; 輸入層接受傳入的樣本數據。 在訓練過程中隨機生成從輸入層到隱藏層的所有連接權重以及每個隱藏層神經元的偏置值&#xff0c;這些參數在整個…

Android15音頻進階之定位混音線程丟幀問題(一百零八)

簡介: CSDN博客專家、《Android系統多媒體進階實戰》一書作者 新書發布:《Android系統多媒體進階實戰》?? 優質專欄: Audio工程師進階系列【原創干貨持續更新中……】?? 優質專欄: 多媒體系統工程師系列【原創干貨持續更新中……】?? 優質視頻課程:AAOS車載系統+…

_ 為什么在python中可以當變量名

在 Python 中&#xff0c;_&#xff08;下劃線&#xff09;是一個有效的變量名&#xff0c;這主要源于 Python 的命名規則和一些特殊的使用場景。以下是為什么 _ 可以作為變量名的原因和常見用途&#xff1a; --- ### 1. **Python 的命名規則** Python 允許使用字母&#xff…

Electron+Vite+React+TypeScript開發問題手冊

ElectronViteReactTypeScript跨平臺開發全問題手冊 一、開發環境配置類問題 1.1 依賴安裝卡頓&#xff08;國內網絡環境&#xff09; 問題現象&#xff1a;執行npm install時卡在node-gyp編譯或Electron二進制包下載階段 解決方案&#xff1a; # 配置國內鏡像源 npm config …

【計算機網絡入門】初學計算機網絡(七)

目錄 1. 滑動窗口機制 2. 停止等待協議&#xff08;S-W&#xff09; 2.1 滑動窗口機制 2.2 確認機制 2.3 重傳機制 2.4 為什么要給幀編號 3. 后退N幀協議&#xff08;GBN&#xff09; 3.1 滑動窗口機制 3.2 確認機制 3.3 重傳機制 4. 選擇重傳協議&#xff08;SR&a…

《Python實戰進階》No 8:部署 Flask/Django 應用到云平臺(以Aliyun為例)

第8集&#xff1a;部署 Flask/Django 應用到云平臺&#xff08;以Aliyun為例&#xff09; 2025年3月1日更新 增加了 Ubuntu服務器安裝Python詳細教程鏈接。 引言 在現代 Web 開發中&#xff0c;開發一個功能強大的應用只是第一步。為了讓用戶能夠訪問你的應用&#xff0c;你需…

GitLab Pages 托管靜態網站

文章目錄 新建項目配置博客添加 .gitlab-ci.yml其他配置 曾經用 Github Pages 來托管博客內容&#xff0c;但是有一些不足&#xff1a; 在不科學上網的情況下&#xff0c;是沒法訪問的&#xff0c;或者訪問速度非常慢代碼倉庫必須是公開的&#xff0c;如果設置為私有&#xff0…

TVbox蜂蜜影視:智能電視觀影新選擇,簡潔界面與強大功能兼具

蜂蜜影視是一款基于貓影視開源項目 CatVodTVJarLoader 開發的智能電視軟件&#xff0c;專為追求簡潔與高效觀影體驗的用戶設計。該軟件從零開始編寫&#xff0c;界面清爽&#xff0c;操作流暢&#xff0c;特別適合在智能電視上使用。其最大的亮點在于能夠自動跳過失效的播放地址…

形象生動講解Linux 虛擬化 I/O

用現實生活的比喻和簡單例子來解釋 Linux 虛擬化 I/O&#xff0c;就像給朋友講故事一樣。 虛擬化 I/O 要解決什么問題&#xff1f; 想象你有一棟大房子&#xff08;物理服務器&#xff09;&#xff0c;想把它分割成多個小公寓&#xff08;虛擬機&#xff09;出租。每個租客&…

Java內存管理與性能優化實踐

Java內存管理與性能優化實踐 Java作為一種廣泛使用的編程語言&#xff0c;其內存管理和性能優化是開發者在日常工作中需要深入了解的重要內容。Java的內存管理機制借助于垃圾回收&#xff08;GC&#xff09;來自動處理內存的分配和釋放&#xff0c;但要實現高效的內存管理和優…

代碼隨想錄算法訓練營第三十天 | 卡碼網46.攜帶研究材料(二維解法)、卡碼網46.攜帶研究材料(滾動數組)、LeetCode416.分割等和子集

代碼隨想錄算法訓練營第三十天 | 卡碼網46.攜帶研究材料&#xff08;二維解法&#xff09;、卡碼網46.攜帶研究材料&#xff08;滾動數組&#xff09;、LeetCode416.分割等和子集 01-1 卡碼網46.攜帶研究材料&#xff08;二維&#xff09; 相關資源 題目鏈接&#xff1a;46. 攜…

nvidia驅動更新,centos下安裝openwebui+ollama(非docker)

查看centos內核版本 uname -a cat /etc/redhat-release下載對應的程序&#xff08;這個是linux64位版本通用的&#xff09; https://cn.download.nvidia.cn/tesla/550.144.03/NVIDIA-Linux-x86_64-550.144.03.run cudnn想辦法自己下一下&#xff0c;我這里是12.x和11.x通用的…

【AIGC系列】4:Stable Diffusion應用實踐和代碼分析

AIGC系列博文&#xff1a; 【AIGC系列】1&#xff1a;自編碼器&#xff08;AutoEncoder, AE&#xff09; 【AIGC系列】2&#xff1a;DALLE 2模型介紹&#xff08;內含擴散模型介紹&#xff09; 【AIGC系列】3&#xff1a;Stable Diffusion模型原理介紹 【AIGC系列】4&#xff1…

51單片機-串口通信編程

串行口工作之前&#xff0c;應對其進行初始化&#xff0c;主要是設置產生波特率的定時器1、串行口控制盒中斷控制。具體步驟如下&#xff1a; 確定T1的工作方式&#xff08;編程TMOD寄存器&#xff09;計算T1的初值&#xff0c;裝載TH1\TL1啟動T1&#xff08;編程TCON中的TR1位…