ELF文件格式概述(一):ELF header

September 24, 2016 at 12:30 pm

无锁系列似乎比想象中要难产阿,主要是最近还有很多事要干,这周内尽量将第4篇更新出来吧。

今天打算对ELF文件格式进行一个整理。第一次接触到ELF文件格式是在于渊的《自己动手写操作系统》里,在这本书里ELF文件格式并不做为一个重点讨论的对象,当时对这部分内容确实也做了一些笔记,但是内容还是偏少;第二次接触ELF则是在《程序员的自我修养--链接、装载于库》中,这本书对于ELF文件格式的结构还是有比较详细的介绍的。

之所以要重新整理这部分的内容,是因为,最近有个地方需要用到一个ELF文件格式的loader,所以需要对这个文件格式进行进一步的了解。本文主要还是一些属于偏陈述性的东西,大概可以称为半技术文。早前在某个网站上看到过一篇对ELF介绍得非常深入的文章(那篇文章的作者应该是做偏硬件的),但是现在找不到了。无论如何,本文将对ELF相关的知识进行重新整理,主要参考上面提到的两本书籍。

本文主要由如下几个小节构成:
1. ELF文件概述
2. ELF header

ELF文件概述

ELF(Executable and Linkable Format)文件格式是COFF(Common Object File Format, 通用目标文件格式)的一个变种,广泛使用在unix/linux环境下。在这些环境下,它主要用来表示可执行文件、可重定位文件、共享目标文件和核心转储文件。我们重点关注前面两种形式的文件。

按照我们对可执行文件的理解,里面应当包含代码段、数据段,以进行程序的载入,下面我们通过一个示例来看一下ELF文件格式的概貌:

// SimpleSection.c
int printf(const char *format, ... );

int global_init_var = 84;
int global_uninit_var;

void func1(int i){
    printf("%d\n", i);
}

int main(void){
    static int static_var = 85;
    static int static_var2;

    int a = 1;
    int b;

    func1(static_var + static_var2 + a + b);

    return a;
}

我们直接使用-c选项进行编译,得到目标文件SimpleSection.o(可重定位文件):

g++ -c SimpleSection.c

我们先用xxd简单查看一下目标文件:

xxd SimpleSection.o

这里我得到的文件大小是1880字节,不同的系统可能得到不同的值。

0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
0000010: 0100 3e00 0100 0000 0000 0000 0000 0000  ..>.............
0000020: 0000 0000 0000 0000 1804 0000 0000 0000  ................
0000030: 0000 0000 4000 0000 0000 4000 0d00 0a00  ....@.....@.....
0000040: 5548 89e5 4883 ec10 897d fc8b 45fc 89c6  UH..H....}..E...
0000050: bf00 0000 00b8 0000 0000 e800 0000 00c9  ................
0000060: c355 4889 e548 83ec 10c7 45f8 0100 0000  .UH..H....E.....
0000070: 8b15 0000 0000 8b05 0000 0000 01c2 8b45  ...............E
......
0000710: 1800 0000 0000 0000 0900 0000 0300 0000  ................
0000720: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000730: 0803 0000 0000 0000 6600 0000 0000 0000  ........f.......
0000740: 0000 0000 0000 0000 0100 0000 0000 0000  ................
0000750: 0000 0000 0000 0000                      ........

我们的代码产生了一个1880字节的目标文件,不过直接从16进制来看的话,除了看到第一行鲜明的"ELF"以外,其它的东西并看不出什么意义,也识别不出代码段、数据段在哪里。不过无论如何,我们的目标文件就是这么大,接下来,我们只要使用工具挖掘它的含义就可以了(理解了它的实际格式之后,我们便可以自己编写工具,不过目前来看我只需要写一个loader)。这里我们首先使用objdump工具查看一下每个段的构成:

objdump -h SimpleSection.o

得到的结果如下:

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000054  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000094  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  0000009c  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  0000009c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000026  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c6  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000c8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

