PE文件结构(四)

PE文件结构(四)
9.重定位(relocations)
-----------------------
我将要描述的最后一个数据目录是基址重定位目录。它是由可选头数据目录中的IMAGE_DIRECTORY_ENTRY_BASERELOC(基址重定位目录项)项来指向的。典型的,它包含在自己的节中,名字象“.reloc”这样,并且IMAGE_SCN_CNT_INITIALIZED_DATA(已初始化数据内容节)、 IMAGE_SCN_MEM_DISCARDABLE(内存可丢弃节)和IMAGE_SCN_MEM_READ(内存可读节)等标志位被置1。

如果映象文件不能被加载到可选头中提到的优先载入地址“ImageBase”(映象基址)时,重定位数据对加载器来说就是必须的。此时,链接器所提供的固定地址就不再有效,并且加载器将不得不对静态变量、字符串文字等使用的绝对地址进行修正。

所谓重定位目录就是一些连续的块,每一块都包含4K映象文件的重定位信息。块由一个“IMAGE_BASE_RELOCATION(基址重定位)”结构体开始,这个结构体包含一个32位的“VirtualAddress(虚拟地址)”项和一个32位的“SizeOfBlock(块大小)”项。跟在它们后面的就是块的实际重定位数据,每一条都是16位的。
“VirtualAddress(虚拟地址)”就是重定位所在块需要应用的基本的RVA;“SizeOfBlock(块大小)”就是整个块的字节大小;跟在后面的重定位的数目是:('SizeOfBlock'-sizeof(IMAGE_BASE_RELOCATION))/2个。当你碰到一个“VirtualAddress(虚拟地址)”值为0的“IMAGE_BASE_RELOCATION(基址重定位)”结构体时,重定位信息就结束了。

每一个16位的重定位信息由低12位的重定位位置和高4位的重定位类型组成。要得到重定位的RVA,你需要用这个12位的位置加上“IMAGE_BASE_RELOCATION(基址重定位)”中的“VirtualAddress(虚拟地址)”。类型是下面之一:

     IMAGE_REL_BASED_ABSOLUTE (0)
         这种不需操作;用于将块按32位边界对齐。位置应该为0。
     IMAGE_REL_BASED_HIGH (1)
         重定位的高16位必须被用于被偏移量所指向的那个16位的WORD单元,此WORD是一个32位的DWORD的高位WORD。
     IMAGE_REL_BASED_LOW (2)
         重定位的低16位必须被用于被偏移量所指向的那个16位的WORD单元,此WORD是一个32位的DWORD的低位WORD。
     IMAGE_REL_BASED_HIGHLOW (3)
         重定位的全部32位必须应用于上面所说的全部32位。这种(和不需操作的第“0”种)是我在二进制文件种实际发现的仅有的重定位类型。
     IMAGE_REL_BASED_HIGHADJ (4)
         这是一种复杂的。请自己参阅(参考文献[6]),并努力弄懂它的意思:“高调整。这种修正要求一个全32位值。高16位定位于偏移量处,低16位定位在下一个数组元素(此数组元素包括在大小的域中)的偏移量处。它们两个需要被连成一个有符号的变量。加上32位的增量。然后加上0x8000并将有符号变量的高16位存储在偏移量处的16位域中。”
     IMAGE_REL_BASED_MIPS_JMPADDR (5)
         不清楚
     IMAGE_REL_BASED_SECTION (6)
         不清楚
     IMAGE_REL_BASED_REL32 (7)
         不清楚

举一个例子,如果你发现重定位信息是
     0x00004000       (32位, 开始的RVA)
     0x00000010       (32位, 块的大小)
     0x3012           (16位的重定位数据)
     0x3080           (16位的重定位数据)
     0x30f6           (16位的重定位数据)
     0x0000           (16位的重定位数据)
     0x00000000       (下一块的RVA)
     0xff341234
你知道第一块描述的重定位开始于RVA 0x4000处,有16字节长。因为头用掉了8字节,并且一个重定位要用2字节,所以块中计有(16-8)/2=4个重定位。第一个重定位被应用于0x4012处的DWORD,第二个于0x4080处的DWORD,第三个于0x40f6处的DWORD。最后一个不需操作。
下一块的RVA是0,列表结束。

