PE 文件格式(二)

来源:互联网 发布:泛微网络 编辑:程序博客网 时间:2024/05/21 15:40
{
        strupr(argv[i]);
       
        // Is it a switch character?
        if ( (argv[i][0] = = '-') || (argv[i][0] = = '/') )
        {
            if ( argv[i][1] = = 'A' )
            {   fShowRelocations = TRUE;
                fShowRawSectionData = TRUE;
                fShowSymbolTable = TRUE;
                fShowLineNumbers = TRUE; }
            else if ( argv[i][1] = = 'H' )
                fShowRawSectionData = TRUE;
            else if ( argv[i][1] = = 'L' )
                fShowLineNumbers = TRUE;
            else if ( argv[i][1] = = 'R' )
                fShowRelocations = TRUE;
            else if ( argv[i][1] = = 'S' )
                fShowSymbolTable = TRUE;
        }
        else    // Not a switch character.  Must be the filename
        {   return argv[i]; }
    }
}

 

int main(int argc, char *argv[])
{
    PSTR filename;
   
    if ( argc = = 1 )
    {   printf(    HelpText );
        return 1; }
   
    filename = ProcessCommandLine(argc, argv);
    if ( filename )
        DumpFile( filename );
    return 0;
}

 

 
1 WIN32 与 PE 基本概念
让我们复习一下几个透过PE文件的设计了解到的基本概念(见图1)。我用术语"MODULE"来表示一个可执行文件或一个DLL载入内存的代码(CODE)、数据(DATA)、资源(RESOURCES),除了代码和数据是你的程序直接使用的,一个模块还可以由WINDOWS用来确定数据和代码载入的位置的支撑数据结构组成。在16位WINDOWS中,这些支撑数据结构在模块数据库(用一个HMODULE来指示的段)中。在WIN32里面,这些数据结构在PE文件头中,这些我将会简要地解释一下。
 
图1  PE文件略图

关于PE文件最重要的是,磁盘上的可执行文件和它被WINDOWS调入内存之后是非常相像的。WINDOWS载入器不必为从磁盘上载入一个文件而辛辛苦苦创建一个进程。载入器使用内存映射文件机制来把文件中相似的块映射到虚拟空间中。用一个构造式的分析模型,一个PE文件类似一个预制的屋子。它本质上开始于这样一个空间,这个空间后面有几个把它连到其余空间的机件(就是说,把它联系到它的DLL上,等等)。这对PE格式的DLL是一样容易应用的。一旦这个模块被载入,Windows 就可以有效的把它和其它内存映射文件同等对待。
和16位Windows不同的是。16位NE文件的载入器读取文件的一部分并且创建完全不同的数据结构在内存中表示模块。当数据段或者代码段需要载入时,载入器必须从全局堆中新申请一个段,从可执行文件中找出生鲜数据,转到这个位置,读入这些生鲜数据,并且要进行适当的修正。除此而外,每个16位模块都有责任记住当前它使用的所有段选择器,而不管这个段是否被丢弃了,如此等等。
对Win32来讲,模块所使用的所有代码,数据,资源,导入表,和其它需要的模块数据结构都在一个连续的内存块中。在这种形势下,你只需要知道载入器把可执行文件映射到了什么地方。通过作为映像的一部分的指针,你可以很容易的找到这个模块所有不同的块。
另一个你需要知道的概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA来指定。一个RVA只是一些项目相对于文件映射到内存的偏移。比如说,载入器把一个文件映射到虚拟地址0x10000开始的内存块。如果一个映像中的实际的表的首址是0x10464,那么它的RVA就是0x464。
(虚拟地址 0x10464)-(基地址 0x10000)=RVA 0x00464
为了把一个RVA转化成一个有用的指针,只需要把RVA值加到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址,在Win32中这是一个很重要的概念。为了方便起见,WindowsNT 和 Windows9x用模块的基地址作为这个模块的实例句柄(HINSTANCE)。在Win32中,把模块的基地址叫做HINSTANCE可能导致混淆,因为术语"实例句柄"来自16位Windows。一个程序在16位Windows中的每个拷贝得到它自己分开的数据段(和一个联系起来的全局句柄)来把它和这个程序其它的拷贝分别开来,就形成了术语"实例句柄"。在Win32中,每个程序不必和其它程序区别开来,因为他们不共享相同的地址空间。术语INSTANCE仍然保持16位windows和32位Windows之间的连续性。在Win32中重要的是你可以对任何DLL调用GetModuleHandle()得到一个指针去访问它的组件(译注)。
译注:如果 dllname 为 NULL,则得到执行体自己的模块句柄。这是非常有用的,如通常编译器产生的启动代码将取得这个句柄并将它作为一个参数hInstance传给WinMain !
你最终需要理解的PE文件的概念是"块(Section)"。PE文件中的一个块和NE文件中的一个段或者资源等价。块可以包含代码或者数据。和段不同的是,块是内存中连续的空间,而没有尺寸限制。当你的连接器和库为你建立,并且包含对操作系统非常重要的信息的其它的数据块时,这些块包含你的程序直接声明和使用的代码或数据。在一些PE格式的描述中,块也叫做对象。术语对象有如此多的涵义,以至于只能把代码和数据叫做"块"。
2 PE首部
和其它可执行文件格式一样,PE文件在众所周知的地方有一些定义文件其余部分面貌的域。首部就包含这样象代码和数据的位置和尺寸的地方,操作系统要对它进行干预,比如初始堆栈大小,和其它重要的块的信息,我将要简短的介绍一下。和微软其它可执行格式相比,主要的首部不是在文件的最开始。典型的PE文件最开始的数百个字节被DOS残留部分占用。这个残留部分是一个可以打印如"这个程序不能在DOS下运行!"这类信息的小程序。所以,你在一个不支持Win32的系统中运行这个程序,便可以得到这类错误信息。当载入器把一个Win32程序映射到内存,这个映射文件的第一个字节对应于DOS残留部分的第一个字节。那是无疑的。和你启动的任一个基于Win32 的程序一起,都有一个基于DOS的程序连带被载入。和微软的其它可执行格式一样,你可以通过查找它的起始偏移来得到真实首部,这个偏移放在DOS残留首部中。WINNT.H头文件包含了DOS残留程序的数据结构定义,使得很容易找到PE首部的起始位置。e_lfanew 域是PE真实首部的偏移。为了得到PE首部在内存中的指针,只需要把这个值加到映像的基址上即可。
file://忽略类型转化和指针转化 ...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你有了PE主首部的指针,游戏就可以开始了!PE主首部是一个IMAGE_NT_HEADERS的结构,在WINNT.H中定义。这个结构由一个双字(DWORD)和两个子结构组成,布局如下:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
标志域用ASCII表示就是"PE/0/0"。如果在DOS首部中用了e_lfanew域,你得到一个NE标志而不是PE,那么这是16位NE文件。同样的,在标志域中的LE表示这是一个Windows3.x 的虚拟设备驱动程序(VxD)。LX表示这个文件是OS/2 2.0文件。
PE  DWORD标志后的是结构 IMAGE_FILE_HEADER 。这个域只包含这个文件最基本的信息。这个结构表现为并未从它的原始COFF实现更改过。除了是PE首部的一部分,它还表现在微软Win32编译器生成的COFF OBJ 文件的最开始部分。IMAGE_FILE_HEADER的这个域显示在下面:
表2  IMAGE_FILE_HEADER Fields
WORD Machine
表示CPU的类型,下面定义了一些CPU的ID
0x14d Intel i860
0x14c Intel I386 (same ID used for 486 and 586)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP

 

