ELF文件结构

5. ELF文件 请点评

ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:

  • 可重定位的目标文件(Relocatable,或者Object File)

  • 可执行文件(Executable)

  • 共享库(Shared Object,或者Shared Library)

共享库留到第 4 节 “共享库”再详细介绍,本节我们以例 18.2 “求一组数的最大值的汇编程序”为例讨论目标文件和可执行文件的格式。现在详细解释一下这个程序的汇编、链接、运行过程:

  1. 写一个汇编程序保存成文本文件max.s

  2. 汇编器读取这个文本文件转换成目标文件max.o,目标文件由若干个Section组成,我们在汇编程序中声明的.section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。

  3. 然后链接器把目标文件中的Section合并成几个Segment[28],生成可执行文件max

  4. 最后加载器(Loader)根据可执行文件中的Segment信息加载运行这个程序。

ELF格式提供了两种不同的视角,链接器把ELF文件看成是Section的集合,而加载器把ELF文件看成是Segment的集合。如下图所示。

图 18.1. ELF文件

ELF文件

左边是从链接器的视角来看ELF文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在链接过程中用不到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息,通过Section Header Table可以找到每个Section在文件中的位置。右边是从加载器的视角来看ELF文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中用不到,所以是可有可无的。从上图可以看出,一个Segment由一个或多个Section组成,这些Section加载到内存时具有相同的访问权限。有些Section只对链接器有意义,在运行时用不到,也不需要加载到内存,那么就不属于任何Segment。注意Section Header Table和Program Header Table并不是一定要位于文件的开头和结尾,其位置由ELF Header指出,上图这么画只是为了清晰。

目标文件需要链接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。

5.1. 目标文件 请点评

下面用readelf工具读出目标文件max.o的ELF Header和Section Header Table,然后我们逐段分析。

$ readelf -a max.o 
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:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          200 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         8
  Section header string table index: 5
...

ELF Header中描述了操作系统是UNIX,体系结构是80386。Section Header Table中有8个Section Header,从文件地址200(0xc8)开始,每个Section Header占40字节,共320字节,到文件地址0x207结束。这个目标文件没有Program Header。文件地址是这样定义的:文件开头第一个字节的地址是0,然后每个字节占一个地址。

...
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 00002a 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0002b0 000010 08      6   1  4
  [ 3] .data             PROGBITS        00000000 000060 000038 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000098 000000 00  WA  0   0  4
  [ 5] .shstrtab         STRTAB          00000000 000098 000030 00      0   0  1
  [ 6] .symtab           SYMTAB          00000000 000208 000080 10      7   7  4
  [ 7] .strtab           STRTAB          00000000 000288 000028 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)

There are no section groups in this file.

There are no program headers in this file.
...

