elf 文件格式

可执行文件格式

现在 PC 平台流行的可执行文件格式主要是 Windows 下的 PE(Portable Executable)和 Linux 下的 ELF(Executable Linktable Format)。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows 下的 .obj 和 Linux 下的 .o),它跟可执行文件的内容很相似,所以一般跟可执行文件格式一起采用一种格式存储。动态链接库(Windows 的 .dll 和 Linux 的 .so)以及静态链接库(Windows 的 .lib 和 Linux 的 .a)文件都按照可执行文件格式存储。

ELF 文件中有什么

来看一个简单的例子:
example_1.c

#include <stdio.h>

int global_init_var = 14;
int global_uninit_var;

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

int main()
{
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1( static_var + static_var2 + a + b );
    return 0;
}

ELF 文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,ELF 文件中还包括了其他的信息,比如符号表、调试信息、字符串等。一般 ELF 文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”(Segment)。程序源代码编译后的机器指令经常被放在代码段中(Code Section)里,代码段里常见的文件名有 “.code” 或 “.text”;全局变量和局部静态变量数据通常放在数据段(Data Section),数据段的一般名字都叫做 “.data”。
一般 C 语言编译后执行语句都编译成机器代码,保存在 .text 段;已初始化的全局变量和局部静态变量都保存在 .data 段;未初始化的全局变量和局部静态变量一般都放在一个叫 .bss 段里。我们知道未初始化的全局变量和局部静态变量默认值都为 0,本来它们也可以被放在 .data 段的,但是因为它们都是 0,所以它们在 .data 段分配空间并且存放数据 0 是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为 .bss 段。所以 .bss 段只是为未初始化的全局变量和局部静态变量预留位置而已。
总体来说,程序源代码被编译后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和 .bss 段属于程序数据。

ELF 文件结构描述

省去一些 ELF 的繁琐的结构,把最重要的结构提取出来:

ElF 文件结构
ELF Header
.text
.data
.bss
other sections ……
Section header table
String header tables Symbol Tables ……

ELF 目标文件格式的最前部是 ELF 文件头,它包含了描述整个文件的基本属性,比如 ELF 文件版本、目标机器型号、程序入口地址等。紧接着是 ELF 文件各段。其中 ELF 文件中与段有关的重要结构就是段表(Section Header Table),该表描述了 ELF 文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
除了 .text、.data、.bss 这三个最常用的段之外,ELF 文件也有可能包含其他段,用来保存与程序相关的信息。

常用段名 说明
.rodata1 Read only Data,这种段里存放的是只读数据,比如字符串常量,全局 const 变量。跟 “.rodata” 一样
.comment 存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息。比如程序的公司名,、发布的版本号等
.strtab String Table 字符串表,用于存储 ELF 文件中用到的各种字符串
.symtab Symbol Table 符号表
.plt .got 动态链接的跳转和全局入口表
.init .fini 程序初始化与总结代码段

这些段的名字都是由 “.” 作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。

ELF 文件头

ELF 文件头中定义了 ELF 魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI 版本、ELF 重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等等。
ELF 文件头及相关常数被定义在 “/usr/include/elf.h” 里。比如说 32 位版本的文件头结构 “Elf32_Ehdr” 的定义如下:

typedef struct
{
      unsigned      char e_ident[16];
      ELf32_Half      e_type;
      Elf32_Half      e_machine;
      Elf32_Word      e_version;
      Elf32_Addr      e_entry;
      Elf32_Off       e_phoff:
      Elf32_Off       e_shoff;
      Elf32_Word      e_flags;
      Elf32_Half      e_enhsize;
      Elf32_Half      e_phentsize;
      Elf32_Half      e_phnum;
      Elf32_Half      e_shenszie;
      Elf32_Half      e_shnum;
      Elf32_Half      e_shstrndx;

ELF 魔数:最开始的 4 个字节是所有 ELF 文件都必须相同的标识码,分别为 0x7F、0x45、0x4c、0x46,第一个字节对应 ASCII 里的 DEL 控制符,后面 3 个字节刚好是 ELF 这 3 个字母的 ASCII 码。这 4 个字节又被称为 ELF 文件的魔数。接下来的一个字节是用来标识 ELF 文件类的, 0x01 表示是 32 位的,0x02 表示是 64 位的。第 6 个字节是字节序,规定该 ELF 文件的字节序, 0x01 代表小端,0x02 代表大端。第 7 个字节规定 ELF 文件的主版本号。

段表

我们知道 ELF 文件中有很多各种各样的段,这个段表(Section Header Table)就是保存这些段的基本属性的结构。ELF 文件的段结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。段表的结构比较简单,它是一个以 “Elf32_shdr” 结构体为元素的数组。“Elf32_shdr” 被定义在 “/usr/include/elf.h” 中:

typedef struct
{
      Elf32_Word      sh_name;
      Elf32_Word      sh_type;
      Elf32_Word      sh_flags;
      Elf32_Addr      sh_addr;
      Elf32_Off       sh_offset;
      Elf32_Word      sh_size;
      Elf32_Word      sh_link;
      Elf32_Word      sh_info;
      Elf32_Word      sh_addralign;
      Elf32_Word      sh_entsize;
}

各个成员的含义如下:

成员 含义
sh_name 段名
sh_type 段的类型
sh_flags 段标志位
sh_addr 段虚拟地址
sh_offset 段偏移
sh_size 段的长度
sh_link 和 sh_info 段链接信息
sh_addralign 段地址对齐
sh_entsize 项的长度

重定位表

链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段中那些绝对地址的引用的位置。这些重定位信息都记录在 ELF 文件的重定位表里面,对于每个需要重定位的代码或者数据段,都会有一个相应的重定位表。比如 exampl_1.o 中的 “.rel.text” 就是对 “printf” 函数的调用;而 “.data” 段则没有绝对地址的引用,它只包含了几个常量,所以就没有针对 “.data” 段的重定位表 “rel.data”。

字符串表

ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见做法就是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。通过这种方法,在 ELF 中引用字符串只需给出一个数字下标即可,不用考虑字符串长度的问题。一般字符串表在 ELF 文件中也以段的形式保存,常见的段名为 “.strtab” 或 “.shstrtab” 。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。顾名思义,字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名。

链接的接口——符号

链接过程的本质就是要把多个不同的目标文件直接相互“粘”到一起,或者说想玩具积木一样,可以拼装形成一个整体。为了使不同目标文件之间能够互相粘合,这些目标文件之间必须有固定的规则才行,就像积木木块必须有凹凸部分才能够拼合。在链接中,目标文件之间相聚拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件 B 要用到了目标文件 A 中的函数 “foo”,那么我们就称目标文件 A 定义了函数 “foo”,称目标文件 B 引用了目标文件 A 中的函数 “foo”。这两个概念也同样适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每个目标文件都会有一个相应的符号表。这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

  1. 定义在本目标文件的全局符号,可以被其他目标文件引用。比如 example_1.o 中的 func1、main 和 global_init_var。
  2. 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号,也就是我们前面所讲的符号引用。比如 example_1.o 中的 printf。
  3. 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如 example_1.o 中的 .text 和 .data。
  4. 局部符号,这类符号只在编译单元内部可见,比如 example_1.o 里面的 static_var 和 static_var2 。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。
  5. 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。

对于我们来说,最值得关注的就是全局符号,即上面分类中的第一类和第二类。因为链接过程只关心全局符号的相互粘合,局部符号、段名、行号等都是次要的,它们对于其它目标文件来说时 “不可见的” ,在链接过程中也是无关紧要的。我们可以使用 readelf 、objdump、nm 等来查看 ELF 文件的符号表。

ELF 符号表的结构

ELF 文件中的符号表往往是文件中的一个段,段名一般叫 “.symtab”。符号表的机构很简单,它是一个 Elf32_Sym 结构的数组,每个 Elf32_Sym 结构对应一个符号。
Elf32_Sym 的结构定义如下:

typedef struct
{
      Elf32_Wrod st_name;
      Elf32_Addr st_value;
      Elf32_Word st_size;
      unsigned char st_info;
      unsigned char st_other;
      Elf32_Half st_shndx;

成员定义如下:

成员 含义
st_name 符号名。这个成员包含了该符号名在字符串表中的下标
st_value 符号相对的值。这个值跟符号有关,可能是一个绝对值,也可能是一个地址等,不同的符号,它所对应的值含义不同。
st_size 符号大小。对于包含数据的符号,这个值是该数据类型的大小。比如一个 double 型的符号它占用 8 个字节。
st_info 符号类型和绑定信息
st_other 该成员目前为 0,无用
st_shndx 符号所在的段

内容来源

《程序员的自我修养》

posted @ 2021-01-27 17:45  PwnKi  阅读(473)  评论(0编辑  收藏  举报