您好,欢迎来到99网。
搜索
您的当前位置:首页ELF 文件格式及示例分析

ELF 文件格式及示例分析

来源:99网

ELF (Executable and Linkable Format)

Linux 上源码编译后的 .o 文件即目标文件,目标文件结构上和可执行文件格式很相似,通过链接器链接相应的库后得到可执行文件 .elf。为了描述方便,文中不区分二者的存储格式。elf 存储格式涵盖了程序的编译、链接、装载和执行过程。了解目标文件的格式对认识操作系统,特别是进程加载方面大有裨益。那么目标文件包含什么东西呢?显而易见,应该包含会代码和数据,另外为了支持链接,其中还有符号表,为了支持调试还会有调试信息等等东西。

本文将以一个代码片段为例,深入分析 elf 文件中常用的各段。本文参考了 《程序员的自我修养》一书,感谢前人的付出。

Executable File /
Object File
------------------------
|    File Header       | <- 文件头,描述 elf 文件的属性...
------------------------
|    .text section     | <- 指令码
------------------------
|    .data section     | <- 初始化的非零全局变量和非零局部静态变量
------------------------
|    .bss section      | <- 未初始化或值为0的全局变量和为0的局部静态变量
------------------------
|    other section     | <- 其他段
------------------------
|    String Tables     | <- 字符串表
------------------------
|    Symbol Tables     | <- 符号表,用于链接
------------------------
| Section header table | <- 段头表,描述各个段的位置、大小等属性
------------------------

分段存储有很多好处:

示例代码

后文针对 elf 文件的分析均以下面的代码为例。读者可以参照着实际体验,加深印象。

int printf(const char * format, ...);

// extern long __bss_start__[];
// extern long __bss_end__[];

int g_init_var = 0x1234; // .data
int g_uinit_var1; // .bss
int g_uinit_var2; // .bss

void func1(int i) // .text
{
        printf("%x\n", i); // .text
}

int main() // .text
{
        static int s_var = 0x1235; // .data
        static int s_var2; // .bss

        int a = 1;
        int b;

        func1(s_var + s_var2 + a + b);
//      printf("%lx, %lx\n", __bss_start__, __bss_end__);
        return 0;
}
# gcc version 7.2.1 20171011 (Linaro GCC 7.2-2017.11)
$ aarch-linux-gnu-gcc -c simpleSection.c
# 得到可重定位文件 simpleSection.o

$ aarch-linux-gnu-gcc simpleSection.c
# 得到可执行文件 a.out

elf 文件头 (File header)

elf 文件头描述了整个 elf 文件的属性。比如程序是 32位还是 位的,elf 文件的大小端,目标硬件平台,elf 文件的属性(可重定位文件、可执行文件等),还包括一个段头表(Section Header Table)信息,它描述了文件中各个段在文件中的偏移以及属性。

// /usr/include/elf.h
typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf_Half	e_type;			/* Object file type */
  Elf_Half	e_machine;		/* Architecture */
  Elf_Word	e_version;		/* Object file version */
  Elf_Addr	e_entry;		/* Entry point virtual address */
  Elf_Off	e_phoff;		/* Program header table file offset */
  Elf_Off	e_shoff;		/* Section header table file offset */
  Elf_Word	e_flags;		/* Processor-specific flags */
  Elf_Half	e_ehsize;		/* ELF header size in bytes */
  Elf_Half	e_phentsize;		/* Program header table entry size */
  Elf_Half	e_phnum;		/* Program header table entry count */
  Elf_Half	e_shentsize;		/* Section header table entry size */
  Elf_Half	e_shnum;		/* Section header table entry count */
  Elf_Half	e_shstrndx;		/* Section header string table index */
} Elf_Ehdr;

elf 文件头信息如下:

这个 simpleSection.o 文件是小端的,类型是可重定位文件,目标机器是 AArch,段头表偏移位于文件 1112 (0x458)字节处。文件头本身占用 字节,每个段头占用 字节,共有 11 个段,各个段头名字组成的字符串形成了一个单独的段,它在所有段中的索引是 10,也就是最后一个段。

