An In-Depth Look into the Win32 Portable Executable File Format

来源:互联网 发布:centos 设置ip地址 编辑:程序博客网 时间:2024/05/01 16:39

Matt Pietrek 
This article assumes you're familiar with C++ and Win32 
Level of Difficulty     1   2   3  
Code download available from the MSDN Code Gallery 
概要:对PE文件格式良好的理解,将会有助于更好的理解windows操作系统。如果你知道在exe/dll 文件内部到底是怎么养的,那么你将会成为一个更富有知识的程序员。该文是系列文章两篇中的一篇,主要讨论了在过去的几年中,pe文件格式发生了怎样的变化,并且对pe文件本身做了一个概述。

    在该文中,作者讨论了pe文件格式是如何适应基于.NET平台的应用程序,pe文件段,相对虚拟地址,数据目录,导入函数。
 
     很久以前,在遥远的银河系,我为Microsoft Systems Journals(也就是现在的msdn杂志)撰写了第一批文章。"Peering Inside the PE: A Tour of the Win32 Portable Executable File Format",这篇文章超出我预期的流行起来。直到今天,我听说甚至是微软内部的人仍然在使用那篇文章,该文在msdn中仍旧可以找到。但不幸的是,(文章不变,技术在变...)文章是静态的,是不会随着时间的推移变化的,在过去的几年中,win32平台上的技术却发生了翻天覆地的变化。那篇文章已经有点过时了。其中的一些观点也都不再正确了。从这个月开始,我将用两篇文章来补救这个问题。
      你可能会想,为什么我需要关心可执行文件(pe)的格式呢?答案和过去一样,操作系统的可执行文件格式和数据结构,从一个侧面揭示出操作系统内在的好多东西。通过理解exe文件/dll文件中的结构,你将会发现,你已经变成一个更出色的程序设计人员了。
       当然,你可以通过阅读微软的技术规格说明书来了解到比我将要讲述的东西更多,更全面的内容。但是,就像大多数规格说明书一样,这些说明书为了全面,都牺牲了可读性,太过的概括,抽象。在本文中,我将集中探讨(如何做,为这么这么做,Hows and whys)和pe文件格式最相关的东西,这些探讨可能和微软的技术规格说明书并不完全一致。此外,在本中的一些观点,并不会出现在微软官方的技术文档中。
Bridging the Gap

      下面将举例说明从我在1994年写第一篇关于pe的文章开始,window平台发生了那些重大的变化。因为16位的windows操作系统已经作古,已经没有必要在pe文件格式与win16平台上的New Executable format之间进行比较了。win32与win16平台有了很大的差异,不过这个差异是我们希望看到的,尽管在windows3.1上运行win32的二进制文件是很不稳定的。
       想当年,win95(开发代号为芝加哥)还没有发布。win nt仍然处于3.5版本,开发链接器的大牛们尚没有主动的开始优化他们的链接器。不过那时候支持mips与dec alpha的windows nt系统已经存在了。
      从那篇文章开始,在windows平台上,到底出现了哪些新的事物呢?64位的windows引入了自己的pe文件格式。windows ce增加了对许多处理器的支持。诸如延迟加载dll的技术,段合并,地址绑定等技术开始崭露头角。还有许多其他的事物在windows的历史当中出现了。
      别忘了.NET平台。她又处于何种位置呢?对于操作系统而言,.NET可执行文件与老的win32可执行文件没啥区别,然而,.NET运行时能够将这些可执行文件中的数据识别为元数据和中间语言,这两样儿都是.NET中核心的东西。在该文中,我讲探索.NET的元文件格式,但详细的讨论将放到以后的文章中进行。
      如果上述所说的内容还不能够是我下定决心,付出额外的努力来完成这篇新的文章的话,那么老文章中的许多错误,疏漏也使得我很没面子。例如,以前我对线程局部存贮的讨论在现在看来就有些问题了。同样的,关于在pe文件中使用的dword大小的日期/时间戳只有你的时区在太平洋时区才会是正确的。
      另外,好多在当时正确的东西,现在看来都错了。我当时说.rdata段其实不会用于存放啥重要的东西。但是,在现在,它居然真的存放重要的东西了。我又说.idata段是可读/可写属性的,但在今天,视图进行api拦截的哥们儿们证明是错误的了。
      除了对pe文件格式的陈述进行了完整的升级外,我还升级了PEDUMP这个小程序,该程序可以用于显示pe文件的内容。它可以完美的运行于32/64位的平台上,可以用于显示32/64位的可执行文件的内容。更重要的是,我对他开源了,你在阅读本文的时候,可以参考其中的代码,来对一些概念有个更清晰,更透彻的理解。

