PE文件格式详解(2)

作者:MSDN
译者:李马 (http://home.nuc.edu.cn/~titilima

)

Windows NT附加域

   添加到Windows NT PE文件格式中的附加域为Windows NT特定的进程行为提供了装载器的支持,以下为这些域的概述。
   ·ImageBase。进程映像地址空间中的首选基地址。Windows NT的Microsoft Win32 SDK链接器将这个值默认设为0x00400000,但是你可以使用-BASE:linker开关改变这个值。
   ·SectionAlignment。从ImageBase开始,每个段都被相继的装入进程的地址空间中。SectionAlignment则规定了装载时段能够占据的最小空间数量——就是说,段是关于SectionAlignment对齐的。
   Windows NT虚拟内存管理器规定,段对齐不能少于页尺寸(当前的x86平台是4096字节),并且必须是成倍的页尺寸。4096字节是x86链接器的默认值,但是它可以通过-ALIGN: linker开关来设置。
   ·FileAlignment。映像文件首先装载的最小的信息块间隔。例如,链接器将一个段实体(段的原始数据)加零扩展为文件中最接近的FileAlignment边界。早先提及的2.39版链接器将映像文件以0x200字节的边界对齐,这个值可以被强制改为512到65535这么多。
   ·MajorOperatingSystemVersion。表示Windows NT操作系统的主版本号;通常对Windows NT 1.0而言,这个值被设为1。
   ·MinorOperatingSystemVersion。表示Windows NT操作系统的次版本号;通常对Windows NT 1.0而言,这个值被设为0。
   ·MajorImageVersion。用来表示应用程序的主版本号;对于Microsoft Excel 4.0而言,这个值是4。
   ·MinorImageVersion。用来表示应用程序的次版本号;对于Microsoft Excel 4.0而言,这个值是0。
   ·MajorSubsystemVersion。表示Windows NT Win32子系统的主版本号;通常对于Windows NT 3.10而言,这个值被设为3。
   ·MinorSubsystemVersion。表示Windows NT Win32子系统的次版本号;通常对于Windows NT 3.10而言,这个值被设为10。
   ·Reserved1。未知目的,通常不被系统使用,并被链接器设为0。
   ·SizeOfImage。表示载入的可执行映像的地址空间中要保留的地址空间大小,这个数字很大程度上受SectionAlignment的影响。例如,考虑一个拥有固定页尺寸4096字节的系统,如果你有一个11个段的可执行文件,它的每个段都少于4096字节,并且关于65536字节边界对齐,那么SizeOfImage域将会被设为11 * 65536 = 720896(176页)。而如果一个相同的文件关于4096字节对齐的话,那么SizeOfImage域的结果将是11 * 4096 = 45056(11页)。这只是个简单的例子,它说明每个段需要少于一个页面的内存。在现实中,链接器通过个别地计算每个段的方法来决定SizeOfImage确切的值。它首先决定每个段需要多少字节,并且最后将页面总数向上取整至最接近的SectionAlignment边界,然后总数就是每个段个别需求之和了。
   ·SizeOfHeaders。这个域表示文件中有多少空间用来保存所有的文件头部,包括MS-DOS头部、PE文件头部、PE可选头部以及PE段头部。文件中所有的段实体就开始于这个位置。
   ·CheckSum。校验和是用来在装载时验证可执行文件的,它是由链接器设置并检验的。由于创建这些校验和的算法是私有信息,所以在此不进行讨论。
   ·Subsystem。用于标识该可执行文件目标子系统的域。每个可能的子系统取值列于WINNT.H的IMAGE_OPTIONAL_HEADER结构之后。
   ·DllCharacteristics。用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记。
   ·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit。这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,栈和堆都拥有1个页面的申请值以及16个页面的保留值。这些值可以使用链接器开关-STACKSIZE:与-HEAPSIZE:来设置。
   ·LoaderFlags。告知装载器是否在装载时中止和调试,或者默认地正常运行。
   ·NumberOfRvaAndSizes。这个域标识了接下来的DataDirectory数组。请注意它被用来标识这个数组,而不是数组中的各个入口数字,这一点非常重要。
   ·DataDirectory。数据目录表示文件中其它可执行信息重要组成部分的位置。它事实上就是一个IMAGE_DATA_DIRECTORY结构的数组,位于可选头部结构的末尾。当前的PE文件格式定义了16种可能的数据目录,这之中的11种现在在使用中。

