一、ELF概述
1、ELF的定义
ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的ELF格式文件包括:可执行文件、可重定位文件(.o)、共享目标文件(.so)、核心转储文件等。
ELF主要用于Linux平台,Windows下是PE/COFF格式。
2、ELF文件的结构
一个完整的ELF文件一般会包括如下几个内容:ELF头、Section头、Program头和Section。
其中由Section头组成的集合称为Section头表,由Program头组成的集合称为Program头表。注意:数个连续的头称之为头表,头表是虚拟出来的定义,文件中不存在头表,只有头。
一个Section头指向一个Section,Section头中包括所指向Section的名字、类型、其在ELF文件中的偏移地址、大小等信息。
一个Program头指向一个Segment,Program头中包括所指向Segment的类型、其在ELF文件中的偏移地址、大小,映射到内存的虚拟地址等信息。一个Segment由一系列连续的Section构成,连续的Section拥有相同的权限,如只读、读写、可读可执行等;
一个ELF头内包含有:Section头表的在ELF文件中的偏移地址、单个Section头的大小、Section头表中Section头的个数;Program头表的在ELF文件中的偏移地址、单个Program头的大小、Program头表中Program头的个数;该ELF文件的类型,若是可执行文件的话,还包含的有程序的入口地址。
3、头的表示方法及其含义
1)变量及其大小:
2)ELF头
- #define EI_NIDENT 16
-
- struct Elf32_Ehdr //共52个字节 //Ehdr表示ELF header
- {
- unsigned char e_ident[EI_NIDENT];
- Elf32_Half e_type; //类型包括:可执行文件、可重定向文件、共享目标文件等
- Elf32_Half e_machine; //有X86、arm之类
- Elf32_Word e_version;
- Elf32_Addr e_entry; //可执行程序的入口地址
- Elf32_Off e_phoff; //Program头表的偏移地址
- Elf32_Off e_shoff; //Section头表的偏移地址
- Elf32_Word e_flags;
- Elf32_Half e_ehsize; //本结构体的size
- Elf32_Half e_phentsize; //单个Program头的size
- Elf32_Half e_phnum; //Segment头表中Segment头的个数
- Elf32_Half e_shentsize; //单个Section头的szie
- Elf32_Half e_shnum; //Section头表中Section头的个数
- Elf32_Half e_shstrndx; //储存Section名字集合的Section的下标,指".shstrtab"的下标
- };
2)Section头
- struct Elf32_Shdr //共40个字节 //Shdl表示Section header
- {
- Elf32_Word sh_name; //所指向Section的名字,如".text"、".data"、".bss"等
- Elf32_Word sh_type; //所指向Section的类型,如:符号表、字符串表等
- Elf32_Word sh_flags;
- Elf32_Addr sh_addr;
- Elf32_Off sh_offset; //所指向Section在ELF文件中的偏移量
- Elf32_Word sh_size; //所指向Section的size
- Elf32_Word sh_link; //和其关联的Section头的下标索引
- Elf32_Word sh_info;
- Elf32_Word sh_addralign; //字节对齐
- Elf32_Word sh_entsize;
- };
3)Program头
- struct Elf32_phdr //32个字节 //phdr表示Program header
- {
- Elf32_Word p_type; //如PT_LOAD表示,对应Segment可被加载到内存中
- Elf32_Off p_offset; //Segment在ELF文件中的偏移量
- Elf32_Addr p_vaddr; //Segment映射到内存后的虚拟地址
- Elf32_Addr p_paddr; //Segment映射到内存后的物理地址,此时与虚拟地址相同
- Elf32_Word p_filesz; //Segment在ELF文件中占用的size
- Elf32_Word p_memsz; //Segment映射到内存后占用的size
- Elf32_Word p_flage; //读、写、执行权限
- Elf32_Word p_align; //字节对齐,p_vaddr和p_paddr对p_align取模后为0
- };
更详细内容请参考:ELF文件格式解析
4、实例解析
可执行文件中Program头表是必须的,可重定向文件(.o)中Section头表是必须的,共享目标文件(.so)中两者都是必须的。
1)可重定向文件分析
ELF头信息如下所示:
在此文件中,可看到其类型为REL, 即可重定向文件。其中Program头的个数为0,Section头的个数为8个,没有程序入口地址。
下图是8个Section头的详细信息:
其中Addr在此处被填充为了0的原因是,其目前并不需要被加载到内存中,在链接的时候才会被填充。
根据上述各Section的偏移量及size可推断出其在该可重定向文件中空间布局,如下表所示:
偏移量(Off) | 大小(size) | Section | 备注 |
0x0 | 0x34 | ELF头 | 0x34表示十进制的52,刚好为ELF头的大小 |
0x34 | 0x2a | .text | |
0x60 | 0x38 | .data | |
0x98 | 0x0 | .bss | |
0x98 | 0x30 | .shstrtab | |
0xc8 | 0x140 | Section头表 | 一个Section头的大小为40个字节,共8个头,大小为0x140 |
0x208 | 0x80 | .symtab | |
0x288 | 0x28 | .strtab | |
0x2b0 | 0x10 | .rel.text |
下面详述上面几种类型的Section:
Ⅰ .shstrtab
.shstrtab中存放着各个Section的名字。
Ⅱ .strtab
.symtab中存放着程序中用到的符号的名字。
Ⅲ .bss
程序中未初始化的全局变量都会被归类到bss段,并在程序加载的时候被初始化为0。
在加载.bss的时候和.data一样,都属于可读可写的数据,但在ELF文件中.data需要占用一段内存空间来保存变量的初始化值,而.bss却不需要。
也就是说,.bss只占用一个Section头的大小,而不需要对应的Section。如上表中可以看出.bss所描述Section的size为0。
Ⅳ .rel.text
.rel.text用于告诉链接器,哪些地方需要重定向。
Ⅴ .symtab
.symtab内存放着程序中用到的符号,包括变量符号、函数符号,如printf、main等。
.symtab有如下定义:
- struct Elf32_sym //
- {
- Elf32_Word st_name; //符号的名字
- Elf32_Addr st_value; //符号相对于其所在Section偏移的相对地址
- Elf32_Word st_size; //符号的size
- unsigned char st_info; //低四位表示符号的作用范围(全局或局部),高四位表示符号的类型(变量、函数等)
- unsigned char st_other;
- Elf32_Half st_shndx; //该符号的值在哪个Section下存储
- };
实例:
以上图中的data_items为例,其Ndx为3,表示其在第3个Section,即.data。data_items的value值为00000000,表示其相对于.data的偏移地址为0,即data_itms在.data的开头。
_start的value也为00000000,表示其在.text的开头,也即整个代码的入口是_start。
2)可执行文件分析
可执行文件的ELF头信息如下所示:
相对于可重定向文件来说,其类型变为了EXEC,少了两个Section header,多了两个Program头,并且有可执行程序的入口地址。
6个Section头如下所示:
从图中可以看出,.text和.data的Addr不再为0,有了实际的值,这便是在链接过程中装载上的。
.bss段因为没有使用到,所以被删除掉了。
.rel.text在链接之后,便完成了自己的使命,也就被删除掉了。
根据上述各Section的偏移量及size可推断出其在该可执行文件中空间布局,如下表所示:
偏移量(Off) | 大小(size) | Section | 备注 |
0x0 | 0x34 | ELF头 | 0x34表示十进制的52,刚好为ELF头的大小 |
0x34 | 0x40 | Program头表 | 一个Program头的大小为32字节,共2个头,大小为0x40 |
0x74 | 0x2a | .text | |
0xa0 | 0x38 | .data | |
0xd8 | 0x27 | .shstrtab | |
0x100 | 0xf0 | Section头表 | 一个Section头的大小为40个字节,共6个头,大小为0xf0 |
0x1f0 | 0xa0 | .symtab | |
0x290 | 0x40 | .strtab |
2个Program头如下所示:
结合Program头和Section的空间布局表可以看出,ELF头、Program头表和Section头表共同组成了第一个Segment;.data单独组成了另一个Segment。
VirtAddr列指出第一个Segment加载到虚拟地址0x0804 8000(注意在x86平台上后面的PhysAddr列是没有意义的),第二个Segment加载到地址0x0804 90a0。
Flg列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。
最后一列Align的值0x1000(4K)是x86平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页,对应关系如下图所示:
这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。
此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然偏移多少,比如第二个Segment在文件中的偏移是0xa0,在内存页面0x0804 9000中的偏移仍然是0xa0,所以是从0x0804 90a0开始,这样规定是为了简化链接器和加载器的实现。
从上图也可以看出.text段的加载地址应该是0x0804 8074,这也正是程序的入口地址。
原来目标文件符号表中的Value
都是相对地址,现在都改成绝对地址了。此外还多了三个符号__bss_start
、_edata
和_end
,这些是在链接过程中添进去的,加载器可以利用这些信息把.bss
段初始化为0。
5、可执行ELF文件的装载过程
附录
1)Section与Segment的英文含义
section和segment都有部分的意思,但
section指的“部分”是不同质的,如:The TOEFL is divided into three sectiond, namely listening, structure and reading.在这里托福考试是由三部分组成的,这三部分是不一样的,即不同质的。
而segment指的“部分”是同质的,如:I want the middle segment of the rope. 我想要中间那段绳...
2)段错误(Segment Error)
当程序试图访问不允许访问的内存位置,或试图以不允许的方式访问内存位置(例如尝试写入只读位置,或覆盖部分操作系统)时会发生段错误。
常见的段错误,包括:
1)使用未经初始化及或已经释放的指针地址
2)访问受系统保护的内存地址
3)写入只读的内存地址
4)数组越界
5)堆栈溢出
参考资料:
</article>