ELF文件解析
文章目錄
- ELF文件解析
- 1. 簡介
- 1.1 分類
- 1.2 作用
- 2. 格式分析
- 2.1 源碼
- 2.2 ELF頭部
- 2.3 Section
- 2.4 字符串表
- 2.5 符號表
- 2.6 重定位
- 3. 動態裝載與動態鏈接
- 3.1 程序頭
- 3.1.1 基地址
- 3.1.2 段權限
- 3.1.3 實例
- 3.2 注釋段
- 3.3 動態鏈接
- 3.3.1 程序解析器
- 3.3.2 動態鏈接器
- 3.3.3 動態段
- 3.3.4 共享目標的依賴關系
- 3.3.5 全局偏移量表
- 3.3.6 函數地址
- 3.3.7 函數鏈接表
- 3.3.8 解析符號
- 3.4 哈希表
- 3.5 初始化和終止函數
ELF文件解析
ELF(Executable and Linkable Format)是一種用于二進制文件、可執行文件、目標代碼、共享庫和核心轉儲格式文件的文件格式。
本文我們來解析一些ELF文件的具體格式信息。
1. 簡介
1.1 分類
ELF文件有四種類型:
重定位文件(ET_REL),也就是常稱的目標文件,包含適合于與其他目標文件鏈接來創建可執行文件或者共享目標文件的代碼和數據。
可執行文件(ET_EXEC),包含適合于執行的一個程序,此文件規定了exec() 如何創建一個程序的進程映像。
共享目標文件(ET_DYN),即共享對象文件、動態庫文件, 包含可在兩種上下文中鏈接的代碼和數據。
- 首先鏈接編輯器可以將它和其它可重定位文件和共享目標文件一起處理, 生成另外一個目標文件。
- 其次動態鏈接器可能將它與某 個可執行文件以及其它共享目標一起組合,創建進程映像。
核心轉儲文件(ET_CORE),包括程序運行的內存數據和代碼。
除此之外還會有類型不確定的ELF文件(ET_NONE),即未知文件。
1.2 作用
ELF文件參與了二進制文件的兩個過程:
所以可以從不同的角度來看待ELF格式的文件:
如果用于編譯和鏈接(可重定位文件),則編譯器和鏈接器將把ELF文件看作是節頭表描述的節的集合(Section),程序頭表可選。
如果用于加載執行(可執行文件),則加載器則將把ELF文件看作是程序頭表描述的段的集合(Segment),一個段可能包含多個節,節頭表可選。
如果是共享文件,則兩者都含有。
所以ELF文件的大致結構可以視為如下:
| 鏈接視圖 | 執行視圖 |
| ELF文件頭部 | ELF文件頭部 |
| 程序頭部表(可選) | 程序頭部表 |
| Section 1 | Segment 1 |
| Section ... | |
| Section N | Segment 2 |
| Section ... | |
| Section ... | Segment ... |
| 節區頭部表 | 節區頭部表(可選) |
對于程序頭部表和節區頭部表來說:
如下我們可以看到Section和Segment的對應關系(Section to Segment mapping):
$ readelf -l helloElf file type is DYN (Shared object file) Entry point 0x1060 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 0x00000000000000000x00000000000005f8 0x00000000000005f8 R 0x1000LOAD 0x0000000000001000 0x0000000000001000 0x00000000000010000x00000000000001f5 0x00000000000001f5 R E 0x1000LOAD 0x0000000000002000 0x0000000000002000 0x00000000000020000x0000000000000160 0x0000000000000160 R 0x1000LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000258 0x0000000000000260 RW 0x1000DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc80x00000000000001f0 0x00000000000001f0 RW 0x8NOTE 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8NOTE 0x0000000000000358 0x0000000000000358 0x00000000000003580x0000000000000044 0x0000000000000044 R 0x4GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x00000000000020140x0000000000000044 0x0000000000000044 R 0x4GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 0x10GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000248 0x0000000000000248 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 .got2. 格式分析
2.1 源碼
我們來準備一個最簡單的源碼:
#include <stdio.h>int main(int argc, char* argv[]) {printf("hello world!\n");return 0; }分別將其編譯成為三種不同的文件:
2.2 ELF頭部
ELF文件頭部定義如下:
#define EI_NIDENT 16#define SHN_UNDEF 0typedef struct {unsigned char e_ident[EI_NIDENT];uint16_t e_type; //文件標識和類型信息(包括魔術)uint16_t e_machine; //適用的處理器體系結構uint32_t e_version; //目標文件的版本,EV_CURRENTElfN_Addr e_entry; //程序入口地址ElfN_Off e_phoff; //程序頭表(program header table)開始處在文件中的偏移量ElfN_Off e_shoff; //節頭表(section header table)開始處在文件中的偏移量uint32_t e_flags; //處理器特定的標志位uint16_t e_ehsize; // ELF 文件頭的大小,以字節為單位uint16_t e_phentsize; //程序頭表中每一個表項的大小,以字節為單位uint16_t e_phnum; //程序頭表中總共有多少個表項uint16_t e_shentsize; //節頭表中每一個表項的大小,以字節為單位uint16_t e_shnum; //節頭表中總共有多少個表項uint16_t e_shstrndx; //節頭表中與節名字表相對應的表項的索引。如果文件沒有節名字表,此值應設置為 SHN_UNDEF。 } ElfN_Ehdr;對于這個結構體的每個字段,我們可用在man(5)文檔中找到詳細信息,這里不再詳細介紹,使用readelf -h可用查看ELF的文件頭,如下:
$ 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 (Shared object file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x1060Start of program headers: 64 (bytes into file)Start of section headers: 16928 (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: 36Section header string table index: 35我們使用hexdump查看原始文件內容信息如下:
$ hexdump -C -n 512 hello 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 03 00 3e 00 01 00 00 00 60 10 00 00 00 00 00 00 |..>.....`.......| 00000020 40 00 00 00 00 00 00 00 20 42 00 00 00 00 00 00 |@....... B......| 00000030 00 00 00 00 40 00 38 00 0d 00 40 00 24 00 23 00 |....@.8...@.$.#.| 00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......| 00000050 40 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 |@.......@.......| 00000060 d8 02 00 00 00 00 00 00 d8 02 00 00 00 00 00 00 |................| 00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................| 00000080 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................| 00000090 18 03 00 00 00 00 00 00 1c 00 00 00 00 00 00 00 |................| 000000a0 1c 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 |................|2.3 Section
ELF文件中的節是從編譯器鏈接角度來看文件的組成的。從鏈接器的角度上來看,包括指令、數據、符號以及重定位表等等。
在ELF 文件頭中,有很多字段是描述Section頭的:
某些表項的索引值被保留,有特殊的含義。 ELF 文件的節頭表中不會出現索引值為以下各值的表項:
//SHN 大致是section indexes的簡寫/* special section indexes */ #define SHN_UNDEF 0 //一個未定義的、不存在的節的索引 #define SHN_LORESERVE 0xff00 //被保留索引號區間的下限 #define SHN_LOPROC 0xff00 //處理器定制節所保留的索引號區間的下限 #define SHN_HIPROC 0xff1f //處理器定制節所保留的索引號區間的上限 #define SHN_LIVEPATCH 0xff20 #define SHN_ABS 0xfff1 //此節中所定義的符號有絕對的值,這個值不會因重定位而改變 #define SHN_COMMON 0xfff2 //此節中所定義的符號是公共的 #define SHN_HIRESERVE 0xffff //被保留索引號區間的上限在程序的編譯鏈接過程中,編譯器將一個一個.o文件鏈接成一個可以執行的ELF文件的過程中,同時也生成了一個表。這個表記錄了各個Section所處的區域。在程序中,程序的section header有多個項,但是大小是一樣,結構定義如下:
typedef struct {uint32_t sh_name; //名稱,字符串表的索引(偏移值 .shstrtab)uint32_t sh_type; //節類型, SHT_XXXuint64_t sh_flags; //節的讀寫執行屬性Elf64_Addr sh_addr; //映射地址,如果本節的內容需要映射到進程空間中去,此成員指定映射的起始地址;如果不需要映射,此值為 0。Elf64_Off sh_offset; //偏移uint64_t sh_size; //大小uint32_t sh_link; //此成員是一個索引值,指向節頭表中本節所對應的位置uint32_t sh_info; //此成員含有此節的附加信息uint64_t sh_addralign; //對齊字節uint64_t sh_entsize; //有一些節的內容是一張表,其中每一個表項的大小是固定的,比如符號表。對于這種表來說,本成員指定其每一個表項的大小。 } Elf64_Shdr;在這個結構體里面,sh_type和sh_flags代表節的類型和屬性,有如下定義
/* sh_type */ #define SHT_NULL 0 //一個無效的節頭,它也沒有對應的節 #define SHT_PROGBITS 1 //此值表明本節所含有的信息是由程序定義的 #define SHT_SYMTAB 2 //完整符號表 #define SHT_STRTAB 3 //字符串表 #define SHT_RELA 4 //重定位節, 含有帶明確加數(addend)的重定位項(一般來說jmp指令需要跳過指令長度) #define SHT_HASH 5 //哈希表,所有參與動態連接的目標文件都必須要包含一個符號哈希表。 #define SHT_DYNAMIC 6 //動態連接信息 #define SHT_NOTE 7 //表明本節包含的信息用于以某種方式來標記本文件 #define SHT_NOBITS 8 //此值表明這一節的內容是空的,節并不占用實際的空間 #define SHT_REL 9 //無附加的重定位項 #define SHT_SHLIB 10 //保留值 #define SHT_DYNSYM 11 //動態鏈接符號表 #define SHT_NUM 12 #define SHT_LOPROC 0x70000000 //為特殊處理器保留的節類型索引值的下邊界 #define SHT_HIPROC 0x7fffffff //為特殊處理器保留的節類型索引值的上邊界 #define SHT_LOUSER 0x80000000 //為應用程序保留節類型索引值的下邊界 #define SHT_HIUSER 0xffffffff //為應用程序保留節類型索引值的下邊界/* sh_flags */ #define SHF_WRITE 0x1 //本節所包含的內容在進程運行過程中是可寫的 #define SHF_ALLOC 0x2 //表示本節內容在進程運行過程中要占用內存單元(并不是所有節都會占用實際的內存,有一些起控制作用的節,在目標文件映射到進程空間時,并不需要占用內存) #define SHF_EXECINSTR 0x4 //表示此節內容是指令代碼此外對于Section header有兩個比較特殊的成員:
對于某些節類型來說,sh_link和sh_info含有特殊的信息,見下表。
| SHT_DYNAMIC | 用于本節中項目的字符串表在節頭表中相應的索引值 | 0 |
| SHT_HASH | 用于本節中哈希表的符號表在節頭表中相應的索引值 | 0 |
| SHT_REL /SHT_RELA | 相應符號表在節頭表中的索引值 | 本重定位節所應用到目標節在節頭表中的索引值 |
| SHT_SYMTAB / SHT_DYNSYM | 相關字符串表的節頭索引 | 符號表中最后一個本地符號的索引值加 1 |
| 其它 | SHN_UNDEF | 0 |
Section 通過偏移和大小來確定具體信息,同樣使用readelf -t我們可用查看所有section 頭部數組的結構,如下:
$ readelf -t hello There are 36 section headers, starting at offset 0x4220:Section Headers:[Nr] NameType Address Offset LinkSize EntSize Info AlignFlags[ 0] NULL 0000000000000000 0000000000000000 00000000000000000 0000000000000000 0 0[0000000000000000]: [ 1] .interpPROGBITS 0000000000000318 0000000000000318 0000000000000001c 0000000000000000 0 1[0000000000000002]: ALLOC[ 2] .note.gnu.propertyNOTE 0000000000000338 0000000000000338 00000000000000020 0000000000000000 0 8[0000000000000002]: ALLOC同樣我們知道在Section header在文件中的偏移(ELF文件頭部有結構),可用查看數據如下:
$ hexdump -C -n 512 -s 0x4220 hello 00004220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00004260 1b 00 00 00 01 00 00 00 02 00 00 00 00 00 00 00 |................| 00004270 18 03 00 00 00 00 00 00 18 03 00 00 00 00 00 00 |................| 00004280 1c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004290 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000042a0 23 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |#...............| 000042b0 38 03 00 00 00 00 00 00 38 03 00 00 00 00 00 00 |8.......8.......| 000042c0 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ...............| 000042d0 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000042e0 36 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |6...............| 000042f0 58 03 00 00 00 00 00 00 58 03 00 00 00 00 00 00 |X.......X.......| 00004300 24 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |$...............| 00004310 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004320 49 00 00 00 07 00 00 00 02 00 00 00 00 00 00 00 |I...............| 00004330 7c 03 00 00 00 00 00 00 7c 03 00 00 00 00 00 00 ||.......|.......| 00004340 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ...............| 00004350 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00004360 57 00 00 00 f6 ff ff 6f 02 00 00 00 00 00 00 00 |W......o........| 00004370 a0 03 00 00 00 00 00 00 a0 03 00 00 00 00 00 00 |................| 00004380 24 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 |$...............| 00004390 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000043a0 61 00 00 00 0b 00 00 00 02 00 00 00 00 00 00 00 |a...............| 000043b0 c8 03 00 00 00 00 00 00 c8 03 00 00 00 00 00 00 |................| 000043c0 a8 00 00 00 00 00 00 00 07 00 00 00 01 00 00 00 |................| 000043d0 08 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 |................| 000043e0 69 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00 |i...............| 000043f0 70 04 00 00 00 00 00 00 70 04 00 00 00 00 00 00 |p.......p.......| 00004400 82 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|因為Elf64_Shdr的大小為0x40,所以我們可用看一下幾個Section:
這些值都是對應的uint32_t sh_name成員在文件中的值。因為對于section的名稱,有專門的section 來記錄,索引為uint16_t e_shstrndx;,因此我們可用找到這個Section的具體內容如下:
$ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr| 00004133 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e 00 2e 67 |..gnu.version..g| 00004143 6e 75 2e 76 65 72 73 69 6f 6e 5f 72 00 2e 72 65 |nu.version_r..re| 00004153 6c 61 2e 64 79 6e 00 2e 72 65 6c 61 2e 70 6c 74 |la.dyn..rela.plt| 00004163 00 2e 69 6e 69 74 00 2e 70 6c 74 2e 67 6f 74 00 |..init..plt.got.| 00004173 2e 70 6c 74 2e 73 65 63 00 2e 74 65 78 74 00 2e |.plt.sec..text..| 00004183 66 69 6e 69 00 2e 72 6f 64 61 74 61 00 2e 65 68 |fini..rodata..eh| 00004193 5f 66 72 61 6d 65 5f 68 64 72 00 2e 65 68 5f 66 |_frame_hdr..eh_f| 000041a3 72 61 6d 65 00 2e 69 6e 69 74 5f 61 72 72 61 79 |rame..init_array| 000041b3 00 2e 66 69 6e 69 5f 61 72 72 61 79 00 2e 64 79 |..fini_array..dy| 000041c3 6e 61 6d 69 63 00 2e 64 61 74 61 00 2e 62 73 73 |namic..data..bss| 000041d3 00 2e 63 6f 6d 6d 65 6e 74 00 2e 64 65 62 75 67 |..comment..debug| 000041e3 5f 61 72 61 6e 67 65 73 00 2e 64 65 62 75 67 5f |_aranges..debug_| 000041f3 69 6e 66 6f 00 2e 64 65 62 75 67 5f 61 62 62 72 |info..debug_abbr| 00004203 65 76 00 2e 64 65 62 75 67 5f 6c 69 6e 65 00 2e |ev..debug_line..| 00004213 64 65 62 75 67 5f 73 74 72 00 00 00 00 00 00 00 |debug_str.......| 00004223 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|和Section 頭部完全匹配,如下:
$ readelf -t hello There are 36 section headers, starting at offset 0x4220:Section Headers:[Nr] NameType Address Offset LinkSize EntSize Info AlignFlags[ 0] NULL 0000000000000000 0000000000000000 00000000000000000 0000000000000000 0 0[0000000000000000]: [ 1] .interpPROGBITS 0000000000000318 0000000000000318 0000000000000001c 0000000000000000 0 1[0000000000000002]: ALLOC[ 2] .note.gnu.propertyNOTE 0000000000000338 0000000000000338 00000000000000020 0000000000000000 0 8[0000000000000002]: ALLOC[ 3] .note.gnu.build-idNOTE 0000000000000358 0000000000000358 00000000000000024 0000000000000000 0 4[0000000000000002]: ALLOC[ 4] .note.ABI-tagNOTE 000000000000037c 000000000000037c 00000000000000020 0000000000000000 0 4[0000000000000002]: ALLOC因此這里我們也知道uint32_t sh_name是字符串表中的偏移值。
2.4 字符串表
字符串表是一個包含了若干以NULL結尾的字符序列,即字符串。在目標文件中這些字符串通常是符號的名字或者節的名字。在目標文件的其它部分中,當需要引用某個字符串時,只需要提供該字符串在字符串表中的序號即可。
字符串表中的第一個字符串(序號為 NULL)永遠是空串,即NULL,它可以用于表示一個空的名字或者沒有名字。所以,字符串表的第一個字節是NULL。由于每一個字符串都是以NULL結尾,所以字符串表的最后一個字節也必然為NULL。
字符串表也可以是空的,不含有任何字符串,這時,節頭中的 sh_size 成員必須是 0。
一個目標文件中可能有多個字符串表,例如:
我們看一個.shstrtab字符串表的實例:
$ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr|這個字符串表大致可用表示如下:
| 0x0 | NULL |
| 0x1 | .symtab |
| 0x9 | .strtab |
| 0x11 | .shstrtab |
| 0x1b | .interp |
這里也可用看到序號(sh_name)其實就是字符串表中的偏移值。
我們看一下一個字符串表的DUMP信息,如下:
$ readelf -t hello There are 36 section headers, starting at offset 0x4220:Section Headers:[Nr] NameType Address Offset LinkSize EntSize Info AlignFlags[ 0] NULL 0000000000000000 0000000000000000 00000000000000000 0000000000000000 0 0[0000000000000000]: [ ...][34] .strtabSTRTAB 0000000000000000 0000000000003ec0 00000000000000203 0000000000000000 0 1[0000000000000000]: [35] .shstrtabSTRTAB 0000000000000000 00000000000040c3 0000000000000015a 0000000000000000 0 1[0000000000000000]:這里有兩個字符串表:
dump的文件內容如下:
$ hexdump -C -n 512 -s 0x3ec0 hello 00003ec0 00 63 72 74 73 74 75 66 66 2e 63 00 64 65 72 65 |.crtstuff.c.dere| 00003ed0 67 69 73 74 65 72 5f 74 6d 5f 63 6c 6f 6e 65 73 |gister_tm_clones| 00003ee0 00 5f 5f 64 6f 5f 67 6c 6f 62 61 6c 5f 64 74 6f |.__do_global_dto| 00003ef0 72 73 5f 61 75 78 00 63 6f 6d 70 6c 65 74 65 64 |rs_aux.completed| 00003f00 2e 37 39 37 30 00 5f 5f 64 6f 5f 67 6c 6f 62 61 |.7970.__do_globa| 00003f10 6c 5f 64 74 6f 72 73 5f 61 75 78 5f 66 69 6e 69 |l_dtors_aux_fini| 00003f20 5f 61 72 72 61 79 5f 65 6e 74 72 79 00 66 72 61 |_array_entry.fra| 00003f30 6d 65 5f 64 75 6d 6d 79 00 5f 5f 66 72 61 6d 65 |me_dummy.__frame| 00003f40 5f 64 75 6d 6d 79 5f 69 6e 69 74 5f 61 72 72 61 |_dummy_init_arra| 00003f50 79 5f 65 6e 74 72 79 00 68 65 6c 6c 6f 2e 63 00 |y_entry.hello.c.| 00003f60 5f 5f 46 52 41 4d 45 5f 45 4e 44 5f 5f 00 5f 5f |__FRAME_END__.__|$ hexdump -C -n 512 -s 0x40c3 hello 000040c3 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 000040d3 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 000040e3 72 70 00 2e 6e 6f 74 65 2e 67 6e 75 2e 70 72 6f |rp..note.gnu.pro| 000040f3 70 65 72 74 79 00 2e 6e 6f 74 65 2e 67 6e 75 2e |perty..note.gnu.| 00004103 62 75 69 6c 64 2d 69 64 00 2e 6e 6f 74 65 2e 41 |build-id..note.A| 00004113 42 49 2d 74 61 67 00 2e 67 6e 75 2e 68 61 73 68 |BI-tag..gnu.hash| 00004123 00 2e 64 79 6e 73 79 6d 00 2e 64 79 6e 73 74 72 |..dynsym..dynstr| 00004133 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e 00 2e 67 |..gnu.version..g| 00004143 6e 75 2e 76 65 72 73 69 6f 6e 5f 72 00 2e 72 65 |nu.version_r..re| 00004153 6c 61 2e 64 79 6e 00 2e 72 65 6c 61 2e 70 6c 74 |la.dyn..rela.plt| 00004163 00 2e 69 6e 69 74 00 2e 70 6c 74 2e 67 6f 74 00 |..init..plt.got.|2.5 符號表
目標文件中的符號表(symbol table)所包含的信息用于定位和重定位程序中的符號定義和引用。目標文件的其它部分通過一個符號在這個表中的索引值來使用該符號。索引值從 0 開始計數,但值為 0 的表項(即第一項)并沒有實際的意義,它表示未定義的符號。這里用常量 STN_UNDEF 來表示未定義的符號。
一般來說,符號表包括兩個部分:
符號表項的定義格式如下:
typedef struct {uint32_t st_name; //字符串表的索引(偏移),字符串表有節頭部指定下標Elf32_Addr st_value; //符號的值或者地址(重定位文件中是偏移)uint32_t st_size; //各種符號的大小各不相同,比如一個對象的大小就是它實際占用的字節數。如果一個符號的大小為 0 或者大小未知,則這個值為 0。unsigned char st_info; //符號的類型和屬性unsigned char st_other; //本數據成員目前暫未使用,在目標文件中一律賦值為 0uint16_t st_shndx; //相關聯的節(符號在那個節中的索引) } Elf32_Sym;typedef struct {uint32_t st_name;unsigned char st_info;unsigned char st_other;uint16_t st_shndx;Elf64_Addr st_value;uint64_t st_size; } Elf64_Sym;對于st_value的值,沒有固定的類型,它可能代表一個數值,也可以是一個地址,具體是什么要看上下文。對于不同的目標文件類型,符號表項的 st_value 的含義略有不同:
- 在重定位文件中,如果一個符號對應的節的索引值是SHN_COMMON, st_value值是這個節內容的字節對齊數。
- 在重定位文件中,如果一個符號是已定義的,那么它的st_value值是該符號的起始地址在其所在節中的偏移量,而其所在的節的索引由st_shndx給出。
- 在可執行文件和共享庫文件中, st_value不再是一個節內的偏移量,而是一個虛擬地址,直接指向符號所在的內存位置。這種情況下, st_shndx就不再需要了。
對于符號成員st_info比較重要,由一系列的比特位構成,標識了“符號綁定(symbol binding)”、“符號類型(symbol type)”和“符號信息(symbol infomation)”三種屬性。下面幾個宏分別用于讀取這三種屬性值。
#define ELF_ST_BIND(x) ((x) >> 4) #define ELF_ST_TYPE(x) (((unsigned int) x) & 0xf)#define ELF32_ST_BIND(x) ELF_ST_BIND(x) #define ELF32_ST_TYPE(x) ELF_ST_TYPE(x)#define ELF64_ST_BIND(x) ELF_ST_BIND(x) #define ELF64_ST_TYPE(x) ELF_ST_TYPE(x)符號綁定(Symbol Binding),符號綁定屬性由ELF32_ST_BIND指定,如下:
| STB_LOCAL | 0 |
| STB_GLOBAL | 1 |
| STB_WEAK | 2 |
| STB_LOPROC | 13 |
| STB_HIPROC | 15 |
- 當連接編輯器把若干個可重定位目標文件連接起來時,同名的STB_GLOBAL 符號不允許出現多次。而如果在一個目標文件中已經定義了一個全局的符號(global symbol),當一個同名的弱符號(weak symbol)出現時,并不會發生錯誤。連接編輯器會以全局符號為準,忽略弱符號。與全局符號相似,如果已經存在的是一個公用符號,即 st_shndx 域為SHN_COMMON 值的符號,當一個同名的弱符號(weak symbol)出現時,也不會發生錯誤。連接編輯器會以公用符號為準,忽略弱符號。
- 在查找符號定義時,連接編輯器可能會搜索存檔的庫文件。如果是查找全局符號,連接編輯器會提取包含該未定義的全局符號的存檔成員,存檔成員可能是一個全局的符號,也可能是弱符號;而如果是查找弱符號,連接編輯器不會去提取存檔成員。未解析的弱符號值為 0。
符號類型(Symbol Types),符號類型屬性由ELF32_ST_TYPE指定,如下:
| STT_NOTYPE | 0 |
| STT_OBJECT | 1 |
| STT_FUNC | 2 |
| STT_SECTION | 3 |
| STT_FILE | 4 |
| STT_LOPROC | 13 |
| STT_HIPROC | 15 |
此外還有個st_shndx也是比較重要,任何一個符號表項的定義都與某一個“節”相聯系,因為符號是為節而定義,在節中被引用。st_shndx數據成員即指明了相關聯的節。本數據成員是一個索引值,它指向相關聯的節在節頭表中的索引。在重定位過程中,節的位置會改變,本數據成員的值也隨之改變,繼續指向節的新位置。當本數據成員指向下面三種特殊的節索引值時,本符號具有如下特別的意義:
我們可用使用readelf 和 objdump來查看符號信息,如下:
$ readelf -s helloSymbol table '.dynsym' contains 7 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)Symbol table '.symtab' contains 70 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000318 0 SECTION LOCAL DEFAULT 1 2: 0000000000000338 0 SECTION LOCAL DEFAULT 2 3: 0000000000000358 0 SECTION LOCAL DEFAULT 3 4: 000000000000037c 0 SECTION LOCAL DEFAULT 4 5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5 6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6 7: 0000000000000470 0 SECTION LOCAL DEFAULT 7 8: 00000000000004f2 0 SECTION LOCAL DEFAULT 8 9: 0000000000000500 0 SECTION LOCAL DEFAULT 9 10: 0000000000000520 0 SECTION LOCAL DEFAULT 10 11: 00000000000005e0 0 SECTION LOCAL DEFAULT 11 12: 0000000000001000 0 SECTION LOCAL DEFAULT 12 13: 0000000000001020 0 SECTION LOCAL DEFAULT 13 14: 0000000000001040 0 SECTION LOCAL DEFAULT 14 15: 0000000000001050 0 SECTION LOCAL DEFAULT 15 16: 0000000000001060 0 SECTION LOCAL DEFAULT 16 17: 00000000000011e8 0 SECTION LOCAL DEFAULT 17 18: 0000000000002000 0 SECTION LOCAL DEFAULT 18 19: 0000000000002014 0 SECTION LOCAL DEFAULT 19 20: 0000000000002058 0 SECTION LOCAL DEFAULT 20 21: 0000000000003db8 0 SECTION LOCAL DEFAULT 21 22: 0000000000003dc0 0 SECTION LOCAL DEFAULT 22 23: 0000000000003dc8 0 SECTION LOCAL DEFAULT 23 24: 0000000000003fb8 0 SECTION LOCAL DEFAULT 24 25: 0000000000004000 0 SECTION LOCAL DEFAULT 25 26: 0000000000004010 0 SECTION LOCAL DEFAULT 26 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27 28: 0000000000000000 0 SECTION LOCAL DEFAULT 28 29: 0000000000000000 0 SECTION LOCAL DEFAULT 29 30: 0000000000000000 0 SECTION LOCAL DEFAULT 30 31: 0000000000000000 0 SECTION LOCAL DEFAULT 31 32: 0000000000000000 0 SECTION LOCAL DEFAULT 32 33: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c34: 0000000000001090 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones35: 00000000000010c0 0 FUNC LOCAL DEFAULT 16 register_tm_clones36: 0000000000001100 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux我們使用hexdump查看文件中符號的原始數據,如下:
$ readelf -S hello There are 36 section headers, starting at offset 0x4220: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.propert NOTE 0000000000000338 000003380000000000000020 0000000000000000 A 0 0 8[ 3] .note.gnu.build-i NOTE 0000000000000358 000003580000000000000024 0000000000000000 A 0 0 4[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c0000000000000020 0000000000000000 A 0 0 4[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a00000000000000024 0000000000000000 A 6 0 8[ 6] .dynsym DYNSYM 00000000000003c8 000003c800000000000000a8 0000000000000018 A 7 1 8[ 7] .dynstr STRTAB 0000000000000470 000004700000000000000082 0000000000000000 A 0 0 1$ hexdump -C -n 512 -s 0x3c8 hello 000003c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000003d8 00 00 00 00 00 00 00 00 3d 00 00 00 20 00 00 00 |........=... ...| 000003e8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000003f8 0b 00 00 00 12 00 00 00 00 00 00 00 00 00 00 00 |................| 00000408 00 00 00 00 00 00 00 00 1f 00 00 00 12 00 00 00 |................| 00000418 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000428 59 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 |Y... ...........| 00000438 00 00 00 00 00 00 00 00 68 00 00 00 20 00 00 00 |........h... ...| 00000448 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000458 10 00 00 00 22 00 00 00 00 00 00 00 00 00 00 00 |...."...........|$ objdump -s --section=.dynsym hellohello: file format elf64-x86-64Contents of section .dynsym:03c8 00000000 00000000 00000000 00000000 ................03d8 00000000 00000000 3d000000 20000000 ........=... ...03e8 00000000 00000000 00000000 00000000 ................03f8 0b000000 12000000 00000000 00000000 ................0408 00000000 00000000 1f000000 12000000 ................0418 00000000 00000000 00000000 00000000 ................0428 59000000 20000000 00000000 00000000 Y... ...........0438 00000000 00000000 68000000 20000000 ........h... ...0448 00000000 00000000 00000000 00000000 ................0458 10000000 22000000 00000000 00000000 ...."...........0468 00000000 00000000 ........2.6 重定位
重定位(relocation)是把符號引用與符號定義連接在一起的過程。比如,當程序調用一個函數時,將從當前運行的指令跳轉到一個新的指令地址去執行。在編寫程序的時候,我們只需指明所要調用的函數名(即符號引用),在重定位的過程中,函數名會與實際的函數所在地址(即符號定義)聯系起來,使程序知道應該跳轉到哪里去。
重定位文件必須知道如何修改其所包含的“節”的內容,在構建可執行文件或共享目標文件的時候,把節中的符號引用換成這些符號在進程空間中的虛擬地址。包含這些轉換信息的數據也就是“重定位項(relocation entries)”。
重定位結構信息定義如下:
typedef struct {Elf32_Addr r_offset; //重定位所作用的位置uint32_t r_info; } Elf32_Rel;typedef struct {Elf64_Addr r_offset;uint64_t r_info; } Elf64_Rel;Relocation structures that need an addend :typedef struct {Elf32_Addr r_offset;uint32_t r_info;int32_t r_addend; //額外的加數 } Elf32_Rela;typedef struct {Elf64_Addr r_offset;uint64_t r_info;int64_t r_addend; } Elf64_Rela;r_offset : 給出重定位所作用的位置。對于重定位文件來說,此值是受重定位作用的存儲單元在節中的字節偏移量(相對節的偏移);對于可執行文件或共享目標文件來說,此值是受重定位作用的存儲單元的虛擬地址。
r_info : 既給出了重定位所作用的符號表索引,也給出了重定位的類型。比如,如果是一個函數的重定位,本數據成員將要持有被調用函數所對應的符號表索引。如果索引值為 STN_UNDEF,即未定義索引,那么重定位過程中將使用 0 作為符號值。以下是應用于 r_info 的宏定義:
一個“重定位節(relocation section)”需要引用另外兩個節:一個是符號表節,一個是被修改節。在重定位節中,節頭的 sh_info 和 sh_link 成員分別指明了引用關系。不同的目標文件中,重定位項的r_offset成員的含義略有不同。
-
在重定位文件中, r_offset成員含有一個節偏移量。也就是說,重定位節本身描述的是如何修改文件中的另一個節的內容,重定位偏移量(r_offset)指向了另一個節中的一個存儲單元地址。
-
在可執行文件或共享目標文件中,r_offset含有的是符號定義在進程空間中的虛擬地址。可執行文件和共享目標文件是用于運行程序而不是構建程序的,所以對它們來說更有用的信息是運行期的內存虛擬地址,而不是某個符號定義在文件中的位置。
綜上所述,鏈接器可用通過這個節里面的內容找到:
而對于動態裝載器,可以通過這個段里面的內容找到:
因此就可用在其他目標文件中找到相應的符號進行重定位,舉個鏈接文件的例子:
//file1.c void fun();void _start() {fun(); }//file2.c void fun() { }我們對這兩個文件進行編譯:
$ gcc -g -c -O0 file1.c $ gcc -g -c -O0 file2.c $ gcc -g -nostdlib file1.o file2.o -o file.o對于file1.o 我們可用看到信息如下:
$ readelf -r file1.oRelocation section '.rela.text' at offset 0x3b8 contains 1 entry:Offset Info Type Sym. Value Sym. Name + Addend 00000000000a 000f00000004 R_X86_64_PLT32 0000000000000000 fun - 4$ objdump -S file1.ofile1.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <_start>: void fun();void _start() {0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbpfun();4: b8 00 00 00 00 mov $0x0,%eax9: e8 00 00 00 00 callq e <_start+0xe> }e: 90 nopf: 5d pop %rbp10: c3 retq偏移地址為00000000000a,以及符號名稱fun,這樣鏈接器就可用對其進行重定位了,結果如下:
$ objdump -S file.ofile.o: file format elf64-x86-64Disassembly of section .text:00000000000002b1 <_start>: void fun();void _start() {2b1: 55 push %rbp2b2: 48 89 e5 mov %rsp,%rbpfun();2b5: b8 00 00 00 00 mov $0x0,%eax2ba: e8 03 00 00 00 callq 2c2 <fun> }2bf: 90 nop2c0: 5d pop %rbp2c1: c3 retq00000000000002c2 <fun>: void fun() {2c2: 55 push %rbp2c3: 48 89 e5 mov %rsp,%rbp }2c6: 90 nop2c7: 5d pop %rbp2c8: c3 retq對于動態加載器來說,重定位節是修改動態導入變量或者函數的地址的,如下:
readelf -r helloRelocation section '.rela.dyn' at offset 0x520 contains 8 entries:Offset Info Type Sym. Value Sym. Name + Addend 000000003db8 000000000008 R_X86_64_RELATIVE 1140 000000003dc0 000000000008 R_X86_64_RELATIVE 1100 000000004008 000000000008 R_X86_64_RELATIVE 4008 000000003fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0 000000003fe0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0 000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000003ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0 000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0Relocation section '.rela.plt' at offset 0x5e0 contains 1 entry:Offset Info Type Sym. Value Sym. Name + Addend 000000003fd0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0$ objdump -s --section=.got hellohello: file format elf64-x86-64Contents of section .got:3fb8 c83d0000 00000000 00000000 00000000 .=..............3fc8 00000000 00000000 30100000 00000000 ........0.......3fd8 00000000 00000000 00000000 00000000 ................3fe8 00000000 00000000 00000000 00000000 ................3ff8 00000000 00000000 ........對于puts函數,重定位的地址為000000003fd0,這是一個虛擬地址,同樣我們通過名稱puts@GLIBC_2.2.5 + 0可以找到符號表對應的虛擬地址,填入地址000000003fd0就完成了整個重定位的過程。
3. 動態裝載與動態鏈接
可執行文件和共享目標文件(動態連接庫)是程序的靜態存儲形式。要執行一個程序,系統要先把相應的可執行文件和動態連接庫裝載到進程空間中,這樣形成一個可運行的進程的內存空間布局,也可以稱它為“進程鏡像”。一個已裝載完成的進程空間會包含多個不同的“段(segment)”,比如代碼段(text segment),數據段(data segment),堆棧段(stack
segment)等等。
準備一個程序的內存鏡像,可以大體上分為裝載和連接兩個步驟。
3.1 程序頭
一個可執行文件或共享目標文件的程序頭表(program header table)是一個數組,數組中的每一個元素稱為“程序頭(program header)”,每一個程序頭描述了一個“段(segment)”或者一塊用于準備執行程序的信息。一個目標文件中的“段(segment)”包含一個或者多個“節(section)”。程序頭只對可執行文件或共享目標文件有意義,對于其它類型的目標文件,該信息可以忽略。在目標文件的文件頭(elf header)中, e_phentsize 和 e_phnum 成員指定了程序頭的大小。
program header table數據結構定義如下:
/* These constants are for the segment types stored in the image headers */ #define PT_NULL 0 #define PT_LOAD 1 #define PT_DYNAMIC 2 #define PT_INTERP 3 #define PT_NOTE 4 #define PT_SHLIB 5 #define PT_PHDR 6 #define PT_TLS 7 /* Thread local storage segment */ #define PT_LOOS 0x60000000 /* OS-specific */ #define PT_HIOS 0x6fffffff /* OS-specific */ #define PT_LOPROC 0x70000000 #define PT_HIPROC 0x7fffffff #define PT_GNU_EH_FRAME 0x6474e550#define PT_GNU_STACK (PT_LOOS + 0x474e551)typedef struct {uint32_t p_type; //描述段的類型,或者如何解析本程序頭的信息uint32_t p_flags;Elf64_Off p_offset; //文件中的偏移Elf64_Addr p_vaddr; //在進程空間中虛擬地址Elf64_Addr p_paddr; //在進程空間中物理地址,不再可用uint64_t p_filesz; //文件大小uint64_t p_memsz; //內存大小uint64_t p_align; //內存對齊 } Elf64_Phdr;對于p_type,取值如下:
3.1.1 基地址
程序頭中出現的虛擬地址不能代表其相應的數據在進程內存空間中的虛擬地址。可執行文件中需要含有絕對的地址,比如變量地址,函數地址等,為了讓程序正確地執行,“段”中出現的虛擬地址必須在創建可執行程序時被重新計算。另一方面,出于 ELF 通用性的要求,目標文件的段中又不能出現絕對地址,其代碼是不應依賴于具體存儲位置的,即同一個段在被加載到兩個不同的進程中時,它的地址可能不同,但它的行為不能表現出不一樣。
在被加載到進程空間里時,盡管“段”會被分配到一個不確定的地址,但是不同的段之間會有確定的“相對位置(relative position)”。也就是說,在目標文件中存儲的兩個段,它們的位置之間有多少偏移,當它們被加載到內存中時,這兩個段的位置之間仍然保持這么大的偏移(距離)。一個段在內存中的虛擬地址與其在目標文件中的地址一般是不相等的,它們之間會有一個偏移量,這個偏移量被稱為“基地址(base address)”,基地址的作用之一就是在動態連接過程中為程序重定位內存鏡像。
一個可執行文件或共享目標文件的基地址是在運行期間由以下三個值計算出來的:內存加載地址,最大頁面大小,程序可裝載段的最低地址。為計算基地址,首先找出類型為 PT_LOAD(即可加載)而且 p_vaddr(段地址)最低的那個段,把這個段在內存中的地址與最大頁面大小相除,得到一個段地址的余數;再把p_vaddr 與最大頁面大小相除,得到一個 p_vaddr 的余數。基地址就是段地址的余數與p_vaddr的余數之差。
3.1.2 段權限
雖然 ELF 文件格式中沒有規定,但是一個可執行程序至少會有一個可加載的段。當為可加載段創建內存鏡像時,系統會按照 p_flags 的指示給段賦予一定的權限。
| PF_X | 0x1 | 可執行 |
| PF_W | 0x2 | 只寫 |
| PF_R | 0x4 | 只讀 |
| PF_MASKPROC | 0xf0000000 | 未指定 |
具體定義值如下:
/* These constants define the permissions on sections in the programheader, p_flags. */ #define PF_R 0x4 #define PF_W 0x2 #define PF_X 0x13.1.3 實例
我們看一個ELF文件的程序頭部信息,如下:
$ readelf -l helloElf file type is DYN (Shared object file) Entry point 0x1060 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 0x00000000000000000x00000000000005f8 0x00000000000005f8 R 0x1000LOAD 0x0000000000001000 0x0000000000001000 0x00000000000010000x00000000000001f5 0x00000000000001f5 R E 0x1000LOAD 0x0000000000002000 0x0000000000002000 0x00000000000020000x0000000000000160 0x0000000000000160 R 0x1000LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000258 0x0000000000000260 RW 0x1000DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc80x00000000000001f0 0x00000000000001f0 RW 0x8NOTE 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8NOTE 0x0000000000000358 0x0000000000000358 0x00000000000003580x0000000000000044 0x0000000000000044 R 0x4GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x00000000000020140x0000000000000044 0x0000000000000044 R 0x4GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 0x10GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000248 0x0000000000000248 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在最后面,我們可用看到每一個段對應的節的關系,一個Segment包含多個Section。
3.2 注釋段
類型為 PT_NOTE 的段往往會包含類型為 SHT_NOTE 的節, SHT_NOTE 節可以為目標文件提供一些特別的信息,用于給其它的程序檢查目標文件的一致性和兼容性。這些信息我們稱為“注釋信息”,這樣的節稱為“注釋節(note section)”,所在的段即為“注釋段(note segment)”。注釋信息可以包含任意數量的“注釋項”,每一個注釋項是一個數組,數組的每一個成員大小為 4 字節,格式依目標處理器而定。下圖解釋了注釋信息是如何組織的,但這僅是一種參考,不是規范的一部分。
| namesz |
| descsz |
| type |
| name… |
| desc… |
對于其中的每一個字段,含義分別如下:
namesz 和 name : Namesz 和 name 成對使用。 Namesz 是一個 4 字節整數,而 name 是一個以NULL結尾的字符串。 Namesz 是 name 字符串的長度。字符串 name 的內容是本項的所有者的名字。沒有正式的機制來避免名字沖突,一般按照慣例,系統提供商應把他們自己的名字寫進 name 項里,比如”XYZ Computer Company”。如果沒有名字
的話, namesz 是 0。由于數組項的大小是向 4 字節對齊的,所以如果字符串長度不是整 4 字節的話,需要填 0 補位。如果有補位的話, namesz 只計字符串長度,不計所補的空位。
descsz 和 desc : Descsz 和 desc 也成對使用,它們的格式與 namesz/name 完全相同。不過,desc 的內容沒有任何規定、限制,甚至建議,它包含哪些信息完全是自由的。
type : 這個字段給出描述項(desc)的解釋,或者說是描述項的類型。每一個提供商都會定義自己的類型,所以同一類型值對于不同的提供商其解釋也是不同的。當一個程序讀取注釋信息的時候,它必須同時辨認出 name 和 type 才能理解 desc 的內容。
下面我們看一下notes段的內容如下:
$ readelf -n mainDisplaying notes found in: .note.ABI-tagOwner Data size DescriptionHNU 0x00000010 NT_VERSION (version)description data: 00 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00Displaying notes found in: .note.gnu.build-idOwner Data size DescriptionGNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)Build ID: 6628b6f822b0c5175adbb067b53d66b4b4a806ba我們可用讀取一下原始內容如下:
$ hexdump -C -s 0x254 -n 32 main 00000254 04 00 00 00 10 00 00 00 01 00 00 00 48 4e 55 00 |............HNU.| 00000264 00 00 00 00 03 00 00 00 02 00 00 00 00 00 00 00 |................| 00000274上面的這些數據和使用命令readelf -n讀取出來的一致。
3.3 動態鏈接
動態鏈接也就是解析符號引用的過程,這個過程在進程初始化和進程運行期間都可能發生。
3.3.1 程序解析器
一個參與動態鏈接的可執行文件會包含一個類型為PT_INTERP的程序頭項。當執行一個程序的時候,系統函數exec(cmd)會被調用,在這個函數中,內核會去讀取這個PT_INTERP段,解析出其包含的一個路徑字符串,這個串指明了一個ELF程序解析器,系統會轉去初始化該解析器的進程鏡像。也就是,在這時系統會暫停原來的工作,不是用待執行文件的段內容去初始化進程空間,而是把進程空間暫時“借”給解析器程序使用。然后,解析器程序將從系統手中接過控制權繼續執行。
例如,我們可用在ELF看到解析器如下:
f$ file main main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6628b6f822b0c5175adbb067b53d66b4b4a806ba, with debug_info, not stripped$ hexdump -C -s 0x238 -n 32 main 00000238 2f 6c 69 62 36 34 2f 6c 64 2d 6c 69 6e 75 78 2d |/lib64/ld-linux-| 00000248 78 38 36 2d 36 34 2e 73 6f 2e 32 00 04 00 00 00 |x86-64.so.2.....| 00000258解析器以兩種方式來接手系統的控制:
第一種,解析器取得可執行文件的描述符,內容指針定位于文件開始處,解析器可以讀取并映射可執行程序的段到內存中。
第二種,對于有些可執行文件格式,系統直接將文件內容載入內存,并不把其文件描述符給解析器。
解析器可以是一個共享目標文件,也可以是一個可執行文件。
-
一般情況下,解析器會是一個共享目標文件,并且其段內容是位置不相關的,所以在不同的進程中,它的地址會不一樣,系統會使用mmap(...)系統調用在動態段區域來為解析器創建段的鏡像。所以,一般情況下不用擔心解析器的段內容會與待執行文件的內容發生地址沖突。
-
如果解析器是獨立的可執行文件,那么系統就要按照解析器程序的程序頭來加載它,在加載的時候就有可能與待執行文件的段相沖突,這種情況下由解析器來負責解決沖突。
3.3.2 動態鏈接器
當創建一個可執行文件時,如果依賴其它的動態鏈接庫,那么鏈接編輯器會在可執行文件的程序頭中加入一個PT_INTERP項,告訴系統這里需要使用動態鏈接器。
可執行文件與動態鏈接器一起創建了進程的鏡像,這個過程包含以下活動:
- 添加可執行文件的段到進程空間;
- 添加共享目標文件的段到進程空間;
- 為可執行文件和共享目標文件進行重定位;
- 如果動態鏈接器使用了可執行文件的文件描述符,應關閉它;
- 把控制權交給程序。
鏈接編輯器也會為動態鏈接庫組織一些數據,以方便它的鏈接過程。在“程序頭”部分提到過,為了方便在運行的時候訪問,這些數據放在可裝載的段中。當然具體的數據格式是依處理器而不同的。
- 類型為SHT_DYNAMIC的.dynamic 節中包含有很多種動態鏈接信息。在這個節的最開始處有一個結構,其中包含有其它動態鏈接信息的地址。
- 類型為SHT_HASH的.hash節中含有符號哈希表。
- 類型為SHT_PROGBITS的.got和.plt節各包含一張表。
共享目標所占據的內存地址可能與文件程序頭表中所記錄的不同。在程序開始執行以前,動態鏈接器會為內存鏡像做重定位,更新絕對地址。當然,庫文件在被裝載時,如果其內存地址與其文件中描述的完全相同的話,那些引用它們的絕對地址就是對的,不需要更新。但事實上,這種情況很少發生。
如果進程的環境變量中含有LD_BIND_NOW,而且其值不為空,那么動態連接器就要在程序開始運行之前把所有重定位都處理完。比如,在該環境變量為以下值時,動態連接器都需要這樣做:
- LD_BIND_NOW = 1
- LD_BIND_NOW = on
- LD_BIND_NOW = off
否則,如果LD_BIND_NOW沒有出現或者其值為空,動態鏈接器就可以把處理重定位的工作推后,即只有當一個符號被引用的時候才去重定位它。因為在程序運行過程中,有一些函數并不會被調用到,推后重定位是一種提高效率的方法,可以避免為這些函數做不必要的重定位。
3.3.3 動態段
如果一個目標文件參與動態鏈接的話,它的程序頭表中一定會包含一個類型為PT_DYNAMIC的表項,其所對應的段稱為動態段(dynamic segment),段的名字為.dynamic(也是.dynamic節)。動態段的作用是提供動態鏈接器所需要的信息,比如依賴于哪些共享目標文件、動態鏈接符號表的位置、動態鏈接重定位表的位置等等。這個動態段中包含有動態節,動態節由符號DYNAMIC所標記,它包含一個由如下結構體組成的數組。
typedef struct {Elf32_Sword d_tag;union {Elf32_Word d_val;Elf32_Addr d_ptr;} d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC[];typedef struct {Elf64_Sxword d_tag;union {Elf64_Xword d_val;Elf64_Addr d_ptr;} d_un; } Elf64_Dyn; extern Elf64_Dyn _DYNAMIC[];對于每一個這種類型的目標項,d_tag控制著對d_un的解析:
d_tag字段表示當前表項的具體類型,部分可選的枚舉值如下:
#define DT_NULL 0 //用于標記_DYNAMIC 數組的結束 #define DT_NEEDED 1 //依賴庫, DT_STRTAB標記 #define DT_PLTRELSZ 2 //重定位項的總大小 #define DT_PLTGOT 3 //GOT表地址 #define DT_HASH 4 //哈希表地址 #define DT_STRTAB 5 //字符串表的地址 #define DT_SYMTAB 6 //符號表的地址 #define DT_RELA 7 //重定位表的地址 #define DT_RELASZ 8 //重定位表大小 #define DT_RELAENT 9 //重定位表項大小 #define DT_STRSZ 10 //字符串表大小 #define DT_SYMENT 11 //符號表項大小 #define DT_INIT 12 //初始化函數 #define DT_FINI 13 //析構函數 #define DT_SONAME 14 //別名索引 #define DT_RPATH 15 //RPATH #define DT_SYMBOLIC 16 #define DT_REL 17 #define DT_RELSZ 18 #define DT_RELENT 19 #define DT_PLTREL 20 #define DT_DEBUG 21 #define DT_TEXTREL 22 #define DT_JMPREL 23 #define DT_ENCODING 32各個字段解釋如下:
我們來看一下可執行文件動態段的解析過程,首先我們看到的程序頭部信息如下:
$ readelf -l helloElf file type is DYN (Shared object file) Entry point 0x1060 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 0x00000000000000000x00000000000005f8 0x00000000000005f8 R 0x1000LOAD 0x0000000000001000 0x0000000000001000 0x00000000000010000x00000000000001f5 0x00000000000001f5 R E 0x1000LOAD 0x0000000000002000 0x0000000000002000 0x00000000000020000x0000000000000160 0x0000000000000160 R 0x1000LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000258 0x0000000000000260 RW 0x1000DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc80x00000000000001f0 0x00000000000001f0 RW 0x8NOTE 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8NOTE 0x0000000000000358 0x0000000000000358 0x00000000000003580x0000000000000044 0x0000000000000044 R 0x4GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x00000000000003380x0000000000000020 0x0000000000000020 R 0x8GNU_EH_FRAME 0x0000000000002014 0x0000000000002014 0x00000000000020140x0000000000000044 0x0000000000000044 R 0x4GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 0x10GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db80x0000000000000248 0x0000000000000248 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接著我們可以看到動態段的內容如下:
$ objdump -s --section=.dynamic hellohello: file format elf64-x86-64Contents of section .dynamic:3dc8 01000000 00000000 01000000 00000000 ................3dd8 0c000000 00000000 00100000 00000000 ................3de8 0d000000 00000000 e8110000 00000000 ................3df8 19000000 00000000 b83d0000 00000000 .........=......3e08 1b000000 00000000 08000000 00000000 ................3e18 1a000000 00000000 c03d0000 00000000 .........=......3e28 1c000000 00000000 08000000 00000000 ................3e38 f5feff6f 00000000 a0030000 00000000 ...o............3e48 05000000 00000000 70040000 00000000 ........p.......3e58 06000000 00000000 c8030000 00000000 ................3e68 0a000000 00000000 82000000 00000000 ................3e78 0b000000 00000000 18000000 00000000 ................3e88 15000000 00000000 00000000 00000000 ................3e98 03000000 00000000 b83f0000 00000000 .........?......3ea8 02000000 00000000 18000000 00000000 ................3eb8 14000000 00000000 07000000 00000000 ................3ec8 17000000 00000000 e0050000 00000000 ................3ed8 07000000 00000000 20050000 00000000 ........ .......3ee8 08000000 00000000 c0000000 00000000 ................3ef8 09000000 00000000 18000000 00000000 ................3f08 1e000000 00000000 08000000 00000000 ................3f18 fbffff6f 00000000 01000008 00000000 ...o............3f28 feffff6f 00000000 00050000 00000000 ...o............3f38 ffffff6f 00000000 01000000 00000000 ...o............3f48 f0ffff6f 00000000 f2040000 00000000 ...o............3f58 f9ffff6f 00000000 03000000 00000000 ...o............3f68 00000000 00000000 00000000 00000000 ................3f78 00000000 00000000 00000000 00000000 ................3f88 00000000 00000000 00000000 00000000 ................3f98 00000000 00000000 00000000 00000000 ................3fa8 00000000 00000000 00000000 00000000 ................虛擬的偏移地址為3dc8,跟我們目標匹配,這里我們看一下DT_NEEDED的數據為:3dc8 01000000 00000000 01000000 00000000;可以知道為DT_STRTAB的索引1的位置,DT_STRTAB的數據如下:3e48 05000000 00000000 70040000 00000000
查看表信息如下:
$ objdump -s --section=.dynstr hellohello: file format elf64-x86-64Contents of section .dynstr:0470 006c6962 632e736f 2e360070 75747300 .libc.so.6.puts.0480 5f5f6378 615f6669 6e616c69 7a65005f __cxa_finalize._0490 5f6c6962 635f7374 6172745f 6d61696e _libc_start_main04a0 00474c49 42435f32 2e322e35 005f4954 .GLIBC_2.2.5._IT04b0 4d5f6465 72656769 73746572 544d436c M_deregisterTMCl04c0 6f6e6554 61626c65 005f5f67 6d6f6e5f oneTable.__gmon_04d0 73746172 745f5f00 5f49544d 5f726567 start__._ITM_reg04e0 69737465 72544d43 6c6f6e65 5461626c isterTMCloneTabl04f0 6500 e.注意,上面這些地址信息都是虛擬地址,并不是文件的偏移地址。
3.3.4 共享目標的依賴關系
當動態鏈接器為一個目標文件創建內存段的時候,動態結構中的DT_NEEDED項會指明所依賴的庫,動態鏈接器會鏈接被引用的符號和它們所依賴的庫,這個過程會反復地執行,直到一個完整的進程鏡像被構建好。當解析一個符號引用的時候,動態鏈接器以一種“廣度優先”的算法來查找符號表。就是說,動態鏈接器首先查找可執行程序自己的符號表,然后是DT_NEEDED項所指明的庫的符號表,再接下來是下一層依賴庫的符號表,依次下去。共享目標文件必須是可讀的,其它權限沒有要求。
即使一個共享目標在依賴關系中被引用多次,動態鏈接器也只會鏈接它一次。在依賴關系列表中的名字,即可以是DT_SONAME字符串,也可以是用于創建目標文件的共享目標文件的路徑名。
如果一個共享目標名字中含有斜線(/)字符,比如/usr/lib/lib2或者directory/file,動態鏈接就直接把字符串作為路徑名。如果名字中沒有斜線,比如lib1,需要根據以下三種規則來查找庫文件:
第一,動態數組標記DT_RPATH可能給出了一個含有一系列目錄名的字符串,各目錄名以冒號:相隔。比如,如果字符串是/home/dir/lib:/home/dir2/lib:,表明動態鏈接器的查找路徑依次是/home/dir/lib、 /home/dir2/lib和當前目錄。
第二,進程的環境變量中會有一個LD_LIBRARY_PATH變量,它也含有一個目錄名列表,各目錄名以冒號:相隔,各目錄名列表以分號;相隔(LD_LIBRARY_PATH路徑的優先級要低于DT_RPATH所指明的路徑)。
第三,如果如上兩組路徑都無法找到所要的庫,動態鏈接庫就搜索/usr/lib。
3.3.5 全局偏移量表
全局偏移量表(global offset table)在私有數據中包含絕對地址。出于方便共享和重用的考慮,目標文件中的很多內容是“位置無關”的,其映射到進程內存中的什么位置是不一定的,所以只適合使用相對地址,全局偏移量表是一個例外。
總的來說,位置獨立的代碼不能含有絕對的虛擬地址。全局偏移量表選擇了在私有數據中含有絕對地址,這種辦法在沒有犧牲位置獨立性和可共享性的前提下保存了絕對地址。引用全局偏移量表的程序可以同時使用位置獨立的地址和絕對地址,把位置無關的引用重定向到絕對地址上去。
如果一個程序要求直接訪問符號的絕對地址,那么這個符號在全局偏移量表中就必須有一個對應的項。可執行文件和共享目標文件有各自的全局偏移量表,所以一個符號的地址可能會出現在多個表中。動態鏈接器會在程序開始執行之前,處理好所有全局偏移量表的重定位工作,所以在程序執行的時候,可以保證所有這些符號都有正確的絕對地址。
全局偏移量表的第 0 項是保留的,它用于持有動態結構的地址,由符號DYNAMIC引用。這樣,其它程序,比如動態鏈接器就可以直接找到其動態結構,而不用借助重定位項。這對于動態鏈接器來說尤為重要,因為它必須在不依賴于其它程序重定位其內存鏡像的情況下初始化自己。在 Intel 架構中,全局偏移量表中的第 1 項和第 2 項也是保留的,它們持有函數連接表的信息。
系統可能為同一個共享目標在不同的程序中選擇不同的段地址;甚至也可能每次為同一個程序選擇不同的地址。但是,在單次執行中,一旦一個進程的鏡像建立起來之后,直到程序退出,內存段的地址都不會再改變了。
3.3.6 函數地址
在可執行文件和共享目標文件中,當引用到同一個函數時,函數地址可能并不相同。在共享目標文件中,函數的地址被動態鏈接器正常地解析為它所在的虛擬地址。但在可執行文件中則不同,但可執行文件引用一個共享庫中的函數時,它不是直接指向函數的虛擬地址,而是被動態鏈接器定向到函數鏈接表中的一個表項。
但是,這樣的話,來自可執行文件的函數地址和來自共享目標文件的同一函數地址就會不同,為了避免在比較兩個函數地址時出現這樣的邏輯錯誤,鏈接編輯器和動態鏈接器做了一些特別操作。當可執行文件引用一個在共享目標文件中定義的函數時,鏈接編輯器就把這個函數的函數鏈接表項的地址放到其相應的符號表項中去。動態鏈接器會特別對待這種符號表項。在可執行文件中,如果動態鏈接器查找一個符號時遇到了這種符號表項,就會按照以下規則行事:
如果符號表項的st_shndx成員不是SHN_UNDEF,動態鏈接器就找到了一個符號的定義,把表項的st_value成員作為符號的地址。
如果符號表項的st_shndx成員是SHN_UNDEF,并且符號類型是STT_FUNC, st_value成員又非0的話,動態鏈接器就認定這是一個特殊的項,把st_value成員作為符號的地址。
否則,動態鏈接器認為這個符號是在可執行文件中未定義的。
有些重定位與函數鏈接表項有關,這些表項用于給函數調用做定向,而不是引用函數地址。這種重定位不能像上面所描述的那樣,用特別的方式去處理函數地址,因為動態鏈接器不可以把函數鏈接表項重定向到它們自己。
3.3.7 函數鏈接表
全局偏移量表用于把位置獨立的地址重定向到絕對地址,與此功能類似,函數鏈接表(procedure linkage table)的作用是把位置獨立的函數調用重定向到絕對地址。鏈接編輯器不能解析函數在不同目標文件之間的跳轉,那么,它就把對其它目標文件中函數的調用重定向到一個函數鏈接表項中去。動態鏈接器決定目標的絕對地址,并且會相應地修改全局偏移量表的內存鏡像。這樣,動態鏈接器就可以在不犧牲位置無關性和代碼的可共享性條件下,實現到絕對地址的重定位。可執行文件和共享目標文件有各自的函數鏈接表。
關于PLT和GOT的關系和動態解析的過程見其他分析文章。
3.3.8 解析符號
在以下的這些步驟中,動態鏈接器與程序合作來解析函數鏈接表和全局偏移量表中所有的符號引用。
在一開始創建程序內存鏡像的時候,動態鏈接器把全局偏移量表中的第2和第3個表項設為特定值。
如果函數連接表是位置獨立的,全局偏移量表的地址必須存儲在%ebx 中。進程空間中的每一個共享目標文件都有自己的函數連接表,每一個表都是用于本文件內的函數調用。那么,主調函數就要負責在調用函數連接表項之前設置全局偏移量表。
環境變量LD_BIND_NOW可以改變動態連接器的行為,如果它的值為非NULL,動態連接器在傳遞控制權給程序之前會估計函數連接表項。否則,如果其值為NULL,這種估計仍然會進行,但并不是在初始化的時候,這個過程會被推后,直到在執行過程中,該函數連接表項被用到才開始。
“延遲綁定/懶綁定” (lazy binding)一般來說都會提高應用程序的性能,因為這樣可以避免用不到的符號在動態連接過程中被解析。但是,在兩種情況下,延遲綁定的效果并不理想。
第一種情況,如果對一個共享目標函數的第一次引用比其后的引用要花更多時間的話,在第一次引用時,程序就要暫停下來,由動態連接器去解析符號,如果應用程序對這種不可預知的暫停比較敏感的話,后期綁定就不適用。
第二種情況,如果動態連接器解析一個符號失敗,程序將會被終止。如果沒有打開后期綁定的話,這一切都發生在程序實際得到控制權之前,進程將在初始化過程中被終止。而如果打開了后期綁定的話,錯誤會發生在程序運行過程中,如果應用程序對這種不可預知的錯誤敏感的話,后期綁定也不適用。
3.4 哈希表
一個 Elf32_Word 目標組成的哈希表支持符號表的訪問。下面表示一個哈希表的具體結構
| nbucket |
| nchain |
| bucket[0] … bucket[nbucket - 1] |
| chain[0] … chain[nchain - 1] |
Bucket 數組中含有 nbucket 個項, chain 數組中含有 nchain 個項,序號都從 0 開始。 Bucket 和 chain 中包含的都是符號表中的索引。符號表中的項數必須等于nchain,所以符號表中的索引號也可以用來索引 chain 表。一個哈希數輸入一個符號名,輸出一個值用于計算 bucket 索引。如果給出一個符號名,經哈希函數計算得到值 x,那么 x%nbucket 是 bucket 表內的索引, bucket[x%nbucket]給出一個符號表的索引值 y, y 同時也是 chain 表內的索引值。如果符號表內索引值為 y 的元素并不是所要的,那么 chain[y]給出符號表中下一個哈希值相同的項的索引。如果所有哈希值相同的項都不是所要的,最后的一個 chain[y]將包含值STN_UNDEF,說明這個符號表中并不含有此符號。
tatic unsigned elfhash(const char *_name) {const unsigned char *name = (const unsigned char *) _name;unsigned h = 0, g;while(*name) {h = (h << 4) + *name++;g = h & 0xf0000000;h ^= g;h ^= g >> 24;}return h; }static Elf32_Sym *soinfo_elf_lookup(struct soinfo *si, unsigned hash, const char *name) {Elf32_Sym *symtab = si->symtab;const char *strtab = si->strtab;unsigned n;for( n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n] ) {Elf32_Sym *s = symtab + n;if( strcmp(strtab + s->st_name, name) == 0 ) {return s;}}return NULL; }3.5 初始化和終止函數
當動態鏈接器構建好進程鏡像,并完成重定位后,每一個共享目標都有機會執行一些初始化代碼。所有共享目標的初始化都發生在程序開始執行前。
一個目標的初始化代碼被執行以前,必須保證它所依賴的所有目標已經被初始化過。這里所說的“依賴”,即出現在動態結構的DT_NEEDED項里。如果兩個目標,它們互相依賴,或者彼此間的依賴關系構成環狀的話,哪個應該被先初始化,這里未作定義。例如:
如果一個目標A依賴于另外一個目標B,而目標B又依賴于目標C的話。當需要對 做初始化時,應先遞規地初始化B和C,即先初始化 C,然后是,最后是A。
如果一個目標A依賴于另處兩個目標B和C,而B和C之間沒有依賴關系的話,B和C誰先被初始化都可以。
與初始化過程相似,每一個共享目標還可以有終止函數,將在進程準備終止的時候被調用。動態鏈接器調用終止函數的順序正好與初始化過程相反,如果一個目標沒有定義初始化函數的話,動態鏈接器應假設它有一個空的初始化函數并且被調用,并按照相反的順序來調用其終止函數。
動態鏈接器必須保證,無論是初始化函數還是終止函數都不能被重復調用。共享目標把初始化和終止函數分別定義在動態結構的DT_INIT和DT_FINI項中,初始化和終止函數的代碼存放在.init 和.fini節中。
我們看一下這兩個函數的尋找過程:
$ objdump -s --section=.dynamic hellohello: file format elf64-x86-64Contents of section .dynamic:3dc8 01000000 00000000 01000000 00000000 ................3dd8 0c000000 00000000 00100000 00000000 ................3de8 0d000000 00000000 e8110000 00000000 ................3df8 19000000 00000000 b83d0000 00000000 .........=......3e08 1b000000 00000000 08000000 00000000 ................3e18 1a000000 00000000 c03d0000 00000000 .........=......3e28 1c000000 00000000 08000000 00000000 ................3e38 f5feff6f 00000000 a0030000 00000000 ...o............3e48 05000000 00000000 70040000 00000000 ........p.......3e58 06000000 00000000 c8030000 00000000 ................3e68 0a000000 00000000 82000000 00000000 ................3e78 0b000000 00000000 18000000 00000000 ................3e88 15000000 00000000 00000000 00000000 ................3e98 03000000 00000000 b83f0000 00000000 .........?......3ea8 02000000 00000000 18000000 00000000 ................3eb8 14000000 00000000 07000000 00000000 ................3ec8 17000000 00000000 e0050000 00000000 ................3ed8 07000000 00000000 20050000 00000000 ........ .......3ee8 08000000 00000000 c0000000 00000000 ................3ef8 09000000 00000000 18000000 00000000 ................3f08 1e000000 00000000 08000000 00000000 ................3f18 fbffff6f 00000000 01000008 00000000 ...o............3f28 feffff6f 00000000 00050000 00000000 ...o............3f38 ffffff6f 00000000 01000000 00000000 ...o............3f48 f0ffff6f 00000000 f2040000 00000000 ...o............3f58 f9ffff6f 00000000 03000000 00000000 ...o............3f68 00000000 00000000 00000000 00000000 ................3f78 00000000 00000000 00000000 00000000 ................3f88 00000000 00000000 00000000 00000000 ................3f98 00000000 00000000 00000000 00000000 ................3fa8 00000000 00000000 00000000 00000000 ................3dd8 0c000000 00000000 00100000 00000000 和 3de8 0d000000 00000000 e8110000 00000000 分別指示了.init 和.fini的初始化和終止函數,這兩個節的反匯編內容如下:
$ objdump -d --section=.init hello hello: file format elf64-x86-64Disassembly of section .init:0000000000001000 <_init>:1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__>100f: 48 85 c0 test %rax,%rax1012: 74 02 je 1016 <_init+0x16>1014: ff d0 callq *%rax1016: 48 83 c4 08 add $0x8,%rsp101a: c3 retq $ objdump -d --section=.fini hello hello: file format elf64-x86-64Disassembly of section .fini:00000000000011e8 <_fini>:11e8: f3 0f 1e fa endbr64 11ec: 48 83 ec 08 sub $0x8,%rsp11f0: 48 83 c4 08 add $0x8,%rsp11f4: c3 retq總結
- 上一篇: php 克隆对象,php中对象的复制与克
- 下一篇: win7 IE8无法升级IE11