目标文件和可执行文件(二)

目录

ELF文件结构描述

1. 文件头

<1> 魔数

<2> 文件类型

 <3> 机器类型

 2. 段表

                            <1> 段的类型

 <2> 段的标志

 <3> 段的链接信息

3.  重定位表

4. 字符串表


ELF文件结构描述

        上一节我们已经介绍了ELF文件,并且对ELF文件的内部构造有了一定的了解,接下来我们继续查看ELF文件的结构格式,下图是提取重要结构的ELF框架

         ELF(Executable and Linkable Format)目标文件格式是一种用于表示可执行程序、共享库、目标代码和核心转储文件的标准文件格式。它是现代Unix和类Unix系统中广泛使用的二进制文件格式之一。(1)ELF文件头(ELF Header):位于文件的最前部,包含描述整个文件的基本属性。包括ELF文件的版本、目标机器型号、程序入口地址(装载时会讲到)等信息。它为解释器提供了创建和加载可执行文件的必要信息。(2)段表(Section Header Table):紧接着ELF文件头之后,描述了ELF文件包含的所有段(Sections)的信息。每个段对应于特定类型的数据,如代码段、数据段等。段表包含了每个段的段名、段的长度、在文件中的偏移、读写权限和其他属性。接下来我们会详细展开。我们有SimpleSection.c这个c文件。

int printf(const char * format,...);
int global_init_var=84;
int global_uninit_var;

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

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

1. 文件头

该命令查看目标文件的结构和内容

objdump -h SimpleSection.o

        我们可以用readelf命令查看ELF文件头

readelf -h SimpleSection.o

         从上而输出的结果可以看到,ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。

        ELF文件头结构及相关常数被定义在“usr/include/elf.h”里,因为ELF文件在各种平台下都通用,ELF文件有32位版本和64位版本。它的文件头结构也有这两种版本,分别叫做“EIf32Ehdr”和“EIf64Ehdr”。32位版本与64位版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不~样。为了对每个成员的大小做出明确的规定以便于在不同的编译环境下都拥有相同的字段长度,“elf.h”使用typedef定义了一套自己的变量体系。

         让我们拿ELF文件头结构跟前面readelf输出的ELF文件头信息相比照,可以看到输出的信息与ELF文件头中的结构很多都一一对应。有点例外的是“Elf32Ehdr”中的e_ident这个成员对应了readelf输出结果中的“Class”、“Data”、“Version”、“OS/ABI”和“ABI Version”这5个参数。剩下的参数与“Elf32Ehdr”中的成员都一 一对应。

    <1> 魔数

         ELF魔数我们可以从前面readelf的输出看到,最前面的“Magic”的16个字节刚好对应“Elf32Ehdr”的e_ident这个成员。这l6个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个LF字长(32位/64位)、字节序、ELF文件版本。

  1. 前四个字节:ELF标识码,固定为0x7F 0x45 0x4C 0x46,用来确认该文件是ELF文件。

  2. 第五个字节:ELF文件的类别,0x01表示32位文件,0x02表示64位文件。

  3. 第六个字节:字节序,用于标识ELF文件是大端(Big-Endian)还是小端(Little-Endian)存储方式。

  4. 第七个字节:ELF文件的版本号,一般为0x01,表示第一版ELF标准。

  5. 后面九个字节:这些字节在ELF标准中没有定义,一般情况下填充为零,但有些平台可能会用这些字节作为扩展标志。

        ELF文件的魔数对于确认文件类型和加载可执行文件至关重要,操作系统会检查文件的魔数来确定其是否为有效的ELF文件,从而决定是否加载和执行。而接下来的字段则包含了更多关于ELF文件的信息,例如文件类型、字长、程序入口地址、段表位置等等。

<2> 文件类型

        文件类型 e_type战员表示ELF文件类型,即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以“ET”开头。

 <3> 机器类型

        ELF文件格式的设计确实是为了在多个平台下使用,但并不意味着同一个ELF文件可以在不同的平台上直接运行(类似于Java的字节码文件)。相反,不同平台下的ELF文件都需要遵循同一套ELF标准,以便操作系统能够正确解析和加载这些文件。在ELF文件头的e_machine成员中,确实表示了该ELF文件的平台属性,即所支持的目标机器类型。该字段的值对应于一组预定义的常量,这些常量以"EM"开头,并用于标识不同的目标机器类型。下面是一些常见的e_machine常量及其对应的目标机器类型:

 2. 段表

        ELF文件中的段表(Section Header Table)是保存各个段的基本属性的重要结构。它描述了ELF文件中的各个段,包括代码段、数据段、只读数据段、BSS段等的信息,以及其他辅助性的段如符号表、字符串表、重定位表等。段表在ELF文件中的位置由ELF文件头的"e_shoff"成员指定,可以通过这个偏移值找到段表的起始位置。ELF文件的段结构就是由段表决定的,编译器、链接器、装载器都是依靠段表来定位和访问各个段的属性。

   objdump -h命令通常只显示关键的段,如代码段、数据段等,而会省略一些辅助性的段,如符号表、字符串表、重定位表等。这是为了简化输出,因为这些辅助性的段在一些场景下可能不是那么重要,而且它们的数量可能会很大。要查看完整的段表结构以及所有段的详细信息,可以使用readelf工具。readelf是一个更为详细和全面的ELF文件解析工具,它能够显示ELF文件的所有结构,包括段表、符号表、字符串表等。使用readelf -S命令可以显示ELF文件的段表及其详细信息:

readelf -S SimpleSection.o

         readelf输出的结果就是ELF文件段表的内容,那么就让我们对照这个输出来看看段表的结构。段表的结构比较简单,它是一个以“Ef32Shdr”结构体为元素的数组。数组元素的个数等丁段的个数,每个“Elf32_Shdr”结构体对应.一个段。“Elf32Shdr”又被称为段描述符(Section Descriptor)。对于SimpleSection.o来说,段表就是有l3个元素的数组。ELF段表的这个数组的第一个元素是无效的段描述符,它的类型为“NULL”,除此之外每个段描述符都对应一个段。也就是SimpleSection.o共有l2个有效的段。

        Elf32_Shdr被定义在“/usr/include/elf.h” ,让我们对照Elf32Shd_r和“readelf -S”的输出结果,可以很明显看到,结构体的每一个成员对应于输出结果中从第二列“Name”开始的每一列。于是SimpleSection的段表的位置。

 <1> 段的类型

        段的名字(section name)在链接和编译过程中确实有其重要作用,但它并不能直接表示段的类型。实际上,段的类型由段头表(Section Header Table)中的sh_type字段决定,而段的名字只是为了方便人类理解和标识段的用途而存在。主要决定段的属性的是段的类型(sh_type)和段的标志位(sh_flags)

 <2> 段的标志

        段的标志位(sh_flag)段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。相关常量以SHF_开头。

 <3> 段的链接信息

     sh_linksh_info是段表(Section Header Table)中的两个重要成员,它们与段的链接(linking)相关,特别是对于一些与链接相关的段类型,如重定位表、符号表等(很重要)。

3.  重定位表

        在ELF文件中,每个需要重定位的代码段或数据段都有一个相应的重定位表。例如,SimpleSection.o文件中的“.rel.text”是针对“.text”段的重定位表,因为“.text”段中至少有一个对绝对地址的引用,如对“printf”函数的调用。而“.data”段则没有对绝对地址的引用,所以SimpleSection.o中没有针对“.data”段的重定位表“rel.data”。重定位表是ELF的一个段,其类型(sh_type)为“SHT_REL”。这个段的“sh_link”字段表示符号表的下标,而“sh_info”字段表示它作用于哪个段。例如,“rel.text”作用于“.text”段,而“.text”段的下标为“1”,那么“rel.text”的“sh_info”为“1”。之后我们会在静态链接详细介绍。

4. 字符串表

        在ELF文件中,使用了很多字符串,例如段名、变量名、符号名等。由于字符串的长度通常是不定的,将它们直接存储在各个地方会导致文件结构变得复杂且难以管理。为了解决这个问题,ELF文件采用了一种常见的做法,即将所有字符串集中起来存放到一个表中,然后使用字符串在表中的偏移来引用它们。如表所示

偏移     字节内容
0        空字符串
1        'h'
2        'e'
3        'l'
4        'l'
5        'o'
6        'w'
7        'o'
8        'r'
9        'l'
10       'd'
11       空字符 ('\0')
12       'M'
13       'y'
14       'v'
15       'a'
16       'r'
17       'i'
18       'a'
19       'b'
20       空字符 ('\0')
偏移    字符串
0       空字符串
1       "helloworld"
6       "world"
12      "Myvariable"

        通过将字符串集中存放到字符串表中,可以大大简化引用字符串的方式。字符串表允许以常量时间复杂度(O(1))的方式根据给定的偏移获取字符串,因为字符串表中的字符串都是按照顺序排列的,而且每个字符串以null终止,因此可以轻松地根据偏移找到字符串的起始位置。

        在ELF文件中,常见的字符串表有两种:

  1. 字符串表(String Table):它包含普通的字符串,例如符号的名字、节的名字等。这些字符串被其他结构引用,比如符号表中的符号名、节表中的节名等。这使得在ELF文件中引用这些字符串时只需给出一个数字下标,而无需考虑字符串的长度问题。

  2. 段表字符串表(Section Header String Table):它用于保存段表中使用到的字符串,最常见的就是段的名字(sh_name)。每个段表项中都有一个字段(sh_name),它表示该段的名字在段表字符串表中的偏移。通过在段表字符串表中查找偏移对应的字符串,可以得到每个段的名字,这样可以让解析者更容易理解和使用ELF文件的结构。

        在ELF文件头中的"e_shstrndx"字段表示段表字符串表在段表中的索引(下标)。通过这个字段,我们可以快速找到段表字符串表在段表中的位置。

        在解析ELF文件时,首先读取ELF文件头,其中包含了重要的信息,如文件类型、机器类型、入口点地址、段表偏移和段表中段的数量等。其中,"e_shoff"字段表示段表的偏移,即段表在ELF文件中的位置。"e_shentsize"字段表示每个段表项的大小,即每个段在段表中占用的字节数。"e_shnum"字段表示段表中段的数量,即段表中有多少个段。最后,"e_shstrndx"字段表示段表字符串表在段表中的下标,即在段表中的位置。

        通过这些信息,我们可以根据"e_shoff"字段找到段表的起始位置,并根据"e_shentsize"字段和"e_shnum"字段遍历所有段表项,找到对应的段表字符串表。然后,通过"e_shstrndx"字段的值,我们可以找到段表字符串表在段表中的位置。最终,我们就可以得到ELF文件中的所有段的信息,包括段名、段偏移、段大小等。

参考《程序员的自我修养》俞甲子

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值