天天看点

技术文章:Linux系统的可执行文件,ELF格式

作者:底层技术栈

ELF文件,是Linux系统上的可执行文件(类似windows上的exe文件)。

ELF的全称是可执行与可重定位文件。

在Linux上,不管是编译完的目标文件(.o文件),还是连接后的可执行文件,都是elf格式。

ELF在设计时同时兼容了目标文件、可执行文件、动态库三种场景。

ELF文件的构成,分为如下几个部分:

1,ELF文件头,

它是ELF文件的“总目录”,管理着整个文件的数据该怎么解释。

ELF文件头的数据结构,如下图:

技术文章:Linux系统的可执行文件,ELF格式

ELF文件头

几个重要的项目已经加了注释。

它的前16个字节是ELF文件的“魔数”:0x7f,‘E’,'L','F'。

它是ELF文件的识别码,Linux系统就是靠这几个字符来判断是不是ELF文件的。

e_type:

指的是ELF文件的类型,包括目标文件ET_REL、可执行文件ET_EXE、动态库ET_DYN三种。

e_machine:

指的是CPU类型,常用的也只有3种:EM_386、EM_ARM、EM_X86_64,分别是32位的x86、ARM、和64位的x86_64。

当然也支持一些其他型号的CPU,但并不常见。

e_entry:

指的是程序的入口地址,Linux系统把程序加载到内存之后,就跳转到这个地址运行,然后才会调用main()函数。

如果有电脑病毒修改了e_entry,就可以让linux系统先运行病毒代码了。

e_phoff:

程序头表的文件偏移量,即离文件的第1个字节有多少字节。

按照C语言的惯例,文件第1个字节的文件偏移量是0。

程序头,是Linux系统加载可执行文件和动态库时使用的,在目标文件里没有程序头。

e_shoff:

“节头表”的文件偏移量,也是离文件第1个字节的字节数。

“节头”,是程序具体数据位置的索引。

例如数据段.data在哪里,代码段.text在哪里,都要查看对应的“节头”。

e_phnum和e_shnum:

分别是程序头和“节头”的个数。

e_shstrndx:

节名字符串的索引,它表示节名字符串是(所有节里的)哪个节,一般情况下是最后一个节。

这一项也是很重要的,通过它才可以知道每个节的名字是什么,才可以知道每个节是做什么的。

2,程序头,

在可执行文件和动态库加载时,操作系统根据程序头的信息把代码和数据放在合适的内存位置,然后程序才可以正常运行。

例如有个全局变量a,代码里有一行a = 10,要给a赋值必须知道它准确的内存位置。

这个内存位置是在连接时确定的,在加载时也必须保持同样的内存镜像才可以,否则这个10就不知道赋值到哪里去了。

程序头的作用,就是告诉Linux系统把数据段加载到哪里,把代码段加载到哪里,让代码可以 正确地读写数据。

技术文章:Linux系统的可执行文件,ELF格式

程序头

3,“节头”,

“节头”,顾名思义,是每个节的头[呲牙]也就是每个节的数据的索引信息。

每个“节”的内容都是程序的代码、数据、符号表、或调试信息,它们在ELF文件里的存放位置就是通过节头管理的。

节,英文是section。

在ELF的文档里一般用sh表示节头,section header的缩写。

技术文章:Linux系统的可执行文件,ELF格式

节头

sh_name:

表示“节”的名字,但它只是给出在(节名)字符串表里的偏移量,字符串是以0结尾的,这样才可以找到真正的名字。

之所以不直接给出节名,是为了把字符串集中存放在一个连续的区域,以节省空间。

sh_type:

表示“节”的类型,例如重定位节、符号表、字符串表、调试信息,等等。

代码段、数据段也是“节”的一种,它们都需要加载到内存,区别只是读、写、执行的权限不同。

代码段是只读的,数据段不可执行。

当程序动态连接时需要的PLT表和GOT表,也是一个节。它们也需要加载到内存,并且Linux的加载器会修改GOT表,让它指向动态库里的函数地址。

动态连接还是比较复杂的,哪天单独再说吧。

sh_entsize:

当节的数据内容是一个数组时,sh_entsize表示每项的大小。

例如,符号表就是一个数组,每一项就是一个符号(函数或全局变量),每项的大小都是固定的,它们的“节头”里就有这一项sh_entsize。

如果用不到这一项,则填为0。

sh_offset和sh_addr:

每个节的数据实际上有2个地址:一个是数据在文件里的地址(sh_offset),一个是数据要加载到内存里的地址(sh_addr)。

这两个地址不是一样的,而且内存里的地址一般还有对齐要求,但文件里不需要对齐。

sh_link和sh_info:

这两项的具体解释需要根据“节”的类型来。

例如,重定位节重定位的是函数或全局变量的地址,而函数或全局变量的信息又在符号表里,所以重定位节要关联到符号表:

这样就可以知道它重定位的符号表是哪一个“节”,这个节的索引号就写在这两项里。

4,数据区,

数据区,是程序真正的代码、数据、符号表、重定位信息、调试信息等有效数据。

刚编译完的文件并不能直接运行,因为里面的函数、全局变量的地址都不是真实地址,而只是给连接器使用的重定位信息。

这些重定位信息,必须被连接器计算出真实地址来进行填充之后,才可以运行。

符号表,是一个记录函数和全局变量的名称及地址的表格。

连接器会读取符号表,我们平时查看动态库包含哪些函数时也会读取它。

编译器在生成机器码之后,需要按照ELF格式把二进制代码和数据写入文件里,然后用连接器把函数和全局变量处理成真实地址,就可以被Linux系统加载运行了。

scf编译器框架的ELF文件格式、连接器代码、gdb调试信息的实现,都在scf/elf目录。

继续阅读