Overview of the PE File Format

      在原始的win32技术规格说明书中,微软引入了pe文件格式,也就是传说中的PE 格式。其实,pe文件是从更早的(通用对象文件格式)coff发展而来的。之所以这么说,是因为原来win nt开发团队中大多数人来自于Digital Equipment Corporation。这些人当然会基于他们已有的代码来构建基于win nt 平台上的pe文件格式啦。

      之所以选择“Portable Executable”(可移植可执行)这个名字,是因为当初的初衷就是让pe这种文件格式能够通吃windows的各个平台,通吃各种被支持的cpu。在原有的coff格式上做了巨大的扩展后,目标达成了,pe格式诞生了。它可以运行于win nt以及后续的版本,win95以及后续的版本,连win ce也可以。
      由Microsoft得编译器生成的OBJ文件(目标文件)就是使用的coff格式。通过查看它的一些字段,你会发现coff文件格式是多么的古老,这些字段是用八进制编码的(骨灰级的文件还是用八进制)。coff文件与pe文件共用一些数据结构/枚举。我将在下文中提到关于他们的部分内容
      64位的windows仅需对pe文件格式做稍许的修改就可以啦,新的pe叫做pe+。其中并没有增加新的字段,反而是删除了一个字段。剩下的改变就是将32位的字段扩充为64位的。在大多数情况下,你都可以不用关心32位到64位的转化问题,windows的头文件帮你做了一个变换。你用c++些代码就无需关系这些问题。
     exe与dll只有语义上的区别,二者极其的相似。他们都使用pe格式。仅有的区别是让操作系统如何识别他们,是exe呢,还是dll呢。dll的后缀名都可以自定义。如后缀为ocx和cpl的文件其实都是dll。
      pe文件格式的一个非常好的特性就是磁盘上的pe文件与加载到内存后的pe文件据有相同的数据结构。加载一个dll/exe文件到内存中,主要的工作就是将pe文件中的一些东东映射到内存中。因此诸如IMAGE_NT_HEADERS 这样的数据结构在内存中与在磁盘上是完全一致的。基于这个特性,如果你知道磁盘上pe文件中的一些东东,那么当他被载入内存后,你一样可以找到他们。
      很重要的一点是,pe文件不只是如内存映射文件一样被简单的映射到内存。相反,windows加载器查看pe文件并决定加载那些部分。载入内存后,高、低地址空间的内容与磁盘上的文件是一致的,磁盘上高地址的内容,也会被映射到内存的高地址。但是磁盘上的pe文件中的内容的偏移量与内存中该内容的偏移量并不一定完全一样(当然也可以一样)。
      当pe文件被载入内存后,他的身份就变啦,变成了所谓的module(模块),pe文件被映射到内存的起始地址就是所谓的HMODULE。记住这点非常的重要,给定一个hmodule,你就可以预知在那个地址存放着什么东西,有了这个超级武器,你可以进行诸如api拦截的事情了。
      内存中,模块代表了代码、数据、资源(对话框、菜单,字符串,各种图片...),这些东东来自于被加载的pe文件。pe文件中其他的一些部分可能只会被加载器读取(重定位段),并不会被真正的映射到内存,还有一些部分更倒霉啊,加载器根本就不理睬他们,诸如调试信息。理所应当,这些东西是被放到pe文件中的末尾的,哎。。。做人又何尝不是如此呢。
      在winnt.h中对pe格式进行了描述,在哪里你会找到你需要的一切,如结构啊,枚举啊,宏定义啊。当然其他地方也有相关描述,如msdn中。但winnt.h中无疑是最终的定论啦。
     检测pe文件有好多工具可用。其中有来自于visual studio的Dumpbin,来自于sdk的depends。depends非常好,以一种非常简洁的方式展现了pe文件导入/导出了哪些函数。另一个不错的工具就是PEBrowse Professional。本文提及的PEDUMP当然也是不错滴...
      从api角度来说,微软提供的,用于解析pe文件的函数都在 IMAGEHLP.DLL中。
     在开始讨论pe文件格式之前,有些概念性的东西还是要先说下滴,这些概念将贯穿本文啊。如相对虚拟地址,数据目录,函数是如何被导入的等等.....

