ELF 文件格式及示例分析

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 | <- 段头表,描述各个段的位置、大小等属性
------------------------

分段存储有很多好处:

  1. 安全,代码段可以设置成只读的,防止恶意篡改
  2. 性能,现代CPU设计上往往指令和数据缓存分开
  3. 省空间,代码段共享,如动态库

示例代码

后文针对 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)
$ aarch64-linux-gnu-gcc -c simpleSection.c
# 得到可重定位文件 simpleSection.o

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

elf 文件头 (File header)

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

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

elf 文件头信息如下:

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

aarch64-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:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           AArch64
  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:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         11
  Section header string table index: 10

段头表 (Section Header Table)

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

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

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

$ aarch64-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。

示例

根据段头表的信息,我们可以把 simpleSection.o 的所有段的位置分布列出来。根据文件头信息,每个段表的长度为 64 字节,共 11 个段,因此文件长度最终为 0x458 + 11 * 64 = 0x718,即 1816 字节,与文件的实际大小一致。注意图里是按地址偏移来示例各个段的分布,与段的下标顺序并不完全一致。

// 这里没有表达为了对齐而加的 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
$ aarch64-linux-gnu-objdump -s simpleSection.o

simpleSection.o:     file format elf64-littleaarch64

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
$ aarch64-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
$ aarch64-linux-gnu-size simpleSection.o
   text    data     bss     dec     hex filename
    120       8       4     132      84 simpleSection.o

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

$ aarch64-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 段。

The Berkeley style output counts read only data in the “text” column, not in the “data” column, the “dec” and “hex” columns both display the sum of the “text”, “data”, and “bss” columns in decimal and hexadecimal respectively.

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

$ aarch64-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

可以看到最终的可执行文件的段中,.bss.comment 共用了一个地址,.bss 段在可执行文件中实际上是空的,未占用空间,只是在段表里记录了其大小是 0x10,共 16 个字节。

...
  [23] .bss              NOBITS           0000000000411038  00001038
       0000000000000010  0000000000000000  WA       0     0     4
  [24] .comment          PROGBITS         0000000000000000  00001038
       000000000000002d  0000000000000001  MS       0     0     1
...
特殊符号

我们可以在代码中通过 __bss_start____bss_end__ 来引用 .bss 段的起始和结束地址,这是链接器提供的特殊符号,它们并没有在源程序中定义,它们实际上定义在链接脚本中。

# List symbols in files
$ aarch64-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 64 61 74 61 00 2e 62 73 73  .text..data..bss
00000430: 00 2e 72 6f 64 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
{
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf64_Section	st_shndx;		/* Section index */
  Elf64_Addr	st_value;		/* Symbol value */
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;
$ aarch64-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

同段头表一样,第一个元素是无效的,也就是未定义符号。可以看到 mainfunc1 是全局可见的符号,段表下标为 1,即 .text 段。value 是函数相对于代码段起始地址的偏移。

总结

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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值