Linux ELF文件格式介绍

文章目录

  • 一、引言
  • 二、介绍
  • 三、ELF目标文件格式
    • 3.1 常见段及对应用途
    • 3.2 目标文件内容解析
      • 3.2.1 代码段.text
      • 3.2.2 只读数据段.rodata
      • 3.2.3 数据段.data
      • 3.2.4 .bss段
      • 3.2.5 重定位表(Reloacation Table)相关段.rela.xxx
      • 3.2.6 字符串表.strtab和.shstrtab
      • 3.2.7 符号表.symtab
    • 3.3 可执行文件
  • 四、ELF文件-逆向工具
  • 五、参考资料

一、引言

在讲解elf文件格式之前,我们来回顾一下,一个用C语言编写的高级语言程序是从编写到打包、再到编译执行的基本过程,我们知道在CPU上执行的是低级别的机器语言,从高级语言到低级别的机器语言肯定是要经过翻译过程,这个过程大体的过程如下图所示:

在这里插入图片描述
在Unix系统中,从源文件到可执行目标文件是由编译驱动程序完成的,如大名鼎鼎的gcc,翻译过程包括图中的四个阶段;

Ø 预处理阶段

预处理器(cpp)根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。主要是进行文本替换、宏展开、删除注释这类简单工作。

对应的命令:linux> gcc -E hello.c hello.i

Ø 编译阶段

编译器将文本文件hello.i翻译成hello.s,包含相应的汇编语言程序

对应的命令:linux> gcc -S hello.c hello.s

Ø 汇编阶段

将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。

把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

对应的命令:linux> gcc -c hello.c hello.o

Ø 链接阶段

此时hello程序调用了printf函数。 printf函数存在于一个名为printf.o的单独的预编译目标文件中。 链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。最后可执行文件加载到储存器后由系统负责执行, 函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为.a。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,gcc在编译时默认使用动态库。

二、介绍

可执行链接格式(Executable and Linking Format)最初是由 UNIX 系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。工具接口标准(Tool Interface Standards,TIS)委员会将还在发展的 ELF 标准选作为一种可移植的目标文件格式。

Linux下面一共有四类目标文件是按照ELF的格式来保存的,如下:
Linux ELF文件格式介绍_第1张图片
由此我们可知由汇编器生成的就是可重定位目标文件,经过链接器作用后才生成可执行目标文件,链接器的作用就是以一组可重定位目标文件作为输入,生成可加载和运行的可执行目标文件,具体需要完成以下两个工作:

  • 符号解析:符号解析的目的是将目标文件中每个符号(静态变量、函数、全局变量)和其定义进行关联
  • 重定位:将每个符号的定义与具体在虚拟内存中的位置进行关联

最终生成可执行目标文件

注意:.a的静态链接库可以理解为.o的打包,因此本质上也属于ELF类型。

说到这里好像还是没有说清楚这两种目标文件有什么区别,我们还是先把这个问题放一下,相信你看完下一节,应该会有答案,下面我们开始引入目标文件ELF文件。

三、ELF目标文件格式

目标文件再不同的系统或平台上具有不同的命名格式,在Unix和X86-64 Linux上称为ELF(Executable and Linkable Format, ELF)。

ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合
Linux ELF文件格式介绍_第2张图片
左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。

我们在汇编程序中用.section声明的Section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。Segment是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,比如有两个Section都要求加载到内存后可读可写,就属于同一个Segment。有些Section只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内存,那么就不属于任何Segment。

目标文件需要链接器做进一步处理,所以一定有Section Header Table可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。

3.1 常见段及对应用途

这里贴一下《程序员的自我修养》一书中的一个ELF文件常见段和对应用途总结表:
Linux ELF文件格式介绍_第3张图片

3.2 目标文件内容解析

调试环境:在这里插入图片描述

下图用一个简单的示例直观的表示了程序编译后的ELF目标文件格式(省略了一些段):

下面以一个修改过的《程序员的自我修养》一书中的例子来实际看看目标文件的各个段的情况

文件:test.c

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

int global_init_var = 1;	//data段
int global_uninit_var;		//bss段

void func1(int i){
  
	printf("%d\n", i);		//%d\n 在rodata段
}
int main(void){
  
	static int static_init_var = 2;	//data
	static int static_uninit_var;	//bss
    static const int static_const_init_var = 3;	//rodata
	static const int static_const_uninit_var;	
	const int const_init_var = 4;
	int init_var = 5;
	int uninit_var;
	func1(static_init_var + static_uninit_var + static_const_uninit_var + static_const_uninit_var + init_var + uninit_var);
	return init_var;
}

