您的位置:首页 > 其它

ELF format -- How programs look from the inside

2010-12-21 18:02 531 查看
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应当避免,除非你真的很懒很懒!;-)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: