ELF format -- How programs look from the inside

ELF format -- How programs look from the inside

ELF 是用于Linux系统下一种文件格式,包括目标文件,二进制文件,共享库和core dump。

它非常简单,而且具有好的输出格式。

ELF针对不同的架构具有相同的布局,但是排列次序(endianness)和字长可能不同;重定位类型,符号类型和可能具有平台相关的值,当然也包括平台相关的代码。

 

一个ELF文件对它包含的数据提供两种视图,链接视图和执行视图。这两个视图能够被两个头访问:Section Header Table(SHT)和Program Header Table(PHT).

 

Linking View: Section Header Table(SHT)

SHT提供对ELF文件所有section的概述。特别令人关注的是REL section(relocations),SYMTAB/DYNSYM(symbol tables),VERSYM/VERDEF/VERNEED sections(symbol versioning information)。

[john@localhost objmodel]$ readelf -S /bin/bash

There are 32 section headers, starting at offset 0xd5ea8:

 

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        08047134 000134 000013 00   A  0   0  1

  [ 2] .note.ABI-tag     NOTE            08047148 000148 000020 00   A  0   0  4

  [ 3] .note.gnu.build-i NOTE            08047168 000168 000024 00   A  0   0  4

  [ 4] .gnu.hash         GNU_HASH        0804718c 00018c 003600 04   A  5   0  4

  [ 5] .dynsym           DYNSYM          0804a78c 00378c 0083c0 10   A  8   1  4

  [ 6] .gnu.liblist      GNU_LIBLIST     08052b4c 00bb4c 000050 14   A  8   0  4

  [ 7] .gnu.conflict     RELA            08052b9c 00bb9c 000240 0c   A  5   0  4

  [ 8] .dynstr           STRTAB          08053b3a 00cb3a 008076 00   A  0   0  1

  [ 9] .gnu.version      VERSYM          0805bbb0 014bb0 001078 02   A  5   0  2

  [10] .gnu.version_r    VERNEED         0805cc28 015c28 0000b0 00   A  8   2  4

  [11] .rel.dyn          REL             0805ccd8 015cd8 000040 08   A  5   0  4

  [12] .rel.plt          REL             0805cd18 015d18 000638 08   A  5  14  4

  [13] .init             PROGBITS        0805d350 016350 000030 00  AX  0   0  4

  [14] .plt              PROGBITS        0805d380 016380 000c80 04  AX  0   0  4

  [15] .text             PROGBITS        0805e000 017000 0886ac 00  AX  0   0 16

  [16] .fini             PROGBITS        080e66ac 09f6ac 00001c 00  AX  0   0  4

  [17] .rodata           PROGBITS        080e66e0 09f6e0 01905e 00   A  0   0 32

  [18] .eh_frame_hdr     PROGBITS        080ff740 0b8740 003aa4 00   A  0   0  4

  [19] .eh_frame         PROGBITS        081031e4 0bc1e4 014b84 00   A  0   0  4

  [20] .ctors            PROGBITS        08118000 0d1000 000008 00  WA  0   0  4

  [21] .dtors            PROGBITS        08118008 0d1008 000008 00  WA  0   0  4

  [22] .jcr              PROGBITS        08118010 0d1010 000004 00  WA  0   0  4

  [23] .dynamic          DYNAMIC         08118014 0d1014 0000d8 08  WA  8   0  4

  [24] .got              PROGBITS        081180ec 0d10ec 000004 04  WA  0   0  4

  [25] .got.plt          PROGBITS        081180f0 0d10f0 000328 04  WA  0   0  4

  [26] .data             PROGBITS        08118420 0d1420 004390 00  WA  0   0 32

  [27] .dynbss           PROGBITS        0811c7c0 0d57c0 000044 00  WA  0   0 32

  [28] .bss              NOBITS          0811c804 0d5804 004e08 00  WA  0   0 32

  [29] .gnu_debuglink    PROGBITS        00000000 0d5804 000010 00      0   0  4

  [30] .gnu.prelink_undo PROGBITS        00000000 0d5814 00056c 01      0   0  4

  [31] .shstrtab         STRTAB          00000000 0d5d80 000127 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)

 

Execution view: Program Header Table (PHT)

PHT包含一些信息,让内核知道怎么启动程序。LOAD指令决定ELF文件的那部分需要映射到内存。INTERP指令指定一个ELF解释器, 在Linux系统下一般是/lib/ld-linux.so.2。