PE File Sections

      pe文件中的段代表了某种类型的代码、数据。代码就是代码,但是数据的种类就很多了。除了可读可写的数据,还有包括api导入导入表,资源,重定位信息在内的多种数据。每一个段儿都有其一组内存属性,这些属性标示段是否可写,段中存放的是数据还是代码,是被各个进程共享还是本进程独有。
    一般来说,段中的数据或代码在某种程度上都是相关的。在一个pe文件中最起码也有代码、数据两个段吧。其实,一般来说还会有第三种类型的段存在于pe文件中。我将在下个月的栏目中继续讨论各种类型的段儿。
     每一个段都有一个特有的名字,这个名字呢用于标示该段的用途。例如,一个叫做.rdata的段存放只读数据。段名只是为了人们容易辨识,容易理解,对操作系统来说,一点儿意义也没有。微软在段之前加一个前缀,但这不是必须的。有好多年,Borland公司的链接器就将段命名为data和code,哎,多清楚啊。
      编译器有一组默认(标准)的段名。你也可以创建并命名段,链接器会很乐意的将你命名的段包含到最终的可执行文件中。在vc++中,你可以使用#pragma 语句来达到此目的,如#pragma data_seg( "MY_DATA" )使的vc++生成的所有的数据存放到名为MY_DATA的段中。而不是存放到默认的.data段中。默认的段名多数情况下可以满足需求,但是也有例外,如你有一个要求,必须将代码/数据放到特定的段中。
      链接器不是最早生成段的地方;早在编译器生成的obj文件中,段就被生成了。链接器的工作就是将所有obj文件和library所有必须的段合并到最终的可执行文件当中。例如,每一个obj文件中必定含.text段,包含代码,链接器将各个obj中所有名为.text的段提取出来,并合并到pe文件中的一个.text段中(百川归海)。来自于.lib文件中的代码和数据一会被包含到可执行文件中。
      There is a rather complete set of rules that linkers follow to decide which sections to combine and how. I gave an introduction to the linker algorithms in the July 1997 Under The Hood column in MSJ.

      obj文件中的某些段其实是用作给链接器传递信息,并不会被包含到最终的pe文件中。
      段有两个对齐(alignment)值,一个用于磁盘文件中,另一个用于内存中。pe文件头指定了这两个值,这两个值可以不同。每一个段儿都开始于该对齐值的n倍处。例如,在pe文件中,典型的对齐值为0x200。因此,每一个段开始于0x200的倍数处。(0x400,0x800...)。

      一旦pe文件被映射到内存,每一个段就总是占据一个内存页面。也就是说,当pe文件中的段被映射到内存,每个段的第一个字节对应于一个内存页。在x86平台,页大小为4KB对齐,在x64平台上,页大小是8KB对齐。如下所示:

Section Table
  01 .text     VirtSize: 00074658  VirtAddr:  00001000
    raw data offs:   00000400  raw data size: 00074800
???
  02 .data     VirtSize: 000028CA  VirtAddr:  00076000
    raw data offs:   00074C00  raw data size: 00002400


.text段开始于0x400(磁盘上的pe文件中),将会被加载到距离hmodule为0x1000的地址处。
      创建pe文件中的偏移量与载入内存后的偏移量一样的pe文件是可以的。但这会加大pe文件的体积,但是在win9x或winme上可以加快加载速度。在visual studio 6.0中引入的默认的/OPT:WIN98 链接器选项可达到上述目的。Visual Studio? .NET中会根据pe文件是不是足够小动态决定是不是要使用/OPT:NOWIN98选项。
     链接器一个有趣的功能是合并段。如果两个段有相似,兼容的属性,他们就可以在链接的时候被合并为一个段。通过链接器的linker /merge 开关就可达此目的。如,下面所示就是合并.rdata和.text段为一个叫做.text的段中。

/MERGE:.rdata=.text

      段合并的好处就是节省空间,在磁盘上和在内存均是如此。每个段占据一个内存页。如果你可以将4个段合并为3个,那么就省了一个内存页面了。
      因为没有硬性的规定,如果你合并段,事情就会变得稍微复杂了。例如,可以合并.rdata段到.text段中,但是你不可以合并.rsrc,.reloc,或是.pdata段到其他段儿中。In Visual Studio .NET中这是不允许的,但在release版中,是链接器经常合并部分.idata段到其他段中,如.rdata段。
      因为导入数据部分(导入的函数,导入的变量...)是有加载器在加载阶段写入的,有可能会想,怎么能够写入只读的数据段呢?答案是加载器会在加载阶段,动态的将段的属性修改为可写,写完后改回去,真灵活啊。。。。

