程序运行之ELF文件结构
ELF目标文件格式的最前部是ELF文件头。包含了整个文件的基本属性。比如ELF文件版本,目标机器型号,程序入口地址等。然后是ELF的各个段,其中ELF文件中与段有关的重要结构就是段表。段表描述了ELF文件包含的所有段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限及段的其他属性。
一文件头;
通过readelf命令来详细查看ELF文件的头信息:
root@zhf-maple:/home/zhf/c_prj#readelf-hmain.o
ELF头:
Magic:7f454c46020101000000000000000000
类别:ELF64
数据:2补码,小端序(littleendian)
版本:1(current)
OS/ABI:UNIX-SystemV
ABI版本:0
类型:REL(可重定位文件)
系统架构:AdvancedMicroDevicesX86-64
版本:0x1
入口点地址:0x0
程序头起点:0(bytesintofile)
Startofsectionheaders:1048(bytesintofile)
标志:0x0
本头的大小:64(字节)
程序头大小:0(字节)
Numberofprogramheaders:0
节头大小:64(字节)
节头数量:13
字符串表索引节头:12
我们将Elf32_Ehdr结构与readelf命令输出的信息对比可得如下对应关系:
成员 | readelf输出结果 |
e_ident | Magic,类别,数据,版本,OS/ABI,ABI |
e_type | 类型 |
e_machine | 系统架构 |
e_version | 版本 |
e_entry | 入口点地址 |
e_phoff | startofprogramheaders |
e_shoff | startofsectionheaders |
e_flags | 标志 |
e_ehsize | 文件头的大小 |
e_phentsize | 程序头大小 |
e_phnum | numberofprogramheaders |
e_shentsize | 节头大小 |
e_shnum | numberofprogramheaders |
e_shstrndx | 字符串表段索引 |
下面我来逐步分析各个部分:
魔数:
Magic:7f454c46020101000000000000000000
7f、45、4c、46分别对应ascii码的Del(删除)、字母E、字母L、字母F。这四个字节被称为ELF文件的魔数,操作系统在加载可执行文件时会确认魔数是否正确,如果不正确则拒绝加载。
第五个字节标识ELF文件是32位(01)还是64位(02)的。
第六个字节标识该ELF文件字节序是小端(01)还是大端(02)的。
第七个字节指示ELF文件的版本号,一般是01。
后九个字节ELF标准未做定义。一般为00.
文件类型:
e_type成员标识文件类型,ELF文件有三种类型,如下表所示。
常量标识 | 值 | 类型 |
ET_REL | 1 | 可重定位文件,一般位.o文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件,一般位.so文件 |
机器类型:
ELF文件格式被设计成可以在多个平台下使用,但并不表示同一个ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF标准.e_machine成员就表示该ELF文件的平台属性。
常量标识 | 值 | 系统架构 |
EM_M32 | 1 | AT&TWE32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel80386 |
EM_68K | 4 | Motorolam68kfamily |
EM_88K | 5 | Motorolam88kfamily |
EM_860 | 6 | Intel80860 |
二段表:
在ELF文件中有各种各样的段,段表就是保存这些段的基本属性的结构。它描述了ELF的各个段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限及段的其他属性。编译器,链接器和装载器都是依靠段表来定位和访问各个段的属性的。前面使用objdump-h来显示各个段,但只是显示了关键的几个段。我们用readelf来输出段表的内容。段表是存在Elf32_Shdr中,它是一个结构体数组。各个成员的含义如下表:
成员 | 含义 |
sh_name | 段名,位于一个叫“.shstrtab”的字符串表 |
sh_type | 段的类型,详细内容看后文 |
sh_flags | 段的标志位,详细内容见后文 |
sh_addr | 段在被加载后在进程地址空间中的虚拟地址,当段不能被加载时,它为0 |
sH_offset | 段在elf文件中的偏移,如果该段不存在于文件中,则它无意义 |
sh_szie | 段的长度 |
sh_link | 段的链接信息,详细内容见后文 |
sh_info | 段的链接信息,详细内容见后文 |
sh_addralign | 段地址对齐 |
sh_entsize | 项的长度,有的段包含一些固定大小的项,比如符号表,sh_enrsize就是用来指示这些项的大小 |
我们用readelf-Smain.o来查看内容。
root@zhf-maple:/home/zhf/c_prj#readelf-Smain.o
共有13个节头,从偏移量0x418开始:
节头:
[号]名称类型地址偏移量
大小全体大小旗标链接信息对齐
[0]NULL000000000000000000000000
00000000000000000000000000000000000
[1].textPROGBITS000000000000000000000040
000000000000004c0000000000000000AX001
[2].rela.textRELA000000000000000000000320
00000000000000600000000000000018I1018
[3].dataPROGBITS00000000000000000000008c
00000000000000080000000000000000WA004
[4].bssNOBITS000000000000000000000094
00000000000000040000000000000000WA004
[5].rodataPROGBITS000000000000000000000094
00000000000000040000000000000000A001
[6].commentPROGBITS000000000000000000000098
00000000000000240000000000000001MS001
[7].note.GNU-stackPROGBITS0000000000000000000000bc
00000000000000000000000000000000001
[8].eh_framePROGBITS0000000000000000000000c0
00000000000000580000000000000000A008
[9].rela.eh_frameRELA000000000000000000000380
00000000000000300000000000000018I1088
[10].symtabSYMTAB000000000000000000000118
0000000000000198000000000000001811118
[11].strtabSTRTAB0000000000000000000002b0
000000000000006c0000000000000000001
[12].shstrtabSTRTAB0000000000000000000003b0
00000000000000610000000000000000001
KeytoFlags:
W(write),A(alloc),X(execute),M(merge),S(strings),I(info),
L(linkorder),O(extraOSprocessingrequired),G(group),T(TLS),
C(compressed),x(unknown),o(OSspecific),E(exclude),
l(large),p(processorspecific)
从上面的输出的第一行可以看到,段表从0x418开始,观察其他段的偏移量可以发现,段表位于所有段之后,就是文件的末尾,该ELF文件有12个段,每个段的大小也就是sizeof(Elf32_Shdr)的大小,为40字节,所以整个段表的大小就是12*40=480个字节,再加上之前的0x418,总共1048+480=1528个字节.
段的类型
上面输出的第三列就是段的类型,段的类型的相关常量如下表所示:
常量 | 值 | 含义 |
SHT_NULL | 无效段 | |
SHT_PROGBITS | 1 | 程序段、代码段、数据段都是此种类型 |
SHT_SYMTAB | 2 | 表示该段的类内容为符号表 |
SHT_STRTAB | 3 | 表示该段的内容是字符串表 |
SHT_RELA | 4 | 重定位表,该段包含了重定位信息 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没有内容,比如.bss段 |
SHT_REL | 9 | 该段包含了重定位信息 |
SHT_SHLIB | 10 | 该段保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
段的标志位
以上输出的flg一列指出了该段在进程虚拟空间中的属性。
常量 | 值 | 含义 |
SHF_WRITE | 1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 2 | 表示该段需要在进程空间中分配空间 |
SHF_EXECINSTR | 4 | 表示该段在进程空间中可以被执行 |
段的链接信息
sh_type | sh_link | sh_info |
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | |
SHT_HASH | 该段所使用的符号表在段表中的下标 | |
SHT_REL,SHT_RELA | 该段所使用的符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB、SHT_DNYSYM | 操作系统相关 | 操作系统相关 |
other | SHN_UNDEF |
可重定位表:
链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件表里面,对于每个须要重定位的代码段和数据段,都会有一个相应的重定位表,例如.rel.text表对应.text段。也就是说,重定位表记录了须要被重定位的地址都在相应段的哪些地方。比如.rela.text就是针对.text段的重定位表,因为.text段中至少有一个绝对地址的引用,那就是对printf函数的调用。而.data没有对绝对地址的调用
字符串表:
ELF文件中用到了很多字符串。比如段名,变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示比较困难。常见的做法就是把字符串集中起来存放到一个表。然后使用字符串在表中的偏移来引用字符串。
下图展示了一个长度为25字节的字符串表:
字符串引用示例:
常见的段名是.strtab(StringTable)或者是.shstrtab(SectionHeaderStringTable) }