gcc -c test.c编译得到text.o

下面用readelf工具读出目标文件test.o的ELF Header和Section Header Table,然后我们逐段分析。

首先会是一个文件头,它描述了这个ELF文件的属性,包括是否是可执行文件、大端还是小端、文件适应的目标硬件架构等。文件头内容如下:readelf -h test.o
Linux ELF文件格式介绍_第4张图片
文件头中还有一个段表(Section Header Table),包含了各个段的信息,段表如下:readelf -S test.o
Linux ELF文件格式介绍_第5张图片
可以看到一共有13各个段,而后面要用到的ojbdump -h命令则会省略一些不关键的辅助性质的段。

根据以上信息可以描绘出整个目标文件的布局。参考:ELF文件格式

改用objdump命令查看段情况如下:objdump -h test.o
Linux ELF文件格式介绍_第6张图片
可以看到有id从0-5的6个段(省略了非关键的段),可以使用objdump -s -d test.o命令可以看到各段存放的内容按16进制展示如下:
Linux ELF文件格式介绍_第7张图片
而3(.bss)因为没有实际内容所以不包含在里面,其中4、5、6是辅助功能用到的段,这里先不讨论,下面我们看下0 1 2 3三个段,也就是代码段.text,只读数据段.rodata,数据段.data,以及.bss段。

3.2.1 代码段.text

代码段里保存的都是机器码,用objdump -s -d test.o命令可以得到反汇编之后的汇编代码,内容如下:
Linux ELF文件格式介绍_第8张图片
这里不过多解释汇编语句,可以看到内容对应我们写的两个函数。

3.2.2 只读数据段.rodata

.rodata,根据字面意思就很好理解,read only data,和.data段类似,但是是保存只读的静态常量,

在这里插入图片描述
可以看到有两个只读数据,因为字节序(大端小端)的关系字节的顺序和我们的习惯顺序是反着的,0x25640a00是"%d\n"对应的ascii码加上一个结束的\0,而0x03000000则是对应的static const int static_const_init_var = 3;

只有静态变量或者常量才有必要提前定义在数据段里,所以可以看到const int const_init_var这种并不会保存在数据段里,而是直接在指令里写死临时分配在栈上,可以参考text段的汇编代码。

3.2.3 数据段.data

数据段保存已经初始化的全局静态变量和局部静态变量,0x01000000和0x02000000分别对应int global_init_var = 1;static int static_init_var = 2;
在这里插入图片描述

3.2.4 .bss段

.bss段(Block Started by Symbol)则保存未初始化的全局变量和局部静态变量,实际上只是place holder,不会保存实际内容,可以说是通过.bss段给变量预留空间,不需要占用ELF文件的空间,加载到内存里才会实际占用空间。上面的例子里我们也能看.bss段在列表里,但并没有.bss段的内容。需要注意的一种特殊情况是初始化为0也有可能被编译器当成未初始化放在.bss段里以节省空间。

可以总结为:

  • Uninitialized global/static data
  • “Block Started by Symbol”
  • Better Save Space”
  • Has section header but occupies no space

这个名字不像其他段那么直观,有兴趣进一步深入了解的可以参照【参考4】和【参考5】。

3.2.5 重定位表(Reloacation Table)相关段.rela.xxx

重定位表是用于链接阶段的重定位的,在独立地生成每个编译单元的时候很多变量和函数的地址是没法确定的,需要在链接阶段进行修正,后续静态链接会详细说明这个过程,这里先看下重定位表的结构,每一个需要重定位操作的段都会对应一个重定位表段,比如.text对应了一个.rela.text,可以用objdump -r命令查看,可以看到上面的示例程序有两个重定位表段,分别是.text的和.eh_frame的。
Linux ELF文件格式介绍_第9张图片
以printf的调用,也就是.text重定位表中的第2行为例,这行是说OFFSET为1b的地方需要后续链接阶段重定位,.text段的1b位置正是对printf的call指令的寻址部分,也就是printf的地址需要重定位,重定位类型为R_X86_64_PC32,这是一种相对寻址的重定位类型,后续聊静态链接的时候再展开讲。

3.2.6 字符串表.strtab和.shstrtab

