阅读:2845回复:0
[原创]ELF格式学习笔记
Author: liucy
1. ELF概念 1.1 ELF(Executable and Linking Format) 即可执行连接文件格式,是Linux,SVR4和Solaris2.0默认的目标文件格式,目前标准接口委员会TIS已将ELF标准化为一种可移植的目标文件格式。 1.2 ELF有三种文件类型格式。  一个可重定位(relocatable)文件。该文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。  一个可执行(executable)文件。该文件保存着一个用来执行的程序,并指出了exec(BA_OS)如何来创建程序进程映象。  一个共享object文件。该文件保存着代码和合适的数据,用来被下面的两个链接器链接:第一个是连接编辑器,可以和其他的可重定位和共享object文件来创建其他的objec;第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。 1.3 ELF目标格式不同视角关系。 Linking 视角 Execution 视角 ELF header ELF header Program header table (optional) Program header table Section 1 Segment 1 Section … Segment … Section n Segment n Section header table Section header table (optional) 2. ELF各部分介绍 2.1 ELF header Elf头在程序的开始部位,作为引路表描述整个ELF的文件结构,其信息大致分为四部分:一是系统相关信息,二是目标文件类型,三是加载相关信息,四是链接相关信息。其中系统相关信息包括elf文件魔数(标识elf文件),平台位数,数据编码方式,elf头部版本,硬件平台e_machine,目标文件版本e_version,处理器特定标志e_ftags(这些信息的引入极大增强了elf文件的可移植性,使交叉编译成为可能);目标文件类型用e_type的值表示,可重定位文件为1,可执行文件为2,共享文件为3;加载相关信息有:程序进入点e_entry.程序头表偏移量e_phoff,elf头部长度e_ehsize,程序头表中一个条目的长度e_phentsize,程序头表条目数目e_phnum;链接相关信息有:节头表偏移量e_shoff,节头表中一个条目的长度e_shentsize,节头表条目个数e_shnum ,节头表字符索引e_shstmdx。 #define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr; 其中,数据类型大小如下: Name Size Alignment Purpose Elf32_Addr 4 4 Unsigned program address Elf32_Half 2 2 Unsigned medium integer Elf32_Off 4 4 Unsigned file offset Elf32_Sword 4 4 Signed large integer Elf32_Word 4 4 Unsigned large integer unsigned char 1 1 Unsigned small integer 程序为例: #include <stdio.h> int main(void) { printf("hello world\n"); return 0; } gcc -o hello hello.c hello文件内容: 见图一 图片:pic1.JPG 使用readelf来读获取的信息: 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: 0x80482f0 Start of program headers: 52 (bytes into file) Start of section headers: 2124 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 40 (bytes) Number of section headers: 30 Section header string table index: 27 下面详细列出内容: (a). 前16个字节是属于unsigned char e_ident[EI_NIDENT],标识ELF的一些信息: 00000000-00000003 :7F 45 4C 46 ELF文件以一个magic number开头(7F),接下来是’E’,’L’,’F’这三个字符。 00000004:01 这位是标明文件的类型,如果是0为非法,1为32位目标,2为64位目标,当初属于预留的,呵呵。在这里为32位目标文件。 00000005:01 这位是处理器的编码方式,01表示高位在前,02表示低位在前,0是非法数据编码。 00000006:01 这位是头部版本号。 00000007-0000000f:00 00 00 00 00 00 00 00 00 标记 e_ident 中未使用字节的开始。初始化为 0。 (b) e_type,用来指明文件类型 00000010-00000011:02 00 2代表可执行文件,1是可重定位的目标文件(gcc –c得到的是这样的文件),而3表示共享目标文件(gcc –shared得到的文件如此),同样,0是指未知的目标文件。其它的值含义如下: ET_CORE 4 Core file ET_LOPROC 0xff00 Processor-specific ET_HIPROC 0xffff Processor-specific (c) e_machine,用以指定系统的体系结构 00000012-00000013:03 00 这里的3是指Intel 80386结构,据某篇文档说,ia32的结构上这位是必须指定为EM_386的,即值为3。其它取值含义如下:0是未指定,1是EM_M32,即AT&TWE 32100,2是EM_SPARC,即SPARC结构,4和5分别指EM_68K(Motorola 68000)和EM_88K(Motorola 88000),7指EM_860(Intel 80860),8指EM_MIPS(MPIS RS3000)。 (d) e_version,指明目标文件版本 00000014-00000017:01 00 00 00 取1为当前版本EV_CURRENT,取0为非法版本EV_NONE (e) e_entry,程序入口的虚拟地址。如果目标文件没有程序入口,可以为 0 00000018-0000001b:F0 82 04 08 (f) e_phoff和e_shoff,指程序头部表格和节区表格的偏移量,以字节计算。如果没有,则可以为0。 0000001c-00000023:34 00 00 00 4C 08 00 00 (g) e_flags,是与文件和处理器相关的标志。 00000024-00000027:00 00 00 00 (h)e_ehsize了,说明了ELF头部的大小(字节) 00000028-00000029:34 00 (i) e_phentsize,e_phnum,和e_shentsize,e_shnum,表明程序头部表格的表项大小和项目数以及节区头部表格的表项大小和数目,每个2字节长。 0000002a-00000031:20 00 08 00 28 00 1E 00 (j) e_shstrndx是节区头部表格中与名称字串相关的表项索引。如果没有节区名称字符串表,此参数可为SHN_UNDEF。 00000032-00000033:1B 00 2.2 program header table 程序头表告诉系统如何建立一个进程映像.它是从加载执行的角度来看待elf文件.从它的角度看.elf文件被分成许多段,elf文件中的代码、链接信息和注释都以段的形式存放。每个段都在程序头表中有一个表项描述,包含以下属性:段的类型,段的驻留位置相对于文件开始处的偏移,段在内存中的首字节地址,段的物理地址,段在文件映像中的字节数.段在内存映像中的字节数,段在内存和文件中的对齐标记。 typedef struct elf32_phdr{ Elf32_Word p_type; //段的类型 Elf32_Off p_offset; //段的位置相对于文件开始处的偏移 Elf32_Addr p_vaddr; //段在内存中的首字节地址 Elf32_Addr p_paddr;//段的物理地址 Elf32_Word p_filesz;//段在文件映像中的字节数 Elf32_Word p_memsz;//段在内存映像中的字节数 Elf32_Word p_flags;//段的标记 Elf32_Word p_align;,/段在内存中的对齐标记 )Elf32_Phdr; hello 的例子: 见图2 图片:pic2.JPG 使用readelf来读获取的HELLO信息: Elf file type is EXEC (Executable file) Entry point 0x80482f0 There are 8 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4 INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00540 0x00540 R E 0x1000 LOAD 0x000540 0x08049540 0x08049540 0x000fc 0x00104 RW 0x1000 DYNAMIC 0x000554 0x08049554 0x08049554 0x000c8 0x000c8 RW 0x4 NOTE 0x000148 0x08048148 0x08048148 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x0004cc 0x080484cc 0x080484cc 0x0001c 0x0001c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 2.3 section header table 节头表描述程序节,为编译器和链接器服务。它把elf文件分成了许多节.每个节保存着用于不同目的的数据.这些数据可能被前面的程序头重复使用,完成一次任务所需的信息往往被分散到不同的节里。由于节中数据的用途不同,节被分成不同的类型,每种类型的节都有自己组织数据的方式。每一个节在节头表中都有一个表项描述该节的属性,节的属性包括小节名在字符表中的索引,类型,属性,运行时的虚拟地址,文件偏移,以字节为单位的大小,小节的对齐等信息。 typedef struct{ Elf32_Word sh_name;//小节名在字符表中的索引 E1t32_Word sh_type;//小节的类型 Elf32_Word sh_flags;//小节属性 Elf32_Addr sh_addr; //小节在运行时的虚拟地址 Elf32_Off sh_offset;//小节的文件偏移 Elf32_Word sh_size;//小节的大小.以字节为单位 Elf32_Word sh_link;//链接的另外一小节的索引 Elf32 Word sh_info;//附加的小节信息 Elf32 Word sh_addralign;//小节的对齐 Elf32 Word sh_entsize; //一些sections保存着一张固定大小入口的表。就像符号表 }Elf32_Shdr; 以hello 为例: 见图二 There are 30 section headers, starting at offset 0x84c: 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 08048134 000134 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 08048168 000168 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0804818c 00018c 000020 04 A 5 0 4 [ 5] .dynsym DYNSYM 080481ac 0001ac 000050 10 A 6 1 4 [ 6] .dynstr STRTAB 080481fc 0001fc 00004a 00 A 0 0 1 [ 7] .gnu.version VERSYM 08048246 000246 00000a 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 08048250 000250 000020 00 A 6 1 4 [ 9] .rel.dyn REL 08048270 000270 000008 08 A 5 0 4 [10] .rel.plt REL 08048278 000278 000018 08 A 5 12 4 [11] .init PROGBITS 08048290 000290 000017 00 AX 0 0 4 [12] .plt PROGBITS 080482a8 0002a8 000040 04 AX 0 0 4 [13] .text PROGBITS 080482f0 0002f0 0001a8 00 AX 0 0 16 [14] .fini PROGBITS 08048498 000498 00001c 00 AX 0 0 4 [15] .rodata PROGBITS 080484b4 0004b4 000018 00 A 0 0 4 [16] .eh_frame_hdr PROGBITS 080484cc 0004cc 00001c 00 A 0 0 4 [17] .eh_frame PROGBITS 080484e8 0004e8 000058 00 A 0 0 4 [18] .ctors PROGBITS 08049540 000540 000008 00 WA 0 0 4 [19] .dtors PROGBITS 08049548 000548 000008 00 WA 0 0 4 [20] .jcr PROGBITS 08049550 000550 000004 00 WA 0 0 4 [21] .dynamic DYNAMIC 08049554 000554 0000c8 08 WA 6 0 4 [22] .got PROGBITS 0804961c 00061c 000004 04 WA 0 0 4 [23] .got.plt PROGBITS 08049620 000620 000018 04 WA 0 0 4 [24] .data PROGBITS 08049638 000638 000004 00 WA 0 0 4 [25] .bss NOBITS 0804963c 00063c 000008 00 WA 0 0 4 [26] .comment PROGBITS 00000000 00063c 000114 00 0 0 1 [27] .shstrtab STRTAB 00000000 000750 0000fc 00 0 0 1 [28] .symtab SYMTAB 00000000 000cfc 000420 10 29 46 4 [29] .strtab STRTAB 00000000 00111c 00020b 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) 这里可以参照前面一节中Section to Segment mapping:内容,可以看出节与段的映射关系。 3. ELF的一些特点(下面是摘录内容) 3.1 平台相关性 平台相关信息可在编译由编译器决定,定义再ELF.h头文件中。 linux系统加载ELF可执行文件时,首先做一些简单一致性检查.其代码如下 if(memcmp(elf_ex.e_ident,ELFMAG,SELFMAG)!=0) goto out; //检查文件头开始四个字符是否为ELF魔数 if(elf_ex.e_type!=ET_EXEC&&elf_ex.e_type!=ET_DYN) goto out;//检查文件类型是否为可执行文件或共享目标文件 if(!elf_check_arch(&elf_ex)) goto out;//检查硬件平台是否一致 其中的elf_check_arch(x)在不同的硬件平台上有不同的定义,其由系统的硬件平台决定。这样,在硬件平台相同的系统上,ELF可以不作修改的执行。因此,它可以支持不同平台上的交叉编译(cross_compilation)和交叉链接(cross_linking)。 3.2 PIC ELF可以生成位置无关的代码(position-independent code,PIC)。用户对gcc使用-fPIC指示GNU编译系统生成PIC代码,可以加载到内存地址空间的任何地址执行,它是实现共享库或共享可执行代码的基础. PIC代码是通过GOT间接的引用每个全局变量来实现的。其原理就是使用的转移指令都是相对于当前程序计数器(IP)的偏移量;代码中引用变量、函数的地址都是相对于某个基地址的偏移量,总之,从不引用一个绝对地址。为了这些相对地址能够再加载后正确的位置,编译器在数据段开始的地方创建了一个全局偏移量表(global offset table.GOT)。GOT表中每一项都是本运行模块要引用的一个全局变量或函数 的地址,并且编译器还为GOT每个表目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个表目,使得它包含正确的绝对地址。在x86体系结构上,本运行模块的GOT表首地址始终保存在%ebx寄存器中。 PLT(Procedure Linkage Table)表每一项都是一小段代码,对应于本运行模块要引用 的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下: .PLTfun: jmp *fun@GOT(%ebx) pushl $offset jmp .PLT0@PC 其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当 于nop空指令。 如一个elf可执行文件需要调用共享库中的函数,那么它就有自己的GOT和PLT(procedure linkage table,过程链接表),这两个节之间的交互实现了延迟绑定(lazy binging),这种方法将过程地址的绑定推迟到第一次调用该函数。为了实现延迟绑定,GOT的头三条表目是特殊的:GOT[0]包含.dynamic段的地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;GOT[1]包含动态链接器的标识;GOT[2]包含动态链接器的延迟绑定代码的入口点。GOT的其他表目为本模块要引用的一个全局变量或函数的地址。PLT是一个以16字节(32位平台中)表目的数组形式出现的代码序列。其中PLT[0]是一个特殊的表目,它跳转到动态链接器中执行;每个定义在共享库中并被本模块调用的函数在PLT中都有一个表目,从PLT[1]开始.模块对函数的调用会转到相应PLT表目中执行,这些表目由三条指令构成:一条指令是跳转到相应的GOT存储的地址值中,二条指令把函数相应的ID压入栈中,第三条指令跳转到PLT[O]中调用动态链接器解析函数地址,并把函数真正地址存入相应的GOT表目中。被调用函数GOT相应表目中存储的最初地址为相应PLT表目中第二条指令的地址值,函数第一次被调用后GOT表目中的值就为函数的真正地址。因此,第一次调用函数时开销比较大.但是其后的每次调用都只会花费一条指令和一个间接的存储器引用。 用户程序中对fun的直接调用经编译连接后生成一条call fun@PLT指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到.PLT0。该PLT项保留给编译器生成的额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入fun@GOT表项。图示如下: user program -------------- call fun@PLT | v DLL PLT table loader -------------- -------------- ----------------------- fun: <-- jmp*fun@GOT --> change GOT entry from | $loader to $fun, v then jump to there GOT table -------------- fun@GOT:$loader 第一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT 表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才 要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转 的代码进行修补,所以整个代码段都能在进程间共享。 本来有两幅图片,可不会传。就当附件来传了。 |
|
最新喜欢:qian35...
|