WIN32汇编语言教程:第17章 PE文件 · 17.1 PE文件的结构(5)
3. RVA和文件偏移的转换
在前面的内容中已经多次提到“RVA”一词,对于初次接触PE文件的读者来说,RVA是个比较费解的概念,特别是在一开始就去接触RVA的情况下。RVA的概念是和PE文件从磁盘到内存的映射息息相关的,在了解了这方面的内容后,再来看RVA就不成问题了。
RVA是相对虚拟地址(Relative Virtual Address)的缩写,顾名思义,它是一个“相对”地址,也可以说是“偏移量”,PE文件的各种数据结构中涉及到地址的字段大部分都是以RVA表示的。
准确地说,RVA就是当PE文件被装载到内存中后,某个数据的位置相对于文件头的偏移量。举个例子,如果Windows装载器将一个PE文件装入00400000h处的内存中,而某个节中的某个数据被装入0040xxxxh处,那么这个数据的RVA就是(0040xxxxh-00400000h)=xxxxh,反过来说,将RVA的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。
图17.3 RVA的含义
很明显,PE文件中出现RVA的概念是因为PE的内存映像和磁盘文件映像是不同的,如图17.3中的A和A'所示,同一数据相对于文件头的偏移量在内存中和在磁盘文件中可能是不同的,为了提高效率,PE文件头中使用的都是内存映像中的偏移量,也就是RVA。从图17.3中也可以得到另一个结论,那就是RVA仅仅是对于处于节中的数据而言的,对于文件头和节表来说无所谓RVA和文件偏移,因为它们在被映射到内存中后不管是大小还是偏移都不会有任何改变。
使用RVA,使文件装入内存后的数据定位变得方便,然而却给处理磁盘上的静态PE文件带来了很大的麻烦,举例来说,假如要读取PE文件中的资源(如图17.3中的A所示),第一个步骤就是从PE文件头的数据目录中访问第3个IMAGE_DATA_DIRECTORY结构,并从结构中得到资源所处的偏移量,但是这样得到的偏移量是个RVA,它只能用于在内存中查找由A'位置所指示的资源。用它直接在磁盘文件中定位A位置是错误的。
当处理PE文件时,任何的RVA必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,惟一可用的方法就是最土最笨的方法:
(1)循环扫描节表并得到每个节在内存中的起始RVA(根据VirtualAddress字段),并根据节的大小(SizeOfRawData字段)算出节的结束RVA,最后比较判断目标RVA是否落在某个节之内。
(2)如果目标RVA处于某个节之内,那么用目标RVA减去节的起始RVA,这样就得到了目标RVA相对于节起始地址的偏移量RVA'。
(3)在节表中获取节在文件中所处的偏移(PointerToRawData字段),将这个偏移值加上上一步得到的RVA'值,这才是数据在文件中的真正偏移位置。
这里是两个通用的函数,其中_RVAToOffset函数将RVA转换成文件偏移,输入的参数是已经读取到内存中的文件头的地址和RVA的值;_GetRVASection函数用来获取RVA所在的节的名称。这两个函数被保存在_RvaToFileOffset.asm文件中,并将在以后的例子中用到,函数中使用的算法就是上面3个步骤中列出的算法。
.const szNotFound db '无法查找',0 .code ;#################################################################### ; 将 RVA 转换成文件偏移 ;#################################################################### _RVAToOffset proc _lpFileHead,_dwRVA local @dwReturn pushad mov esi,_lpFileHead assume esi:ptr IMAGE_DOS_HEADER add esi,[esi].e_lfanew ;******************************************************************** ; 得到PE文件头的位置,这个位置加上文件头长度就是节表的位置 ;******************************************************************** assume esi:ptr IMAGE_NT_HEADERS mov edi,_dwRVA mov edx,esi add edx,sizeof IMAGE_NT_HEADERS assume edx:ptr IMAGE_SECTION_HEADER movzx ecx,[esi].FileHeader.NumberOfSections ;******************************************************************** ; 扫描每个节并判断 RVA 是否位于这个节内 ;******************************************************************** .repeat mov eax,[edx].VirtualAddress add eax,[edx].SizeOfRawData .if (edi >= [edx].VirtualAddress) && (edi < eax) mov eax,[edx].VirtualAddress sub edi,eax mov eax,[edx].PointerToRawData add eax,edi jmp @F .endif add edx,sizeof IMAGE_SECTION_HEADER .untilcxz assume edx:nothing assume esi:nothing mov eax,-1 @@: mov @dwReturn,eax popad mov eax,@dwReturn ret _RVAToOffset endp ;#################################################################### ; 获取 RVA 所在的节的名称 ;#################################################################### _GetRVASection proc _lpFileHead,_dwRVA local @dwReturn pushad mov esi,_lpFileHead assume esi:ptr IMAGE_DOS_HEADER add esi,[esi].e_lfanew assume esi:ptr IMAGE_NT_HEADERS mov edi,_dwRVA mov edx,esi add edx,sizeof IMAGE_NT_HEADERS assume edx:ptr IMAGE_SECTION_HEADER movzx ecx,[esi].FileHeader.NumberOfSections ;******************************************************************** ; 扫描每个节区并判断 RVA 是否位于这个节内 ;******************************************************************** .repeat mov eax,[edx].VirtualAddress add eax,[edx].SizeOfRawData .if (edi >= [edx].VirtualAddress) && (edi < eax) mov eax,edx jmp @F .endif add edx,sizeof IMAGE_SECTION_HEADER .untilcxz assume edx:nothing assume esi:nothing mov eax,offset szNotFound @@: mov @dwReturn,eax popad mov eax,@dwReturn ret _GetRVASection endp ;####################################################################
上页:第17章 PE文件 · 17.1 PE文件的结构(4) 下页:第17章 PE文件 · 17.1 PE文件的结构(6)