程序员编写的程序需要翻译成可执行文件,然后才能被操作系统执行。对于用C语言编写的程序,这个翻译过程包括预编译、编译、汇编和链接。
在PC平台上,不同的操作系统可能有不同的可执行文件格式。
对于Windows操作系统:常见的可执行文件格式是PE格式,例如exe文件就是PE格式。
对于Linux和Unix操作系统:可执行文件格式是ELF格式,比如常见的/bin/bash就是ELF格式。
PE格式和ELF格式很相似,因为它们都是COFF文件格式的变体,其他很多可执行文件格式都是COFF文件格式的变体。
本文主要阐述ELF文件的结构,其他可执行文件格式的概念都差不多,只是细节有些不同。
下面是C语言的代码,文件名是SimpleSection。c .本文围绕这个代码片段阐述ELF文件的结构。
intprintf(const char *格式,);intglobal _ init _ var=84//全局变量intglobal _ uninit _ var//全局未初始化变量Voidfunc1 (inti) {printf ('%d \ n 'I);} int main(void){ static intstatic _ var=85;//静态变量Static _ var 2;//静态inta=1;intbfunc 1(static _ var static _ var 2 a b);return1}
可重定位目标文件:
上面提到的SimpleSection.c文件经过编译汇编生成SimpleSection.o文件。这个文件是一个可重定位的目标文件,它包含代码和数据。这个目标文件没有被链接,所以它可以与其他目标文件或共享库链接成一个可执行文件,链接过程将被重新定位。
您可以执行文件简化。o命令来验证这个文件的类型。命令结果如下所示:
SimpleSection.o:ELF64位LSBrelocatable,x86-64,版本1(SYSV),notstripped
可执行文件:
这样的文件可以直接被操作系统执行,比如/bin/bash。
您可以执行[file/bin/bash]命令来验证这个文件的类型。命令结果如下所示:
/bin/bash : elf 64-bit LSBexecutable,x86-64,版本1(SYSV),动态链接(usessharedlibs),forGNU/Linux2.6.32,BuildID[sha1]=8 BD 6b 05295658d 71 a9 ff 4 eed 7 ca e55609 a 703623,已剥离.
共享目标文件:
包含代码和数据,有两种使用场景。
1.链接器将这类文件与其他可重定位的目标文件和共享目标文件(即共享库)链接起来,生成新的目标文件。
2.动态链接器将这类文件与可执行文件结合起来,实现运行时链接。
执行命令[file/usr/lib64/ld-2.17.so]来验证该文件的类型。命令结果如下所示:
/usr/lib 64/LD-2.17 . so : elf 64-bit LSBsharedobject,x86-64,版本1(SYSV),动态链接,BuildID[sha1]=a 9980 cf 253 c 79740 e 69 f 70 dcb 8 FEA 7 b 8 C2 f 641 b 5,notstripped
ELF文件结构
从上图可以看出,一个ELF文件由一个ELFHeader、一个Sectionheadertable和多个sections组成。
ELF文件头(ELFHeader):包含了整个文件的基本属性,比如段表的起始位置,段表的大小,ELF文件的版本,目标机器型号,程序入口地址等等。
节表(Sectionheadertable):包括所有段的属性信息,描述每个段的名称、段的长度、段在文件中的起始位置以及读写权限。
节(Section):ELF文件的核心内容,一个ELF文件包含许多节,不同类型的节包含不同的内容。例如,文本表示包含代码的代码段。
不同的ELF文件类型(可重定位目标文件、共享库目标文件、可执行文件)包含不同的部分,但至少包括代码和数据部分。
在编译时,可以增加编译选项来控制生成或者去掉某些节,通常这些节都是辅助作用,可有可无。ELF文件头(ELFHeader):
可以通过readelf-h来查看文件头信息,如下图所示
ELF文件头信息
如上图所示,文件头包括了很多属性,下面逐个阐述各个属性。
Magic:ELF文件的魔幻数。
通过Magic不光可以指示这个文件是不是ELF文件,还能确定其它的信息。
Magic大小为16个字节,前7个字节有意义,后面的9个字节都是0,从前7个字节中可以解析出Class,Data,Version,OS/ABI,ABIVersion这个5个属性,其中前4个字节:7f454c46是固定的,所有的ELF文件类型都一样,第5个字节表示32位还是64位,01表示32位,02表示64位,第6个字节表示内存存储方式是大端还是小端的,第7个字节表示ELF的主版本号,一般是1。
Type:ELF文件类型
包括以下几种文件类型:
REL:可重定位目标文件。
EXEC:可执行文件。
DYN:共享目标文件即共享库。
Machine:机器类型
包括ELF文件适用的CPU类型,有以下几种:
M32(AT&TWE32100)
SPARC
Interx86
Motorola68000
Motorola88000
Inter80860
EntryPointaddress:程序的入口虚拟地址
操作系统加载可执行文件后,从这个地址开始执行指令,可重定位目标文件一般没有入口地址,因此为0。
StartOfprogramheaders:程序头表在文件中的偏移量即开始位置。
StartOfSectionheaders:节表在文件中的偏移量即开始位置。
Flags:用来标识与平台相关的属性。
SizeOfthisheader:ELF文件头(ELFHeader)的大小。
SizeOfprogramheaders:程序头表的大小。
NumberOfprogramheaders:程序头表中程序头的个数。
SizeOfSectionHeaders:节表的大小。
NumberOfSectionHeaders:节表中节的个数
Sectionheaderstringtableindex:节名字符串表(shstrtab)在节表中的索引。
总体来说,通过ELF文件头(ELFHEADER)可以确定ELF文件类型,适用的CPU类型,文件版本,程序头表的起始位置,程序头表中程序头的个数,程序头表的大小,节表的起始位置,节表中节的个数,节表的大小,第一条执行的指令的起始位置等。
节表(Sectionheadertable):
通过文件头(ELFHeader)可以确定节表的开始位置和大小,节表由多个固定大小的节表项组成,每个节表项包括了节的属性信息,可以通过readelf-S查看ELF文件的节表,如下图所示:
节表
如上图所示,节表里总共有13个选项,第0项是空的,因此节表中总共12个有效的节表项,每一项长度都是固定的,包括了10个属性:Name,Type,Address,Offset,Size,EntSize,Flags,Link,Info,Align,下面来逐个阐述节的各个属性。
Name:节名
节名存储在【节名字符串表】(.shstrtab表)中,这里显示的名称就来自于.shstrtab表。
Type:节的类型
1.NULL表示无效节。
2.PROGBITS表示该节为程序节,例如代码节,数据节都是这种类型。
3.SYMTAB表示该节为符号表,程序中的变量和函数就属于符号,存储在符号表。
4.STRTAB表示该节为字符串表,用于存储除了节名以外的各类字符串如变量名,函数名。
5.RELA重定位表,表示该节包含重定位信息。
7.HASH表示该节为符号表的哈希表,主要用来用于加快符号的查找速度。
8.DYNAMIC动态链接信息节。
9.NOTE提示性信息节。
10.NOBITS表示该节在文件中的没有内容,不占用存储空间,比如.bss段。
11.REL该节包含了重定位信息。
12.SHLIB保留。
13.DNYSYM动态链接的符号表。
Address:节的虚拟地址。
如果该节可以被加载到内存,该地址就是节被加载到进程虚拟地址空间的虚拟地址,否则地址为0。
Offset:节在文件中的开始位置。
如果该节在文件中存储,这个值表示该节在文件中的开始位置,如果该节不在文件中存储例如.bss节,这个值就没有任何意义。
Size:节的大小,即使节不在文件中存储也可以有大小,例如.bss节不在文件中存储,但是有大小,这个大小主要是指明加载到内存时,分配的内存大小。
EntSize:节的每一项的大小。
有些节由固定大小的项构成,对于这些节来说,EntSize表示项的大小,如果为0表示该节包含的项的大小不固定。
Flags:节在进程虚拟地址空间的属性。
比如该节是否可写,是否可执行等,当然只有节能够被加载到内存时,这个Flags才有效,例如.text,.data,有以下几个值:
1.WRITE:该节在虚拟地址空间可写,一般指数据节。
2.ALLOC:该节在虚拟地址空间需要分配空间,只有节能够被加载到内存时,才有这个属性。
3.EXECINSTR:该节在虚拟地址空间可以被执行,一般指代码节。
Link:不同类型的节含义不同。
当节类型(Type)为DYNAMIC时:用于表示该节使用的字符串表在节表中的索引。
当节类型(Type)为HASH时:用于表示该节使用的符号表在节表中的索引。
当节类型为REL,RELA时:用于表示该节使用符号表在节表中的索引。
当节类型为SYMTAB,DYNSYM时:操作系统相关的。
对于其它节,值为UNDEF。
Info:不同类型的节含义不同。
当节类型为DYNAMIC时,值为0。
当节类型为HASH时,值为0
当节类型为REL,RELA时,该重定位表所作用的节在节表中的下标。
当节类型为SYMTAB,DYNSYM时,操作系统相关的。
对于其它节,值为0。
Align:每个节的对齐大小。
有些节要求节的起始位置,必须是Align的整数倍,例如Align=8表示节的起始位置必须是8个整数倍,通常Align是2的整数倍,如果Align为0或者1表示节没有对齐要求。
总体来说,通过节表可以知道每个节在文件中的起始位置,节的大小,节是不是可以被加载到内存,节加载到内存后的权限,节加载到内存后的虚拟地址,节如果用到其它节,则可以知道其它节在节表中的索引,这样就可以在节表中通过索引找到其它节。
代码节,数据节(.text,.data,.bss):
代码节通常的名字为.code或者.text,数据节分为.data和.bss两部分,如下图
程序对应的节
.text节用于存储机器指令,如上图黄色字体部分,包括函数等。
.data节用于存储已经初始化的全局变量和静态变量,如上图绿色字体部分,函数的局部变量存储在栈中,不在.data中存储。
.bss节用于存储未初始化的全局变量和静态变量,如上图蓝色字体部分,bss节在文件中不占用存储空间,只是一个名义上占个位置而已,当程序被加载到内存时,会在内存分配bss段,将未初始化的全局变量和静态变量存储在那里。
重定位表(.rela):
在代码节和数据节中,有些函数或者变量定义在其它的目标文件中,因此在链接时,需要将这些函数和变量进行重定位,重定位时依据的信息就在重定位表中。
对于代码节,它的重定位信息在.rela.text中,对于数据节,它的重定位信息在.rela.data中。
对于重定位表,因为它用到了符号表,它的Link属性表示符号表在节表中的索引,Info表示这个重定位表作用于哪个节,如下图所示,.rela.text中Link为11,从节表中查找第11项就可以定位到符号表,Info为1,从节表中查找第1项就可以定位到代码节,表示重定位表作用于索引为1的代码节。
重定位表定位符号表和代码节
字符串表(.strtab和.shstrtab):
一个程序中会出现很多字符串,例如变量名,函数名,这些名称的长度通常是不固定的,因此一个节中如果要存储这些信息的话,节的大小就需要是动态的,一种常见的做法是将所有不固定的字符串单独用字符串表进行存储,其它的节可以用一个偏移量来表示字符串,这样除了字符串表外,其它的节的长度都是固定的了,便于文件的处理。
其它的节知道了字符串的偏移量后,就可以到字符串表中进行查找,由于字符串的最后一个字符是\0,就可以从偏移量开始截取字符串到\0,一个完整的字符串就可以得到了。
在ELF文件中,字符串表有.strtab和.shstrtab两种,.strtab用于保存普通的字符串,比如变量,函数的名称,.shstrtab通常用于保存节表中用到的字符串,比如节的名称。
符号表(.symtab):
链接过程本质上就是将多个目标文件粘合在一起,就像搭建积木一样,每个积木就是一个目标文件,每个积木都有凹凸部分,这样不同的积木之间才能粘合在一起,目标文件的凹凸部分就是函数或者变量地址,重定位的过程就是替换地址的过程。
一个目标文件A定义了函数func1,另外一个目标文件B引用了函数func1,func1是函数的名字,对于变量也是类似,函数和变量叫做符号,函数名和变量名就叫做符号名。
每个目标文件都有一个符号表,每个符号表中记录了目标文件用到的所有符号,每个符号都对应一个符号值,对于变量和函数来说,符号值就是变量和函数的地址。
符号表中除了函数和变量外,还有其它不常用到的符号,经过统一整理后,可以对所有的符号进行如下分类:
强符号:定义在本目标文件中的全局符号并且已经初始化,这些符号可以被其它目标文件引用,例如SimpleSection.c中的func1,main,global_init_var。
弱符号:定义在本目标文件中的全局符号但是没有初始化,这些符号可以被其它目标文件引用,例如SimpleSection.c中的global_uninit_var。
强引用:声明在本目标文件中的全局符号,但并未定义在当前目标文件,这个符号引用了其他目标文件中的符号,例如SimpleSeciton.c的printf函数,对于变量来说,可以通过extern关键字进行声明。
对于强引用,如果链接过程中,在其它目标文件中没有找到该引用,则报找不到符号的错误。
弱引用:通过__attribute__((weakref))方式声明弱引用,这个符号引用了其他目标文件中的符号,例如__attribute__((weakref))voidfoo();就声明了foo函数就是一个弱引用。
对于弱引用,如果静态链接或者加载链接时,在其它目标文件中没有找到该引用,则不会报错,因此弱引用可以用于动态链接。
局部符号:在目标文件中可见,其它目标文件不可见,比如SimpleSection.c中的static_var和static_var2。
链接过程中比较关心的符号就是强符号,弱符号,强引用,弱引用,局部符号则是次要的,它们对其它目标文件是不可见的。
可以通过readelf-s查看符号表信息
符号表信息
由上图得知,SimpleSection.o总共有16个符号,每个符号都有几个属性,下面对属性介绍如下:
Size:符号大小。
对于包含数据的符号,这个值表示数据类型的大小,例如一个double类型占用8个字节。
Type:符号类型。
有以下几种符号类型:
NOTYPE表示未知类型符号,一般指的是符号定义在其他目标文件中,例如强引用,弱引用。
OBJECT表示数据对象,比如变量,数组等。
FUNC表示函数或者其它可执行代码。
SECTION表示一个节,符号的Bind属性必须是Local,即它必须是一个局部符号。
FILE表示一个文件名,一般为该目标文件对应的源文件名。
Bind:符号绑定信息。
Local表示局部符号,只在当前目标文件可见,其它目标文件不可见。
GLOBAL表示全局符号可以是强符号,也可是是强引用。
WEAK表示弱引用。
Ndx:符号所在节的信息。
ABS表示该符号包含一个绝对的值,比如表示文件名的符号就属于这种类型。
COM表示该符号是一个”COMMON”块类型的符号,一般来说,未初始化的全局符号就是这种类型。
UND表示该符号未定义,该符号在当前目标文件中被引用了,但是没有在目标文件中定义。
其它,表示符号所在节在节表中的下标。
Name:符号名。
通常存储于字符串表(.strtab)。
Value:符号值。
当Ndx的值是节表索引时,可以知道这个符号在哪个节中,例如SimpleSection.o中的func1函数,func1这个符号它的Ndx就是1,通过1可以在节表中找到代码节.text,此时符号值就是func1在.text节中偏移量。
当Ndx是COM时,符号值就是该符号的对齐属性。
当文件是可执行文件,符号值就是符号的虚拟地址。
除了在符号表中定位的符号外,还有些特殊符号。
特殊符号不在符号表中定义,但是程序中可以直接引用它,这些特殊符号通常被定义在链接器的链接脚本中,因此这些符号类似与内置符号,不需要在符号表中定义,由如下几类特殊符号:
__executable_start:表示程序的起始地址,是程序的最开始地址。
__etext或_etext或etext:表示代码段的结束地址。
_edata或edata:表示数据段的结束地址。
_end或end:程序的结束地址。
以上地址都是程序被装载时的虚拟地址。
其它节:
rodata1:
只读数据节,比如字符串常量,全局const变量都存储在这里。
comment:
存储编译器版本信息。
.debug:
存储调试类信息。
.hash:
符号哈希表,用于加速符号的查找过程。
.line:
调试时的行号表,即源代码和编译后的指令的对应表。
.note:
额外的编译器信息,比如程序的公司名,发布版本号等。
.plt:
动态链接的跳转表。
.got:
全局入口表,用于动态链接跳转到变量和函数。
其它常用的查看ELF文件信息命令
通过objdump-h命令可以查看ELF文件的各个常用的节的信息
可以通过objdump-s-d查看ELF文件头信息,符号表,以及反汇编指令。
下一篇:山药和山药有什么区别(古语有云)