Linux筆記---動靜態庫(原理篇)

1. ELF文件格式

動靜態庫文件的構成是什么樣的呢?或者說二者的內容是什么?

實際上,可執行文件,目標文件,靜態庫文件,動態庫文件都是使用ELF文件格式進行組織的。

ELF(Executable and Linkable Format)文件格式是Unix系統及其衍生系統中廣泛使用的可執行文件、共享庫和核心轉儲的二進制文件格式。

主要包括以下四種文件*

  • 可重定位文件(Relocatable File):包含適合于與其他目標文件鏈接來創建可執行文件或者共享目標文件的代碼和數據,如xxx.o文件。 ?
  • 可執行文件(Executable File):包含適合于執行的一個程序,規定了exec()如何創建一個程序的進程映像,如a.out文件。 ?
  • 共享目標文件(Shared Object File):包含可在兩種上下文中鏈接的代碼和數據。首先鏈接編輯器可以將它和其它可重定位文件和共享目標文件一起處理,生成另外一個目標文件;其次,動態鏈接器可能將它與某個可執行文件以及其它共享目標一起組合,創建進程映像,如xxx.so文件。 ?
  • 內核轉儲(core dumps):存放當前進程的執行上下文,用于dump信號觸發。

結構組成

  • ELF頭(ELF header):位于文件的開始位置,主要目的是定位文件的其他部分。

  • 程序頭表(Program header table):列舉了所有有效的段(segments)和它們的屬性。

  • 節(Section):文件的最小邏輯組織單元,用于存儲程序在編譯、鏈接或執行過程中需要的特定類型數據。每個Section都有明確的用途,例如存儲代碼、全局變量、符號表或調試信息等。

  • 節頭表(Section header table):包含對節(sections)的描述。

總結來說,除了節以外的三個部分實際上都是幫助定位的輔助信息,程序的核心信息在節部分。

我們可以通過 readelf 工具來查看一個可執行程序的各個部分,下面我們以ls為例展示各個部分。

1.1 ELF header

位于文件的開始位置,主要目的是定位文件的其他部分。包含文件類型、目標結構、ELF文件格式的版本、程序入口地址、程序頭表的文件偏移、節頭表的文件偏移等重要信息。

