结合实例解读ELF文件
时间:2009-04-28 来源:engine_chen
结合实例解读ELF文件 |
来源:Linux联盟收集整理 作者: |
<IFRAME id=google_ads_frame1 style="LEFT: 0px; POSITION: absolute; TOP: 0px" name=google_ads_frame marginWidth=0 marginHeight=0 src="http://googleads.g.doubleclick.net/pagead/ads?client=ca-pub-3701573918329010&dt=1240922759625&lmt=1239602403&output=html&slotname=1645276672&correlator=1240922759625&url=http%3A%2F%2Fwww.xxlinux.com%2Flinux%2Farticle%2Faccidence%2Finstall%2F20070409%2F8124.html&ref=http%3A%2F%2F203.208.39.99%2Fsearch%3Fq%3Dcache%3ANL6naQoZekEJ%3Awww.91linux.com%2Fhtml%2Farticle%2Fprogram%2Fcpp%2F20080901%2F13369_2.html%2Bout%2B%25E6%25A0%25BC%25E5%25BC%258F%26cd%3D6%26hl%3Dzh-CN%26ct%3Dclnk%26gl%3Dcn%26st_usg%3DALhdy28uVNM8zA_tq0oKPY7P6sbGSURDxQ&frm=0&ga_vid=616159317.1240922760&ga_sid=1240922760&ga_hid=1412005881&flash=9.0.124.0&u_h=768&u_w=1366&u_ah=738&u_aw=1366&u_cd=32&u_tz=480&u_java=true&dtd=421&w=300&h=250&xpc=n6K5vh8c5W&p=http%3A//www.xxlinux.com" frameBorder=0 width=300 scrolling=no height=250 allowTransparency></IFRAME>
一. 预备知识 网上有很多文章讲叙了ELF文件的格式, 加载过程等, 其中我觉得比较实用的是这几篇: 1. breadbox 的< EXECUTABLE AND LINKABLE FORMAT (ELF)> 英文文档很多地方都有下载, alert7主页里面有中文翻译和英文原文. 英文原文: http://elfhack.whitecell.org/mydocs/elf.txt alert7翻译整理的中文: http://elfhack.whitecell.org/mydocs/ELF_chinese.txt 2. alert7的<ELF动态解析符号过程(修订版)> http://elfhack.whitecell.org/myd ... esolve_process1.txt 但2只是阐述了一下动态解析符号过程, 1 是列举了一大堆的参数结构, 其中的联系, 结构等如果你开始看肯定会摸不着头脑, 我自己因为要写一个程序, 用到ELF文件的一些东西, 想想肯定还有很多人有和我一样的困惑, 本文权当做我写的笔记, 如果同时也能给你带来帮助, 就不枉此文了. 本文假设你已经看过了1或者2等文章, 熟悉linux系统, gdb等. 二. 分析平台 [netconf@linux1 elf]$ uname -a Linux linux1 2.4.18-14 #1 Wed Sep 4 13:35:50 EDT 2002 i686 i686 i386 GNU/Linux [netconf@linux1 elf]$ cat /proc/version Linux version 2.4.18-14 ([email protected]) (gcc version 3.2 20020903 (Red Hat Linux 8.0 3.2-7)) #1 Wed Sep 4 13:35:50 EDT 2002 [netconf@linux1 elf]$ rpm -qf /usr/bin/readelf /usr/bin/hexdump binutils-2.13.90.0.2-2 util-linux-2.11r-10 [netconf@linux1 elf]$ 三. 分析的例子/程序 [netconf@linux1 elf]$ cat elf8.c #include <elf.h> int foo1() { printf([+] foo1 addr:%p\n,foo1); foo2(); } int foo2() { printf([+] foo2 addr:%p\n,foo2); foo3(); } int foo3() { printf([+] foo3 addr:%p\n,foo3); foo4(); } int foo4() { printf([+] foo4 addr:%p\n,foo4); } main() { foo1(); } [netconf@linux1 elf]$ gcc -o elf8 elf8.c [netconf@linux1 elf]$ ./elf8 [+] foo1 addr:0x8048328 [+] foo2 addr:0x804834a [+] foo3 addr:0x804836c [+] foo4 addr:0x804838e 四.分析过程 1. ELF header 首先, elf文件的开头是一个Ehdr的结构. 该结构定义在/usr/include/elf.h中, 我们看看该结构: typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf32_Half e_type; /* 目标文件类型 */ Elf32_Half e_machine; /* Architecture */ Elf32_Word e_version; /* Object file version */ Elf32_Addr e_entry; /* 入口地址 */ Elf32_Off e_phoff; /* Program header table文件偏移 */ Elf32_Off e_shoff; /* Section header table 文件偏移 */ Elf32_Word e_flags; /* Processor-specific flags */ Elf32_Half e_ehsize; /* ELF header 大小 */ Elf32_Half e_phentsize; /* 每个Program header大小 */ Elf32_Half e_phnum; /* 一共多少个Program header */ Elf32_Half e_shentsize; /* 每个Section header大小 */ Elf32_Half e_shnum; /* 一共多少个 Section header */ Elf32_Half e_shstrndx; /* Section的字符表在section header table的索引值 */ } Elf32_Ehdr; 除了Elf32_Half是2个字节(16位)外, 其他变量定义等都是4个字节(32位). 从上面的结构可以看出来sizeof(Elf32_Ehdr)=13*4=0x34 我们来看看文件elf8头52字节的内容: [netconf@linux1 elf]$ hexdump -s 0 -n 52 -C elf8 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 03 00 01 00 00 00 78 82 04 08 34 00 00 00 |........x...4...| 00000020 78 21 00 00 00 00 00 00 34 00 20 00 06 00 28 00 |x!......4. ...(.| 00000030 22 00 1f 00 |...| ok, 我们对照这个结构一个个来分析: e_ident[EI_NIDENT]: 16字节: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 ELFMAG0 0x7f e_ident[0] ELFMAG1 'E' e_ident[1] ELFMAG2 'L' e_ident[2] ELFMAG3 'F' e_ident[3] ELFCLASS32 1 e_ident[4] ELFDATA2LSB 1 e_ident[5] EI_VERSION 1 e_ident[6] 剩下的全为0 e_type:2字节: 02 00 表示可执行文件(ET_EXEC 2 Executable file) e_machine: 2字节: 03 00 表示386体系文件(EM_386 3 Intel 80386) e_version: 4字节: 01 00 00 00 和e_ident里面的EI_VERSION含义一样. e_entry: 4字节: 78 82 04 08 表示程序入口地址0x08048278 e_phoff: 4字节: 34 00 00 00 表示program head table在文件中的偏移量(开始位置) e_shoff: 4字节: 78 21 00 00 表示section head table在文件中的偏移量(开始位置) e_flags: 4字节: 00 00 00 00 e_ehsize: 2字节: 34 00 表示elf header大小, 其实就是sizeof(Elf32_Ehdr) e_phentsize: 2字节: 20 00 表示每个program header 的大小(0x20) e_phnum: 2字节: 06 00 表示一共多少个program header(0x06) e_shentsize: 2字节: 28 00 表示每个section header的大小(0x28) e_shnum: 2字节: 22 00, 表示有多少个section header e_shstrndx: 2字节: 1f 00 表示section string table在section header table中的索引值.(即第几个section描述了section string table的位置和大小) 是不是很简单?我们来看看readelf的分析结果: [netconf@linux1 elf]$ readelf -h elf8 ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048278 Start of program headers: 52 (bytes into file) Start of section headers: 8568 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 6 Size of section headers: 40 (bytes) Number of section headers: 34 Section header string table index: 31 2. Program header 除了开始的elf header位置是固定的外, 其他位置都是相关联的, 或者说都是要到elf header里面读取的. 看elf.h里面关于这个结构的定义: typedef struct { Elf32_Word p_type; /* Segment type */ Elf32_Off p_offset; /* Segment file offset */ Elf32_Addr p_vaddr; /* Segment virtual address */ Elf32_Addr p_paddr; /* Segment physical address */ Elf32_Word p_filesz; /* Segment size in file */ Elf32_Word p_memsz; /* Segment size in memory */ Elf32_Word p_flags; /* Segment flags */ Elf32_Word p_align; /* Segment alignment */ } Elf32_Phdr; 我们来计算一下这个结构的大小, sizeof(Elf32_Phdr)=0x20, 从前面的elf header信息我们也同样可以知道这个长度是0x20, elf文件中一共有6个这样的Phdr, 最开始的一个Phdr在文件的0x34偏移处. 我们可以这样读取: lseek(FP,0x34,0); fread(buffer,6,0x20,FP); 将所有的Phdr都读入到buffer里面. 我们分析其中的一个结构: [netconf@linux1 elf]$ hexdump -s 52 -n 32 -C elf8 00000034 06 00 00 00 34 00 00 00 34 80 04 08 34 80 04 08 |....4...4...4...| 00000044 c0 00 00 00 c0 00 00 00 05 00 00 00 04 00 00 00 |?..?..........| 按结构进行分析: p_type: 4字节: 06 00 00 00, 表示该段是PT_PHDR类型(自己的入口) p_offset: 4字节: 34 00 00 00 在文件中的偏移量. p_vaddr: 4字节: 34 80 04 08 虚拟地址:0x08048034 p_paddr: 4字节: 34 80 04 08 物理地址: 0x08048034 p_filesz: 4字节: c0 00 00 00 段的大小:0xc0 p_memsz: 4字节: c0 00 00 00 在内存中的大小: 0xc0 p_flags: 4字节: 05 00 00 00 段标记 p_align: 4字节: 04 00 00 00 Phdr结构大概和上面差不多, 每一个结构都定义了该elf文件的特性以及每个segment的属性,大小等. 我们来看一看PT_INTERP段的信息: [netconf@linux1 elf]$ hexdump -s 84 -n 32 -C elf8 00000054 03 00 00 00 f4 00 00 00 f4 80 04 08 f4 80 04 08 |....?..?..?..| 00000064 13 00 00 00 13 00 00 00 04 00 00 00 01 00 00 00 |................| 这个segment指出了程序依赖解释器的路径/文件名, 定义在0xf4的偏移量处, 大小是0x13字节. [netconf@linux1 elf]$ hexdump -s 0xf4 -n 19 -C elf8 000000f4 2f 6c 69 62 2f 6c 64 2d 6c 69 6e 75 78 2e 73 6f |/lib/ld-linux.so| 00000104 2e 32 00 |.2.| 该elf文件依赖/lib/ld-linux.so.2来解释. 我们可以用readelf来检测看看: [netconf@linux1 elf]$ readelf -l elf8 Elf file type is EXEC (Executable file) Entry point 0x8048278 There are 6 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4 INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00454 0x00454 R E 0x1000 LOAD 0x000454 0x08049454 0x08049454 0x00104 0x00108 RW 0x1000 DYNAMIC 0x000464 0x08049464 0x08049464 0x000c8 0x000c8 RW 0x4 NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4 所有的代码都是从0X8048000处开始 ELF_header占52个字节(0x34) PHDR表示可执行文件中除ELF_header开始的地方。因为ELF_header占52个字节(0x34),所以从0x08048034开始。 INTERP表示从0x080480f4开始读取0x00013个字节为程序解释器即/lib/ld-linux.so.2 两个LOAD,第一个为代码(因为是RE),从VMA可以看出是从0X8048000开始的,即整个可执行文件。 第二个是BSS和DATA段等 是接着代码段之后的。0x08049454=0x08048000+0x00454 DYNAMIC是分配的动态空间,接在BSS和data后面。 0x8048034后面的(即prog table表后面的section)段的详细分配请用 objdump -h elf8查看。 3. section 同样的, 我们先给出section的结构: typedef struct { Elf32_Word sh_name; /* 在section string table中的索引 */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; 计算一下大小是sizeof(Elf32_Shdr)=0x28 和前面elf header的e_shentsize大小刚好一样. 从elf header的e_shoff 值我们可以得到第一个section在文件中的位置/偏移量0x2178, 从e_shnum可以知道一共有0x22个section.用C语言的算法可以这样表示: fseek(FP, 0x2178, 0); fread(buffer,0x22,0x28,FP); 就可以把所有的section读入到buffer里面. 我们来对照文件结构看看一个section的含义(第4个section(注意:从第0个开始)在文件中的的偏移量应该是: 0x2178+4*0x28=0x2218,我们以第4个section为例子): [netconf@linux1 elf]$ hexdump -s 0x2218 -n 40 -C elf8 00002218 37 00 00 00 0b 00 00 00 02 00 00 00 50 81 04 08 |7...........P...| 00002228 50 01 00 00 50 00 00 00 05 00 00 00 01 00 00 00 |P...P...........| 00002238 04 00 00 00 10 00 00 00 |........| 其中: sh_name: 4字节: 37 00 00 00 表示该section的名称在section string table中的索引. sh_type: 4字节: 0b 00 00 00 表示该section的类型, 从elf.h中可以看到定义0x0b=11的是:SHT_DYNSYM 11 /* Dynamic linker symbol table */ 表示是一个动态连接符号表. sh_flags: 4字节: 02 00 00 00 表示该section的类型.0x02是一个ALLOC型的section. sh_addr: 4字节: 50 81 04 08 执行的时候, 该section的虚拟地址0x08048150 sh_offset: 4字节: 50 01 00 00 表示该section内容在文件的位置/偏移量0x150 sh_size: 4字节: 50 00 00 00 section大小0x50 sh_link: 4字节 01 00 00 00 连接到其他section sh_info: 4字节: 04 00 00 00 section的其他信息 sh_addralign: 4字节: 04 00 00 00 section的对齐调整值 sh_entsize: 4字节: 10 00 00 00 如果该section定义的是一个表,那么这个值表示该表里面每个结构的大小. 注意: sh_link和sh_info的值随着sh_type不同有不同的含义. sh_type sh_link sh_info ======= ======= ======= SHT_DYNAMIC The section header index of 0 the string table used by entries in the section. SHT_HASH The section header index of 0 the symbol table to which the hash table applies. SHT_REL, The section header index of The section header index of SHT_RELA the associated symbol table. the section to which the relocation applies. SHT_SYMTAB, The section header index of One greater than the symbol SHT_DYNSYM the associated string table. table index of the last local symbol (binding STB_LOCAL). other SHN_UNDEF 0 section表是elf文件中最重要的一环, 基本上所有的东东都要查找它. 比如.dynsym, .symtab, .got, .bss等等. 后面我们就要提到的symbol表也要从这里获取相关信息. 我们来看一下elf header里面提到的e_shstrndx, 这个值定义了section string table的位置在第几个section里面的, 这里我们可以计算一下它的位置: e_shoff+e_shstrndx* e_shentsize (起始位置+序号*每个的大小) =0x2178+31*0x28= 0x2650 我们看看这个section的内容: [netconf@linux1 elf]$ hexdump -s 0x2650 -n 40 -C elf8 00002650 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |................| 00002660 4a 20 00 00 2b 01 00 00 00 00 00 00 00 00 00 00 |J ..+...........| 00002670 01 00 00 00 00 00 00 00 |........| 这里面可以给我们的信息是: sh_name=0x11, sh_type=0x03,sh_offset=0x204a, sh_size=0x12b sh_name目前没用. Sh_type:0x03表示: SHT_STRTAB 3 /* String table */ Ok,我们找到了string table, 让我们看看string table里面有什么吧: [netconf@linux1 elf]$ hexdump -s 0x204a -n 299 -C elf8 0000204a 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 |..symtab..strtab| 0000205a 00 2e 73 68 73 74 72 74 61 62 00 2e 69 6e 74 65 |..shstrtab..inte| 0000206a 72 70 00 2e 6e 6f 74 65 2e 41 42 49 2d 74 61 67 |rp..note.ABI-tag| 0000207a 00 2e 68 61 73 68 00 2e 64 79 6e 73 79 6d 00 2e |..hash..dynsym..| 0000208a 64 79 6e 73 74 72 00 2e 67 6e 75 2e 76 65 72 73 |dynstr..gnu.vers| 0000209a 69 6f 6e 00 2e 67 6e 75 2e 76 65 72 73 69 6f 6e |ion..gnu.version| 000020aa 5f 72 00 2e 72 65 6c 2e 64 79 6e 00 2e 72 65 6c |_r..rel.dyn..rel| 000020ba 2e 70 6c 74 00 2e 69 6e 69 74 00 2e 74 65 78 74 |.plt..init..text| 000020ca 00 2e 66 69 6e 69 00 2e 72 6f 64 61 74 61 00 2e |..fini..rodata..| 000020da 64 61 74 61 00 2e 65 68 5f 66 72 61 6d 65 00 2e |data..eh_frame..| 000020ea 64 79 6e 61 6d 69 63 00 2e 63 74 6f 72 73 00 2e |dynamic..ctors..| 000020fa 64 74 6f 72 73 00 2e 6a 63 72 00 2e 67 6f 74 00 |dtors..jcr..got.| 0000210a 2e 62 73 73 00 2e 63 6f 6d 6d 65 6e 74 00 2e 64 |.bss..comment..d| 0000211a 65 62 75 67 5f 61 72 61 6e 67 65 73 00 2e 64 65 |ebug_aranges..de| 0000212a 62 75 67 5f 70 75 62 6e 61 6d 65 73 00 2e 64 65 |bug_pubnames..de| 0000213a 62 75 67 5f 69 6e 66 6f 00 2e 64 65 62 75 67 5f |bug_info..debug_| 0000214a 61 62 62 72 65 76 00 2e 64 65 62 75 67 5f 6c 69 |abbrev..debug_li| 0000215a 6e 65 00 2e 64 65 62 75 67 5f 66 72 61 6d 65 00 |ne..debug_frame.| 0000216a 2e 64 65 62 75 67 5f 73 74 72 00 |.debug_str.| 这里就是我们要找的东西了. 通过sh_name的值, 可以在这个string table里面找到自己对应的section name. 比如我们刚才看的第4个section的sh_name是0x37,我们找string table里面偏移量是0x37的内容(到0x00结束). 我们来看看:字符串起始位置0x204a+0x37=0x2081 就先读出10字节内容吧: [netconf@linux1 elf]$ hexdump -s 0x2081 -n 10 -C elf8 00002081 2e 64 79 6e 73 79 6d 00 2e 64 |.dynsym..d| 0x00前面就是.dynsym了, 这个就是我们要找的section name了. 通过前面的过程我们就可以找到一一对应的section名字, 来看看readelf的结果: [netconf@linux1 elf]$ readelf -S elf8 There are 34 section headers, starting at offset 0x2178: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4 [ 3] .hash HASH 08048128 000128 000028 04 A 4 0 4 [ 4] .dynsym DYNSYM 08048150 000150 000050 10 A 5 1 4 [ 5] .dynstr STRTAB 080481a0 0001a0 00004c 00 A 0 0 1 [ 6] .gnu.version VERSYM 080481ec 0001ec 00000a 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 080481f8 0001f8 000020 00 A 5 1 4 [ 8] .rel.dyn REL 08048218 000218 000008 08 A 4 0 4 [ 9] .rel.plt REL 08048220 000220 000010 08 A 4 b 4 [10] .init PROGBITS 08048230 000230 000018 00 AX 0 0 4 [11] .plt PROGBITS 08048248 000248 000030 04 AX 0 0 4 [12] .text PROGBITS 08048278 000278 000170 00 AX 0 0 4 [13] .fini PROGBITS 080483e8 0003e8 00001c 00 AX 0 0 4 [14] .rodata PROGBITS 08048404 000404 000050 00 A 0 0 4 [15] .data PROGBITS 08049454 000454 00000c 00 WA 0 0 4 [16] .eh_frame PROGBITS 08049460 000460 000004 00 WA 0 0 4 [17] .dynamic DYNAMIC 08049464 000464 0000c8 08 WA 5 0 4 [18] .ctors PROGBITS 0804952c 00052c 000008 00 WA 0 0 4 [19] .dtors PROGBITS 08049534 000534 000008 00 WA 0 0 4 [20] .jcr PROGBITS 0804953c 00053c 000004 00 WA 0 0 4 [21] .got PROGBITS 08049540 000540 000018 04 WA 0 0 4 [22] .bss NOBITS 08049558 000558 000004 00 WA 0 0 4 [23] .comment PROGBITS 00000000 000558 000132 00 0 0 1 [24] .debug_aranges PROGBITS 00000000 000690 000058 00 0 0 8 [25] .debug_pubnames PROGBITS 00000000 0006e8 000025 00 0 0 1 [26] .debug_info PROGBITS 00000000 00070d 000c85 00 0 0 1 [27] .debug_abbrev PROGBITS 00000000 001392 000127 00 0 0 1 [28] .debug_line PROGBITS 00000000 0014b9 0001f2 00 0 0 1 [29] .debug_frame PROGBITS 00000000 0016ac 000014 00 0 0 4 [30] .debug_str PROGBITS 00000000 0016c0 00098a 01 MS 0 0 1 [31] .shstrtab STRTAB 00000000 00204a 00012b 00 0 0 1 [32] .symtab SYMTAB 00000000 0026c8 0004c0 10 33 37 4 [33] .strtab STRTAB 00000000 002b88 0001dd 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) [netconf@linux1 elf]$
[32] .symtab SYMTAB 00000000 0026c8 0004c0 10 33 37 4这个section仍然是0X28大小,他指向section段中具体数据段的位置。从这看出一共是0x4c0大小。由下面Symbol结构可以知道每个符号信息是0x10,这样说明该段里面一共有76个重新定位/加载的符号。
St_name是符号的名称的index(详见下文) 当编译一个源文件生成目标文件时,会在目标文件中生成符号表和重定位表。
符号表包含在文件中定义的全局符号以及在文件中引用的外部符号(外部函数或变量)。
重定位表告诉链接器在哪些位置要进行重定位操作。
编译生成的目标文件在文件的开始处会有一个elf头,描绘了整个文件的组织结构。它还包括很多节(section)。这些节有的是系统定义好的,有些是用户在文件在通过.section命令自定义的,链接器会将各输入目标文件中的相同的节合并。
链接器对编译生成的目标文件进行链接时,首先进行符号解析,找出外部符号在哪定义。如果外部符号在一个静态库中定义,则直接将对应的定义代码复制到最终生成的目标文件中。接着链接器进行符号重定位。编译器在生成目标文件时,通常使用从0开始的相对地址,而在链接过程中,链接器从一个指定的地址开始,根据输入目标文件的顺序,以段(segment)为单位将它们拼装起来。其中每个段可以包括很多个节(section)。除了目标文件的拼装,重定位过程中还完成了下面两个任务:一是生成最终的符号表,二是对代码段中的某些位置进行修改,要修改的位置由编译器生成的重定位表指出。
链接过程中还会生成两个表:got表和plt表。
got表中每一项都是本运行模块要引用的全局变量或函数的地址,可以用got表来间接引用全局变量。函数也可以把got表的首地址作为一个基准,用相对该基准偏移量来引用静态函数。由于动态链接器(ld-linux.so)不会把运行模块加载到固定地址,在不同进程的地址空间中各运行模块的绝对地址、相对地址都不同。这种不同反映到got表上,京是每个进程的每个运行模块都有独立的got表,所以进程间不能共享got表。
plt表中第一项都是一小段代码,对应于本运行模块要引用的一个全局函数。当链接器发现某个符号引用是共享目标文件中的一个函数时,就在pltk 创建一个入口。
链接生成的目标文件在文件开头也有一个elf头号,描绘了整个文件的组织结构,这个文件中会有多个段(segment),每个段都由相应的节(section)拼装而成。
对由链接器链接生成的可执行目标文件进行加载运行时,内核首先读取elf头。根据头部数据指示分别读入各种数据结构,找出可加载的段闭并调用mmap()函数将其加载到内存。内核找到标记为PT_INTERP的段,这个段对应着动态链接器的名称,然后加载动态链接器。linux中通常是/lib/ld-linux.so.2.接着内核将控制权交给动态链接器。动态链接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。之后动态链接器开始对程序中的外部引用进行重定位,即告诉程序其引用的外部变量/函数的地址。R_386_GLOB_DAT类型的入口涉及到got表。R_3862_JMP_SLOT类型的入口涉及到plt表。动态链接还有一个延迟(lazy)特性,即真正引用时才进行重定位(环境变量LD_BIND_NOW为空值NULL时)。接下来动态链接器执行elf文件中标记为.init节的代码,进行程序运行的初始化。最后动态链接器把控制权交给程序, 从elf头中定义的入口处开始执行程序。 |