Relative Virtual Addresses

      当可执行文件被载入内存后,该可执行文件中的许多位置(偏移量)的有效的内存地址均需要被重新制定。例如,当你引用一个全局变量的地址时,你需要知道该全局变量的内存地址。pe文件可以被加载到进程地址空间的几乎任何地方。虽然pe文件的确含有一个首选地址(eg:exe:0x40000000),但是我们不能够总是依赖这个地址。基于这个原因,我们需要一种独立于具体加载地址的方式来指定地址。
     为了pe文件中避免硬编码,我们使用RVAs(相对虚拟地址)。RVA就是相对于pe文件被加载的首地址得一个偏移量。例如,一个exe文件被加载到0x400000,他的代码段被加载到 0x401000这个地址。那么RVA地址就是

(target address) 0x401000 - (load address)0x400000  = (RVA)0x1000.


     想要将RVA转换真正的地址,反算上述过程:用RVA+加载地址。顺便提一句,用pe的说法,真正的内存地址被称作虚拟地址。加载到内存的首地址也就是上文所说的HMODULE。
      Want to go spelunking through some arbitrary DLL's data structures in memory? Here's how. Call GetModuleHandle with the name of the DLL. The HMODULE that's returned is just a load address; you can apply your knowledge of the PE file structures to find anything you want within the module.

 

The Data Directory
     
在可执行文件中有许多数据结构需要被快速的定位。很明显的一些例子如导入,导出,资源,基地址重定位。所有这些为大家熟知的数据结构都可以以一种一致的方式查找,存放这些东东的地方就叫做数据目录。
    
数据目录就是一个含有16个结构体的数组。每一个数组元素均有一个预定义的含义,表征它的用途。IMAGE_DIRECTORY_ENTRY_ xxx #defines 是数组的索引值。

函数导入
     
当你调用其他dll中的函数或是引用其他dll中的数据时,你其实就是在导入该dll。当任何pe文件被加载的时候,windows Loader(加载器)的一项工作就是定位(地址)所有被导入的函数/数据,使的他们的地址可用。
     
当你直接链接(不用你做啥事情,有编译器、链接器替你完成)dll中的代码、数据的时候,你就是在隐式的导入这个dll。对你的代码来说,你并没有做任何事情使的导入的api的地址可用(在你的代码中可以调用api,你要知道api在进程地址空间中的地址)。windows 加载器包办了这个事情。另一个可选的方式是显示的链接。也就是会说显示的确保目标dll被加载到进程的地址空间,然后查找api函数的内存地址。显示链接是通过Loadlibrary和GetProcAddress两个函数完成的。
      When you implicitly link against an API, LoadLibrary and GetProcAddress-like code still executes, but the loader does it for you automatically. The loader also ensures that any additional DLLs needed by the PE file being loaded are also loaded. For instance, every normal program created with Visual C++? links against KERNEL32.DLL.

当你隐式的链接api的时候,类似于LoadLibrary和GetProcAddress的代码仍然执行,但是加载器替你做了这件事情。加载器确保任何被该pe文件依赖的dll均被加载。例如每一个用c++创建的程序都会链接Kernel32.dll,Kernel32.dll又会导入ntdll.dll中的函数,如果你使用了gdi32.dll中的函数,而gdi32又会依赖USER32, ADVAPI32, NTDLL, and KERNEL32 DLLs,总之windows 加载器会确保所有被导入的符号均被解决(resolved),也就是使的他们的地址可用。

      当隐式链接的时候,解决符号的过程发生在程序第一次启动的时候,如果遇到任何问题,加载就会失败。
     
vc6增加了延迟加载的功能,这是一个介于隐式链接与显示链接之间的杂合体。当你延迟加载一个dll,链接器会产生一些东西,类似于正常导入dll。然而,操作系统或略这些东西。相反,在一次对延迟加载dll中某函数的调用,有链接器添加的特殊的占位程序(stub)是的dll被加载,紧接着是调用GetProcAddress定位被调用api的地址。一些小把戏会却把以后对该api的调用就如同正常导入dll一样的高效。
     
在pe文件中,含有一个数据结构构成的数组,每一个dll对一个数据结构。每一个数据结构包含被导入的dll的名字,以及一个指向函数指针数组的指针。函数指针数据也就是所谓的导入(函数)地址表IAT。每一个被导入的api在IAT中占有一席之地,加载器会导入api占据的位置写入有效的地址。这点非常重要,一旦pe文件被加载,IAT中就包含了导入函数的有效的地址了。

      IAT的美妙之处在于,在偌大的pe文件中,导入的api仅存于此。不管你在你源代码的多少地方调用了该导入api,所有的调用都是通过存储于iat的同一个函数指针完成的。

      让我们看一下对一个导入api的调用代码是什么样子的。有两中情况要考虑,高效的方式,和相对低效的方式。在理想的情况下,一个对导入api的调用代码如下:

CALL DWORD PTR [0x00405030]

If you're not familiar with x86 assembly language, this is a call through a function pointer. Whatever DWORD-sized value is at 0x405030 is where the CALL instruction will send control. In the previous example, address 0x405030 lies within the IAT.

这是一个通过函数指针完成的函数调用。CALL指令将控制权传送到 存储于0x405030地址上、DWORD大小 的一个值 所代表的地址上(也就是先从0x405030地址取出一个dword大小的值,这个值是一个内存地址)。在上面的例子中0x405030在IAT中。

对被导入api的低效的调用时这个样子的:

CALL 0x0040100C
...
0x0040100C:
JMP       DWORD PTR [0x00405030]

在这种情况下,CALL指令转送控制权到一个小占位程序,该占位程序是一个JMP语句,跳转到地址0x405030处,再次,0x405030是IAT数组中的一项。简而言之,低效的导入api调用方式将多占用额外的5个字节,并且由于额外的jmp语句,所以需要更长的执行时间。

      你可能会想了,为什么低效的方式还会被使用呢。有一个比较好的解释。由于编译器自身的原因,他不能区分对导入api的调用和对程序本身的函数的调用。由于这个原因,编译器产生如下代码:

CALL XXXXXXXX,其中XXXXXXXX是真实的代码地址,这个地址稍后会被链接器填入。

 注意最后一个调用不是通过函数指针,而是真正的代码地址。为了保持平衡,链接器需要用一段代码来替换 XXXXXXXX。最简单的方式就是使得该调用指向一个jmp语句。
     
JMP语句来自哪里呢?出人意料的,它来自于导入函数的导入库。(生成dll的时候,配套产生一个.lib)当链接器链接各个obj文件的时候,如果发现有一个api调用,就用JMP DWORD PTR [0x00405030] 替换CALL 0x0040100C 这样的硬编码的代码地址,0x00405030是IAT中的一项,该项会在加载阶段被loader替换为真正有效地被导入函数地址!!!。默认情况下,如果不采取任何措施,都是采用低效调用。

      逻辑上,下一个问题就是会问,如何得到高效的调用呢。答案就是你需要给编译器一个hit(提示)。函数修饰符 __declspec(dllimport)告诉编译器,该函数存在于另一个dll中。编译器就会产生如下代码:

CALL DWORD PTR [XXXXXXXX]

      另外,编译器会产生些信息,告诉链接器来解决上诉指令中的函数指针部分为一个__imp_functionname形式的符号名。例如,如果你调用了MyFunction那么符号名就将会是__imp_MyFunction。查看导入库,你会发现,除了规则的符号名外,同时存在着一个以__imp__为前缀的符号名。这个__imp__ 符号名会被直接解析成IAT中的一项,而不是jmp语句。

 

      这个特性对与我们日常的编程有何启示呢?如果你编写导出函数,并且提供一个.h头文件给你的用户,记得使用 __declspec(dllimport)修饰符来修饰每个导出函数。

 

__declspec(dllimport) void Foo(void);