readelf -h /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -h /usr/bin/ls
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:               0x6aa0Start of program headers:          64 (bytes into file)Start of section headers:          136232 (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
Magic:                            標識ELF文件的魔數字節序列。
Class:                            文件位數(32位或64位)。
Data:                             數據存儲的字節序(小端或大端)。
Version:                          ELF格式版本號。
OS/ABI:                           目標操作系統和應用二進制接口(ABI)。
ABI Version:                      ABI的版本號。
Type:                             文件類型(可執行、共享庫等)。
Machine:                          目標CPU架構。
Version:                          ELF版本(通常為1)。
Entry point address:              程序執行的起始地址。
Start of program headers:         程序頭表在文件中的偏移量。
Start of section headers:         節頭表在文件中的偏移量。
Flags:                            處理器特定的標志位。
Size of this header:              ELF頭的大小(字節)。
Size of program headers:          單個程序頭條目的大小。
Number of program headers:        程序頭條目數量。
Size of section headers:          單個節頭條目的大小。
Number of section headers:        節頭條目數量。
Section header string table index:節名稱字符串表的索引號。

?1.2?Section header table?

ELF文件的鏈接視圖(Linking view),包含對節(sections)的描述。即一個ELF文件中到底有哪些具體的sections,以及這些sections的屬性信息。

簡單來說,這部分包含了一個個的section header,它們與section一一對應,指示各個section的屬性信息,這與文件系統中的inode table的設計思路相同。

readelf -S /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -S /usr/bin/ls
There are 31 section headers, starting at offset 0x21428: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  000003b0000000000000004c  0000000000000000   A       6     0     8[ 6] .dynsym           DYNSYM           0000000000000400  000004000000000000000b88  0000000000000018   A       7     1     8[ 7] .dynstr           STRTAB           0000000000000f88  00000f8800000000000005a6  0000000000000000   A       0     0     1[ 8] .gnu.version      VERSYM           000000000000152e  0000152e00000000000000f6  0000000000000002   A       6     0     2[ 9] .gnu.version_r    VERNEED          0000000000001628  0000162800000000000000c0  0000000000000000   A       7     2     8[10] .rela.dyn         RELA             00000000000016e8  000016e80000000000001410  0000000000000018   A       6     0     8[11] .rela.plt         RELA             0000000000002af8  00002af80000000000000960  0000000000000018  AI       6    25     8[12] .init             PROGBITS         0000000000004000  00004000000000000000001b  0000000000000000  AX       0     0     4[13] .plt              PROGBITS         0000000000004020  000040200000000000000650  0000000000000010  AX       0     0     16[14] .plt.got          PROGBITS         0000000000004670  000046700000000000000030  0000000000000010  AX       0     0     16[15] .plt.sec          PROGBITS         00000000000046a0  000046a00000000000000640  0000000000000010  AX       0     0     16[16] .text             PROGBITS         0000000000004ce0  00004ce000000000000123a2  0000000000000000  AX       0     0     16[17] .fini             PROGBITS         0000000000017084  00017084000000000000000d  0000000000000000  AX       0     0     4[18] .rodata           PROGBITS         0000000000018000  000180000000000000004dcc  0000000000000000   A       0     0     32[19] .eh_frame_hdr     PROGBITS         000000000001cdcc  0001cdcc000000000000056c  0000000000000000   A       0     0     4[20] .eh_frame         PROGBITS         000000000001d338  0001d3380000000000002120  0000000000000000   A       0     0     8[21] .init_array       INIT_ARRAY       0000000000020fd0  0001ffd00000000000000008  0000000000000008  WA       0     0     8[22] .fini_array       FINI_ARRAY       0000000000020fd8  0001ffd80000000000000008  0000000000000008  WA       0     0     8[23] .data.rel.ro      PROGBITS         0000000000020fe0  0001ffe00000000000000a78  0000000000000000  WA       0     0     32[24] .dynamic          DYNAMIC          0000000000021a58  00020a580000000000000200  0000000000000010  WA       7     0     8[25] .got              PROGBITS         0000000000021c58  00020c5800000000000003a0  0000000000000008  WA       0     0     8[26] .data             PROGBITS         0000000000022000  000210000000000000000278  0000000000000000  WA       0     0     32[27] .bss              NOBITS           0000000000022280  0002127800000000000012c0  0000000000000000  WA       0     0     32[28] .gnu_debugaltlink PROGBITS         0000000000000000  000212780000000000000049  0000000000000000           0     0     1[29] .gnu_debuglink    PROGBITS         0000000000000000  000212c40000000000000034  0000000000000000           0     0     4[30] .shstrtab         STRTAB           0000000000000000  000212f8000000000000012f  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)// 注:
// [Nr]	    節的索引號(從 1 開始),0 表示無效節。
// Name	    節的名稱(如 .text、.data、.rodata)。
// Type	    節的類型(如 PROGBITS 表示程序數據,SYMTAB 表示符號表)。
// Address	節加載到內存時的虛擬地址(未加載時為 0)。
// Offset	節在文件中的起始偏移量(字節)。
// Size	    節的大小(字節)。
// EntSize	如果節是表格(如符號表),表示每個條目的大小;否則為 0。
// Flags	節的屬性標志(如 A 可分配,X 可執行,W 可寫)。
// Link	    鏈接到其他節的索引(如符號表會鏈接到字符串表)。
// Info	    節的附加信息(如符號表的局部符號起始索引)。
// Align	節的對齊要求(如 16 表示按 16 字節對齊)。

鏈接視圖的含義是:目標文件以及動靜態庫在進行鏈接形成可執行程序時,是以section為單位進行的,即各個section各自進行合并。

1.3?Program header table

ELF文件的執行視圖(execution view),列舉了所有有效的段(segments)和它們的屬性。

同樣地,這部分包含了一個個的program header(或者說segment header),它們與segment一一對應,指示各個segment的屬性信息。

可執行程序在被加載到內存當中時,多個節會合并成一個段(合并原則:相同屬性,比如可讀,可寫,可執行,需要加載時申請空間等)。所以,Program header table其實就是對中間部分的一個重新劃分。

readelf -l /usr/bin/ls
shishen@hcss-ecs-b8e6:~$ readelf -l /usr/bin/lsElf file type is DYN (Position-Independent Executable file)
Entry point 0x6aa0
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 0x00000000000000000x0000000000003458 0x0000000000003458  R      0x1000LOAD           0x0000000000004000 0x0000000000004000 0x00000000000040000x0000000000013091 0x0000000000013091  R E    0x1000LOAD           0x0000000000018000 0x0000000000018000 0x00000000000180000x0000000000007458 0x0000000000007458  R      0x1000LOAD           0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd00x00000000000012a8 0x0000000000002570  RW     0x1000DYNAMIC        0x0000000000020a58 0x0000000000021a58 0x0000000000021a580x0000000000000200 0x0000000000000200  RW     0x8NOTE           0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030  R      0x8NOTE           0x0000000000000368 0x0000000000000368 0x00000000000003680x0000000000000044 0x0000000000000044  R      0x4GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000030 0x0000000000000030  R      0x8GNU_EH_FRAME   0x000000000001cdcc 0x000000000001cdcc 0x000000000001cdcc0x000000000000056c 0x000000000000056c  R      0x4GNU_STACK      0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000  RW     0x10GNU_RELRO      0x000000000001ffd0 0x0000000000020fd0 0x0000000000020fd00x0000000000001030 0x0000000000001030  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 .data.rel.ro .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 .data.rel.ro .dynamic .got // 注:
// Type	    段的類型(如 LOAD 表示可加載段,DYNAMIC 表示動態鏈接信息)。
// Offset	段在文件中的起始偏移量(字節)。
// VirtAddr	段加載到內存時的虛擬地址(程序運行時訪問的地址)。
// PhysAddr	段加載到內存時的物理地址(通常與 VirtAddr 相同,現代系統忽略)。
// FileSiz	段在文件中的大小(字節)。
// MemSiz	段在內存中的大小(字節,可能大于 FileSiz,如 .bss 節會填充零)。
// Flags	段的權限標志(R=讀,W=寫,X=執行)。
// Align	段在內存和文件中的對齊要求(如 0x1000 表示按 4KB 對齊)。

