摘要:本文主要讲解elf文件格式,通过readelf命令结合底层的相关数据结构,讲解相关内容,分析程序运行的基本原理。
本文来源:elf 文件格式探秘——程序运行背后的故事 http://blog.csdn.net/trochiluses/article/details/10373921
1.elf 文件格式概览
elf文件大体上由文件头和相关的section组成,而每个section由header和data组成。
2.elf文件头
文件头的数据结构:
1
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
5 Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
10 Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
15 Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
使用readelf命令查看文件头:
$ readelf -h /bin/ls
1 ELF Header:
2 Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
3 Class: ELF32
4 Data: 2's complement, little endian
5 Version: 1 (current)
6 OS/ABI: UNIX - System V
7 ABI Version: 0
8 Type: EXEC (Executable file)
9 Machine: Intel 80386
10 Version: 0x1
11 Entry point address: 0x804be34
12 Start of program headers: 52 (bytes into file)
13 Start of section headers: 103388 (bytes into file)
14 Flags: 0x0
15 Size of this header: 52 (bytes)
16 Size of program headers: 32 (bytes)
17 Number of program headers: 9
18 Size of section headers: 40 (bytes)
19 Number of section headers: 28
20 Section header string table index: 27
在 readelf 的输出中:
第 1 行,ELF Header: 指名 ELF 文件头开始。
第 2 行,Magic 魔数,用来指名该文件是一个 ELF 目标文件。第一个字节 7F 是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。
第 3 行,CLASS 表示文件类型,这里是 32位的 ELF 格式。
第 4 行,Data 表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。
第 5 行,当前 ELF 文件头版本号,这里版本号为 1 。
第 6 行,OS/ABI ,指出操作系统类型,ABI 是 Application Binary Interface 的缩写。
第 7 行,ABI 版本号,当前为 0 。
第 8 行,Type 表示文件类型。ELF 文件有 3 种类型,一种是如上所示的 Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。
第 9 行,机器平台类型。
第 10 行,当前目标文件的版本号。
第 11 行,程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。
第 12 行,与 11 行同理,这个目标文件没有 Program Headers。
第 13 行,sections 头开始处,这里 208 是十进制,表示从地址偏移 0xD0 处开始。
第 14 行,是一个与处理器相关联的标志,x86 平台上该处为 0 。
第 15 行,ELF 文件头的字节数。
第 16 行,程序头的大小。
第 17 行,segment的个数9。
第 18 行,sections header 的大小,这里每个 section 头大小为 40 个字节。
第 19 行,一共有多少个 section 头,这里是 8 个。
第 20 行,section 头字符串表索引号,从 Section Headers 输出部分可以看到其内容的偏移在 0xa0 处,从此处开始到0xcf 结束保存着各个 sections 的名字,如 .data,.text,.bss等。
e_ident是elf文件的第一项,以一个magic字节开头。第一个字节是0x7f,然后紧跟“ELF”,我们可以用hexdump来查看它。
$ hexdump -C /bin/ls | more
00000000 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 |.ELF............|
5 ... (rest of the program follows) ...
关于程序入口——不是main的地址:
1
$ cat test.c
#include <stdio.h>
5 int main(void)
{
printf("main is : %p\n", &main);
return 0;
}
10
$ gcc -Wall -o test test.c
$ ./test
main is : 0x10000430
15
$ readelf --headers ./test | grep 'Entry point'
Entry point address: 0x100002b0
$ objdump --disassemble ./test | grep 100002b0
20 100002b0 <_start>:
100002b0: 7c 29 0b 78 mr r9,r1
实际上,程序入口地址是“_start”,而这个地址空间和程序运行时候的地址空间是独立的。
在 Section Headers 这里,可以看到 .bss 和 .shstrtab 的偏移都为 0xa0 。这是因为,没有被初始化的全局变量,会在加载阶段被用 0 来初始化,这时候它和 .data 段一样可读可写。但在编译阶段,.data 段会被分配一部分空间已存放数据(这里从偏移 0x6c 开始),而 .bss 则没有,.bss 仅有的是 section headers 。
链接器从 .rel.text 就可以知道哪些地方需要进行重定位(relocate) 。
.symtab 是符号表。
Ndx 是符号表所在的 section 的 section header 编号。如 .data 段的 section header 编号是 3,而string1,string2,lenght 都是在 .data 段的。
3.符号表和重定位
符号表是符号和地址的映射,主要用于链接:如果有一个外部申明extern in function,我们引用了function,那么连接器就要查找function对应的地址,从而来调用它。
和符号表紧密相关的是重定位,我们可以在“动态链接”这一节详细理解什么是重定位。
4.section and segments
elf文件格式制定了elf文件的两种视角:链接和执行。这样给系统的设计者提供了很大的灵活性。
我们接下来讲解目标文件中的section,这些section将被链接成可执行文件。一个或者多个section被映射到一个segment里,segment属于可执行文的一部分。
4.1segments
如同我们在前面提到的那样,elf文件除了包含整体的文件头以外,还包含各个部分的program headers,这些program headers向操作系统描述了程序load和execute所需要的一切信息。联系内存的段式管理,同一个段的属性是相同的,下面是一个传统的program headers的数据结构:
1
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
5 Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
10 Elf32_Word p_align;
}
在elf总体的header里面,e_phoff, e_phnum and e_phentsize表示program headers的偏移,数量和大小,有了这些信息我们可以轻易知道program headers的相关情况,从而利用它们。
如同上面提到的那样,program headers并不仅仅是segments信息。p_type定义了program header的类型(PT_INTERP表明这个program header是一个string类型的指针,指向针对一个二进制文件的解释器)。我们原来对比过编译型语言和解释型语言,而且作出了以下区分:编译器建立了一个可以独立运行的二进制文件。那么为什么它需要一个解释器呢?一般而言,实际情况下更为复杂:现代系统装载可执行文件需要很大的灵活性,为了达到这一目的,一些信息只能在程序运行的时候获取。我们将在动态链接一章里解释这个问题。因此,需要对二进制文件做出一些小的更改以让它能够在实时运转的时候正常工作。因此,实际情况下的二进制解释器是一个动态装载器,它用一些列步骤完成可执行文件的加载和运行。
其他参数的解释:p_offset相对首地址的偏移,p_vaddr虚拟地址(一些嵌入式系统中没有实现虚拟内存,那么它等于p_paddr),p_paddr物理内存地址;p_filesz文件大小;p_memsz实际在内存中占的空间大小,一般大于前者,多余部分用0填充;p_flags读写可执行权限位;p_align对齐.
看例子:
$ readelf -l /bin/ls
Elf file type is EXEC (Executable file)
Entry point 0x804be34
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x18d24 0x18d24 R E 0x1000
LOAD 0x018ef8 0x08061ef8 0x08061ef8 0x003e8 0x01008 RW 0x1000
DYNAMIC 0x018f0c 0x08061f0c 0x08061f0c 0x000e0 0x000e0 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x015f7c 0x0805df7c 0x0805df7c 0x006f4 0x006f4 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x018ef8 0x08061ef8 0x08061ef8 0x00108 0x00108 R 0x1
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
08 .ctors .dtors .jcr .dynamic .got
上述各段组成了最终在内存中执行的程序,其还提供了各段在虚拟地址空间和物理地址空间中的大小、位置、标志、访问授权和对齐方面的信息。各段语义如下:
PHDR保存程序头表
INTERP指定程序从可行性文件映射到内存之后,必须调用的解释器,它是通过链接其他库来满足未解析的引用,用于在虚拟地址空间中插入程序运行所需的动态库。
LOAD表示一个需要从二进制文件映射到虚拟地址空间的段,其中保存了常量数据(如字符串),程序目标代码等。
DYNAMIC段保存了由动态连接器(即INTERP段中指定的解释器)使用的信息。
4.2sections
多个section组成segment。section将二进制文件组织成逻辑区域,用于在编译器和链接器之间交流信息。section的头部和segment有些类似:
1
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
5 Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
10 Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}
15
sh_type具有更多的类型:SH_PROGBITS表明这是程序将要使用的数据的seciton,其他的值表示是否它是一个符号表等等。allocate属性表示这个section需要被分配内存。接下来,看一个实例:
1
#include <stdio.h>
int big_big_array[10*1024*1024];
5
char *a_string = "Hello, World!";
int a_var_with_value = 0x100;
10 int main(void)
{
big_big_array[0] = 100;
printf("%s\n", a_string);
a_var_with_value += 20;
15 }
编译后执行:
1
$ readelf --all ./sections
ELF Header:
...
5 Size of section headers: 40 (bytes)
Number of section headers: 37
Section header string table index: 34
Section Headers:
10 [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 10000114 000114 00000d 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 10000124 000124 000020 00 A 0 0 4
[ 3] .hash HASH 10000144 000144 00002c 04 A 4 0 4
15 [ 4] .dynsym DYNSYM 10000170 000170 000060 10 A 5 1 4
[ 5] .dynstr STRTAB 100001d0 0001d0 00005e 00 A 0 0 1
[ 6] .gnu.version VERSYM 1000022e 00022e 00000c 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 1000023c 00023c 000020 00 A 5 1 4
[ 8] .rela.dyn RELA 1000025c 00025c 00000c 0c A 4 0 4
20 [ 9] .rela.plt RELA 10000268 000268 000018 0c A 4 25 4
[10] .init PROGBITS 10000280 000280 000028 00 AX 0 0 4
[11] .text PROGBITS 100002b0 0002b0 000560 00 AX 0 0 16
[12] .fini PROGBITS 10000810 000810 000020 00 AX 0 0 4
[13] .rodata PROGBITS 10000830 000830 000024 00 A 0 0 4
25 [14] .sdata2 PROGBITS 10000854 000854 000000 00 A 0 0 4
[15] .eh_frame PROGBITS 10000854 000854 000004 00 A 0 0 4
[16] .ctors PROGBITS 10010858 000858 000008 00 WA 0 0 4
[17] .dtors PROGBITS 10010860 000860 000008 00 WA 0 0 4
[18] .jcr PROGBITS 10010868 000868 000004 00 WA 0 0 4
30 [19] .got2 PROGBITS 1001086c 00086c 000010 00 WA 0 0 1
[20] .dynamic DYNAMIC 1001087c 00087c 0000c8 08 WA 5 0 4
[21] .data PROGBITS 10010944 000944 000008 00 WA 0 0 4
[22] .got PROGBITS 1001094c 00094c 000014 04 WAX 0 0 4
[23] .sdata PROGBITS 10010960 000960 000008 00 WA 0 0 4
35 [24] .sbss NOBITS 10010968 000968 000000 00 WA 0 0 1
[25] .plt NOBITS 10010968 000968 000060 00 WAX 0 0 4
[26] .bss NOBITS 100109c8 000968 2800004 00 WA 0 0 4
[27] .comment PROGBITS 00000000 000968 00018f 00 0 0 1
[28] .debug_aranges PROGBITS 00000000 000af8 000078 00 0 0 8
40 [29] .debug_pubnames PROGBITS 00000000 000b70 000025 00 0 0 1
[30] .debug_info PROGBITS 00000000 000b95 0002e5 00 0 0 1
[31] .debug_abbrev PROGBITS 00000000 000e7a 000076 00 0 0 1
[32] .debug_line PROGBITS 00000000 000ef0 0001de 00 0 0 1
[33] .debug_str PROGBITS 00000000 0010ce 0000f0 01 MS 0 0 1
45 [34] .shstrtab STRTAB 00000000 0011be 00013b 00 0 0 1
[35] .symtab SYMTAB 00000000 0018c4 000c90 10 36 65 4
[36] .strtab STRTAB 00000000 002554 000909 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
50 I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
...
55
Symbol table '.symtab' contains 201 entries:
Num: Value Size Type Bind Vis Ndx Name
...
99: 100109cc 0x2800000 OBJECT GLOBAL DEFAULT 26 big_big_array
60 ...
110: 10010960 4 OBJECT GLOBAL DEFAULT 23 a_string
...
130: 10010964 4 OBJECT GLOBAL DEFAULT 23 a_var_with_value
...
65 144: 10000430 96 FUNC GLOBAL DEFAULT 11 main
上面的输出中,我们为了方面分析,截断了一些冗余信息。
首先,来看big_big_array变量:在0x100109cc处,我们可以看见它在.bss这个段中(因为0x100109cc是.bss开始的地方),注意.bss的属性nobits,表明它在磁盘上是不占用数据存储空间的。同样,我们可以分析其他变量和main函数,注意main函数在.text段中。
或者使用如下命令:
$ readelf -S /bin/ls
There are 28 section headers, starting at offset 0x193dc:
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 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 00006c 04 A 5 0 4
[ 5] .dynsym DYNSYM 08048218 000218 0007a0 10 A 6 1 4
[ 6] .dynstr STRTAB 080489b8 0009b8 00058c 00 A 0 0 1
[ 7] .gnu.version VERSYM 08048f44 000f44 0000f4 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08049038 001038 0000e0 00 A 6 3 4
[ 9] .rel.dyn REL 08049118 001118 000038 08 A 5 0 4
[10] .rel.plt REL 08049150 001150 000350 08 A 5 12 4
[11] .init PROGBITS 080494a0 0014a0 00002e 00 AX 0 0 4
[12] .plt PROGBITS 080494d0 0014d0 0006b0 04 AX 0 0 16
[13] .text PROGBITS 08049b80 001b80 0104ac 00 AX 0 0 16
[14] .fini PROGBITS 0805a02c 01202c 00001a 00 AX 0 0 4
[15] .rodata PROGBITS 0805a060 012060 003f1b 00 A 0 0 32
[16] .eh_frame_hdr PROGBITS 0805df7c 015f7c 0006f4 00 A 0 0 4
[17] .eh_frame PROGBITS 0805e670 016670 0026b4 00 A 0 0 4
[18] .ctors PROGBITS 08061ef8 018ef8 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08061f00 018f00 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08061f08 018f08 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08061f0c 018f0c 0000e0 08 WA 6 0 4
[22] .got PROGBITS 08061fec 018fec 000008 04 WA 0 0 4
[23] .got.plt PROGBITS 08061ff4 018ff4 0001b4 04 WA 0 0 4
[24] .data PROGBITS 080621c0 0191c0 000120 00 WA 0 0 32
[25] .bss NOBITS 080622e0 0192e0 000c20 00 WA 0 0 32
[26] .gnu_debuglink PROGBITS 00000000 0192e0 000008 00 0 0 1
[27] .shstrtab STRTAB 00000000 0192e8 0000f2 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
PROGBITS(程序必须解释的信息,如二进制代码),STRTAB用于存储与ELF格式有关的字符串,但与程序没有直接关联,如各个节的名称(.text, .comment)
.data保存初始化过的数据,这是普通程序数据的一部分,可以在程序运行期间修改。
.rodata保存了只读数据,可以读取但不能修改,例如printf语句中的所有静态字符串封装到该节。
.init和.fini保存了进程初始化和结束所用的代码,这通常是由编译器自动添加的。
.hash是一个散列表,允许在不对全表元素进行线性搜索的情况下,快速访问所有符号表项。
4.3section&&segment
1
$ readelf --segments /bin/ls
Elf file type is EXEC (Executable file)
5 Entry point 0x100026c0
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
10 PHDR 0x000034 0x10000034 0x10000034 0x00100 0x00100 R E 0x4
INTERP 0x000154 0x10000154 0x10000154 0x0000d 0x0000d R 0x1
[Requesting program interpreter: /lib/ld.so.1]
LOAD 0x000000 0x10000000 0x10000000 0x14d5c 0x14d5c R E 0x10000
LOAD 0x014d60 0x10024d60 0x10024d60 0x002b0 0x00b7c RWE 0x10000
15 DYNAMIC 0x014f00 0x10024f00 0x10024f00 0x000d8 0x000d8 RW 0x4
NOTE 0x000164 0x10000164 0x10000164 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x014d30 0x10014d30 0x10014d30 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
20 Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_ r .rela.dyn .rela.plt .init .text .fini .rodata .eh_frame_hdr
25 03 .data .eh_frame .got2 .dynamic .ctors .dtors .jcr .got .sdata .sbss .p lt .bss
04 .dynamic
05 .note.ABI-tag
06 .eh_frame_hdr
07
30
.interp section is placed into an INTERP
flagged segment. Notice that readelf tells us it is requesting the interpreter /lib/ld.so.1
; this is the dynamic linker which is run to prepare the binary for execution.
注意两个LOAD段在权限位上的区别。
5.Debugging
Tradionally the primary method of post mortem debugging is referred to as the core dump. The termcore comes from the original physical characteristics of magnetic core memory, which uses the orientation of small magnetic rings to store state.
1
$ cat coredump.c
int main(void) {
char *foo = (char*)0x12345;
5 *foo = 'a';
return 0;
}
10 $ gcc -Wall -g -o coredump coredump.c
$ ./coredump
Segmentation fault (core dumped)
15 $ file ./core
./core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from './coredump'
$ gdb ./coredump
...
20 (gdb) core core
[New LWP 31614]
Core was generated by `./coredump'.
Program terminated with signal 11, Segmentation fault.
#0 0x080483c4 in main () at coredump.c:3
25 3 *foo = 'a';
(gdb)
5.1Symbols and Debugging Information
gdb需要原始的可执行文件和core dump来提供debug session。我们可以使用gcc的-g选项来增加debug信息,这些debug信息将存储在elf文件的特殊section之中。这些debug信息包括变量大小,数组长度,寄存器使用等信息。
尽管这些调试信息不是程序运行所必须的,但是它们占用了很大的磁盘空间。 objcopy工具可以用来提取这个debug信息(--only-keep-debug),接着可以给这些被截断的信息增加一个链接(--add-gnu-debuglink)。这样以后,原始的可执行文件中将多出一个称为 .gnu_debuglink的section,它包含一个hash,这样debugging session开启调试器能够确保它把正确的调试信息和可执行文件链接起来。
1
$ gcc -g -shared -o libtest.so libtest.c
$ objcopy --only-keep-debug libtest.so libtest.debug
$ objcopy --add-gnu-debuglink=libtest.debug libtest.so
5 $ objdump -s -j .gnu_debuglink libtest.so
libtest.so: file format elf32-i386
Contents of section .gnu_debuglink:
10 0000 6c696274 6573742e 64656275 67000000 libtest.debug...
0010 52a7fd0a R...
6.readefl常用选项
readelf [-a|--all]
[-h|--file-header]
[-l|--program-headers|--segments]
[-S|--section-headers|--sections]
[-s|--syms|--symbols]
elffile...
参考文献:
[1]Chapter 8. Behind the process coderwall.com
[2]ELF格式文件符号表全解析及readelf命令使用方法 http://blog.csdn.net/edonlii/article/details/8779075