从Section Header中读出各Section的描述信息,其中.text.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。Addr是这些段加载到内存中的地址(我们讲过程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。OffSize列指出了各Section的起始文件地址和长度。比如.data段从文件地址0x60开始,一共0x38个字节,回去翻一下程序,.data段定义了14个4字节的整数,一共是56个字节,也就是0x38。根据以上信息可以描绘出整个目标文件的布局。

表 18.1. 目标文件的布局

起始文件地址Section或Header
0ELF Header
0x34.text
0x60.data
0x98.bss(此段为空)
0x98.shstrtab
0xc8Section Header Table
0x208.symtab
0x288.strtab
0x2b0.rel.text

这个文件不大,我们直接用hexdump工具把目标文件的字节全部打印出来看。

$ hexdump -C max.o 
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 03 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  c8 00 00 00 00 00 00 00  34 00 00 00 00 00 28 00  |........4.....(.|
00000030  08 00 05 00 bf 00 00 00  00 8b 04 bd 00 00 00 00  |................|
00000040  89 c3 83 f8 00 74 10 47  8b 04 bd 00 00 00 00 39  |.....t.G.......9|
00000050  d8 7e ef 89 c3 eb eb b8  01 00 00 00 cd 80 00 00  |.~..............|
00000060  03 00 00 00 43 00 00 00  22 00 00 00 de 00 00 00  |....C...".......|
00000070  2d 00 00 00 4b 00 00 00  36 00 00 00 22 00 00 00  |-...K...6..."...|
00000080  2c 00 00 00 21 00 00 00  16 00 00 00 0b 00 00 00  |,...!...........|
00000090  42 00 00 00 00 00 00 00  00 2e 73 79 6d 74 61 62  |B.........symtab|
000000a0  00 2e 73 74 72 74 61 62  00 2e 73 68 73 74 72 74  |..strtab..shstrt|
000000b0  61 62 00 2e 72 65 6c 2e  74 65 78 74 00 2e 64 61  |ab..rel.text..da|
000000c0  74 61 00 2e 62 73 73 00  00 00 00 00 00 00 00 00  |ta..bss.........|
000000d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000f0  1f 00 00 00 01 00 00 00  06 00 00 00 00 00 00 00  |................|
00000100  34 00 00 00 2a 00 00 00  00 00 00 00 00 00 00 00  |4...*...........|
00000110  04 00 00 00 00 00 00 00  1b 00 00 00 09 00 00 00  |................|
00000120  00 00 00 00 00 00 00 00  b0 02 00 00 10 00 00 00  |................|
00000130  06 00 00 00 01 00 00 00  04 00 00 00 08 00 00 00  |................|
00000140  25 00 00 00 01 00 00 00  03 00 00 00 00 00 00 00  |%...............|
00000150  60 00 00 00 38 00 00 00  00 00 00 00 00 00 00 00  |`...8...........|
00000160  04 00 00 00 00 00 00 00  2b 00 00 00 08 00 00 00  |........+.......|
00000170  03 00 00 00 00 00 00 00  98 00 00 00 00 00 00 00  |................|
00000180  00 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
00000190  11 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
000001a0  98 00 00 00 30 00 00 00  00 00 00 00 00 00 00 00  |....0...........|
000001b0  01 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
000001c0  00 00 00 00 00 00 00 00  08 02 00 00 80 00 00 00  |................|
000001d0  07 00 00 00 07 00 00 00  04 00 00 00 10 00 00 00  |................|
000001e0  09 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
000001f0  88 02 00 00 28 00 00 00  00 00 00 00 00 00 00 00  |....(...........|
00000200  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000220  00 00 00 00 03 00 01 00  00 00 00 00 00 00 00 00  |................|
00000230  00 00 00 00 03 00 03 00  00 00 00 00 00 00 00 00  |................|
00000240  00 00 00 00 03 00 04 00  01 00 00 00 00 00 00 00  |................|
00000250  00 00 00 00 00 00 03 00  0c 00 00 00 0e 00 00 00  |................|
00000260  00 00 00 00 00 00 01 00  17 00 00 00 23 00 00 00  |............#...|
00000270  00 00 00 00 00 00 01 00  21 00 00 00 00 00 00 00  |........!.......|
00000280  00 00 00 00 10 00 01 00  00 64 61 74 61 5f 69 74  |.........data_it|
00000290  65 6d 73 00 73 74 61 72  74 5f 6c 6f 6f 70 00 6c  |ems.start_loop.l|
000002a0  6f 6f 70 5f 65 78 69 74  00 5f 73 74 61 72 74 00  |oop_exit._start.|
000002b0  08 00 00 00 01 02 00 00  17 00 00 00 01 02 00 00  |................|
000002c0

左边一列是文件地址,中间是每个字节的十六进制表示,右边是把这些字节解释成ASCII码所对应的字符。中间有一个*号表示省略的部分全是0。.data段对应的是这一块:

...
00000060  03 00 00 00 43 00 00 00  22 00 00 00 de 00 00 00  |....C...".......|
00000070  2d 00 00 00 4b 00 00 00  36 00 00 00 22 00 00 00  |-...K...6..."...|
00000080  2c 00 00 00 21 00 00 00  16 00 00 00 0b 00 00 00  |,...!...........|
00000090  42 00 00 00 00 00 00 00
...

.data段将被原封不动地加载到内存中,下一小节会看到.data段被加载到内存地址0x080490a0~0x080490d7。

.shstrtab.strtab这两个Section中存放的都是ASCII码:

...
                                   00 2e 73 79 6d 74 61 62  |B.........symtab|
000000a0  00 2e 73 74 72 74 61 62  00 2e 73 68 73 74 72 74  |..strtab..shstrt|
000000b0  61 62 00 2e 72 65 6c 2e  74 65 78 74 00 2e 64 61  |ab..rel.text..da|
000000c0  74 61 00 2e 62 73 73 00                           |ta..bss.........|
...
                                   00 64 61 74 61 5f 69 74  |.........data_it|
00000290  65 6d 73 00 73 74 61 72  74 5f 6c 6f 6f 70 00 6c  |ems.start_loop.l|
000002a0  6f 6f 70 5f 65 78 69 74  00 5f 73 74 61 72 74 00  |oop_exit._start.|
...

可见.shstrtab段保存着各Section的名字,.strtab段保存着程序中用到的符号的名字。每个名字都是以'\0'结尾的字符串。

我们知道,C语言的全局变量如果在代码中没有初始化,就会在程序加载时用0初始化。这种数据属于.bss段,在加载时它和.data段一样都是可读可写的数据,但是在ELF文件中.data段需要占用一部分空间保存初始值,而.bss段则不需要。也就是说,.bss段在文件中只占一个Section Header而没有对应的Section,程序加载时.bss段占多大内存空间在Section Header中描述。在我们这个例子中没有用到.bss段,在第 3 节 “变量的存储布局”会看到这样的例子。

我们继续分析readelf输出的最后一部分,是从.rel.text.symtab这两个Section中读出的信息。

...
Relocation section '.rel.text' at offset 0x2b0 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000008  00000201 R_386_32          00000000   .data
00000017  00000201 R_386_32          00000000   .data

There are no unwind sections in this file.

Symbol table '.symtab' contains 8 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 SECTION LOCAL  DEFAULT    1 
     2: 00000000     0 SECTION LOCAL  DEFAULT    3 
     3: 00000000     0 SECTION LOCAL  DEFAULT    4 
     4: 00000000     0 NOTYPE  LOCAL  DEFAULT    3 data_items
     5: 0000000e     0 NOTYPE  LOCAL  DEFAULT    1 start_loop
     6: 00000023     0 NOTYPE  LOCAL  DEFAULT    1 loop_exit
     7: 00000000     0 NOTYPE  GLOBAL DEFAULT    1 _start

No version information found in this file.

.rel.text告诉链接器指令中的哪些地方需要做重定位,在下一小节详细讨论。

.symtab是符号表。Ndx列是每个符号所在的Section编号,例如符号data_items在第3个Section里(也就是.data段),各Section的编号见Section Header Table。Value列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址,比如data_items位于.data段的开头,所以地址是0,_start位于.text段的开头,所以地址也是0,但是start_looploop_exit相对于.text段的地址就不是0了。从Bind这一列可以看出_start这个符号是GLOBAL的,而其它符号是LOCAL的,GLOBAL符号是在汇编程序中用.globl指示声明过的符号。

现在剩下.text段没有分析,objdump工具可以把程序中的机器指令反汇编(Disassemble),那么反汇编的结果是否跟原来写的汇编代码一模一样呢?我们对比分析一下。

$ objdump -d max.o

max.o:     file format elf32-i386


Disassembly of section .text:

00000000 <_start>:
   0:	bf 00 00 00 00       	mov    $0x0,%edi
   5:	8b 04 bd 00 00 00 00 	mov    0x0(,%edi,4),%eax
   c:	89 c3                	mov    %eax,%ebx

0000000e <start_loop>:
   e:	83 f8 00             	cmp    $0x0,%eax
  11:	74 10                	je     23 <loop_exit>
  13:	47                   	inc    %edi
  14:	8b 04 bd 00 00 00 00 	mov    0x0(,%edi,4),%eax
  1b:	39 d8                	cmp    %ebx,%eax
  1d:	7e ef                	jle    e <start_loop>
  1f:	89 c3                	mov    %eax,%ebx
  21:	eb eb                	jmp    e <start_loop>

00000023 <loop_exit>:
  23:	b8 01 00 00 00       	mov    $0x1,%eax
  28:	cd 80                	int    $0x80

左边是机器指令的字节,右边是反汇编结果。显然,所有的符号都被替换成地址了,比如je 23,注意没有加$的数表示内存地址,而不表示立即数。这条指令后面的<loop_exit>并不是指令的一部分,而是反汇编器从.symtab.strtab中查到的符号名称,写在后面是为了有更好的可读性。目前所有指令中用到的符号地址都是相对地址,下一步链接器要修改这些指令,把其中的地址都改成加载时的内存地址,这些指令才能正确执行。

5.2. 可执行文件 请点评

现在我们按上一节的步骤分析可执行文件max,看看链接器都做了什么改动。

$ readelf -a max
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:               0x8048074
  Start of program headers:          52 (bytes into file)
  Start of section headers:          256 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         6
  Section header string table index: 3

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        08048074 000074 00002a 00  AX  0   0  4
  [ 2] .data             PROGBITS        080490a0 0000a0 000038 00  WA  0   0  4
  [ 3] .shstrtab         STRTAB          00000000 0000d8 000027 00      0   0  1
  [ 4] .symtab           SYMTAB          00000000 0001f0 0000a0 10      5   6  4
  [ 5] .strtab           STRTAB          00000000 000290 000040 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)

There are no section groups in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000
  LOAD           0x0000a0 0x080490a0 0x080490a0 0x00038 0x00038 RW  0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .text 
   01     .data 

There is no dynamic section in this file.

There are no relocations in this file.

There are no unwind sections in this file.

Symbol table '.symtab' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 08048074     0 SECTION LOCAL  DEFAULT    1 
     2: 080490a0     0 SECTION LOCAL  DEFAULT    2 
     3: 080490a0     0 NOTYPE  LOCAL  DEFAULT    2 data_items
     4: 08048082     0 NOTYPE  LOCAL  DEFAULT    1 start_loop
     5: 08048097     0 NOTYPE  LOCAL  DEFAULT    1 loop_exit
     6: 08048074     0 NOTYPE  GLOBAL DEFAULT    1 _start
     7: 080490d8     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
     8: 080490d8     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     9: 080490d8     0 NOTYPE  GLOBAL DEFAULT  ABS _end

No version information found in this file.

在ELF Header中,Type改成了EXEC,由目标文件变成可执行文件了,Entry point address改成了0x8048074(这是_start符号的地址),还可以看出,多了两个Program Header,少了两个Section Header。

在Section Header Table中,.text.data段的加载地址分别改成了0x08048074和0x080490a0。.bss段没有用到,所以被删掉了。.rel.text段就是用于链接过程的,做完链接就没用了,所以也删掉了。

多出来的Program Header Table描述了两个Segment的信息。.text段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSiz指出总长度是0x9e),.data段组成另一个Segment(总长度是0x38)。VirtAddr列指出第一个Segment加载到虚拟地址0x08048000(注意在x86平台上后面的PhysAddr列是没有意义的,并不代表实际的物理地址),第二个Segment加载到地址0x080490a0。Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时文件也要按内存页面大小分成若干页,文件中的一页对应内存中的一页,对应关系如下图所示。

图 18.2. 文件和加载地址的对应关系

文件和加载地址的对应关系

这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然要偏移多少,比如第二个Segment在文件中的偏移是0xa0,在内存页面0x08049000中的偏移仍然是0xa0,所以从0x080490a0开始,这样规定是为了简化链接器和加载器的实现。从上图也可以看出.text段的加载地址应该是0x08048074_start符号位于.text段的开头,所以_start符号的地址也是0x08048074,从符号表中可以验证这一点。

原来目标文件符号表中的Value都是相对地址,现在都改成绝对地址了。此外还多了三个符号__bss_start_edata_end,这些符号在链接脚本中定义,被链接器添加到可执行文件中,链接脚本在第 1 节 “多目标文件的链接”介绍。

再看一下反汇编的结果:

$ objdump -d max

max:     file format elf32-i386


Disassembly of section .text:

08048074 <_start>:
 8048074:	bf 00 00 00 00       	mov    $0x0,%edi
 8048079:	8b 04 bd a0 90 04 08 	mov    0x80490a0(,%edi,4),%eax
 8048080:	89 c3                	mov    %eax,%ebx

08048082 <start_loop>:
 8048082:	83 f8 00             	cmp    $0x0,%eax
 8048085:	74 10                	je     8048097 <loop_exit>
 8048087:	47                   	inc    %edi
 8048088:	8b 04 bd a0 90 04 08 	mov    0x80490a0(,%edi,4),%eax
 804808f:	39 d8                	cmp    %ebx,%eax
 8048091:	7e ef                	jle    8048082 <start_loop>
 8048093:	89 c3                	mov    %eax,%ebx
 8048095:	eb eb                	jmp    8048082 <start_loop>

08048097 <loop_exit>:
 8048097:	b8 01 00 00 00       	mov    $0x1,%eax
 804809c:	cd 80                	int    $0x80

指令中的相对地址都改成绝对地址了。我们仔细检查一下改了哪些地方。首先看跳转指令,原来目标文件的指令是这样:

...
  11:	74 10                	je     23 <loop_exit>
...
  1d:	7e ef                	jle    e <start_loop>
...
  21:	eb eb                	jmp    e <start_loop>
...

现在改成了这样:

...
 8048085:	74 10                	je     8048097 <loop_exit>
...
 8048091:	7e ef                	jle    8048082 <start_loop>
...
 8048095:	eb eb                	jmp    8048082 <start_loop>
...

改了吗?其实只是反汇编的结果不同了,指令的机器码根本没变。为什么不用改指令就能跳转到新的地址呢?因为跳转指令中指定的是相对于当前指令向前或向后跳多少字节,而不是指定一个完整的内存地址,内存地址有32位,这些跳转指令只有16位,显然也不可能指定一个完整的内存地址,这称为相对跳转。这种相对跳转指令只有16位,只能在当前指令前后的一个小范围内跳转,不可能跳得太远,也有的跳转指令指定一个完整的内存地址,可以跳到任何地方,这称绝对跳转,在第 4.2 节 “动态链接的过程”我们会看到这样的例子。

再看内存访问指令,原来目标文件的指令是这样:

...
   5:	8b 04 bd 00 00 00 00 	mov    0x0(,%edi,4),%eax
...
  14:	8b 04 bd 00 00 00 00 	mov    0x0(,%edi,4),%eax
...

现在改成了这样:

...
 8048079:	8b 04 bd a0 90 04 08 	mov    0x80490a0(,%edi,4),%eax
...
 8048088:	8b 04 bd a0 90 04 08 	mov    0x80490a0(,%edi,4),%eax
...

指令中的地址原本是0x00000000,现在改成了0x080490a0(注意是小端字节序)。那么链接器怎么知道要改这两处呢?是根据目标文件中的.rel.text段提供的重定位信息来改的:

...
Relocation section '.rel.text' at offset 0x2b0 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000008  00000201 R_386_32          00000000   .data
00000017  00000201 R_386_32          00000000   .data
...

第一列Offset的值就是.text段需要改的地方,在.text段中的相对地址是8和0x17,正是这两条指令中00 00 00 00的位置。



[28] Segment也可以翻译成“”,为了避免混淆,在本书中只把Section称为段,而Segment直接用英文。

====

http://learn.akae.cn/media/ch18s05.html


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        工作关系,这个周花了一天时间好好研究了以下ELF文件及可执行ELF文件的加载。中间过程可谓收获不小,呵呵,因为之前搞linux驱动、ARM裸奔始终没有认真研究过ELF文件,这次深入学习一下,把之前很多没弄清的原理基本摸清楚了。
        首先简单说明一下ELF文件的用途(呵呵,昨天讲解的时候直奔ELF的细节,结果被PL BS,说俺这样讲别人没做这块的根本不知道在讲什么,所以今天。。。),ELF文件一种UNIX文件格式,全称是executable and linkable format,即可执行链接格式,在UNIX系统及linux、BSD等类UNIX系统中广泛使用。这种文件格式主要作为链接目标文件用。ELF格式文件实际上有3类,可重定位文件、可执行文件、共享目标库文件。这三种文件用途不同,内部结构略有差别。可执行文件是由程序段(segment)构成,每个段由一个段描述项来描述该段的相关信息,比如该段在ELF文件内的偏移地址、该段的类型、该段在运行时的加载地址、该段的大小等等,所有段的描述项构成一个结构体数组,即程序段描述表,有些地方也叫程序头描述表,该表在ELF文件中的位置是在ELF文件头之后,而程序的各个段在ELF文件中则是紧接这程序段描述表之后。可重定位文件则是由节区(section)构成,与可执行文件的程序段类似,每个节区也有一个节区描述项,所有的节区描述项也构成一个数组。可重定位文件与可执行文件在结构上稍微有些区别,就是节区描述表是放在节区后面的,即在ELF文件内部是接着节区之后的,而不是像可执行文件中的程序段描述表那样放在程序段之前。另外可执行文件和可重定位文件的一个重要区别就是,可执行文件中,节区描述表是可选的,并非必须,实际情况中一般是不用节区描述表的;可重定位文件中,程序段描述表也是可选的,而非必须,同样实际情况中一般不用程序段描述表。至于这两种文件中对应的描述表如果不存在,则在ELF文件头中将对应成员设为0来表示。最后,共享目标库文件则是综合前面两种文件,段内分节区,段描述表和节区描述表都有!
        介绍完ELF的大致结构之后,针对可执行ELF文件介绍一下该类文件中的几个比较重要的信息及相互之间的关系(下面所有介绍全部针对可执行文件)。ELF文件头可以用ELF32_Ehdr结构体来描述,该结构体具体信息不再罗嗦,网上随便找一下一大把介绍的,只介绍其中几个比较关键的成员。
        首先是e_entry,这个成员描述的是可执行文件加载到内存中以后机器可执行的程序的入口地址!关于该程序还是罗嗦一下,因为她真的真的灰常灰常的重要!!!首先,e_enty的值是一个地址,指向内存中的一个单元;其次,e_entry指向的内存单元是存储CPU执行该ELF文件时执行的第一条指令的。简单说就是一个程序源代码中无论如何都会有一个唯一的入口,一般情况下,在有OS的情形下,对应C程序中的main,而在裸奔的情况下,一般都会用一个_start来指定程序的入口,这些标示符在源代码被链接成可执行文件并被加载到内存中之后,都会有一个明确的地址,CPU从该地址而且仅能从该地址开始执行这个程序才能正确执行,这个地址在链接成ELF文件时就被连接器写到ELF文件头中的e_entry成员中了,CPU正是通过e_entry才知道程序加载到内存之后该从什么地址(CPU是不认识程序符号的,比如main,_start之类的,CPU只知道地址)开始执行这个程序。
        其次是e_poff,该成员标明了在ELF文件中,程序段描述表在ELF文件中的偏移。不罗嗦,因为必须要知道程序段描述表的位置,而通过e_phoff便可知道。
        再次就是e_phentsize和e_phnum啦。对于ELF文件,每个程序段描述项的大小是一致的,因此之用一个成员来表示程序段描述项大小便可,不用每个描述项的大小都用一个成员来描述,e_phentsize正是用来描述描述项大小的。e_phnum则是描述程序段描述项个数的,即程序段描述表中表项的个数,重复一点,有几个描述项就表明ELF文件中有几个程序段。
        接下来介绍上面说的程序段描述项。顾名思义,程序段描述项是描述程序段的,一点也不假!可执行程序中一般都会有.text,.data,.bss这些段,一般情况下是这样,但是不排除例外,比如还有.stack或者其他一些程序猿自定义段之类的,甚至有时仅仅只有一个.text段,不存在其他任何段也是可以滴。这些段在让CPU能执行前都需要先正确的加载到内存中去,这就需要相当多的信息才可以保证加载的正确性了,比如一个段要加载到内存的什么位置啊(p_vaddr或者p_paddr),加载程序从ELF的神马位置开始读取该段数据啊(p_poff),读取多少数据到内存中去啊(p_filesz和p_memsz)。
        到这里已经差不多把ELF可执行文件的主要信息交代了,最后再罗嗦以下e_entry和入口所在段的p_vaddr(或者p_paddr)的关系吧。不知道是不是也会有人在开始有同我一样的疑惑,就是这俩成员的关系,能否替换使用!因为开始我一直YY可执行段开始存放的就是机器码,即可执行的第一条机器码就放在段的开始位置,于是俺就继续YY,那这个段加载到内存的地址p_vaddr就应该是和e_entry是一样的啦,而且无论何时都应该是一样的啦!于是乎还到处找资料,甚至上网所有相关信息来确认俺的YY想法!结果搜到一个仅仅简单介绍了一下p_vaddr <= e_entry < p_vaddr + p_filesz,当时看到这个真是蛋疼的紧!不明白那个<=中的<是何种情形下才成立!后来想了好久,冥冥之中才意识到segment的开头也是有可能会插播一下消息或者广告之类的东东,然后才接第一条机器码,如此以后e_entry便不会等于p_vaddr了,而是会稍微大一点点,至此方幡然醒悟!

====

http://blog.chinaunix.net/uid-7828352-id-3065366.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值