Section to Segment mapping部分就顯示了各個segment包含了哪些section。

執行視圖的含義是:該部分負責告訴操作系統,如何加載可執行文件,完成進程內存的初始化。一個可執行程序的格式中,一定有 program header table。

  • 節(Section) 是鏈接時的邏輯劃分(如 .text、.data),數量多且大小不一。

  • 段(Segment) 是執行時的物理加載單元(如代碼段、數據段),合并相同權限的節后,操作系統只需按段映射內存,減少內存碎片和系統調用次數。

加載到內存時將節合并為段的原因

  • 符合操作系統的內存頁管理:操作系統以 頁(Page) 為單位管理內存(如4KB)。段會按頁對齊,避免跨頁的節導致內存浪費或權限沖突。如果不進行合并,假設頁面大小為4096字節(內存塊基本大小,加載,管理的基本單位),如果.text部分為4097字節,.init部分為512字節,那么它們將占用3個頁面,而合并后,它們只需2個頁面。
  • 統一內存訪問權限:每個段有明確的權限(讀/寫/執行),而節可能分散且權限不同。

?2. 靜態鏈接原理

2.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");
}

我們將這兩個原文件進行編譯得到目標文件:

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ gcc -c *.c
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ ls
code.c  code.o  hello.c  hello.o

這里,我們需要用到一個指令objdump -d:將代碼段(.text)進行反匯編查看。

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ objdump -d hello.o > hello.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ objdump -d code.o > code.s
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ ls
code.c  code.o  code.s  hello.c  hello.o  hello.s

?這里的call很明顯就是函數調用,call指令的編碼為e8。

對比源文件可以看出,無論是printf函數還是run函數,e8跳轉到的地址都為0。這是因為,在完成鏈接之前,編譯器并不知道這些外部函數的實現與定義,無法為其分配地址(邏輯地址),就以0代替其地址。

所以,在鏈接時,編譯器還需要將代碼段中這些為0的地址修改為實際為這些函數分配的地址。

要完成這項工作,編譯器還需要符號表的幫助。每個目標文件都有自己的符號表,在進行鏈接時,大家相互對照符號表,就能找到外部函數或變量是在哪一個文件當中聲明的了,進而就能為其分配地址。

readelf -s # 讀取目標文件的符號表
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ readelf -s hello.oSymbol table '.symtab' contains 7 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata4: 0000000000000000    40 FUNC    GLOBAL DEFAULT    1 main5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ readelf -s code.oSymbol table '.symtab' contains 6 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata4: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 run5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts// 注:printf底層就是puts

?其中,每一行就代表一個符號,我們主要關注Ndx列和Name列。

  • ABS:該符號的值是絕對地址,不依賴于任何節(Section),通常是文件名或特殊定義的全局符號。
  • UND:該符號在當前目標文件中未定義,需要在鏈接時從其他目標文件或庫中解析(如外部函數或全局變量)。
  • 數字:表示符號定義在對應索引號的節中。例如:Ndx=1:符號屬于 .text 節(代碼段)。Ndx=5:符號屬于 .rodata 節(只讀數據段)。

?可以看到,在hello.o中,run和puts都是未定義,而code.o中,run有定義但puts依然未定義。

當我們將二者進行鏈接之后:

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ gcc -o main hello.o code.o
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ objdump -d main > main.s

?puts我們暫時也不關心,因為puts使用的是動態鏈接。可以看到,run函數的地址為1171,該地址也標注在run函數的名稱之前。

這就說明編譯器在鏈接時,找到了run函數,并為其分配了地址。

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ readelf -s mainSymbol table '.dynsym' contains 7 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (2)2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (3)4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND [...]@GLIBC_2.2.5 (3)Symbol table '.symtab' contains 38 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS Scrt1.o2: 000000000000038c    32 OBJECT  LOCAL  DEFAULT    4 __abi_tag3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c4: 0000000000001090     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones5: 00000000000010c0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones6: 0000000000001100     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux7: 0000000000004010     1 OBJECT  LOCAL  DEFAULT   26 completed.08: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtor[...]9: 0000000000001140     0 FUNC    LOCAL  DEFAULT   16 frame_dummy10: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_in[...]11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c13: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c14: 0000000000002120     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__15: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 16: 0000000000003dc8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC17: 000000000000201c     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR18: 0000000000003fb8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_19: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_mai[...]20: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]21: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start22: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.523: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata24: 0000000000001171    26 FUNC    GLOBAL DEFAULT   16 run25: 000000000000118c     0 FUNC    GLOBAL HIDDEN    17 _fini26: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start27: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__28: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle29: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used30: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end31: 0000000000001060    38 FUNC    GLOBAL DEFAULT   16 _start32: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start33: 0000000000001149    40 FUNC    GLOBAL DEFAULT   16 main34: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__35: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]36: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@G[...]37: 0000000000001000     0 FUNC    GLOBAL HIDDEN    12 _init

從上面的結果中可以看到run函數對應的section的編號為16,這意味著run函數在編號為16的節中。

readelf -S main

從main.s中可以得到驗證(只關注開頭的描述以及main函數和run函數即可):

Disassembly of section .text:0000000000001060 <_start>:1060:	f3 0f 1e fa          	endbr64 1064:	31 ed                	xor    %ebp,%ebp1066:	49 89 d1             	mov    %rdx,%r91069:	5e                   	pop    %rsi106a:	48 89 e2             	mov    %rsp,%rdx106d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp1071:	50                   	push   %rax1072:	54                   	push   %rsp1073:	45 31 c0             	xor    %r8d,%r8d1076:	31 c9                	xor    %ecx,%ecx1078:	48 8d 3d ca 00 00 00 	lea    0xca(%rip),%rdi        # 1149 <main>107f:	ff 15 53 2f 00 00    	call   *0x2f53(%rip)        # 3fd8 <__libc_start_main@GLIBC_2.34>1085:	f4                   	hlt    1086:	66 2e 0f 1f 84 00 00 	cs nopw 0x0(%rax,%rax,1)108d:	00 00 00 0000000000001090 <deregister_tm_clones>:1090:	48 8d 3d 79 2f 00 00 	lea    0x2f79(%rip),%rdi        # 4010 <__TMC_END__>1097:	48 8d 05 72 2f 00 00 	lea    0x2f72(%rip),%rax        # 4010 <__TMC_END__>109e:	48 39 f8             	cmp    %rdi,%rax10a1:	74 15                	je     10b8 <deregister_tm_clones+0x28>10a3:	48 8b 05 36 2f 00 00 	mov    0x2f36(%rip),%rax        # 3fe0 <_ITM_deregisterTMCloneTable@Base>10aa:	48 85 c0             	test   %rax,%rax10ad:	74 09                	je     10b8 <deregister_tm_clones+0x28>10af:	ff e0                	jmp    *%rax10b1:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)10b8:	c3                   	ret    10b9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)00000000000010c0 <register_tm_clones>:10c0:	48 8d 3d 49 2f 00 00 	lea    0x2f49(%rip),%rdi        # 4010 <__TMC_END__>10c7:	48 8d 35 42 2f 00 00 	lea    0x2f42(%rip),%rsi        # 4010 <__TMC_END__>10ce:	48 29 fe             	sub    %rdi,%rsi10d1:	48 89 f0             	mov    %rsi,%rax10d4:	48 c1 ee 3f          	shr    $0x3f,%rsi10d8:	48 c1 f8 03          	sar    $0x3,%rax10dc:	48 01 c6             	add    %rax,%rsi10df:	48 d1 fe             	sar    %rsi10e2:	74 14                	je     10f8 <register_tm_clones+0x38>10e4:	48 8b 05 05 2f 00 00 	mov    0x2f05(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable@Base>10eb:	48 85 c0             	test   %rax,%rax10ee:	74 08                	je     10f8 <register_tm_clones+0x38>10f0:	ff e0                	jmp    *%rax10f2:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)10f8:	c3                   	ret    10f9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)0000000000001100 <__do_global_dtors_aux>:1100:	f3 0f 1e fa          	endbr64 1104:	80 3d 05 2f 00 00 00 	cmpb   $0x0,0x2f05(%rip)        # 4010 <__TMC_END__>110b:	75 2b                	jne    1138 <__do_global_dtors_aux+0x38>110d:	55                   	push   %rbp110e:	48 83 3d e2 2e 00 00 	cmpq   $0x0,0x2ee2(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>1115:	00 1116:	48 89 e5             	mov    %rsp,%rbp1119:	74 0c                	je     1127 <__do_global_dtors_aux+0x27>111b:	48 8b 3d e6 2e 00 00 	mov    0x2ee6(%rip),%rdi        # 4008 <__dso_handle>1122:	e8 19 ff ff ff       	call   1040 <__cxa_finalize@plt>1127:	e8 64 ff ff ff       	call   1090 <deregister_tm_clones>112c:	c6 05 dd 2e 00 00 01 	movb   $0x1,0x2edd(%rip)        # 4010 <__TMC_END__>1133:	5d                   	pop    %rbp1134:	c3                   	ret    1135:	0f 1f 00             	nopl   (%rax)1138:	c3                   	ret    1139:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)0000000000001140 <frame_dummy>:1140:	f3 0f 1e fa          	endbr64 1144:	e9 77 ff ff ff       	jmp    10c0 <register_tm_clones>0000000000001149 <main>:1149:	f3 0f 1e fa          	endbr64 114d:	55                   	push   %rbp114e:	48 89 e5             	mov    %rsp,%rbp1151:	48 8d 05 ac 0e 00 00 	lea    0xeac(%rip),%rax        # 2004 <_IO_stdin_used+0x4>1158:	48 89 c7             	mov    %rax,%rdi115b:	e8 f0 fe ff ff       	call   1050 <puts@plt>1160:	b8 00 00 00 00       	mov    $0x0,%eax1165:	e8 07 00 00 00       	call   1171 <run>116a:	b8 00 00 00 00       	mov    $0x0,%eax116f:	5d                   	pop    %rbp1170:	c3                   	ret    0000000000001171 <run>:1171:	f3 0f 1e fa          	endbr64 1175:	55                   	push   %rbp1176:	48 89 e5             	mov    %rsp,%rbp1179:	48 8d 05 91 0e 00 00 	lea    0xe91(%rip),%rax        # 2011 <_IO_stdin_used+0x11>1180:	48 89 c7             	mov    %rax,%rdi1183:	e8 c8 fe ff ff       	call   1050 <puts@plt>1188:	90                   	nop1189:	5d                   	pop    %rbp118a:	c3                   	ret    Disassembly of section .fini:

目標文件又叫可重定位文件,這里的重定位就是指的為這些外部函數或變量重新分配地址的過程。

總結來說,靜態鏈接的原理就是將各個目標文件的對應節分別合并,并對照符號表完成對外部函數或變量的重定位。

2.1 虛擬地址空間補充

我們前面在main.s中看到,run函數的地址是1171,但實際上這個說法并不準確。準確的說法是:1171是run函數在代碼段的地址,也即run函數在代碼段的偏移量(各個段內部從0開始編址)。

run函數最終被加載到內存當中的虛擬地址應該是代碼段的地址+偏移量

其中代碼段的地址是在每次程序被加載到內存當中時隨機分配的。

我們修改一下hello.c的代碼:

#include<stdio.h>void run();int main() {printf("hello world!\n");run();printf("%p\n", &run);return 0;
}

重新編譯鏈接之后,run函數的地址變為:

00000000000011af <run>:

運行./main可以看到結果:

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ ./main
hello world!
running...
0x55b1458801af
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ ./main
hello world!
running...
0x55ca012441af
shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ ./main
hello world!
running...
0x56372f8ed1af

?很明顯,run函數的虛擬地址是極富規律且與其偏移量強相關的。

ELF文件是地址空間初始化的基礎,但完整的內存布局是內核、ELF、動態鏈接器共同作用的結果。

0c2446a5a8924ffaa82f49e01281a855.png

3. 動態鏈接的原理

進程是如何跳轉到動態庫并共享動態庫的代碼的呢?

概括來說很簡單,將動態庫函數的邏輯地址映射到物理地址空間中動態庫代碼所在位置即可。

庫的起始虛擬地址 + 方法偏移量 ---> 庫的起始物理地址 + 方法偏移量

但實際上其中的細節與機制并不簡單。

3.1 鏈接的時機

將動態庫函數的邏輯地址映射到物理地址空間中共享庫代碼所在位置,這一過程顯然是在程序被加載到內存時完成的。也就是說動態鏈接的時機就是程序被加載到內存時。

對于這一點,我們還可以更加詳細一點。

上文當中我們說到,目標文件完成鏈接之后,程序當中多了許多庫當中的函數。

?除了main和run以外的函數。來自于庫 /lib64/ld-linux-x86-64.so.2 ,用于程序初始化:

在C/C++程序中,當程序開始執行時,它首先并不會直接跳轉到 main 函數。實際上,程序的入口點是 _start ,這是一個由C運行時庫(通常是glibc)或鏈接器(如ld)提供的特殊函數。

從main.s中可以看到,_start的地址位1060,而ELF header中指明的程序入口地址就是1060:

shishen@hcss-ecs-b8e6:~/113code/linux-c/動靜態庫/display$ readelf -h main
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:               0x1060 # 程序入口地址Start of program headers:          64 (bytes into file)Start of section headers:          14032 (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

