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文件结构(五)
|