如果你查看windows系统头文件,你会发现他们就是使用__declspec(dllimport)来修饰Windows APIs。windows并不是直接修饰api的,是使用在Windows.h中定义的DECLSPEC_IMPORT 宏来动态的修饰windows api。查看这些头文件,就会明白如何使用这些宏了。
PE File Structure

      现在让我们钻研一下pe文件真正的格式。将从文件的起始位置开始,然后描述在每一个pe文件中都会存在的结构。然后,我将描述更具体的数据结构,如存在于pe文件段中的导入数据或是资源。这些结构定义在winnt.h中。除非特别说明。
      大多数情况下,32位的数据结构和64位的数据结构是配对儿出现的。如IMAGE_NT_HEADERS32对应IMAGE_NT_HEADERS64。除了一些在64位版本中加宽的字段,这些结构总是一致的。如果你想编写可移植的代码(32-->64),在windows.h中定义了(#define)大小不可知的别名,如IMAGE_NT_HEADERS,真正的结构依赖于你编译的时候,具体的模式(32or64),也就是_WIN64 宏有没有被定义。如果你要操作的pe文件的格式特性与你的编译的平台不同,你才需要使用具体的结构。

如你在32位平台上编译代码,而这些代码要操作64位的pe文件,那么你就需要使用具体的结构体了,而不能使用size未知的结构体(通用结构体)。
The MS-DOS Header
      
每一个pe文件以一个MS-DOS子系统开始。之所以需要这个ms-dos,是因为在windows早期,还没有大批量的用户使用windows操作系统前,如果你在没有安装windows的机器上运行该可执行程序,至少会有一个提示该程序只能运行于windows之上的提示。
      
pe文件的头几个字节是传统的MS-DOS头,叫做IMAGE_DOS_HEADER。该结构两个重要的成员是

e_magic 和e_lfanew。e_lfanew 包含pe头的偏移量。e_magic 必须被设置为 0x5A4D。该值有一个宏定义IMAGE_DOS_SIGNATURE标示。在ascii表示中, 0x5A4D 代表MZ,Mark Zbikowski的首字母,他是原始的ms-dos架构师之一。
The IMAGE_NT_HEADERS Header
      The IMAGE_NT_HEADERS structure is the primary location where specifics of the PE file are stored. Its offset is given by the e_lfanew field in the IMAGE_DOS_HEADER at the beginning of the file. There are actually two versions of the IMAGE_NT_HEADER structure, one for 32-bit executables and the other for 64-bit versions. The differences are so minor that I'll consider them to be the same for the purposes of this discussion. The only correct, Microsoft-approved way of differentiating between the two formats is via the value of the Magic field in the IMAGE_OPTIONAL_HEADER (described shortly).
      An IMAGE_NT_HEADER is comprised of three fields:
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

In a valid PE file, the Signature field is set to the value 0x00004550, which in ASCII is "PE00". A #define, IMAGE_NT_SIGNATURE, is defined for this value. The second field, a struct of type IMAGE_FILE_HEADER, predates PE files. It contains some basic information about the file; most importantly, a field describing the size of the optional data that follows it. In PE files, this optional data is very much required, but is still called the IMAGE_OPTIONAL_HEADER.
      Figure 3 shows the fields of the IMAGE_FILE_HEADER structure, with additional notes for the fields. This structure can also be found at the very beginning of COFF OBJ files. Figure 4 lists the common values of IMAGE_FILE_xxx. Figure 5 shows the members of the IMAGE_OPTIONAL_HEADER structure.
      The DataDirectory array at the end of the IMAGE_OPTIONAL_HEADERs is the address book for important locations within the executable. Each DataDirectory entry looks like this:
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;     // RVA of the data
    DWORD   Size;               // Size of the data
};

The Section Table
      Immediately following the IMAGE_NT_HEADERS is the section table. The section table is an array of IMAGE_SECTION_HEADERs structures. An IMAGE_SECTION_HEADER provides information about its associated section, including location, length, and characteristics. Figure 6 contains a description of the IMAGE_SECTION_HEADER fields. The number of IMAGE_SECTION_HEADER structures is given by the IMAGE_NT_HEADERS.FileHeader.NumberOfSections field.
      The file alignment of sections in the executable file can have a significant impact on the resulting file size. In Visual Studio 6.0, the linker defaulted to a section alignment of 4KB, unless /OPT:NOWIN98 or the /ALIGN switch was used. The Visual Studio .NET linker, while still defaulting to /OPT:WIN98, determines if the executable is below a certain size and if that is the case uses 0x200-byte alignment.
      Another interesting alignment comes from the .NET file specification. It says that .NET executables should have an in-memory alignment of 8KB, rather than the expected 4KB for x86 binaries. This is to ensure that .NET executables built with x86 entry point code can still run under IA-64. If the in-memory section alignment were 4KB, the IA-64 loader wouldn't be able to load the file, since pages are 8KB on 64-bit Windows.
Wrap-up
      That's it for the headers of PE files. In Part 2 of this article I'll continue the tour of portable executable files by looking at commonly encountered sections. Then I'll describe the major data structures within those sections, including imports, exports, and resources. And finally, I'll go over the source for the updated and vastly improved PEDUMP.
 
For related articles see:
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
For background information see:
The Common Object File Format (COFF)

 
Matt Pietrek is an independent writer, consultant, and trainer. He was the lead architect for Compuware/NuMega's Bounds Checker product line for eight years and has authored three books on Windows system programming. His Web site, at
http://www.wheaty.net, has a FAQ page and information on previous columns and articles.
 

 

而不是刚才的:
CALL XXXXXXXX

原创粉丝点击