在 _start 函數中,會執行一系列初始化操作,這些操作包括:

  1. 設置堆棧:為程序創建一個初始的堆棧環境。
  2. 初始化數據段:將程序的數據段(如全局變量和靜態變量)從初始化數據段復制到相應的內存位置,并清零未初始化的數據段。
  3. 動態鏈接這是關鍵的一步, _start 函數會調用動態鏈接器的代碼來解析和加載程序所依賴的動態庫(shared libraries)。動態鏈接器會處理所有的符號解析和重定位,確保程序中的函數調用和變量訪問能夠正確地映射到動態庫中的實際地址。
  4. 調用?__libc_start_main :一旦動態鏈接完成, _start 函數會調用__libc_start_main (這是glibc提供的?個函數)。 __libc_start_main 函數負責執行一些額外的初始化工作,比如設置信號處理函數、初始化線程庫(如果使用了線程)等。
  5. 調用?main 函數:最后, __libc_start_main 函數會調用程序的 main 函數,此時程序的執行控制權才正式交給用戶編寫的代碼。
  6. 處理 main 函數的返回值:當 main 函數返回時, __libc_start_main 會負責處理這個返回值,并最終調用?_exit 函數來終止程序。

動態鏈接器:

  • 動態鏈接器(如ld-linux.so)負責在程序運行時加載動態庫。
  • 當程序啟動時,動態鏈接器會解析程序中的動態庫依賴,并加載這些庫到內存中。環境變量和配置文件:
  • Linux系統通過環境變量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)來指定動態庫的搜索路徑。
  • 這些路徑會被動態鏈接器在加載動態庫時搜索。
  • 緩存文件:為了提高動態庫的加載效率,Linux系統會維護一個名為/etc/ld.so.cache的緩存文件。該文件包含了系統中所有已知動態庫的路徑和相關信息,動態鏈接器在加載動態庫時會首先搜索這個緩存文件。

上述過程描述了C/C++程序在 main 函數之前執行的一系列操作,但這些操作對于大多數程序員來說是透明的。程序員通常只需要關注 main 函數中的代碼,而不需要關心底層的初始化過程。然而,了解這些底層細節有助于更好地理解程序的執行流程和調試問題。

3.2?全局偏移量表GOT

  • 動態庫的代碼可能是會變化的,所以在編譯鏈接時可執行程序當中是無法完成動態庫的重定位的。
  • 于是,我們只能在程序被加載到內存之后,找到被加載到內存中的動態庫,再進行重定位。即,加載地址重定位。
  • 但代碼段的權限為只讀,我們無法對代碼段進行修改。為了實現這一點,動態鏈接需要一種機制來在運行時查找和綁定符號的地址,這就是GOT的作用。

全局偏移量表(GOT,Global Offset Table)是Linux系統下ELF格式可執行文件中用于定位全局變量和函數的表,主要用于動態鏈接。

其實際上就是在專門預留的一片用來存放函數的跳轉地址的區域:.got節。

 [24] .got              PROGBITS         0000000000003fb0  00002fb00000000000000050  0000000000000008  WA       0     0     8