好,你怎么处理一个重定位呢?
你能知道映象文件“被”重定位到可选头“ImageBase(映象基址)”的优先载入地址;你也能知道你真正载入的地址。如果它们相同,你什么也不用做。如果它们不同,你需计算出实际基址-优先基址的差并加上重定位位置的值(有符号,可能为负值),此值你可通过上面讲述的方法找到。



九、致谢(Acknowledgments)
---------------------------
感谢David Binette的调试和校读。(剩下的错误全部都是我的。)
也感谢wotsit.org网站让我将此文放到他们的网站上。



十、版权(Copyright)
---------------------
本文的版权属于B. Luevelsmeyer,1999年。它是免费的,你可以任意的使用,但后果自负。它含有错误并不完整,特此警告。



十一、Bug报告(Bug reports)
----------------------------
Bug报告(或其他建议)请发送至:
[email protected]



十二、版本(Versions)
----------------------
你可在文件的顶部找到当前的版本号。

1998-04-06
   第一次公开发表

1998-07-29
   将映象文件版本和子系统版本中错误的“byte”改为“word”
   更正“栈只限于1 MB”的错误(实际上没有上限)
   更正一些输入错误

1999-03-15
   更正输出目录的描述,原来非常不全
   调整输入目录的描述,原来讲的不清
   更正输入错误并为其它节改了一些词句
 


十三、参考文献(Literature)
----------------------------
[1]
"Peering Inside the PE: A Tour of the Win32 Portable Executable File
Format" (M. Pietrek), in: Microsoft Systems Journal 3/1994

[2]
"Why to Use _declspec(dllimport) & _declspec(dllexport) In Code", MS
Knowledge Base Q132044

[3]《Windows 问与答》
"Windows Q&A" (M. Pietrek), in: Microsoft Systems Journal 8/1995

[4]《编写多语言资源》
"Writing Multiple-Language Resources", MS Knowledge Base Q89866

[5]
"The Portable Executable File Format from Top to Bottom" (Randy Kath),
in: Microsoft Developer Network

[6]《Windows下TIS格式规范1.0版》
Tool Interface Standard (TIS) Formats Specification for Windows Version
1.0 (Intel Order Number 241597, Intel Corporation 1993)




附录(Appendix: hello world):
-------------------------------
在这个附录中我将给大家展示一下怎样手工建立一个程序。因为我不会DEC Alpha的,本例将使用Intel汇编语言。

本程序相当于

     #include <stdio.h>
     int main(void)
     {
         puts(hello,world);
         return 0;
     }

首先,我使用Win32函数来翻译它以取代C运行时库:

     #define STD_OUTPUT_HANDLE -11UL
     #define hello "hello, world\n"

     __declspec(dllimport) unsigned long __stdcall
     GetStdHandle(unsigned long hdl);

     __declspec(dllimport) unsigned long __stdcall
     WriteConsoleA(unsigned long hConsoleOutput,
                     const void *buffer,
                     unsigned long chrs,
                     unsigned long *written,
                     unsigned long unused
                     );

     static unsigned long written;

     void startup(void)
     {
         WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE),hello,sizeof(hello)-1,&written,0);
         return;
     }

现在我将笨拙的将它汇编出来:
     startup:
                 ; WriteConsole()的参数, 反向的
     6A 00                      push       0x00000000
     68 ?? ?? ?? ??             push       offset _written
     6A 0D                      push       0x0000000d
     68 ?? ?? ?? ??             push       offset hello
                 ; GetStdHandle()的参数
     6A F5                      push       0xfffffff5
     2E FF 15 ?? ?? ?? ??       call       dword ptr cs:__imp__GetStdHandle@4
                 ; 结果是WriteConsole()的参数
     50                         push       eax
     2E FF 15 ?? ?? ?? ??       call       dword ptr cs:__imp__WriteConsoleA@20
     C3                         ret      

     hello:
     68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A    "hello, world\n"
     _written:
     00 00 00 00

以上就是编译的部分。任何人都能做到这点。从现在起让我们扮演起链接器的角色,这会非常有趣 :-)

