景德镇信息港

当前位置:

UNIXLinux平台上可执行文件格式分

2019/06/20 来源:景德镇信息港

导读

ELF文件格式分析ELF 文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so

  ELF文件格式分析

  ELF 文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking View),一种称为执行视图(Execution View)。

  首先看看ELF文件的总体布局: ELF header(ELF头部)

  Program header table(程序头表)

  Segment1(段1)

  Segment2(段2)

  ………

  Sengmentn(段n)

  Setion header table(节头表,可选)

  段由若干个节(Section)构成,节头表对每一个节的信息有相关描述。对可执行程序而言,节头表是可选的。参考资料 1中作者谈到把节头表的所有数据全部设置为0,程序也能正确运行!ELF头部是一个关于本文件的路线图(road map),从总体上描述文件的结构。下面是ELF头部的数据结构: 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; /* ELF头部长度 */

  Elf32_Half e_phentsize; /* 程序头部中一个条目的长度 */

  Elf32_Half e_phnum; /* 程序头部条目个数 */

  Elf32_Half e_shentsize; /* 节头部中一个条目的长度 */

  Elf32_Half e_shnum; /* 节头部条目个数 */

  Elf32_Half e_shstrndx; /* 节头部字符表索引 */

  } Elf32_Ehdr;

  下面我们对ELF头表中一些重要的字段作出相关说明,完整的ELF定义请参阅参考资料 6和参考资料7。

  e_ident [0]-e_ident[3]包含了ELF文件的魔数,依次是0x7f、'E'、'L'、'F'。注意,任何一个ELF 文件必须包含此魔数。参考资料 3中讨论了利用程序、工具、/Proc文件系统等多种查看ELF魔数的方法。e_ident[4]表示硬件系统的位数,1代表32位,2代表64位。 e_ident[5] 表示数据编码方式,1代表小印第安排序(有意义的字节占有的地址),2代表大印第安排序(有意义的字节占有的地址)。e_ident [6]指定ELF头部的版本,当前必须为1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式规范中定义这几个字节是被忽略的,但实际上是这几个字节完全可以可被利用。如病毒Lin/Glaurung.676/666(参考资料 1)设置 e_ident[7]为0x21,表示本文件已被感染;或者存放可执行代码(参考资料 2)。ELF头部中大多数字段都是对子头部数据的描述,其意义相对比较简单。值得注意的是某些病毒可能修改字段e_entry(程序进入点)的值,以指向病毒代码,例如上面提到的病毒Lin/Glaurung.676/666。

  一个实际可执行文件的文件头部形式如下:(利用命令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: 0x80483cc

  Start of program headers: 52 (bytes into file)

  Start of section headers: 14936 (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

  紧接ELF头部的是程序头表,它是一个结构数组,包含了ELF头表中字段e_phnum定义的条目,结构描述一个段或其他系统准备执行该程序所需要的信息。 typedef struct {

  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;

  在详细讨论可执行文件程序头表之前,首先查看一个实际文件的输出: Program Headers:

  Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align

  PHDR 0x000034 0x 0x 0x000c0 0x000c0 R E 0x4

  INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1

  [Requesting program interpreter: /lib/.2]

  LOAD 0x000000 0x 0x 0x00684 0x00684 R E 0x1000

  LOAD 0x000684 0x 0x 0x00118 0x00130 RW 0x1000

  DYNAMIC 0x000690 0x 0x 0x000c8 0x000c8 RW 0x4

  NOTE 0x000108 0x 0x 0x00020 0x00020 R 0x4Section to Segment mapping:

  Segment Sections...

  00

  01 .interp

  02 .interp .I-tag .hash sym str .rsion .

  rsion_r .rel

  .t .init .plt .text .fini .rodata .eh_frame

  03 .data amic .ctors .dtors .jcr .got .bss

  04 amic

  05 .I-tag

  Section Headers:

  [Nr] Name Type Addr Off Size ES Flg Lk Inf Al

  [ 0] NULL 000000 000000 00 0 0 0

  [ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1

  [ 2] .I-tag NOTE 000108 000020 00 A 0 0 4

  [ 3] .hash HASH 000128 000040 04 A 4 0 4

  [ 4] sym DYNSYM 000168 0000b0 10 A 5 1 4

  [ 5] str STRTAB 000218 00007b 00 A 0 0 1

  [ 6] .rsion VERSYM 000294 000016 02 A 4 0 2

  [ 7] .rsion_r VERNEED 080482ac 0002ac 000030 00 A 5 1 4

  [ 8] .rel REL 080482dc 0002dc 000008 08 A 4 0 4

  [ 9] .t REL 080482e4 0002e4 000040 08 A 4 b 4

  [10] .init PROGBITS 000324 000017 00 AX 0 0 4

  [11] .plt PROGBITS c 00033c 000090 04 AX 0 0 4

  [12] .text PROGBITS 080483cc 0003cc 0001f8 00 AX 0 0 4

  [13] .fini PROGBITS 080485c4 0005c4 00001b 00 AX 0 0 4

  [14] .rodata PROGBITS 080485e0 0005e0 00009f 00 A 0 0 32

  [15] .eh_frame PROGBITS 000680 000004 00 A 0 0 4

  [16] .data PROGBITS 000684 00000c 00 WA 0 0 4

  [17] amic DYNAMIC 000690 0000c8 08 WA 5 0 4

  [18] .ctors PROGBITS 000758 000008 00 WA 0 0 4

  [19] .dtors PROGBITS 000760 000008 00 WA 0 0 4

  [20] .jcr PROGBITS 000768 000004 00 WA 0 0 4

  [21] .got PROGBITS c 00076c 000030 04 WA 0 0 4

  [22] .bss NOBITS c 00079c 000018 00 WA 0 0 4

  [23] .comment PROGBITS 00079c 000132 00 0 0 1

  [24] .debug_aranges PROGBITS 0008d0 000098 00 0 0 8

  [25] .debug_pubnames PROGBITS 000968 000040 00 0 0 1

  [26] .debug_info PROGBITS 0009a8 001cc6 00 0 0 1

  [27] .debug_abbrev PROGBITS 00266e 0002cc 00 0 0 1

  [28] .debug_line PROGBITS 00293a 0003dc 00 0 0 1

  [29] .debug_frame PROGBITS 002d18 000048 00 0 0 4

  [30] .debug_str PROGBITS 002d60 000bcd 01 MS 0 0 1

  [31] .shstrtab STRTAB 00392d 00012b 00 0 0 1

  [32] .symtab SYMTAB 003fa8 000740 10 33 56 4

  [33] .strtab STRTAB 0046e8 000467 00 0 0 1

  对一个ELF可执行程序而言,一个基本的段是标记p_type为PT_INTERP的段,它表明了运行此程序所需要的程序解释器(/lib/ld- .2),实际上也就是动态连接器(dynamic linker)。重要的段是标记p_type为PT_LOAD的段,它表明了为运行程序而需要加载到内存的数据。查看上面实际输入,可以看见有两个可 LOAD段,个为只读可执行(FLg为R E),第二个为可读可写(Flg为RW)。段1包含了文本节.text,注意到ELF文件头部中程序进入点的值为0x80483cc,正好是指向节. text在内存中的地址。段二包含了数据节.data,此数据节中数据是可读可写的,相对的只读数据节.rodata包含在段1中。ELF格式可以比 COFF格式包含更多的调试信息,如上面所列出的形式为.debug_xxx的节。在I386平台LINUX系统下,用命令file查看一个ELF可执行程序的可能输出是:t: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。

  ELF文件中包含了动态连接器的全路径,内核定位"正确"的动态连接器在内存中的地址是"正确"运行可执行文件的保证,参考资料 13讨论了如何通过查找动态连接器在内存中的地址以达到颠覆(Subversiver)动态连接机制的方法。

  我们讨论ELF文件的动态连接机制。每一个外部定义的符号在全局偏移表(Global Offset Table GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。对外部定义函数解析可能是整个ELF文件规范中复杂的,下面是函数符号解析过程的一个描述。

  1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。

  2:PLT表的形式如下 .PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */

  jmp *8(%ebx)

  nop; nop

  nop; nop

  .PLT1: jmp *name1@GOT(%ebx)

  pushl $offset

  jmp .PLT0@PC

  .PLT2: jmp *func@GOT(%ebx)

  pushl $offset

  jmp .PLT0@PC

  3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。

  4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl $offset,其中$offset是符号func的重定位偏移量。注意到这是一个二次跳转。

  5:在符号func的重定位偏移量压栈后,控制跳到PLT表的条目,把GOT[1]的内容压栈,并跳转到GOT[2]对应的地址。

  6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。

  7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。

  动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。

  动态连接是一个非常重要的进步,这意味着库文件可以被升级、移动到其他目录等等而不需要重新编译程序(当然,这不意味库可以任意修改,如函数入参的个数、数据类型应保持兼容性)。从很大程度上说,动态连接机制是ELF格式代替t格式的决定性原因。如果说面对对象的编程本质是面对接口(interface)的编程,那么动态连接机制则是这种思想的地一个非常典型的应用,具体的讲,动态连接机制与设计模式中的桥接(BRIDGE)方法比较类似,而它的LAZY特性则与代理(PROXY)方法非常相似。动态连接操作的细节描述请参阅参考资料 8,9,10,11。通过阅读命令readelf、objdump 的源代码以及参考资料 14中所提及的相关软件源代码,可以对ELF文件的格式有更彻底的了解。

  总结

  不同时期的可执行文件格式深刻的反映了技术进步的过程,技术进步通常是针对解决存在的问题和适应新的环境。早期的UNIX系统使用t格式,随着操作系统和硬件系统的进步,t格式的局限性越来越明显。新的可执行文件格式COFF在UNIX System VR3中出现,COFF格式相对t格式变化是多了一个节头表(section head table),能够在包含基础的文本段、数据段、BSS段之外包含更多的段,但是COFF对动态连接和C++程序的支持仍然比较困难。为了解决上述问题, UNIX系统实验室(UNIX SYSTEM Laboratories USL) 开发出ELF文件格式,它被作为应用程序二进制接口(Application binary Interface ABI)的一部分,其目的是替代传统的t格式。例如,ELF文件格式中引入初始化段.init和结束段.fini(分别对应构造函数和析构函数)则主要是为了支持C++程序。1994年6月ELF格式出现在LINUX系统上,现在ELF格式作为UNIX/LINUX主要的可执行文件格式。当然我们完全有理由相信,在将来还会有新的可执行文件格式出现。

  上述三种可执行文件格式都很好的体现了设计思想中分层的概念,由一个总的头部刻画了文件的基本要素,再由若干子头部/条目刻画了文件的若干细节。比较一下可执行文件格式和以太数据包中以太头、IP头、TCP头的设计,我想我们能很好的感受分层这一重要的设计思想。参考资料 21从全局的角度讨论了各种文件的格式,并提出一个比较夸张的结论:Everything Is Byte!

  的题外话:大多数资料中对t格式的评价较低,常见的词语有黑暗年代(dark ages)、丑陋(ugly)等等,当然,从现代的观点来看,的确是比较简单,但是如果没有曾经的简单何来今天的精巧?正如我们今天可以评价石器时代的技术是ugly,那么将来的人们也可以嘲讽今天的技术是非常ugly。我想我们也许应该用更平和的心态来对曾经的技术有一个公正的评价。

小孩不吃饭怎么调理
小孩出汗多是什么原因造成的
小孩经常便秘怎么办
标签