由于.got所在的段是可讀可寫的,所以就可以實現在運行當中動態地完成重定位。

    3.3 過程鏈接表PLT

    我們在main.s中可以看到,動態鏈接庫函數的函數名之后都會跟著PLT:

    PLT(Procedure Linkage Table,過程鏈接表)是程序動態鏈接中的關鍵機制,主要用于延遲綁定(Lazy Binding)動態庫中的函數地址。?

    延遲綁定:程序啟動時不會立即解析所有動態庫函數的地址,而是在首次調用時才通過PLT解析并緩存地址,減少啟動時間。

    3.3.1 GOT與PLT的作用
    • 存儲全局變量和函數地址:GOT存儲了程序中使用的外部函數和全局變量的實際地址,使得程序在運行時能夠正確地訪問這些外部符號。

    • 支持動態鏈接:在動態鏈接過程中,GOT允許程序在運行時解析和綁定外部符號,而不需要在編譯時就確定所有符號的地址。

    • 實現延遲綁定:通過GOT和PLT(過程鏈接表)的配合,實現了函數的延遲綁定,即函數在第一次被調用時才進行地址綁定,提高了程序的啟動速度。

    3.3.2 工作原理
    • 第一次調用:當程序第一次調用某個外部函數時,會通過PLT跳轉到GOT,由于GOT中此時沒有該函數的地址,會再次跳轉回PLT,PLT會將函數的ID壓入棧中,然后調用_dl_runtime_resolve函數進行符號查找和重定位,找到函數地址后,將其填充到GOT中,之后再跳轉到該函數地址執行。

    • 后續調用:當再次調用該函數時,PLT會直接跳轉到GOT中存儲的函數地址,無需再次進行符號查找和重定位。

    3.4?地址無關代碼PIC

    動態庫被加載到內存當中之后,其內部函數的虛擬地址就都是確定了的。當我們使用這些共享代碼時,在我們進程的虛擬地址空間當中也應當為其分配對應的虛擬地址,否則代碼與其地址就對應不起來了(匯編代碼中,每條代碼都有自己的地址)。

    這就會導致一個問題:兩個動態庫要求的地址發生沖突。

    為了解決這個問題,我們希望動態庫中的代碼被加載到任意位置都能運行,這就是地址無關代碼。

    PIC(Position Independent Code)地址無關代碼是一種編程技術,它使得代碼不依賴于特定的內存地址。

    所以我們在編譯動態庫對應的目標文件時,需要加上-fPIC選項:

    gcc -fPIC -c
    3.4.1 原理
    • 相對尋址:PIC代碼通過使用相對尋址方式來訪問數據和代碼,而不是使用絕對地址。這意味著代碼可以在內存中的任何位置加載和執行,而不需要進行重定位。

    • 全局偏移表(GOT):在PIC中,全局變量和函數的地址是通過全局偏移表(GOT)來訪問的。GOT是一個數據結構,用于存儲全局變量和函數的實際地址。當代碼需要訪問這些全局符號時,它會通過GOT中的相應項來間接引用。

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

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

    相關文章

    HVV-某田相關經歷

    一、背景 本次項目為期兩周&#xff0c;由集團主導招募攻擊隊員對集團下屬及其子公司進行的攻防演練。本次項目主導研判分析應急排查內部Nday發掘。 二、研判分析 2.1、帆軟V10 漏洞概述 帆軟 V10 及 V11 版本報表軟件存在反序列化漏洞&#xff0c;攻擊者可利用該漏洞使用…

    AI與物聯網的深度融合:開啟智能生活新時代

    在當今數字化時代&#xff0c;人工智能&#xff08;AI&#xff09;和物聯網&#xff08;IoT&#xff09;作為兩大前沿技術&#xff0c;正在加速融合&#xff0c;為我們的生活和工作帶來前所未有的變革。這種融合不僅提升了設備的智能化水平&#xff0c;還為各行各業帶來了新的機…

    Linux `init` 相關命令的完整使用指南

    Linux init 相關命令的完整使用指南—目錄 一、init 系統簡介二、運行級別&#xff08;Runlevel&#xff09;詳解三、常用 init 命令及使用方法1. 切換運行級別2. 查看當前運行級別3. 服務管理4. 緊急模式&#xff08;Rescue Mode&#xff09; 四、不同 Init 系統的兼容性1. Sy…

    UNet 改進(12):UNet with ECA (Efficient Channel Attention) 網絡

    詳解 下面將詳細解析這個實現了ECA注意力機制的UNet網絡代碼。 1. 代碼概述 代碼實現了一個帶有Efficient Channel Attention (ECA)模塊的UNet網絡架構。 UNet是一種常用于圖像分割任務的編碼器-解碼器結構網絡,而ECA模塊則是一種輕量級的通道注意力機制,可以增強網絡對重…

    視頻監控EasyCVR視頻匯聚平臺接入海康監控攝像頭如何配置http監聽功能?

    一、方案概述 本方案主要通過EasyCVR視頻管理平臺&#xff0c;實現報警信息的高效傳輸與實時監控。海康監控設備能通過HTTP協議將報警信息發送至指定的目的IP或域名&#xff0c;而EasyCVR平臺則可以接收并處理這些報警信息&#xff0c;同時提供豐富的監控與管理功能&#xff0…

    人工智能與網絡安全:AI如何預防、檢測和應對網絡攻擊?

    引言&#xff1a;網絡安全新戰場&#xff0c;AI成關鍵角色 在數字化浪潮不斷推進的今天&#xff0c;網絡安全問題已經成為每一家企業、每一個組織無法回避的“隱形戰場”。無論是電商平臺、金融機構&#xff0c;還是政府機關、制造企業&#xff0c;都可能面臨數據泄露、勒索病毒…

    3D人臉掃描技術如何讓真人“進入“虛擬,虛擬數字人反向“激活“現實?

    隨著虛擬人技術的飛速發展&#xff0c;超寫實數字人已經成為數字娛樂、廣告營銷和虛擬互動領域的核心趨勢。無論是企業家、知名主持人還是明星&#xff0c;數字分身正在以高度還原的形象替代真人參與各類活動&#xff0c;甚至成為品牌代言、直播互動的新寵。 3D人臉掃描&#…

    遞歸函數詳解

    定義 遞歸是指一個函數在其定義中直接或間接地調用自身的方法。通過這種方式&#xff0c;函數可以將一個復雜的問題分解為規模更小的、與原問題相似的子問題&#xff0c;然后通過不斷地解決這些子問題來最終解決整個問題。 組成部分 遞歸主體 這是函數中遞歸調用自身的部分…

    ASP.NET Core Web API 配置系統集成

    文章目錄 前言一、配置源與默認設置二、使用步驟1&#xff09;創建項目并添加配置2&#xff09;配置文件3&#xff09;強類型配置類4&#xff09;配置Program.cs5&#xff09;控制器中使用配置6&#xff09;配置優先級測試7&#xff09;動態重載配置測試8&#xff09;運行結果示…

    在生信分析中,從生物學數據庫中下載的序列存放在哪里?要不要建立一個小型數據庫,或者存放在Gitee上?

    李升偉 整理 在Galaxy平臺中使用時&#xff0c;從NCBI等生物學數據庫下載的DNA序列的存儲位置和管理方式需要根據具體的工作流程和需求進行調整。以下是詳細的分步說明和建議&#xff1a; 一、Galaxy中DNA序列的默認存儲位置 在Galaxy的“歷史記錄”&#xff08;History&…

    SDK游戲盾如何接入?復雜嗎?

    接入SDK游戲盾&#xff08;通常指游戲安全防護類SDK&#xff0c;如防DDoS攻擊、防作弊、防外掛等功能&#xff09;的流程和復雜度取決于具體的服務商&#xff08;如騰訊云、上海云盾等&#xff09;以及游戲類型和技術架構。以下是一般性的接入步驟、復雜度評估及注意事項&#…

    通過類似數據蒸餾或主動學習采樣的方法,更加高效地學習良品數據分布

    好的&#xff0c;我們先聚焦第一個突破點&#xff1a; 通過類似數據蒸餾或主動學習采樣的方法&#xff0c;更加高效地學習良品數據分布。 這里我提供一個完整的代碼示例&#xff1a; ? Masked圖像重建 殘差熱力圖 這屬于自監督蒸餾方法的一個變體&#xff1a; 使用一個 預…

    【課題推薦】多速率自適應卡爾曼濾波(MRAKF)用于目標跟蹤

    多速率自適應卡爾曼濾波(Multi-Rate Adaptive Kalman Filter, MRAKF)是一種針對多傳感器異步數據融合的濾波算法,適用于傳感器采樣率不同、噪聲特性時變的目標跟蹤場景。本文給出一個多速率自適應卡爾曼濾波框架,以無人機跟蹤場景為例,融合IMU和GPS數據 文章目錄 背景多速…

    軟考 系統架構設計師系列知識點之雜項集萃(49)

    接前一篇文章&#xff1a;軟考 系統架構設計師系列知識點之雜項集萃&#xff08;48&#xff09; 第76題 某文件管理系統在磁盤上建立了位視圖&#xff08;bitmap&#xff09;&#xff0c;記錄磁盤的使用情況。若磁盤上物理塊的編號依次為&#xff1a;0、1、2、……&#xff1b…

    HTTP:七.HTTP緩存

    HTTP緩存介紹 HTTP緩存是一種通過存儲網絡資源的副本,以減少對原始服務器請求的技術。當客戶端再次請求相同資源時,如果該資源未過期,服務器可以直接從本地緩存中提供響應,而無需再次從原始服務器獲取。這大大減少了網絡延遲,提高了加載速度,并減輕了服務器的負載。HTTP…

    WPF 圖標原地旋轉

    如何使元素原地旋轉 - WPF .NET Framework | Microsoft Learn <ButtonRenderTransformOrigin"0.5,0.5"HorizontalAlignment"Left">Hello,World<Button.RenderTransform><RotateTransform x:Name"MyAnimatedTransform" Angle"…

    NO.91十六屆藍橋杯備戰|圖論基礎-圖的存儲和遍歷|鄰接矩陣|vector|鏈式前向星(C++)

    圖的基本概念 圖的定義 圖G是由頂點集V和邊集E組成&#xff0c;記為G (V, E)&#xff0c;其中V(G)表?圖G中頂點的有限?空集&#xff1b;E(G)表?圖G中頂點之間的關系&#xff08;邊&#xff09;集合。若 V { v 1 , v 2 , … , v n } V \left\{ v_{1},v_{2},\dots,v_{n} …

    【項目日記(一)】-仿mudou庫one thread oneloop式并發服務器實現

    1、模型框架 客戶端處理思想&#xff1a;事件驅動模式 事件驅動處理模式&#xff1a;誰觸發了我就去處理誰。 &#xff08; 如何知道觸發了&#xff09;技術支撐點&#xff1a;I/O的多路復用 &#xff08;多路轉接技術&#xff09; 1、單Reactor單線程&#xff1a;在單個線程…

    Go語言實現OAuth 2.0認證服務器

    文章目錄 1. 項目概述1.1 OAuth2 流程 2. OAuth 2.0 Storage接口解析2.1 基礎方法2.2 客戶端管理相關方法2.3 授權碼相關方法2.4 訪問令牌相關方法2.5 刷新令牌相關方法 2.6 方法調用時序2.7 關鍵注意點3. MySQL存儲實現原理3.1 數據庫設計3.2 核心實現 4. OAuth 2.0授權碼流程…

    結合 Python 與 MySQL 構建你的 GenBI Agent_基于 MCP Server

    寫在前面 商業智能(BI)正在經歷一場由大型語言模型(LLM)驅動的深刻變革。傳統的 BI 工具通常需要用戶學習復雜的界面或查詢語言,而生成式商業智能 (Generative BI, GenBI) 則旨在讓用戶通過自然語言與數據交互,提出問題,并獲得由 AI 生成的數據洞察、可視化建議甚至完整…