前文索引:
(1)《Inside Microsoft IL Assembler》学习笔记1:初步认识IL代码
(2)《Inside Microsoft IL Assembler》学习笔记2:让IL代码简短些
托管模块(managed module)的文件格式是标准的windows PE格式。所以,在深入了解托管模块之前,稍微对PE文件格式做一点了解是很有必要的。当然,PE文件格式诞生于十几年前,关于它的描述已经很多(最经典的莫过于 《Peering Inside the PE: A Tour of the Win32 Portable Executable File Format》),笔者没有必要在这里再做重复。只是简单把笔者认为比较重要的内容大体列一下(主要参考资料还包括Iczelion的PE教程),主要还是为了能让自己的笔记内容完整些,详细的知识还需要您参考其他资料(特别是如果您还不清楚诸如“RVA和VA之间的区别”这样的基础概念的话)。
1.两个状态。
PE文件在使用的时候会被整个直接加载到某个虚拟内存地址,在刚刚加载到内存的时候,这个文件在内存里的映像和它原本保存在磁盘上的静态状态没有任何区别。所以,PE虽然是一个静态的物理文件,但这个文件的目的是为了让OS能够轻松的运行它,在这一点上,它的静态和动态结构是完全一致的。当然,后面还要做一些其他操作,把这个PE和其他的外部环境(比如这个PE的调用者)关联起来。总之,在思考这个文件结构的时候,静动态结构的一致性是很值得注意的。
2.PE加载点,实际虚拟地址的计算方式。
在PE文件内部记录了一个首选的载入地址(虚拟内存地址),但这个地址不是必须的(它可能已经被别的进程占用了),所以PE文件可以载入到进程空间的任何地方(这个实际的载入位置被称为基地址base address)。由于这点,必须有一个方法来指定地址而不依赖于pe载入点的地址。为了避免把内存地址硬编码进pe文件,提出了RVA(相对虚拟地址)。RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为
(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000。
把RVA加上PE的载入点的实际地址就可以把RVA转化实际地址。因此,对于PE文件的内部寻址(也就是当PE加载到内存中以后,如何寻址到PE内的每一个元素),需要关注的事情其实很少:这个PE文件被加载到哪个虚拟地址(基地址base address)上,然后PE文件内部的每个元素就会根据基地址和存储在PE中的相对偏移量(RVA)去寻址。从计算方法上说,VA-base address = RVA。所以,只要我们得到了RVA和base address,就能在运行时算出VA(实际加载的虚拟地址的值)。
3.PE中的主要结构:段落
每一个PE文件都分为若干段落(section),有的段落存放被程序声明并直接使用的代码和数据,有的段落存放一些需要被OS知道的信息。段落靠段落表(section table)来定位。在PE头和PE中包含的数据、代码之间,有一个段落表,指向后面的每一个section。但需注意的是,这种“指向”并不是指向section在静态PE文件中的位置,而是PE被加载到进程的虚拟内存空间之后的真实内存地址。
4.引入表,引出表
如果一个PE文件是完全独立的(它不需要引入其他模块的任何功能,也不需要提供功能给其他模块使用),那么这个文件的内部寻址很简单,只要你理解了Virtual Address, Base Address, RVA这些概念之后就很容易理解它的寻址过程;可惜的是,大部分情况下,PE文件都是需要“引入函数”的。一个引入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为"import(引入)"。引入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函数名及其驻留的DLL名。在PE文件的data directory数组第二项就是引入表地址。引入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。