ELF文件中用到了很多字符串,比如段名、变量名等,通常由.strtab.shstrtab两个段保存,分别为字符串表(string table)和段表字符串表(section header string table),前者用来保存普通的字符串,比如符号的名字,后者用来保存段表中用到的字符串,比如段名。因为字符串是变长的,这里采取的是连续保存并用\0分割,通过offset来获取。

我们可以用readelf命令打出来:readelf -x .strtab test.o
Linux ELF文件格式介绍_第10张图片
readelf -x .shstrtab test.o
Linux ELF文件格式介绍_第11张图片
这里贴个ascii码表方便对照着看:
Linux ELF文件格式介绍_第12张图片
比如.strtab这个字符串,2e7374 72746162就是对应内容,前后各有一个\0,因此有一个为9的offset就可以获取到。

3.2.7 符号表.symtab

要将多个目标文件链接在一起,本质上就是将各个目标文件的内容合并后并且能保证运行时互相的变量访问和函数调用正常,也就是对内外部函数和变量的访问都能找到正确的地址,在链接中将函数和变量统称为符号,函数名或变量名就是符号名,整个链接过程的核心就是根据符号来确定正确的地址。每一个目标文件都会有一个相应的符号表(Symbol Table,.symtab段),记录了目标文件所用到的所有符号,注意是所用到的所有,不管包含在内部的符号还是外部符号。每个定义的符号有一个对应的值,叫符号值,对于变量和函数来说,就是他们的地址。

命令:nm test.o
Linux ELF文件格式介绍_第13张图片

符号有不同的类型,上图说明了:
(1)func1和main的类型是T,说明是在.text段,且全局可见。
(2)global_init_var的类型是D,表明是全局可见且在.data段的。
(3)global_uninit_var的类型是C,表明是全局可见的在common块的。
(4)printf的类型是U,说明是未定义的,该符号在编译单元外部。
(5)static_const_init_var的类型是r,说明在.rodata段。
(6)static_const_uninit_var和static_uninit_var的类型是b,说明在.bss段。
(7)static_init_var的类型是d,表明是局部可见且在.data段,大小写表明了可见性。

3.3 可执行文件

先看可执行文件header的变化 readelf -a test
Linux ELF文件格式介绍_第14张图片

  • Type:类型由目标文件类型变成可执行文件类型
  • Number of program headers: 9 (test.0 是 0)
  • Number of section headers: 31

在看section header的变化 readelf -S test
Linux ELF文件格式介绍_第15张图片
.text.data的加载地址分别改成了0x00400430和0x00601028。.rel.text段就是用于链接过程的,链接完了就没用了,所以也删掉了。

在看多出来的program header
Linux ELF文件格式介绍_第16张图片
这部分要再根据:ELF文件格式 消化一下

四、ELF文件-逆向工具

  • ELF文件-逆向工具
  • GNU Binutils 介绍

本章节用到的工具命令

#查看test的ELF Header头信息
readelf -h test 

#查看Section Header Table中的每个Section Header Entry信息
readelf -S test 
objdump -h test

#查看各段存放的内容按16进制展示如下
objdump -s -d test

Linux 下查看可执行文件的工具(这里列出一部分)

  1. file 可执行文件:可查看可执行文件是ARM架构还是X86架构
  2. nm 可执行文件:可查看文件中的符号,包括全局变量,全局函数等
  3. ldd 可执行文件:可查看文件执行所需要的动态库
  4. strings 可执行文件:可查看文件中所有的符号,包括编译器版本信息
  5. readelf 可执行文件:可查看文件的所有详细信息,包括文件的头信息,动态库信息,段信息等
  6. objdump -S 可执行文件:显示可执行文件、目标文件、静态库、共享库反汇编信息
  7. strip 可执行文件:减肥,删除可执行文件、目标文件、静态库、共享库的符号

五、参考资料

  1. ELF文件格式
  2. elf文件格式总结
  3. C++编译知识笔记(一)——基本知识
  4. C++编译知识笔记(二)——Linux ELF文件解析
  5. C++编译知识笔记(三)——静态链接

其他参考资料:

  1. Linux elf文件分析
  2. ELF文件解析(一):Segment和Section
  3. ELF文件解析(二):ELF header详解
  4. LinuxELF文件格式详解–Linux进程的管理与调度(十二)

你可能感兴趣的:(linux编程,linux,elf)