PE文件到内存的映射
在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,二十采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。
当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
但是要注意的是,系统装载可执行文件的方法又不完全等同于内存映射文件。
当使用内存映射文件的时候,系统对“原著”相当忠实,如果将磁盘文件和内存映像比较的话,可以发现不管是数据本身还是数据之间的相对位置它丫丫的都是完全相同的。
而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化。
Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。
一般情况下,它会处理以下几个方面的内容:
内存页的属性:
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的。但是在装载可执行文件时,与节对应的内存页属性要按照节的属性来设置。所以,在同属于一个模块的内存页中,从不同节映射过来的的内存页的属性是不同的。
节的偏移地址:
节的起始地址在磁盘文件中是按照 IMAGE_OPTIONAL_HEADER32 结构的 FileAlignment 字段的值进行对齐的,而当被加载到内存中时是按照同一结构中的 SectionAlignment 字段的值对其的,两者的值可能不同,所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。
注意,节事实上就是相同属性数据的组合!当节被装入到内存中的时候,相同一个节所对应的内存页都将被赋予相同的页属性,事实上,Windows 系统对内存属性的设置是以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小。(小甲鱼温馨提示:对于32位操作系统来说,这个值一般是4KB==1000H; 对于64位操作系统这个值一般是8KB==2000H)
在磁盘中就没有这个**,因为在磁盘中排放是以什么为主?肯定是以空间为主导,在磁盘只是存放,不是使用,所以不用设置那么详细的属性。试想想看,如果在磁盘中都是以4KB为大小对齐的话,不够就用0来填充,那么一个只占20字节的数据就要消耗4KB的空间来存放,是不是浪费?有木有??
节的尺寸:
对节的尺寸的处理主要分为两个方面:
第一个方面,正如刚刚我们所讲的,由于磁盘映像和内存映像中节对齐存储单位的不同而导致了长度扩展不同(填充的0数量不同嘛~);
第二个方面,是对于包含未初始化数据的节的处理问题。既然是未初始化,那么没有必要为其在磁盘中浪费空间资源,但在内存中不同,因为程序一运行,之前未初始化的数据便有可能要被赋值初始化,那么就必须为他们留下空间。
不进行映射的节:
有些节并不需要被映射到内存中,例如.reloc节,重定位数据对于文件的执行代码来说是透明的,无作用的,它只是提供Windows 装载器使用,执行代码根本不会去访问到它们,所以没有必要将他们映射到物理内存中。
好了,上边的一些知识相信又是对我们之前学习的一点补充和扩展。大家可能对上边的知识觉得又是眼熟,但又觉得有几分陌生。那是当然哈,小甲鱼教学遵循的思路就是今天的知识今天学好它,明天的东西明天再学!一下子你肯定不能接受那么深的东西,像上边的东西如果放在第一讲来讲解,那么恐怕很多朋友不会继续往下看(一头雾水看下去只能两头雾水,哈~),所以咱学习要遵循循环渐进,有些重点分开来重复讲解,虽然重复,但每次都会往下加深一点来让大家容易接受哈。
我们可以继续了,接下来是节表,也称为区块表:
节表(区块表):
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。
另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS 结构中的 FileHeader.NumberOfSections 字段来指定的。
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union
{
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
DWORD VirtualAddress; // 节区的 RVA 地址
DWORD SizeOfRawData; // 在文件中对齐后的尺寸
DWORD PointerToRawData; // 在文件中的偏移量
DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 节属性如可读,可写,可执行等} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
现在对区块表的结构对各个成员进行详细的分析:
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union
{
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
DWORD VirtualAddress; // 节区的 RVA 地址
DWORD SizeOfRawData; // 在文件中对齐后的尺寸
DWORD PointerToRawData; // 在文件中的偏移量
DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 节属性如可读,可写,可执行等} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name:区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。
另外小甲鱼童鞋要跟大家啰嗦一下的是:每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。
因此,小甲鱼建议大家:当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
Virtual Size:对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。
Virtual Address:该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个快的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。
SizeOfRawData:该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment 潜规则处理过的长度。
PointerToRawData:该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量哦。
PointerToRelocations:这哥们在EXE文件中没有意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)
PointerToLinenumbers:行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。
NumberOfRelocations:这哥们在EXE文件中也没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目来着。
NumberOfLinenumbers:该区块在行号表中的行号数目,鸡肋。
Characteristics:该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。
小甲鱼以下给大家列举出最常用的一些属性值:
具体内容可以参考MSDN在线文档:http://msdn.microsoft.com/en-us/library/ms680341(v=vs.85).aspx
|
|
|
The section contains executable code. 包含代码,常与 0x10000000一起设置。 |
|
The section contains initialized data. 该区块包含以初始化的数据。 |
|
The section contains uninitialized data. 该区块包含未初始化的数据。 |
|
The section can be discarded as needed. 该区块可被丢弃,因为当它一旦被装入后, 进程就不在需要它了,典型的如重定位区块。 |
|
The section can be shared in memory. 该区块为共享区块。 |
|
The section can be executed as code. 该区块可以执行。通常当0x00000020被设置 时候,该标志也被设置。 |
|
The section can be read. 该区块可读,可执行文件中的区块总是设置该 标志。 |
|
The section can be written to. 该区块可写。 |