DYNAMIC 入口点指向.dynamic段,包含用于ELF解释器设置二进制的信息。

[john@localhost objmodel]$ readelf -l /bin/bash

 

Elf file type is EXEC (Executable file)

Entry point 0x805e000

There are 8 program headers, starting at offset 52

 

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  PHDR           0x000034 0x08047034 0x08047034 0x00100 0x00100 R E 0x4

  INTERP         0x000134 0x08047134 0x08047134 0x00013 0x00013 R   0x1

      [Requesting program interpreter: /lib/ld-linux.so.2]

  LOAD           0x000000 0x08047000 0x08047000 0xd0d68 0xd0d68 R E 0x1000

  LOAD           0x0d1000 0x08118000 0x08118000 0x04804 0x0960c RW  0x1000

  DYNAMIC        0x0d1014 0x08118014 0x08118014 0x000d8 0x000d8 RW  0x4

  NOTE           0x000148 0x08047148 0x08047148 0x00044 0x00044 R   0x4

  GNU_EH_FRAME   0x0b8740 0x080ff740 0x080ff740 0x03aa4 0x03aa4 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 .gnu.liblist .gnu.conflict .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 .dynbss .bss

   04     .dynamic

   05     .note.ABI-tag .note.gnu.build-id

   06     .eh_frame_hdr

   07

 

把它们放在一起:ELF头

STH和PTH都没有固定的位置,他们可以放在ELF文件的任何地方。 使用ELF头来找到它们,ELF头定位在文件的开始处。

第一个字节包含ELF magic(?)"/x7fELF",接着是ClassID(32/64bit ELF文件),数据格式ID(小端/大端),机器类型,等等。

在ELF头的结尾处有指向SHT和PHT的指针。

[john@localhost objmodel]$ readelf -h /bin/bash

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:               0x805e000

  Start of program headers:          52 (bytes into file)

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

  Section header string table index: 31

 

Relocation Table

重定位表指定哪里需要重定位,使程序能够运行起来。在程序中正常都是符号重定位,例如动态链接器需要用符号名来决议,然后把符号地址写到指定的重定位入口点。

重定位类型是架构相关的,这儿通常有很多相关的。在i386上最重要的是R_386_COPY 类型,意思是"只是拷贝符号地址到另一个地址", R_386_JUMP_SLOT,用于一般的PLT/GOT函数调用重定位机制。

符号值自身的决议是通过动态链接库(包含在/lib/ld-linux.so.2,通常用到ELF解释器),这是个很复杂的过程。基本上链接器搜索所有加载的ELF对象(二进制文件本身和加载的动态库),当搜索到第一个符号定义时就使用它。

[john@localhost objmodel]$ readelf -r /bin/bash | more

Relocation section '.gnu.conflict' at offset 0xbb9c contains 48 entries:

 Offset     Info    Type            Sym.Value  Sym. Name + Addend

081181a8  0000002a R_386_IRELATIVE                              006c00f0

081181b8  0000002a R_386_IRELATIVE                              007386b0

08118278  0000002a R_386_IRELATIVE                              006c0700

08118280  0000002a R_386_IRELATIVE                              006bee80

081182f8  0000002a R_386_IRELATIVE                              006c6bc0

08118338  0000002a R_386_IRELATIVE                              006c81f0

08118358  0000002a R_386_IRELATIVE                              006c0030

081183a4  0000002a R_386_IRELATIVE                              006bf100

081183b0  0000002a R_386_IRELATIVE                              006bf460

081183d0  0000002a R_386_IRELATIVE                              00738700

081183ec  0000002a R_386_IRELATIVE                              006be880

00448a88  00000001 R_386_32                                     0811c7c0

...

 

输出的符号(exported symbols)

当动态链接器通过浏览动态符号表.dynsym来搜索符号时,所有的能被其他程序使用的符号都会出现(换句话说:exported and in case of a library, part of the ABI),Application Binary Interface.

实际上处理过程是很复杂的(在.hash段中调用hashes),但是最后的结果和描述的是一样的。

[john@localhost objmodel]$ readelf -D -s /bin/bash  | more

 

Symbol table of `.gnu.hash' for image:

  Num Buc:    Value  Size   Type   Bind Vis      Ndx Name

  199   1: 0811f46c     4 OBJECT  GLOBAL DEFAULT  28 prog_completes

  200   1: 080c4780   406 FUNC    GLOBAL DEFAULT  15 sh_regmatch

  201   1: 0807d590    90 FUNC    GLOBAL DEFAULT  15 fatal_error

  202   1: 080965b0    34 FUNC    GLOBAL DEFAULT  15 set_sigwinch_handler

  203   2: 080b6f50   465 FUNC    GLOBAL DEFAULT  15 set_shellopts

  204   2: 08074730   194 FUNC    GLOBAL DEFAULT  15 execute_command

  205   2: 08060c30   354 FUNC    GLOBAL DEFAULT  15 history_delimiting_chars

  206   2: 08118ae8    20 OBJECT  GLOBAL DEFAULT  26 it_builtins

...

 

A more detailed look at versionsort64

机警的读者也许已经注意到versionsort64已经在上文动态符号表中提到过两次,这两个符号具有不同的值。

原因很简单,libc.so.6使用符号版本,这里有两个versionsort64版本是有效的。很不幸,binutils(二进制工具集)readelf 不能显示符号版本,eu-readelf 来至于elfutils包

[john@localhost objmodel]$ readelf -D -s /lib/libc.so.6 | grep versionsort64

 1127   9: 006e19a0    31 FUNC    GLOBAL DEFAULT  12 versionsort64

 1126   9: 00766a40    31 FUNC    GLOBAL DEFAULT  12 versionsort64

 1126 498: 00766a40    31 FUNC    GLOBAL DEFAULT  12 versionsort64

 1127 498: 006e19a0    31 FUNC    GLOBAL DEFAULT  12 versionsort64

 

内核载入程序

ELF文件本身不是特别有趣。ELF文件是怎样被载入内存的,在程序能够执行自己核心之前会发生什么。

一个程序的执行开始与内核的内部,也就是在系统调用中开始。查找文件类型,调用合适的处理程序。binfmt-elf处理程序加载ELF头和PHT, 紧接着是大量完整性检查。

然后内核在PHT的LOAD指令下部分的载入到内存。假如INTERP进入点已经存在,解释器也会被加载。静态链接的二进制可以不需要解释器,动态链接的程序总是需要/lib/ld-linux.so来解释,因为它包含许多启动代码,loads shared libraries needed by the binary, and performs relocation?

最后控制权会转移到程序中,(到解释器中,如果存在,否则就在二进制本身)to the interpreter, if present, otherwise to the binary itself。

[john@localhost objmodel]$ readelf -l /bin/bash

Elf file type is EXEC (Executable file)

Entry point 0x805e000

There are 8 program headers, starting at offset 52

 

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  PHDR           0x000034 0x08047034 0x08047034 0x00100 0x00100 R E 0x4

  INTERP         0x000134 0x08047134 0x08047134 0x00013 0x00013 R   0x1

      [Requesting program interpreter: /lib/ld-linux.so.2]

  LOAD           0x000000 0x08047000 0x08047000 0xd0d68 0xd0d68 R E 0x1000

  LOAD           0x0d1000 0x08118000 0x08118000 0x04804 0x0960c RW  0x1000

  DYNAMIC        0x0d1014 0x08118014 0x08118014 0x000d8 0x000d8 RW  0x4

  NOTE           0x000148 0x08047148 0x08047148 0x00044 0x00044 R   0x4

  GNU_EH_FRAME   0x0b8740 0x080ff740 0x080ff740 0x03aa4 0x03aa4 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 .gnu.liblist .gnu.conflict .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 .dynbss .bss

   04     .dynamic

   05     .note.ABI-tag .note.gnu.build-id

   06     .eh_frame_hdr

 

动态链接和ELF解释器

In case of a statically linked binary that's pretty much it, however with dynamically linked binaries a lot more magic has to go on

首先动态链接器(包含在解释器中)查看.dynamic段,它的地址存储在PHT中。

当发现NEEDED进入点是,在程序运行之前决定哪个库要不加载,×REL× 进入点包含重定位表的地址,×VER*进入点包含符号版本信息,等等。

所以动态链接器加载必要的库,执行重定位(无论是直接在程序启动时或之后,尽快重定位符号是必要的,根据不同的重定位类型。

最后控制权转交给二进制中符号_start的地址。通常许多gcc/glibc启动代码就在这里,最后调用main()。

[john@localhost objmodel]$ readelf -d /bin/bash

 

Dynamic section at offset 0xd1014 contains 26 entries:

  Tag        Type                         Name/Value

 0x00000001 (NEEDED)                     Shared library: [libtinfo.so.5]

 0x00000001 (NEEDED)                     Shared library: [libdl.so.2]

 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

 0x0000000c (INIT)                       0x805d350

 0x0000000d (FINI)                       0x80e66ac

 0x6ffffef5 (GNU_HASH)                   0x804718c

 0x00000005 (STRTAB)                     0x8053b3a

 0x00000006 (SYMTAB)                     0x804a78c

 0x0000000a (STRSZ)                      32867 (bytes)

 0x0000000b (SYMENT)                     16 (bytes)

 0x00000015 (DEBUG)                      0x0

 0x00000003 (PLTGOT)                     0x81180f0

 0x00000002 (PLTRELSZ)                   1592 (bytes)

 0x00000014 (PLTREL)                     REL

 0x00000017 (JMPREL)                     0x805cd18

 0x00000011 (REL)                        0x805ccd8

 0x00000012 (RELSZ)                      64 (bytes)

 0x00000013 (RELENT)                     8 (bytes)

 0x6ffffffe (VERNEED)                    0x805cc28

 0x6fffffff (VERNEEDNUM)                 2

 0x6ffffff0 (VERSYM)                     0x805bbb0

 0x6ffffef9 (GNU_LIBLIST)                0x8052b4c

 0x6ffffdf7 (GNU_LIBLISTSZ)              80 (bytes)

 0x6ffffef8 (GNU_CONFLICT)               0x8052b9c

 0x6ffffdf6 (GNU_CONFLICTSZ)             576 (bytes)

 0x00000000 (NULL)                       0x0

 

动态链接器的符号查找(symbol lookup by the dynamic linker)

在上面提到,符号查找是一个复杂的过程,我将会给一个简单的描述。

对于每个加载的对象,RTLD(Runtime dynamic linker)保持一个加载对象的列表,叫做“查找范围(lookup scope)”,每个范围包含指向所有加载对象的指针(二进制和所有加载的库),但是不同的范围对象的顺序是不同的。可是在每个scope中binary总是第一个对象。

当RTLD决议一个符号时,它第一步检查那个对象需要执行重定位。是查找的二进制文件本身或者一个加载的库。然后它给出那个对象的查找范围,迭代之中的每一个对象。

对于每一个对象,它在动态符号表中查找需要的符号。当匹配上后,它用符号值来做重定位,否则继续查找这个scope中的下一个对象。

 

符号查找规则的重要性(consequences of the symbol lookup rules)

Libs不能够直接跳转到它输出的函数上(它们当然知道它们自己的函数在哪里),但是也必须通过描述的符号查找机制。

总有这样一个事实,二进制总是第一个在每个查找范围中查找,意味着符号定义在二进制中总是覆盖符号定义在库中。

特意采用这样的方式,是允许二进制覆盖它不喜欢的库中的函数。如果没有发生,就将采用库中的函数,也不是由库本身来调用。

通常这是很好的,但是这可能带来一个问题,假如二进制不留意定义了一个符号,这个符号也在许多加载的库使用(think program uses GTK, which pulls in different theme and input plugins depending on the user's system config)。

假如程序定义一个函数void print_error(int error_code, char* str);同时许多插件也定义了同样名字的函数,但是另一个署名,例如int print_error(char* str), 这也许是有问题的。假如插件没有输出print_error符号,这里就没有问题,因为在插件中的代码只是直接调用合适的函数,并不需要查找符号表。

然而假如插件输出符号了,它就要自己查找符号表(因为那是由SystemV ABI规范所要求的,同时也有LSB).那么print_error将会被二进制中的符号干预,那是个不兼容的签名,将会引发一个崩溃。

解决方案(solutions for this)

正确的解决方案是不要输出那些你不期望其它地方用到的符号。这是TheRightWay的,因为1,加速库/插件(没有符号查找,只是在函数内部使用),你不必污染名称空间,你避免了上面所提到的可能出现的问题,最后,符号不是你ABI的一部分,那意味这你可以任意的修改,也不会弄坏任何依赖的程序。

这里有一个快速的方法来避免上述的问题。当在连接的的命令行中加入-Bsymbolic来连接一个库时,库的查找范围改变了,库本身就在第一现场,然后才是二进制。

这不能带给你任何优势,程序也不能干预你的符号,即使它想那么做。因此,-Bsymbolic应当避免,除非你真的很懒很懒!;-)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值