WORD NumberOfSections
这个文件中的块数目。

 

DWORD TimeDateStamp
连接器产生这个文件的日期(对OBJ文件是编译器),这个域保存的数是从1969年12月下午4:00开始到现在经过的秒数。

 

DWORD PointerToSymbolTable
COFF符号表的文件偏移量。这个域只用于有COFF调试信息的OBJ文件和PE文件,PE文件支持多种调试信息格式,所以调试器应该指向数据目录的IMAGE_DIRECTORY_ENTRY_DEBUG条目。

 

DWORD NumberOfSymbols
COFF符号表的符号数目。见上面。

 

WORD SizeOfOptionalHeader
这个结构后面的可选首部的尺寸。在OBJ文件中,这个域是0。在可执行文件中,这是跟在这个结构后的IMAGE_OPTIONAL_HEADER结构的尺寸。

 

WORD Characteristics
关于这个文件信息的标志。一些重要的域如下:

 

0x0001 这个文件中没有重定位信息
0x0002 可执行文件映像(不是OBJ或LIB文件)
0x2000 文件是动态连接库,而非程序

 

其它域定义在WINNT.H中。
PE首部的第三个组成部分是一个IMAGE_OPTIONAL_HEADER型的结构。对PE文件,这一部分当然不是"可选的"。COFF格式允许单独实现来定义一个超出标准IMAGE_FILE_HEADER附加信息的结构。IMAGE_OPTIONAL_HEADER里面的域是PE的实现者感到超出IMAGE_FILE_HEADER基本信息以外非常关键的信息。
并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(见图4)。比较重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。
表3  IMAGE_FILE_HEADER 的域:
WORD Magic
表现为一些类别的标志字,通常是0X010B 。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
生成这个文件的连接器的版本。这个数字以十进制显示比用十六进制好。一个典型的连接器版本是2.23。

 

DWORD SizeOfCode
所有代码块的进位尺寸。通常大多数文件只有一个代码块,所以这个域和 .TEXT 块匹配。

 