数据目录

WINNT.H之中所定义的数据目录为:
//WINNT.H // 目录入口// 导出目录#define IMAGE_DIRECTORY_ENTRY_EXPORT 0// 导入目录#define IMAGE_DIRECTORY_ENTRY_IMPORT 1// 资源目录#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2// 异常目录#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3// 安全目录#define IMAGE_DIRECTORY_ENTRY_SECURITY 4// 重定位基本表#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5// 调试目录#define IMAGE_DIRECTORY_ENTRY_DEBUG 6// 描述字串#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7// 机器值(MIPS GP)#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8// TLS目录#define IMAGE_DIRECTORY_ENTRY_TLS 9// 载入配置目录#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10  
基本上,每个数据目录都是一个被定义为IMAGE_DATA_DIRECTORY的结构。虽然数据目录入口本身是相同的,但是每个特定的目录种类却是完全唯一的。每个数据目录的定义在本文的以后部分被描述为“预定义段”。
//WINNT.Htypedef struct _IMAGE_DATA_DIRECTORY {  ULONG VirtualAddress;  ULONG Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每个数据目录入口指定了该目录的尺寸和相对虚拟地址。如果你要定义一个特定的目录的话,就需要从可选头部中的数据目录数组中决定相对的地址,然后使用虚拟地址来决定该目录位于哪个段中。一旦你决定了哪个段包含了该目录,该段的段头部就会被用于查找数据目录的精确文件偏移量位置。
   所以要获得一个数据目录的话,那么首先你需要了解段的概念。我在下面会对其进行描述,这个讨论之后还有一个有关如何定位数据目录的示例。

PE文件段

   PE文件规范由目前为止定义的那些头部以及一个名为“段”的一般对象组成。段包含了文件的内容,包括代码、数据、资源以及其它可执行信息,每个段都有一个头部和一个实体(原始数据)。我将在下面描述段头部的有关信息,但是段实体则缺少一个严格的文件结构。因此,它们几乎可以被链接器按任何的方法组织,只要它的头部填充了足够能够解释数据的信息。

段头部

   PE文件格式中,所有的段头部位于可选头部之后。每个段头部为40个字节长,并且没有任何的填充信息。段头部被定义为以下的结构:
//WINNT.H#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER {UCHAR Name[IMAGE_SIZEOF_SHORT_NAME];  union {    ULONG PhysicalAddress;    ULONG VirtualSize;  } Misc;  ULONG VirtualAddress;  ULONG SizeOfRawData;  ULONG PointerToRawData;  ULONG PointerToRelocations;  ULONG PointerToLinenumbers;  USHORT NumberOfRelocations;  USHORT NumberOfLinenumbers;  ULONG Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;  
你如何才能获得一个特定段的段头部信息?既然段头部是被连续的组织起来的,而且没有一个特定的顺序,那么段头部必须由名称来定位。以下的函数示范了如何从一个给定了段名称的PE映像文件中获得一个段头部:
//PEFILE.CBOOL WINAPI GetSectionHdrByName(LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection){  PIMAGE_SECTION_HEADER psh;  int nSections = NumOfSections (lpFile);  int i;  if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile))      != NULL)  {    /* 由名称查找段 */    for (i = 0; i < nSections; i++)    {      if (!strcmp(psh->Name, szSection))      {        /* 向头部复制数据 */        CopyMemory((LPVOID)sh, (LPVOID)psh,            sizeof(IMAGE_SECTION_HEADER));        return TRUE;      }      else        psh++;    }  }  return FALSE;}
这个函数通过SECHDROFFSET宏将第一个段头部定位,然后它开始在所有段中循环,并将要寻找的段名称和每个段的名称相比较,直到找到了正确的那一个为止。当找到了段的时候,函数将内存映像文件的数据复制到传入函数的结构中,然后IMAGE_SECTION_HEADER结构的各域就能够被直接存取了。