我需要先找出函数WriteConsoleA()和GetStdHandle()。碰巧它们都在“kernel32.dll”中。(这是“输入库”部分。)

现在我开始做可执行文件。问号代表待定的值;它们将在以后被修正。

首先是DOS-根,开始于0x0,有0x40字节长:
     00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
     30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
正如你所见到的,这不是真正的MS-DOS程序。它只是一个开始部分有“MZ”签名的头和紧跟在头后面的e_lfanew指针,没有任何代码。这是因为它并非打算运行于MS-DOS之上;它之所以在这里只是因为规范的需要。

然后是PE签名,开始于0x40,有0x4字节长:
         50 45 00 00

现在到了文件头,开始于0x44,有0x14字节长:
     Machine                      4c 01        ; i386
     NumberOfSections             02 00        ; 代码段和数据段
     TimeDateStamp                00 00 00 00 ; 谁管它?
     PointerToSymbolTable         00 00 00 00 ; 未用
     NumberOfSymbols              00 00 00 00 ; 未用
     SizeOfOptionalHeader         e0 00        ; 常量
     Characteristics              02 01        ; 32位机器上的可执行文件

接着是可选头,开始于0x58,有0x60字节长:
     Magic                        0b 01        ; 常量
     MajorLinkerVersion           00           ; 我是 0.0 版:-)
     MinorLinkerVersion           00           ;
     SizeOfCode                   20 00 00 00 ; 32字节代码
     SizeOfInitializedData        ?? ?? ?? ?? ; 待找出
     SizeOfUninitializedData      00 00 00 00 ; 我们没有BSS节
     AddressOfEntryPoint          ?? ?? ?? ?? ; 待定
     BaseOfCode                   ?? ?? ?? ?? ; 待定
     BaseOfData                   ?? ?? ?? ?? ; 待定
     ImageBase                    00 00 10 00 ; 1 MB, 随意选
     SectionAlignment             20 00 00 00 ; 32字节对齐
     FileAlignment                20 00 00 00 ; 32字节对齐
     MajorOperatingSystemVersion   04 00       ; NT 4.0
     MinorOperatingSystemVersion   00 00       ;
     MajorImageVersion            00 00        ;0.0版
     MinorImageVersion            00 00        ;
     MajorSubsystemVersion        04 00        ; Win32 4.0
     MinorSubsystemVersion        00 00        ;
     Win32VersionValue            00 00 00 00 ; 未使用?
     SizeOfImage                  ?? ?? ?? ?? ; 待定
     SizeOfHeaders                ?? ?? ?? ?? ; 待定
     CheckSum                     00 00 00 00 ; 非驱动不用
     Subsystem                    03 00        ; Win32控制台
     DllCharacteristics           00 00        ; 未用 (不是一个DLL)
     SizeOfStackReserve           00 00 10 00 ; 1 MB栈
     SizeOfStackCommit            00 10 00 00 ; 开始时4 KB
     SizeOfHeapReserve            00 00 10 00 ; 1 MB堆
     SizeOfHeapCommit             00 10 00 00 ; 开始时4 KB
     LoaderFlags                  00 00 00 00 ; 未知
     NumberOfRvaAndSizes          10 00 00 00 ; 常量

正如你所见,我计划只用2个节,一个用于代码,一个用于所有剩余的东西(数据、常量和输入目录等)。没有重定位和象资源之类其它东西。我也不用BSS节并将变量“written”放入已初始化数据。文件和RAM中的节对齐都是一样的(32字节);这将有助于使任务简单,否则我就得来回地计算RVA很多次。

现在我们设置数据目录,开始于0xb8字节,有 0x80字节长:
        地址            大小
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
     ?? ?? ?? ??     ?? ?? ?? ??          ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_TLS (9)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
     00 00 00 00     00 00 00 00          ; IMAGE_DIRECTORY_ENTRY_IAT (12)
     00 00 00 00     00 00 00 00          ; 13
     00 00 00 00     00 00 00 00          ; 14
     00 00 00 00     00 00 00 00          ; 15
仅使用输入目录。

你可能感兴趣的:(PE文件结构(四))