aarch-linux-gnu-readelf -h simpleSection.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           AArch
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1112 (bytes into file)
  Flags:                             0x0
  Size of this header:                (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:            (bytes)
  Number of section headers:         11
  Section header string table index: 10

段头表 (Section Header Table)

段头表描述了 ELF 文件包含的所有段的信息。每个段都是由下面的结构体所描述。

// /usr/include/elf.h
typedef struct
{
  Elf_Word	sh_name;		/* Section name (string tbl index) */
  Elf_Word	sh_type;		/* Section type */
  Elf_Xword	sh_flags;		/* Section flags */
  Elf_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf_Off	sh_offset;		/* Section file offset */
  Elf_Xword	sh_size;		/* Section size in bytes */
  Elf_Word	sh_link;		/* Link to another section */
  Elf_Word	sh_info;		/* Additional section information */
  Elf_Xword	sh_addralign;		/* Section alignment */
  Elf_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf_Shdr;

通过 readelf 工具可以获取到更详细的信息。

$ aarch-linux-gnu-readelf -S simpleSection.o
There are 11 section headers, starting at offset 0x458:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000074  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000340
       00000000000000c0  0000000000000018   I       8     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000b4
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000bc
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000c0
       0000000000000004  0000000000000000   A       0     0     8
  [ 6] .comment          PROGBITS         0000000000000000  000000c4
       000000000000002e  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000f2
       0000000000000000  0000000000000000           0     0     1
  [ 8] .symtab           SYMTAB           0000000000000000  000000f8
       00000000000001e0  0000000000000018           9    14     8
  [ 9] .strtab           STRTAB           0000000000000000  000002d8
       0000000000000065  0000000000000000           0     0     1
  [10] .shstrtab         STRTAB           0000000000000000  00000400
       0000000000000052  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

可以看到段头表从偏移 0x458 开始,这和文件头中记录的偏移是一致的。第一个段是无效的段,类型是 NULL。

示例

// 这里没有表达为了对齐而加的 padding

----------------- 0x00000000
| ELF Header    |
----------------- 0x00000040
| .text         |
----------------- 0x000000b4
| .data         |
----------------- 0x000000bc
| .bss          |
----------------- 0x000000c0
| .rodata       |
----------------- 0x000000c4
| .comment      |
----------------- 0x000000f2
|.note.GNU-stack|
----------------- 0x000000f8
| .symtab       |
----------------- 0x000002d8
| .strtab       |
----------------- 0x00000340
| .rela.text    |
----------------- 0x00000400
| .shstrtab     |
----------------- 0x00000458
| Section table |
----------------- 0x00000718

$ ls -l simpleSection.o
-rw-rw-r-- 1 xxx xxx 1816 Feb  9 10:39 simpleSection.o

这里要澄清一下,.bss 段看起来占用了4个字节,实际上就算继续增加未初始化的静态局部变量,.rodata 段的偏移还是 0x000000c0。具体的原因下文有分析。

各段解析
# Display the full contents of all sections requested
$ aarch-linux-gnu-objdump -s simpleSection.o

simpleSection.o:     file format elf-littleaarch

Contents of section .text:
 0000 fd7bbea9 fd030091 a01f00b9 00000090  .{..............
 0010 00000091 a11f40b9 00000094 1f2003d5  ......@...... ..
 0020 fd7bc2a8 c0035fd6 fd7bbea9 fd030091  .{...._..{......
 0030 20008052 a01f00b9 00000090 00000091   ..R............
 0040 010040b9 00000090 00000091 000040b9  ..@...........@.
 0050 2100000b a01f40b9 2100000b a01b40b9  !.....@.!.....@.
 0060 2000000b 00000094 00008052 fd7bc2a8   ..........R.{..
 0070 c0035fd6                             .._.
Contents of section .data:
 0000 34120000 35120000                    4...5...
Contents of section .rodata:
 0000 25780a00                             %x..
Contents of section .comment:
 0000 00474343 3a20284c 696e6172 6f204743  .GCC: (Linaro GC
 0010 4320372e 322d3230 31372e31 31292037  C 7.2-2017.11) 7
 0020 2e322e31 20323031 37313031 3100      .2.1 20171011.

# Display assembler contents of executable sections
$ aarch-linux-gnu-objdump -d simpleSection.o

...
Disassembly of section .text:

0000000000000000 <func1>:
   0:   a9be7bfd        stp     x29, x30, [sp, #-32]!
   4:   910003fd        mov     x29, sp
   8:   b9001fa0        str     w0, [x29, #28]
   c:   90000000        adrp    x0, 0 <func1>
...

# Displays the sizes of sections inside binary files
$ aarch-linux-gnu-size simpleSection.o
   text    data     bss     dec     hex filename
    120       8       4     132      84 simpleSection.o

.text 中存的是指令码,长度是 0x74 个字节和段表中记录的一致。不过为什么 size 工具打出来的要多四个字节 😃。

$ aarch-linux-gnu-size -A --common simpleSection.o
simpleSection.o  :
section           size   addr
.text              116      0
.data                8      0
.bss                 4      0
.rodata              4      0
.comment            46      0
.note.GNU-stack      0      0
*COM*                8      0
Total              186

原来是输出格式不同导致的。伯克利格式把 .rodata 的长度计入到 .text 段。

-A|-B --format={sysv|berkeley} Select output style (default is berkeley)。选择 System V 的格式查看,就能对应上了。
.data 存的是初始化了的全局变量和局部静态变量 0x12340x1235
.rodata 存的是打印的格式化字符串 %x\n
.comment 中存的是编译器的信息。
.bss 为未初始化的全局变量和局部静态变量预留了空间,elf 文件中并没有存数据,只是在段表中记录了 .bss 段的信息。

$ aarch-linux-gnu-objdump -t simpleSection.o

...
SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 simpleSection.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000004 l     O .data  0000000000000004 s_var.3113
0000000000000000 l     O .bss   0000000000000004 s_var2.3114
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     O .data  0000000000000004 g_init_var
0000000000000004       O *COM*  0000000000000004 g_uinit_var1
0000000000000004       O *COM*  0000000000000004 g_uinit_var2
0000000000000000 g     F .text  0000000000000028 func1
0000000000000000         *UND*  0000000000000000 printf
0000000000000028 g     F .text  000000000000004c main

.bss 段在 elf 文件中是不占用空间的,只在 .bss 段表中记录了变量的总大小,因为值都为 0,没必要在可执行文件中实际存储值,操作系统将在加载可执行文件的时候解析段大小的信息,然后为它分配内存。通过符号表可以看到只有局部静态变量放在了 .bss,而全局未初始化的变量是一个未定义的 COMMON 符号。这和编程语言和编译器实现相关,最终满足放在该段条件的会在链接成可执行文件时在 .bss 段分配空间,这么做的原因是链接过程涉及到符号的强与弱 (strong & weak symbol),需要在链接阶段做裁决,假如有其他的源文件定义了 int g_uinit_var1 = 1;。那么 g_uinit_var1 将是一个强符号,由于被初始化了将被放在 .data,而不是 .bss

...
  [23] .bss              NOBITS           0000000000411038  00001038
       0000000000000010  0000000000000000  WA       0     0     4
  [24] .comment          PROGBITS         0000000000000000  00001038
       000000000000002d  0000000000000001  MS       0     0     1
...
特殊符号
# List symbols in files
$ aarch-linux-gnu-nm a.out
                 U abort@@GLIBC_2.17
0000000000411048 B __bss_end__
0000000000411048 B _bss_end__
0000000000411038 B __bss_start
0000000000411038 B __bss_start__
...
extern long __bss_start__[];
extern long __bss_end__[];
段头字符串表 (Section header string table)

根据前面的描述,.shstrtab 是段头名称字符串组成的段,位于 0x400 的偏移处。而段头表位于 0x458 的偏移处。我们以 .text 段为例,它的下标是 1,即位于 0x458 + 0x40 = 0x498 偏移处。可以看到 {} 表示的即是 sh_name,它的含义是段名在 .shstrtab 中的偏移,显然 0x420 的位置刚好就是字符串 .text

00000400: 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62  ..symtab..strtab
00000410: 00 2e 73 68 73 74 72 74 61 62 00 2e 72 65 6c 61  ..shstrtab..rela
00000420: 2e 74 65 78 74 00 2e  61 74 61 00 2e 62 73 73  .text..data..bss
00000430: 00 2e 72 6f  61 74 61 00 2e 63 6f 6d 6d 65 6e  ..rodata..commen
00000440: 74 00 2e 6e 6f 74 65 2e 47 4e 55 2d 73 74 61 63  t..note.GNU-stac
00000450: 6b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  k...............
00000460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000490: 00 00 00 00 00 00 00 00 {20 00 00 00} 01 00 00 00  ........ .......
000004a0: 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
符号表结构 (Symbol table)

在链接的时候,函数和变量统称为符号,链接器需要符号信息来完成链接工作。除了函数和变量外,还有其他符号,如段名,行号信息等,这里不详述。前面提到了符号表,符号表也是一个段。其中记录了符号的名称、大小等信息。结构体如下:

typedef struct
{
  Elf_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf_Section	st_shndx;		/* Section index */
  Elf_Addr	st_value;		/* Symbol value */
  Elf_Xword	st_size;		/* Symbol size */
} Elf_Sym;
$ aarch-linux-gnu-readelf -s simpleSection.o

Symbol table '.symtab' contains 20 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS simpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    3 $d
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     7: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 $d
     8: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 $x
     9: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 s_var.3113
    10: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 s_var2.3114
    11: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    4 $d
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
    13: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    14: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_init_var
    15: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_uinit_var1
    16: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_uinit_var2
    17: 0000000000000000    40 FUNC    GLOBAL DEFAULT    1 func1
    18: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    19: 0000000000000028    76 FUNC    GLOBAL DEFAULT    1 main

总结

无论是目标文件、可执行文件、库甚至是核心转储(coredump),它们实际上都基于相似的格式。通过 elf 文件头,我们可以知道段头表的位置、每个段头的大小、段头字符串表的下标,根据这几个信息我们可以找到 elf 文件中所有段的信息,从而解析整个 elf 文件。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 99spj.com 版权所有 湘ICP备2022005869号-5

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务