段头部的域

   ·Name。每个段都有一个8字符长的名称域,并且第一个字符必须是一个句点。
   ·PhysicalAddress或VirtualSize。第二个域是一个union域,现在已不使用了。
   ·VirtualAddress。这个域标识了进程地址空间中要装载这个段的虚拟地址。实际的地址由将这个域的值加上可选头部结构中的ImageBase虚拟地址得到。切记,如果这个映像文件是一个DLL,那么这个DLL就不一定会装载到ImageBase要求的位置。所以一旦这个文件被装载进入了一个进程,实际的ImageBase值应该通过使用GetModuleHandle来检验。
   ·SizeOfRawData。这个域表示了相对FileAlignment的段实体尺寸。文件中实际的段实体尺寸将少于或等于FileAlignment的整倍数。一旦映像被装载进入了一个进程的地址空间,段实体的尺寸将会变得少于或等于FileAlignment的整倍数。
   ·PointerToRawData。这是一个文件中段实体位置的偏移量。
   ·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。这些域在PE格式中不使用。
   ·Characteristics。定义了段的特征。这些值可以在WINNT.H及本光盘(译注:MSDN的光盘)的PE格式规范中找到。

值         定义
0x00000020 代码段
0x00000040 已初始化数据段
0x00000080 未初始化数据段
0x04000000 该段数据不能被缓存
0x08000000 该段不能被分页
0x10000000 共享段
0x20000000 可执行段
0x40000000 可读段
0x80000000 可写段

定位数据目录

   数据目录存在于它们相应的数据段中。典型地来说,数据目录是段实体中的第一个结构,但不是必需的。由于这个缘故,如果你需要定位一个指定的数据目录的话,就需要从段头部和可选头部中获得信息。
   为了让这个过程简单一点,我编写了以下的函数来定位任何一个在WINNT.H之中定义的数据目录。
// PEFILE.CLPVOID WINAPI ImageDirectoryOffset(LPVOID lpFile,    DWORD dwIMAGE_DIRECTORY){  PIMAGE_OPTIONAL_HEADER poh;  PIMAGE_SECTION_HEADER psh;  int nSections = NumOfSections(lpFile);  int i = 0;  LPVOID VAImageDir;  /* 必须为0到(NumberOfRvaAndSizes-1)之间 */  if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)    return NULL;  /* 获得可选头部和段头部的偏移量 */  poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);  psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile);  /* 定位映像目录的相对虚拟地址 */  VAImageDir = (LPVOID)poh->DataDirectory      [dwIMAGE_DIRECTORY].VirtualAddress;  /* 定位包含映像目录的段 */  while (i++ < nSections)  {    if (psh->VirtualAddress <= (DWORD)VAImageDir &&        psh->VirtualAddress +         psh->SizeOfRawData > (DWORD)VAImageDir)      break;    psh++;  }  if (i > nSections)    return NULL;  /* 返回映像导入目录的偏移量 */  return (LPVOID)(((int)lpFile +     (int)VAImageDir. psh->VirtualAddress) +    (int)psh->PointerToRawData);}  
该函数首先确认被请求的数据目录入口数字,然后它分别获取指向可选头部和第一个段头部的两个指针。它从可选头部决定数据目录的虚拟地址,然后它使用这个值来决定数据目录定位在哪个段实体之中。如果适当的段实体已经被标识了,那么数据目录特定的位置就可以通过将它的相对虚拟地址转换为文件中地址的方法来找到。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值