DWORD SizeOfInitializedData
已初始化的数据组成的块的大小(不包括代码段)。然而,和它在文件中的表现形式并不一致。

 

DWORD SizeOfUninitializedData 
<script type="text/javascript"><!--google_ad_client = "pub-7514281633968915";google_ad_width = 468;google_ad_height = 60;google_ad_format = "468x60_as";google_ad_type = "text";google_ad_channel = "";google_color_border = "FFFFFF";google_color_bg = "FFFFFF";google_color_link = "5C7C04";google_color_text = "597901";google_color_url = "597901";//--></script><script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script> 载入器在虚拟内存中申请空间,但在磁盘上的文件中并不占用空间的块的尺寸。这些块在程序启动时不需要指定初值,因此术语名就是"未初始化的数据"。未初始化的数据通常在一个名叫 .bss  的块中。

 

DWORD AddressOfEntryPoint
载入器开始执行这个程序的地址,即这个PE文件的入口地址。这是一个RVA,通常在  .text  块中。

 

DWORD BaseOfCode
代码块起始地址的RVA 。在内存中,代码块通常在PE首部之后,数据块之前。在微软的连接器产生的EXE文件中,这个值通常是0x1000 。Borland 的连接器 TLINK32  也一样,把映像第一个代码块的RVA和映像基址相加,填入这个域。
 译注:这个域好像一直没有什么用

 

DWORD BaseOfData
数据块起始地址的RVA 。在内存中,数据块经常在最后,在PE首部和代码块之后。
译注:这个域好像也一直没有什么用

 

DWORD ImageBase
连接器创建一个可执行文件时,它假定这个文件被映射到内存中的一个指定的地方,这个地址就存在这个域中,假定一个载入地址可以使连接器优化以便节省空间。如果载入器真的把这个文件映射到了这个地方,在运行之前代码不需要任何改变。在为WindowsNT 创建的可执行文件中,默认的ImageBase 是0x10000。对DLL,默认是0x40000。在Window95中,地址0x10000不能用来载入32位EXE文件,因为这个区域在一个被所有进程共享的线性地址空间中。因此,微软把Win32可执行文件的默认基址改为0x40000,假定基址为0x10000 的老程序坐在Windows95 中需要更长的载入时间,这是因为载入器需要重定位基址。
译注:这个域即"Prefered Load Address",如果没有什么意外,这就是该PE文件载入内存后的地址。

 

DWORD SectionAlignment
映射到内存中时,每个块都必须保证开始于这个值的整数倍。为了分页的目的,默认的SectionAlignment 是 0x1000。

 

DWORD FileAlignment
在PE文件中,组成每个块的生鲜数据必须保证开始于这个值的整数倍。默认值是0x200 字节,也许是为了保证块都开始于一个磁盘扇区(一个扇区通常是 512 字节)。这个域和NE文件中的段/资源对齐(segment/resource alignment)尺寸是等价的。和NE文件不同的是,PE文件通常没有数百个的块,所以,为了对齐而浪费的通常空间很少。

 

WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
这个程序运行需要的操作系统的最小版本号。这个域有点含糊,因为Subsystem 域(后面将会说到)可以提供类似的功能。这个域在到目前为止的Win32中默认是1.0。

 

WORD MajorImageVersion
WORD MinorImageVersion
一个可由用户定义的域。这允许你有不同的EXE和DLL版本。你可以通过链接器的 /version 选项设置这个域的值。例如:"link  /version:2.0  myobj.obj"。

 

WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
这个程序运行需要的最小子系统版本号。这个域的一个典型值是3.10 (表示WindowsNT 3.1)。

 

DWORD Reserved1
通常是 0 。

 

DWORD SizeOfImage
载入器必须关心的这个映像所有部分的大小总和。是从映像的开始到最后一个块结尾这段区域的大小。最后一个块结尾按SectionAlignment进位。
 译注:这个很重要,可以大,但不可以小!

 

DWORD SizeOfHeaders
PE首部和块表的大小。块的实际数据紧跟在所有首部组件之后。

 

DWORD CheckSum
这个文件的CRC校验和。在微软可执行格式中,这个域被忽略并且置为0 。这个规则的一个例外情况是信任服务,这类EXE文件必须有一个合法的校验和。

 

WORD Subsystem
可执行文件的用户界面使用的子系统类型。WINNT.H 定义了下面这些值:
NATIVE  1  不需要子系统(比如设备驱动)
<script type="text/javascript"><!--google_ad_client = "pub-7514281633968915";google_ad_width = 468;google_ad_height = 60;google_ad_format = "468x60_as";google_ad_type = "text";google_ad_channel = "";google_color_border = "FFFFFF";google_color_bg = "FFFFFF";google_color_link = "5C7C04";google_color_text = "597901";google_color_url = "597901";//--></script><script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script>