.text和.data就是我们熟悉的代码段和数据段,.bss自然就是bss段,.rodata段里存放一些只读信息,后面的3个段我们可以先忽略。File off项表明了这个段的偏移地址,而Size则对应他们的尺寸,可以看到,代码段从0x40处开始,总共0x54个字节,紧挨着数据段,这里数据段只有8个字节。接着是bss段,它虽然size并不为0,但从.rodata的偏移地址来看,它们实际上并没有内容,这个原因我们等会儿再来看。接着是.rodata段,共4个字节。也就是说,在ELF文件中,这些段都是按照顺序依次排列的。

ELF header

上一小节我们提到代码段从0x40处开始,那么在这之前是什么呢?我们记得我们看到过开头有个ELF。在实际的各个段之前的,就是我们ELF的header(文件头),它包含了描述整个ELF文件的基本信息。我们使用专门的readelf命令来查看ELF文件的头部,查看头部使用-h选项:

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:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1048 (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:         13
  Section header string table index: 10

上述命令给出了ELF文件header的信息,为了更清楚地理解ELF的header,我们直接查看linux下ELF header这个结构体的源代码(通常位于/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;

第一个成员是一个数组,EI_NIDENT是一个值为16的宏,所以这个字段16个字节,对应了上述命令输出结果中的magic,这一个魔数共16个字节,前4个字节是ELF文件的标识,由一个del符和ELF三个字符组成;第5个字节表明ELF文件的种类,目前1表示32位ELF文件,2表示64位ELF文件,这里用的是64位的系统,所以是02;第6个字节表明的是字节序,1表示小端,2表示大端,这里用的是x86的CPU,所以是1;第7个字节表示ELF标准的版本,一般来说就是1,ELF目前并没有第二版。后面的9个字节目前并没有意义,默认填0。
第二个成员表明这个ELF文件的类型,即之前提及的4种类型,这里稍微要注意一下的是,即使是在64位系统上,通常认为的字(word)仍然是32位,所以这里的Elf64_Half是一个16位的数。可以从之前的16进制格式的文件看出值为1(是的,header在文件中的实际存储顺序和结构体的定义顺序是一致的,本应如此,不是吗),表示是一个可重定位文件。
第三个成员表明机器的类型,从16进制里可以看出值为0x003e,表明是一个x86-64的机器。
第四个成员又出现了version,和之前的ELF标准版本一致,这里的值为1。不知是否是历史遗留原因,这个版本号在一个结构体里出现两次,而且无论是32位还是64位系统,这个version域占整整32位。ELF标准委员会可能考虑这里存储一个详细的版本号,而在magic中仅存一个大版本号,它们或许并没有想过1995年委员会就解散了。
第五个成员表明了程序开始时的入口地址,这是一个非常重要的地址,不过对于我们的可重定位文件来说,它还没有进行链接,所以这个地址通常为0。这在readelf命令的结果里也可以体现。
第6个成员也和可执行文件相关,这里我们的可重定位文件这个值也是0。
第7个成员表明了文件里section header table(段表)的偏移,从命令结果看出值为1048(16进制文件中为0x0418,很巧合,0x0418=1048)。正如elf header表明了elf文件的基本信息,section header table则给出了各个段的基本信息,等会儿我们再回过头来细看section header table。
占4字节的第8个成员e_flags的含义和具体的机器相关,这里我们的值为0,我们不去讨论这个字段。
第9个成员就是我们这个header的大小,不出所料是0x40,这正好解释了代码段从文件的0x40处开始的原因。简单计算一下就可以知道我们的结构体正好64个字节。
第10个成员也和可执行文件相关,我们也暂时跳过。
剩下的3个成员都和段表相关,下面我们再着重讨论。

至此,我们对elf header做了一个相对详细的说明,到这里,我们已经可以自己实现readelf -h命令了。

需要整理的内容似乎比想象中的要多一些,果然半技术文就是内容会非常多阿。这里还是做一个划分先吧,所以本来的一个单篇又变成了一个系列......下一篇重点介绍section header table的相关内容。