在之前的专栏中,我们已经将结果了在Windows下可执行文件(PE)文件格式了,个人认为在学习了PE文件结构之后,学习ELF文件结构会容易很多,大家可以去这个专栏学习PE文件格式:PE文件结构学习,本章内容ELF文件概述也会结合着PE文件结构来讲解。
这里先给出PE文件格式的图,大家可以了解了解:
ELF文件结构:
这里先给出一张图,来大致了解ELF文件结构:
本图出自:星盟安全团队公开课PPT
在图中,我们可以看到,ELF文件大致分为三个部分:文件Header,节区块,与节区头表部分。
我们现在就分别来讲讲这三大部分:
一.文件头
在文件头中,又包含了ELF文件头和程序头表。
ELF文件头(ELF Header):
ELF文件头是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"的下标
};
我们来逐一讲解一下其中每一个字段的含义:
- e_ident[EI_NIDENT]:占据16个字节,包含用于标识文件为ELF格式的特定字节序列,如果是32位ELF文件,对应魔数为:‘/x7FELF’,如果是64位ELF文件,对应的魔数为:‘/x7FELF/XO2’。
- e_type:文件类型
占据两个字节,指定文件的类型,可以是可执行文件,目标文件,动态链接库等等。 - e_machine:机器架构
占据两个字节,指定文件所运行的目标体系结构,如x86,x64,arm,mips等。 - e_version:版本
占据两个字节,指定ELF文件的版本 - e_entry:入口地址
占据4个字节(32位),或8个字节(64位),标识可执行文件的入口点,就像PE文件结构里,可选头(拓展头)里的OEP - e_phoof:程序头表偏移
占据4个字节或8个字节,指定程序头表在文件中的偏移量 - e_shoof:节区头表偏移
占据4个字节或8个字节,指定节区投标在文件中的偏移量 - e_flags:标志
占据4个字节,包含于文件相关的一些标志,如是否使用地址重定位等 - e_ehsize:本结构体(ELF文件头)的大小
占据2个字节,指定ELF文件头的大小,用于解析文件的其他部分 - e_phentsize:程序头表条目大小
占据2个字节,指定程序头表中每个条目的大小 - e_phnum:程序头表条目数量
占据2个字节,指定程序头表中的条目数量 - e_shentsize:节区投标条目大小
占据2个字节,指定节区头表中每个条目的大小 - e_shnum:节区投标条目数量
占据2个字节,指定节区投标中的条目数量 - e_shstrndx:字符串表索引
储存Section名字集合的Section的下标,也就是".shstrtab"节的下标
程序头表(Program Header Table):
程序头表是ELF文件头的一部分,它描述了程序在内存中的布局信息,尤其是可执行文件在运行时需要加载到内存中的各个段(Segment)属性,每个程序头表条目对应一个段,指导操作系统加载和执行可执行文件。
这里关于节和段的描述,我们在后面马上讲解到,这里先记住:段是一些具有相同权限的节的集合。
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
};
我们来逐一讲解每一个字段的含义:
- p_type:段类型
占据4个字节,描述段的类型,如可加载的代码段,可加载的数据段,动态链接信息等。 - p_offset:文件偏移
占据4个字节或8个字节,指定该段在文件中的偏移量 - p_offset:虚拟地址
占据4个字节或8个字节,指定该段在内存中的加载地址 - p_paddr:物理地址
占据4个字节或8个字节,指定该段在物理内存中的地址,对可执行文件可能没有意义 - p_filesz:文件大小
占据4个字节或8个字节,指定该段在文件中的大小 - p_memsz:内存大小
占据4个字节或8个字节,指定该段在内存中的大小,可能大于文件大小 - p_flage:段标志
占据4个字节或8个字节,描述段的属性,如权限(读,写,执行等) - p_align:对齐
占据4个字节或8个字节,指定段在内存中的对齐方式
这就是每一个条目的结构,这里要注意的是:程序头表的起始位置,由ELF文件头的e_phoff字段指定,在一个ELF文件中,程序头表中不止一个这样的结构,通常含有多个条目,因为不同的段,含有的权限不同,在程序加载的时候,需要按照段来加载
二.节区头表
这里为什么先将节区头表呢?了解PE文件结构的哥们应该知道,PE文件结构中也有节表,而节区要按照节表区寻找。
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;
};
我们还是逐一来解释每一个字段的含义:
- sh_name:名称索引
占据4个字节,指定一个字符串表中的索引,该字符串表包含了所有节区的名称,这个字段实际上就是:在ELF文件头中e_shstrndx字段指向的字符串表中,该节的名称字符串索引 - sh_type:类型
指定节区的类型,常见的类型包括:SHT_NULL(未使用的节区),SHT_PROGBITS(包含程序定义的信息,如代码段,数据段),SHT_SYMTAB(包含符号表),SHT_STRTAB(包含字符串表) - sh_flags:标志
描述节区的属性,包含权限(读,写,执行等) - sh_addr:虚拟地址
指定节区在内存中的虚拟地址,对于不在内存中执行的节区,这个字段可能没有意义 - sh_offset:文件偏移
指定节区在ELF文件中的偏移量,标识该截取在文件中的位置 - sh_size:文件大小
指定节区在文件中的大小,表示该节区在ELF文件中所占据的字节数 - sh_link:链接
对于某些类型的节区,此字段可能是符号表索引或者字符串表索引,表示与该节区相关联的符号表或者字符串表 - sh_info:信息
对于某些类型的节区,此字段提供额外的信息,具体含义取决于节区的类型 - sh_addralign:字节对齐
指定节区在内存中的对齐大小,即节区在内存中的起始地址需要满足对齐的要求 - sh_entsize:
对于某些类型的节区,此字段指定节区中每个条目的大小,适用于包含数组的节区。
到这里节区头表的各个字段的含义我们就介绍完了,需要注意的是,这里的节区的起始位置,由ELF文件头的e_shoff字段指定,与程序头表一样,这里不止一个该结构,每一个节区有一个节区头表,他们是相连的,根据节区头表,我们就可以在ELF文件中找到对应的节区了。
至此,我们了解到了ELF文件结构中的所有字段,我们来给出一张图,让大家深入理解ELF文件结构:
我们来看看:首先,通过ELF文件头,可以找到程序头表,在这里,是段的描述,包含了每个段的权限,然后通过程序头表,我们又可以找到节区头表,这样,我们就可以精确得找到每一个我们想找的节区。
段与节:
很多人都对段与节不是很理解,这里就给大家讲解讲解,ELF文件的程序头表不是描述了段的信息嘛?实际上这里的段就是几个相同权限的节区组合而成的,比如说,像只读,不可写,不可执行的几个节区,组成一个段,这个段描述保存在程序头表中,但是每一个节区的描述还是在节区头表中有记载,所以我们通过节区头表还是能精确地找到这个段中的每一个节。
虚拟内存装载
我们来举一个例子,就是说,我们现在使用的PC,运行内存有16个G,如果说每一个运行的程序时,都将自身的文件复制到内存中执行,那我们的16个G内存,肯定运行不了几个进程,所以就提出来了虚拟内存的概念,就是让每一个应用程序相信自己有独立的4GB空间,然后将自己的映像贴在该内存中,然后执行。
- 我们先来讲讲,如何将磁盘上存的文件如何贴到内存中:
在磁盘上,为了节省空间,每一个节区或者段之间的距离都缩短了很多,大家在学习PE文件结构和ELF文件结构的时候都发现了两个概念:虚拟内存地址和文件地址,那么将磁盘上的文件加载到内存中的可执行状态,如何加载呢?我们这里以ELF文件为例来讲解一下,首先,将ELF文件头加载到内存(不管是ELF文件还是PE文件,文件头在磁盘和内存中的状态都是一样的),然后找到程序头表,在每一个程序头表的文件偏移上找到该段的文件位置,然后加载到内存中(就是程序头表中记载的虚拟内存地址),在这中间还要注意对齐问题,将所有的段加载完之后,还要兼顾后面的节区头表进行修改,最终符合程序头表的内存地址,大小,对齐和节区头表的内存地址,大小和对齐就算加载完成。
在加载到内存中后,已经是可以执行的状态了,但是还是不能执行,因为还有高2G的内核空间
所有的应用程序,都共用一个内核内存,这一个内核,就可以处理所有的应用程序需求。如图所示;
更底层的知识,就需要大家进行体系化学习了,今天的讲解就到此结束,如果发现有什么理解上的错误,还请大佬指正,我们大家共同进步!!!