PE 文件格式 [译文]

毕设答辩期间长时间未接触英文, 毕业后为找感觉, 翻译此文, 以供同学们参考.
walfud.
2012-7-25


$Id: pe.txt,v 1.7 1998/07/29 17:55:13 LUEVELSMEYER Exp $

 

PE 文件格式
===============

 

目的
-------
PE(通用可执行) 文件格式是 MS windows NT, 95, win32 下的二进制可执行格式, 且在 windows NT 下, 驱动也是这个格式. 它也被能够用于对象文件和库文件.

这个格式是微软设计的, 显然是基于 COFF 的良好背景知识, "常用对象文件格式" 用于 UNIX 和 VMS 中对象文件以及可执行文件.

win32 SDK 包含了一个 <winnt.h> 的头文件, 其中包含了一些 PE 格式的 #define 和 typedef. 我随后会讲述结构成员名和一些 #define.

你也许发现 "imagehelp.dll" 这个 dll 十分有用. 它是 windows NT 的一部分, 但是文档十分少. 它的一些函数在 "Developer Network" 中被描述.

 

General Layout
--------------
在 PE 文件的开始部分我们能发现 MS-DOS 可执行的 ("stub"), 这使任何 PE 文件成为一个有效的 MS-DOS 可执行文件.

在 DOS-stub 的后面, 有一个 32-bit 的 signature, signature 是一个 magic number, 0x00004550(IMAGE_NT_SIGNATURE).

然后有一个文件头(使用 COFF 格式), 标志了这个二进制文件在哪些机器上被支持, 其中有多少个 section, 被 linked 的时间, 是否是一个可执行类型还是一个 DLL 或者其它的类型. (在本文中, 可执行文件和 DLL 的差异是: DLL 不能启动, 只能被别的二进制文件使用, 而二进制文件不能够被链接为一个可执行文件).

此后, 我们便有一个 optional header(它总是有的, 但是还是叫做 optional 是因为, COFF 在 library 中使用 optional header 但是不在 object 中使用, 这就是为什么它叫做 "optional"). 这告诉了我们应该如何加载二进制文件: 起始地址, 应该被保留的堆栈总数, 数据段数据段的 size 等.

optional header 中一个有趣的部分是在 data directories 数组的尾部; 这些 directories 包含了指向 section 数据的指针. 例如, 如果一个二进制文件有一个 export directory, 那么你将能够在数组的 IMAGE_DIRECTORY_ENTRY_EXPORT 成员中发现一个指针, 指向那个 directory, 并且它将指向 sections 中的一个. (译者认为这里没有说明白, 作者的意思是 IMAGE_DIRECTORY_ENTRY_EXPORT 成员中有一个指针, 这个指针指向一个 directory, 这个 directory 里又有很多指针, 分别指向不同的 sections).

紧接着 header, 我们能发现 sections, 在 'section headers' 中介绍. 本质上讲, 程序需要执行的就是 sectinos 的内容, 并且所有的 header 和 directory 项都是为了帮助你找到 section 的. 每一个 section 都有一些关于 alignment, 其包含数据的类型("initialized data" 等等) 以及是否能被 shared 等信息的 flags, 和自身的数据. 大部分, 但不是全部的 sections 都包含一个或多个通过 optional header 中 "data  directory" 数组引用的 directories, 就像 exprted functions 的 directory 或者是 base relocation 的 directory. 有些内容没有 directory 类型, 比如, "executable code" 或是 "initialized data".

    +-------------------+
    | DOS-stub          |
    +-------------------+
    | file-header       |
    +-------------------+
    | optional header   |
    |- - - - - - - - - -|
    |                   |
    | data directories  |
    |                   |
    +-------------------+
    |                   |
    | section headers   |
    |                   |
    +-------------------+
    |                   |
    | section 1         |
    |                   |
    +-------------------+
    |                   |
    | section 2         |
    |                   |
    +-------------------+
    |                   |
    | ...               |
    |                   |
    +-------------------+
    |                   |
    | section n         |
    |                   |
    +-------------------+

DOS-stub 和 Signature
---------------------
DOS-stub 的概念从 16-bit windows 可执行文件(使用 "NE" 格式)就广为人知. stub OS/2 可执行文件, 自解压文档和其它应用程序. 对于 PE 文件, 几乎都包含大约 100 bytes 的错误输出信息, 例如 "this program needs windows NT". 通过 DOS-headers 可以认识到 DOS-stub, 它是一个 struct IMAGE_DOS_HEADER. 前 2 bytes 应该是 MZ(宏 #define IMAGE_DOS_SIGNATURE 就是这个 WORD). 你可通过尾部的 signature 来区分 PE 二进制文件和其他的 stub 二进制文件, 这个 signature 在 header 的 'e_lfanew' 成员(这是一个位于开始出 60 bytes 的 32-bit 变量). 对于 OS/2 和 windows 二进制而言, 这个 signature 是一个 16-bit 的 word; 对于 PE 文件, 它是 32-bit 的 longword, 它的值是 IMAGE_NT_SIGNATURE, 被 #define 为 0x00004550.

File Header
-----------
为了得到 IMAGE_FILE_HEADER, 确认 DOS-header 中的头两个字节是 "MZ", 然后找到 DOS-stub 的 'e_lfanew' 成员并且跳过文件一开始的这些字节. 校验你将会发现的这个 signature. 紧邻之后的就是文件头, 文件头就是一个 IMAGE_FILE_HEADER, 这些成员会从头到尾的被介绍.
第一个成员是 'Machine', 一个 16-bit 的值, 这个值表向系统表明这个二进制文件是可以运行的. 已知的有效值如下有:

    IMAGE_FILE_MACHINE_I386 (0x14c)
        适合于 Intel 80386 及以上处理器
       
    0x014d
        适合于 Intel 80486 及以上处理器
       
    0x014e
        适合于 Intel Pentium 及以上处理器
       
    0x0160
        适合于 R3000(MIPS) 处理器, 高字节序
       
    IMAGE_FILE_MACHINE_R3000 (0x162)
        适合于 R3000(MIPS) 处理器, 低字节序
       
    IMAGE_FILE_MACHINE_R4000 (0x166)
        适合于 R4000(MIPS) 处理器, 低字节序
       
    IMAGE_FILE_MACHINE_R10000 (0x168)
        适合于 R10000(MIPS) 处理器, 低字节序
       
    IMAGE_FILE_MACHINE_ALPHA (0x184)
        适合于 DEC Alpha AXP 处理器
       
    IMAGE_FILE_MACHINE_POWERPC (0x1F0)
        适合于 IBM Power PC, 低字节序

然后是 'NumberOfSections', 一个 16-bit 的值. 这个值是在这个 header 之后的 section 的数量. 我们将在后面讨论 section.

下一个是时间戳 'TimeDateStamp'(32-bit), 给出了文件创建的时间. 你能够通过这个值区分相同文件的不同 version, 甚至如果官方版本号没有修改(译著: 你也能区分它们的新旧). (时间戳的格式没有规定, 但是它必须是某种能够在相同文件中唯一标识不同版本的东西, 但是显然这个值是世界时间的 '自 1970-01-01 00:00:00 起所经过的秒数', 这个格式被大多数 C 编译器 time_t 使用.)
这个时间戳是用来绑定导入目录的, 这将会在稍后讨论.
警告: 一些编译器倾向于将时间戳设置为无意义的值.

成员 'PointerToSymbolTable' 和 'NumberOfSymbols'(都是 32-bit) 被用于调试信息. 我不知道如何解释它们, 并且我发现这个指针通常都是 0.

'SizeOfOptionalHeader'(16-bit) 仅仅是 sizeof(IMAGE_OPTIONAL_HEADER). 你能够使用它去验证 PE 文件结构的正确性.

'Characteristics' 是 16 bits 并且包含了一些列的 flags, 大多数 flags 仅仅对于 对象文件和库文件有效:

    Bit 0(IMAGE_FILE_RELOCS_STRIPPED) 被置位如果文件中没有重定向信息. 这个涉及到每个 section 在他们自己的 section 中的重定向信息; 它不用于执行, 它有重定向信息在下面要介绍的 'base relocation' 目录中.
   
    Bit 1(IMAGE_FILE_EXECUTABLE_IMAGE) 被置位如果文件时可执行的(例如, 它不是一个对象文件或一个库文件)
   
    Bit 2(IMAGE_FILE_LINE_NUMS_STRIPPED) 被置位如果行号信息被去除; 这个位在可执行文件没有用到.
   
    Bit 3(IMAGE_FILE_LOCAL_SYMS_STRIPPED) 被置位如果文件中没有关于本地符号(local symbols) 的信息. (这个位在可执行文件中没有用到.)
   
    Bit 4(IMAGE_FILE_AGGRESIVE_WS_TRIM) 被置位如果操作系统支持入侵式地通过页换出调整运行进程(进程使用的总内存)的工作集(working set). 这个位应该被置位如果是一个类似守护进程的应用程序, 这种程序大部分时间等待, 而每天只会启动一次, 或者类似的应用.
   
    Bit 7(IMAGE_FILE_BYTES_REVERSED_LO) and 15(IMAGE_FILE_BYTES_REVERSED_HI) 被置位如果文件的字节序不是机器所默认的, 所以它必须在读之前交换字节. 这个位对于可执行文件是不可靠的. (操作系统期望可执行文件是正确的字节序).
   
    Bit 8(IMAGE_FILE_32BIT_MACHINE) 被置位如果机器被期望是 32-bit 计算机. 这个位通常被置位.
   
    Bit 9(IMAGE_FILE_DEBUG_STRIPPED) 被置位如果文件中没有调试信息. 这个位在可执行文件中没有用到.
   
    Bit 10(IMAGE_FILE_REMOVEABLE_RUN_FROM_SWAP) 被置位如果应用程序不能从可移动介质中,运行 如软盘或光驱. 在这种情况下, 操作系统被建议拷贝文件到交换文件然后再执行这个拷贝.
   
    Bit 11(IMAGE_FILE_NET_RUN_FROM_SWAP) 被置位如果应用程序不能从网络上执行. 这种情况下, 操作系统被建议将文件拷贝到交换文件然后执行这个拷贝.
   
    Bit 12(IMAGE_FILE_SYSTEM) 被置位如果文件是一个系统文件, 比如是一个驱动. 这个位在可执行文件中没有用到.
   
    Bit 13(IMAGE_FILE_DLL) 被置位如果文件是一个 DLL.
   
    Bit 14(IMAGE_FILE_UP_SYSTEM_ONLY) 被置位如果文件不是为多处理器系统(就是说, 它将 crash 因为它在某些方面依赖这样一个条件: 只能有一个处理器).
   
Relative Virtual Address
--------------
PE 格式充分使用了 RVAs. RVA 亦称 "relative virtual address", 是在当你不知道 base address 的情况下, 用来描述内存地址的. 你需要在 base address 上加上这个值, 从而获得线性地址.
base address 是 PE 镜像被加载到的地址, 可能会随不同的调用而变化.

例如: 假设一个可执行文件被加载到 0x400000 并且从 RVA 0x1560 开始执行. 有效的起始就是地址 0x401560. 如果可执行文件被加载到 0x100000, 那么可执行文件的起始地址就是 0x101560.

事情变得复杂了, 因为部分 PE 文件(sections) 不一定和镜像被加载的对齐方式一致.
例如, 文件的 sections 经常对齐到 512-byte-borders, 但是镜像期望被对齐到 4096-byte-borders. 请看下面的 'SectionAlignment' 和 'FileAlignment' 小节.

所以, 如果在 PE 文件中找到指定 RVA 的信息, 你必须在镜像被加载时计算偏移, 根据文件略过那些偏移.
例如, 假设你知道可执行文件以 RVA 0x1560 开始, 并且想反编译开始处的代码. 为了在文件中找到这个地址, 你将不得不在内存中找到对齐于 4096 字节的那个 section 并且 ".code"-section 起始于内存 0x1000, 长度为 16384 字节; 于是你知道了 RVA 0x1560 在段中的偏移为 0x560. 找到文件中对齐于 512-byte-borders 的这个 section, ".code" 起始于文件偏移为 0x800 的地方, 你知道可执行文代码的起始位置是 0x800 + 0x560 = 0xd60.

很简答吧, 如果你知道它是如何工作的 :-)

Optional Header
---------------
紧随 file header 的式 IMAGE_OPTIONAL_HEADER(虽然名字是 optional, 但它总是存在). 它包含了具体如何对待 PE 文件的信息. 我们也将从上之下的介绍成员.

最开始的 16-bit-word 是一个 'Maigc'(译著: Magic Number), 据我所知, 这个值总是 0x010b.

接下来的两个字节是产生这个文件的连接器版本('MajorLinkerVersion' 和 'MinorLinkerVersion'). 这些值, 再一次说明, 不是可靠的, 也不总能正确的反映连接器的版本.
(一些连接器并不设置这些值.)
来想一下, 如果你不知道 *哪一个* 连接器被使用, 版本又有什么用呢?

接下来的 3 个 longwords (每个 32 bit) 是为了表示可执行代码的的 size ('SizeOfCode'), 已初始化数据的 size  ('SizeOfInitializedData', 也叫做 "data segment"), 和未初始化数据的 size ('SizeOfUninitializedData', 也叫做 "bbs segment"). 这些值是, 再一次说说明, 是不可靠的(例如, data segment 可续实际上被编译器或者连接器分割为很多 segments), 你可通过观察紧邻 optional header 后面的 'sections' 获得更好的 size 信息.

接下来的式 32-bit-value 的 RVA. 这个 RVA 是代码入口点的偏移 ('
AddressOfEntryPoint'). 从这里开始执行; 它是如 DLL 的 LibMain() 或者程序的启动代码 (它会接下来嗲用 main()) 或者是驱动的 DriverEntry().
如果你胆敢 "手动" 加载镜像, 在完成所有的修补和重定向工作后, 你调用这个地址可以启动进程.

接下来的 32-bit-value 是可执行代码 ('BaseOfCode') 和已初始化数据 ('BaseOfData') 的偏移, 两者都类似 RVA, 两者都没有太多意义, 因为你能够在 header 后面的 'sections' 中获得更加可靠的信息.
没有未初始化数据的偏移, 因为未被初始化, 所以镜像很少提供这些数据.

下一个入口是一个 32-bit-value, 它是 RVA 给出的所希望被加载的地址 ('ImageBase'). 这是文件被连接器重定向到的地址; 如果二进制真能够被加载到这个地址, 那么 loader 就不需要再进行重定向这个文件了, 这样会节省很大一部分加载时间.
如果地址已被某个镜像占用 (地址冲突, 如果你加载了多个使用连接器某人重定向的 DLL), 或者这块内存被其它目的占用 (stack, malloc(), 未初始化数据或者其它), 而这个地址是另一个镜像所期望加载的地址, 那么这个所期望的地址就不能被这个镜像使用. 在这种情况下, 镜像必须被加载到其它的地址并且需要重定向 (看下面的 'relocateion directory'). 这具有更深远的问题如果竟像是一个 DLL, 因为这样的话 "bound imports" 不再有效, 并且不得不通过修补这个二进制文件来使用 DLL - 请看下面的 'import directory'.

下面的 2 个 32-bit-value 是 PE 文件中 section 在 RAM  中 ('SectionAlignment', 当镜像被加载) 和在文件中 ('FileAlignment') 的 alignment. 通常两个值都是 32, 或者 FileAlignment 是 512 且 SectionAlignment 是 4096. Section 稍后将会讨论.

接下来的 2 个 16-bit-word 是期望的操作系统版本 ('MajorOperatingSystemVersion' 和 'MinorOperatingSystemVersion'). 这些版本信息想成为操作系统的版本, 但正如子系统版本 (如 Win32) 一样; 它总是不提供, 或者提供错误的. 加载器显然不会使用它.

接下来的 2 个 16-bit-word 是二进制文件的版本, ('MajorImageVersion' 和 'MinorImageVersion'). 许多连接器不正确的设置这个信息, 并且许多编程人员不厌烦去提供他, 所以最好信赖 version-resource 如果它存在. (译著: 真不知道原作者想要说什么.)

接下来的 2 个 16-bit-word 是希望的子系统版本 ('MajorSubsystemVersion' 和 'MinorSubsystemVersion'). 这些应该是 Win32 版本或者 POSIX 版本, 因为 16-bit 程序或者 OS/2 程序显然是没有 PE 结构的.
这个子系统版本应该正确的被提供, 因为它被检验并被使用:
如果应用程序是一个 Win32-GUI-application 并且运行于 NT, 且子系统版本不是 4.0, 则对话框将不会是 3D 样式, 其它的特征也会工作在老样式, 因为应用程序期望运行于使用 program manager 而不是 explorer 等等的 NT 3.51, 此时 NT 4.0 会忠实的模仿老版本行为.

然后是 32 bit 的 'Win32VersionValue'. 我不知道它有什么用处. 在我所有观察过的 PE 文件中它都是 0.

下一个是 32-bit-value, 给出了镜像所需要的内存总量, 单位是 bytes ('SizeOfImage'). 这是所有 section 长度的总和. 它提示了加载器需要多少个 page 才能够装载镜像.

下一个是 32-bit-value, 给出了所有 header 的总长, 包括了 data directory 和 section header ('SizeOfHeaders'). 它也同时是从文件的开始到第一个 section 的 raw data 的偏移.


然后是 32-bit 的 checksum ('CheckSum'). 这个 checksum 是为了当前 NT 的版本, 仅仅检验这个景象是否是一个 NT-driver (如果 checksum 不正确, 这个 driver 就加载失败).
计算 checksum 的算法是微软所有权的, 他们不会告诉你. 无论如何, 很多 Win32 SDK 工具将会计算一个有效的 checksum, 并且 imagehelp.dll 中的 CheckSumMappedFile() 也能够做同样的事.
checksum 是为了放至加载可能导致 crash 的二进制文件 - 然而易 crash 的 driver 将会导致 BSOD, 所以最好还是不要加载它.

这里有一个 160bit-word 的 'Sybsystem', 告诉了你镜像将运行在哪个 NT 子系统上:

    IMAGE_SUBSYSTEM_NATIVE (1)
        二进制不需要子系统. 它是一个 driver.
   
    IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
        这个镜像是一个 Win32 图形化的二进制文件. (它仍然能够通过 AllocConsole() 打开一个控制台, 病史不会自动进行.)
   
    IMAGE_SUBSYSTEM_WINDOWS_CUI (3)
        这个镜像是一个 Win32 控制台二进制文件. (它启动后默认将会得到一个控制台.)
   
    IMAGE_SUBSYSTEM_OS2_CUI (5)
        这个二进制是一个 OS/2 控制台文件. (OS/2 二进制将会是 OS/2 结构, 所以这个值在 PE 文件中很少使用.)
   
    IMAGE_SUBSYSTEM_POSIX_CUI (7)
        这个二进制使用 POSIX 控制台子系统.
   
Windows 95 二进制文件总是使用 Win32 子系统, 所以对于这些二进制而言, 只有 2 和 3 这两个值是合法的; 我不知道 "native" 二进制文件在 Windows 95 上是否可行.

接下来是一个 16-bit-value, 告诉我们这个镜像是否是一个 DLL, 当调用 DLL 的入口时 ('DllCharacteristics'). 这似乎没什么用; 显然 DLL 总是通知每一件事.
    如果 bit 0 被置位, DLL 被通知 process attachment (如 DLL 加载).
    如果 bit 1 被置位, DLL 被通知 thread detachment (如线程结束).
    如果 bit 2 被置位, DLL 被通知 thread attachment (如线程创建).
    如果 bit 3 被置位, DLL 被通知 process detachment (如 DLL 卸载).
   
接下来 4 个 32-bit-value 是 reserved stack 的 size ('SizeOfStackReserve'), initially committed stack size ('SizeOfStackCommit'), reserved heap size ('SizeOfHeapReserve') 以及 committed heap size ('SizeOfHeapCommit').
'reserved' (保留的) 数目是地址空间 (不是实际 RAM) 是为了特殊目的保留的; 在程序启动时, 实际分配的内存是 'committed' 数目. 'committed' 值也是当需要时 stack 和 heap 增长的步长.
所以, 举一个例子, 如果程序有 1 MB 的 reserved heap 和一个 64 KB 的 committed heap, heap 将会以 64 KB 开始, 并且最大增长到 1 MB. 这个 heap 将会以 64 KB 为单位增长.
这里所说的的 'heap' 是 primary (default) heap.
DLL 没有自己的 stack 或者 heap, 所以这个事对于它们是被忽略的.

在这些 stack- 或者 heap-描述后, 我们找打了 32 bit 的 'LoaderFlags', 这个我并没有发现任何有用的描述. 我只发现: 你能够设置某些 bit 实现在加载镜像后自动调用断点或者调试; 无论如何, 这些貌似都没有工作.

然后我们发现了 32 bit 的 'NumberOfRvaAdnSizes', 这个是一个下述的 directory 中有效入口的数量. 我已发现这个值并不可信; 你也许能够希望使用常量 IMAGE_NUMBEROF_DIRECTORY_ENTRIES 来代替, 或者两者都不.

在 'NumberOfRvaAndSizes' 后是一个数组 IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16) IMAGE_DATA_DIRECTORY.
每一个 directory 描述了一些特殊的 location (叫做 'VirtuallAddress' 的 32 bit RVA) 和 size (也是 32 bit, 叫做 'Size') 信息, 这些信息存在于一个在 directory entry 后面的 section 中.
例如, security directory 在 RVA 中发现并且其 size 在 index 4 中.
我所知道的 directory 的 structure 将会在稍后讨论.
得以 directory 的 index 如下:

    IMAGE_DIRECTORY_ENTRY_EXPORT (0)
        导出 symbol 的 directory; 大多数用于 DLL.
        描述见下.
   
    IMAGE_DIRECTORY_ENTRY_IMPORT (1)
        导入 symble 的 directory; 见下.
   
    IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
        资源的 directory. 描述见下.
   
    IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
        异常 directory - 结构和目的未知.
   
    IMAGE_DIRECTORY_ENTRY_SECURITY (4)
        安全 directory - 结构和目的未知.
   
    IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
        Base relocation table - 见下.
   
    IMAGE_DIRECTORY_ENTRY_DEBUG (6)
        debug directory - 内容依不同编译器而定. 而且, 许多编译器将调试信息写到 code section 中, 而不单独建立一个 section.
   
    IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
        描述字符串 - 一些任意的版权信息或者类似的内容.
   
    IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
        Machine Value (MIPS GP) - 结构和目的未知.
   
    IMAGE_DIRECTORY_ENTRY_TLS (9)
        线程局部存储 directory - 结构未知; 包含了 "declspec(thread)" 声明的变量.
   
    IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
        加载配置 directory - 结构和目的未知.
        Load configuration directory - structure and purpose unknown.
   
    IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
        受限导入 (译著: 原文是 Bound import) directory - 请看 import directory 的描述.
   
    IMAGE_DIRECTORY_ENTRY_IAT (12)
        Import address table - 结构和目的未知.
       
举个例子, 如果我们在 index 7 发现了 2 longwords 0x12000 和 33, 并且家在地址是 0x10000, 我们知道版权数据是在 0x10000 + 0x12000 (无论在什么 section 中), 版权信息的长度是 33 bytes.
如果一个特殊类型的二进制文件没有使用 directory, 那么 Size 和 VirtualAddress 都是 0.

Section directory
-----------------

这个 section 中包含了两个主要部分: 第一, 一个 section description (IMAGE_SECTION_HEADER 类型) 以及 raw section data. 所以在 data directory 后我们发现了一个数组 'NumberOfSections' section header.

一个 section header 实际上是一个非常 *小* 的结构, 它包含:

一个决定 section name (ASCII) 的数组 IMAGE_SIZEOF_SHORT_NAME (8 bytes). 如果所有的 8 bytes 都被使用, 则这个字符串就没有 0-terminator 了! name 通常都是像 ".data" 或者 ".text" 或者 ".bbs". 这里并不需要开头的 '.', name 也可以是 "CODE", 或者 "IAT" 或者类似的.
请注意, name 跟 section 的内容完全没有关系. 一个叫做 ".code" 的 section 有可能或有可能不包含可执行的代码; 他也许仅仅包含了 import address table; 他也许页包含了代码 *和* address table *和* 已初始化的数据.
为了找到 section 的信息, 你将不得不通过 optional header 中的 data directorys 查找它. 不要相信 name, 也不要假设 section 的 raw data 起始于 section 的开头部分.

IMAGE_SECTION_HEADER 的下一个成员是一个 32-bit-union 的 'PhysicalAddress' 和 'VirtualSize'. 在 Object File 中, 这是内容被重定向到的地址; 在可执行文件中, 它是内容的 size. 事实上, 这个字段似乎没什么用; 连接器来输入 size 和 address, 并且我发现连接器输入了 0, 所有的可执行文件运行的也很好.

下一成员是 'VirtualAddress', 一个 32-bit-value, 存储了 RVA. (译著: 原文是 a 32-bit-value holding the RVA to the section's data when it is loaded in RAM)

然后是 32 bits 的 'SizeOfRawData', 是 section data 的 size, 向上取整到下一个 'FileAlignment' 的整数倍.

下一个是 'PointerToRawData' (32 bits), 非常有用, 因为它是从文件起始到 section data 的偏移. 如果它是 0, 那么文件中就不包含 section data. (译著: 原文为 If it is 0, the section's data are not contained in the file and will be
arbitrary)

再然后是 'PointerToRelocations' (32 bits) 和 'PointerToLinenumbers' (也是 32 bits), 'NumberOfRelocations' (16 bits) 以及 'NumberOfLinenumbers' (也是 16 bits). 所有的这些信息置位 object file 使用. 可执行文件有专门的 base relocation directory, 以及 line number 信息, 如果全部都存在, 那么通常是在一个特殊目的的调试 segment 中或者其它地方.

section header 中的最后一个成员是 32 bits 的 'Characteristics', 是一堆描述如何对待 section 的内存的标志:

    如果 bit 5 (IMAGE_SCN_CNT_CODE) 被置位, 则 section 中含有可执行代码.

    如果 bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA) 被置位, 则 section 中包含在开始执行前就已经初始化过的数据. 换句话说: 这些 setction 的数据是有意义的.
   
    如果 bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) 被置位, 则 section 中的数据时未初始化过的, 并且在执行前夕将会被全部写为 0. 这通常是 BBS.

    如果 bit 9 (IMAGE_SCN_LNK_INFO) 被置位, 则 section 不包含除注释和描述文档以外的数据.

    如果 bit 11 (IMAGE_SCN_LNK_REMOVE) 被置位, 则数据可执行文件被连接时留下来的 object file 的 setion 的一部分.

    如果 bit 12 (IMAGE_SCN_LNK_COMDAT) 被置位, 则 section 包含 "common block data", 是被某种排序方式包装的功能.

    如果 bit 15 (IMAGE_SCN_MEM_FARDATA) 被置位, 我们需要 far data - 不管它是什么意思. 这个位的意义是不确定的.

    如果 bit 17 (IMAGE_SCN_MEM_PURGEABLE) 被置位, section 的数据是可清除的 - 但我不认为它与 "discardable" 是同一个意思, 后者有自己的 bit, 请看下面.
    相同的 bit 显然是用于标识 16-bit-informatin, 因为它在这里还有一个 IMAGE_SCN_MEM_16BIT 的 define.
    这个位的意义是不确定的.

    如果 bit 18 (IMAGE_SCN_MEM_LOCKED) 被置位, 难道 section 不能被移动到 内存中么? 这个位的意义不确定.

    如果 bit 19 (IMAGE_SCN_MEM_PRELOAD) 被置位, section 应该在执行前被加载到页(译者注: 加载到内存)中么? 这个位的意义不确定.

    Bit 20 至 23 指定了 alignment, 但是我没有关于这方面的信息. 有 IMAGE_SCN_ALIGN_16BYTES 以及类似的 define. 我到现在为止所见过的值仅仅是 0, 这个是默认 16-byte-alignment. 我怀疑这是 library 或者类似的文件中 object 的 alignment.

    如果 bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) 被置位, section 中包含了一些扩展的 relocation, 但是我对它并不了解.

    如果 bit 25 (IMAGE_SCN_MEM_DISCARDABLE) 被置位, sectin 的数据在进程启动后就不再需要了. 例如, relocation 信息就是这样. 我也曾看到只执行一次驱动和服务的启动程序.

    如果 bit 26 (IMAGE_SCN_MEM_NOT_CACHED) 被置位, section 中的数据不应该被缓存. 不要为我问什么不. 这意味着关闭二级缓存么?

    如果 bit 27 (IMAGE_SCN_MEM_NOT_PAGED) 被置位, 则 section 中的数据不能被 page out. 这对驱动来说也许有意义.

    如果 bit 28 (IMAGE_SCN_MEM_SHARED) 被置位, section 中的数据对所有运行的镜像实例共享. 例如, 如果 DLL 中已初始化的数据, 所有运行的 DLL 实例在任何时候都有相同的变量内容.
    注意, 仅仅第一个实例的 section 被初始化.
    section 中包含的 code 总是共享的.

    如果 bit 29 (IMAGE_SCN_MEM_EXECUTE) 被置位, 则这个 section 的内存被进程赋予了 'execute'-权限.

    如果 bit 30 (IMAGE_SCN_MEM_READ) 被置位, section 的内存被进程赋予了 'read'-权限.
   
    如果 bit 30 (IMAGE_SCN_MEM_READ) 被置位, section 的内存被进程赋予了 'write'-权限.
   
在 section header 后我们发现了 section 本身. 他们是, 在文件中, 对齐到 'FileAlignment' 字节(就是说, 在 optional header 以及每一个 section 后面都有填充字节). 当被加载 (到 RAM), section 被对齐到 'SectionAlignment' 字节.

比如, 如果 optional header 结束于文件的 981 并且 'FileAlignment' 是 512, 第一个 section 将开始于 1024. 注意你能够通过 'PointerToRawData' 或者 'VirtualAddress' 发现 section, 所以事实上几乎没有必要因 alignment 而焦虑.

我将尽力对所有内容进行一个图像化:

    +-------------------+
    | DOS-stub          |
    +-------------------+
    | file-header       |
    +-------------------+
    | optional header   |
    |- - - - - - - - - -|
    |                   |----------------+
    | data directories  |                |
    |                   |                |
    |(RVAs to direc-    |-------------+  |
    |tories in sections)|             |  |
    |                   |---------+   |  |
    |                   |         |   |  |
    +-------------------+         |   |  |
    |                   |-----+   |   |  |
    | section headers   |     |   |   |  |
    | (RVAs to section  |--+  |   |   |  |
    |  borders)         |  |  |   |   |  |
    +-------------------+<-+  |   |   |  |
    |                   |     | <-+   |  |
    | section data 1    |     |       |  |
    |                   |     | <-----+  |
    +-------------------+<----+          |
    |                   |                |
    | section data 2    |                |
    |                   | <--------------+
    +-------------------+
   
每个 section 都有一个 section header, 每一个 data directory 将指向一个 section (可能有多个 data directory 指向同一个 section, 也可能有 section 没有 data directory 指向它们).

Sections' raw data
------------------

综述
----
所有的 section 在文件中对齐到 'FileAlignment', 在加载到 RAM 的时候都对齐到 'SectionAlignment'. 这些 section 在 section header 中的 entry 所描述: 你能够通过 'PointerToRawData' 发现文件中的 setion, 并通过 'VirtualAddress' 发现 内存中的 setion; 长度在 'SizeOfRawData' 中.

有很多种 section, 这取决于这些 section 中存的是什么. 绝大部分 (但不是所有的) section 中至少有一个 data directory, 并且在 optional header 的 data directory 数组中有一个指针指向它.

code section
------------
首先, 我将讲述 code section. 这个 section 至少有 'IMAGE_SCN_CNT_CODE', 'IMAGE_SCN_MEM_EXECUTE' 以及 'IMAGE_SCN_MEM_READ' 这些 bit 被置位, 并且 'AddressOfEntryPoint' 将指向 section 中某个位置, 这个位置是开发者期望开始运行的地方.
'BaseOfCode' 通常指向这个 section 的开始, 但是如果在代码开始之前填充了 non-code-byte 的话, 它也许会指向填充物后面的代码开始的位置.
通常, 这个 section 中只有代码没有别的, 并且只有一个 section, 但是不要依赖着一点.
典型的 section name 是 ".text", ".code", "AUTO" 或者类似的.

data section
------------
下一件事是讨论已初始化变量; 这个 section 包含了已初始化的局部静态变量 (如 'static int i = 5;'). 这个 section 拥有至少 'IMAGE_SCN_CNT_INITIALIZED_DATA', 'IMAGE_SCN_MEM_READ' 以及 'IMAGE-SCN_MEM_WRITE' 这些标识位. 某些连接器也许会向这个 section 中放至一些它们自己的常量, 这些常量不具有写标识位. 如果不分数据是可共享的, 或者具有其它特征, 那么就会有更多的 section 分别具有不同的标志位.
这些 section 的范围是 'BaseOfData' 到 'BaseOfData' + 'SizeOfInitializedData'.
典型的 section name 是 ".data", ".idata", "DATA" 等等.

bbs section
-----------
然后是未初始化数据 (如 'static int k;'); 这个 section 十分类似已初始化数据 section, 但是它在文件中的偏移是 0, 这表明它的内容没有保存在文件中, 并且 'IMAGE_SCN_CNT_UNINITIALIZED_DATA' 而不是 'IMAGE_SCN_CNT_INITIALIZED_DATA' 被置位, 这又表明变量的内容应该在加载的时候被设置为 0.
长度是 'SizeOfUninitializedData'.
通常 name 是 ".bbs", "BBS" 或者类似的.

这些 section data 是没有被 data directory 所指向的. 它们的内容和结构是编译器提供的, 不是连接器.
(stack-segment 和 heap-segment 不是二进制文件中的 section, 但是在家在的时候通过 optional header 中的 stacksize- 和 heapsize-entry 被创建.)

copyright
---------
从一个简单的 directory-section 开始, 让我们看一看 data directory 'IMAGE_DIRECTORY_ENTRY_COPYRIGHT'. 内容就是一个 ASCII 字符串的 copyright- 或者 description (并非 0-terminated), 例如 "Gonkualator control application, copyright (c) 1848 Hugendubel & Cie". 这个字符串通常是与 command line 一起提供给连接器. 这个字符串在运行时就不需要了并且可能被丢弃. 它不可写, 事实上, 应用程序根本就不需要存取它.
所以连接器会查找是否已经有一个可丢弃但不可写的 section, 如果没有则创建一个 (name 是 ".descr" 或者类似的). 然后将填充素材写到这个 section 中, 并且是 copyright-directory-pointer 指向这个 string. 'IMAGE_SCN_CNT_INITIALIZED_DATA' bit 也应该被置位.

exported symbols
----------------
下一个最简单的事情是 export directory, 'IMAGE_DIRECTORY_ENTRY_EXPORT'. 这个 directory 通常是在 DLL 中存在; 它包含导出函数 (和导出对象的地址等) 的入口. 可执行文件当然也可能有 export symbols, 但是通常情况下它们没有.
这个段应该是 "initialized data" 和 "readable". 它不能被丢弃, 因为进程也许会在运行时调用 "GetProcAddress()" 来寻找函数的入口.
这个 section 通常叫做 ".edata" 如果它独立出来; 但是通常, 他被合并到其它的 "initialized data" section.

export table ('IMAGE_EXPORT_DIRECTORY') 的结构包含一个 header 和 export data, 也就是: symbol name, 它们的 ordinal 以及到入口处的偏移.

首先, 是一个 32 bits 的没用的, 通常是 0 的 'Characteristics'. 然后是一个 32-bit-'TimeDateStamp', 这大概应该是这个 table 所建立的时间, 格式是 time_t; 可惜, 它不总是有效的 (一些连接器将它设置为 0). 然后是 2 个 16-bit-word 的版本信息 ('MajorVersion' 和 'MinorVersion'), 这两个, 通常也是 0.

写一个是 32 bits 的 'Name'; 这是 RVA 到 DLL name 的 0-terminated ASCII string. (name 是必需的以防 DLL 文件被重命名 - 请看 import directory 的 "binding" 部分.)
然后是 32-bit-'Base'. 我们稍后将会讨论它.

现在我们有一系列的问题, 因为下两个 32-bit-value 是 exported function ('NumberofFunctions') 和 exported name ('NumberOfNames') 的数量. 这两个值并不总是相等, 并且如果你尝试用大于两者之间较小的数字去解密它, 那将会发生奇异的事情. 大部分情况这两个值是相等的. 使用最小的那个数字似乎总是安全的.
我怀疑某种可能性使导出 symbol 而不需要 name (仅仅通过 ordinal), 但是我没有发现任何这方面的话题.

接下来的 3 个 32-bit-value 是到 3 个 array 的 RVA. 这些 array 并行运行: 数组 entry point 'AddressOfFunctions' (作为 32-bit RVA 给出), 数组 32-bit-RVA 到 symbol name 'AddressOfNames', 以及数组 16-bit-ordinal 'AddressOfNameOrdinals'.
(译著: 感觉翻译的不好, 给出原文吧 "
The next 3 32-bit-values are RVAs to 3 arrays. The arrays run parallel:
the array of entry points 'AddressOfFunctions' (given as 32-bit-RVAs),
the array of 32-bit-RVAs to symbol names 'AddressOfNames', and the array
of 16-bit-ordinals 'AddressOfNameOrdinals'.
")

为了获得第 i-th exported symbol 的信息, 找到 export directory, 沿着 3 个 RVA, 跳过 3 个数组的 index i, 你能得到 a)函数 entry point 的 RVA; b)函数 name (一个 0-terminated ASCII-string) 的 RVA; 以及 c)函数的 ordinal (你不得不将 'Base' 和这个值相加得到真正的 ordinal).

对于函数, entry-point-RVA通常指向 code section; 对于 object file, 它几乎总是指向 data- 或者 bbs-section.

imported symbols
----------------
这是艰难的一项.

当编译器发现一个不在当前文件 (通常在 DLL 中) 的函数调用, 大部分情况下很简单, 它不知道外部的任何情况, 所以只是简单的产生一个对该函数 symbol 的一个调用指令, 这个地址会由连接器修复. 连接器使用 import library 提供地址, 这个 library 有所有 export symbol 的 stub (译著: stub 可理解为 "信息"), 每一项都是一个跳转指令; 这个 stub 实际上就是一个 call-target. 这些跳转指令实际会跳转到一个由 import address table 获得的地址.
在更复杂的应用程序中 (当 "declspec(dllimport)" 被使用时), 便以其之道这个函数是被 imported, 于是产生一个某个地址的调用, 这个地址在 import address table 中, 忽略跳转.

到现在为止, 有问题可以去 "参考 [1] 和 [2]" 中寻找答案.

无论如何, DLL 中函数的地址是必需的, 并且当应用程序被加载时提供给加载器.加载器知道哪个 symbol 应该去哪个 library 里寻找, 并且它们的地址通过搜索 import directory 被修复.

我最好给你个例子. 有或者没有 "__declspec(dllimport)" 的调用看上去是这样:

    source:
        int symbol1(char *);
        __declspec(dllimport) int symbol2(char *);
        void foo(void)
        {
            int i = symbol1("bar");
            int j = symbol2("baz");
        }
       
    assembly:
        ...
        call _symbol                ; 没有使用 declspec(dllimport)
        ...
        call ptr cs:0xdeadbeef        ; 使用 declspec(dllimport)
        ...
       
    通过 import library 提供的 stub (没有使用 declspec(dllimport)) 被 located 到了 code section, 并且, 上述代码中的跳转指令:
        _symbol: jmp dword ptr [0xdeadbeef]
所以, 无论如何, 都有一个调用或者跳转, 到 0xdeadbeef (在我们的例子中) 这个地址. 0xdeadbeef 在 import directory 的中间; 它是 "symbol" 这个函数在 DLL "foo.dll" 中的入口.

现在让我们看看 import directory 是如何构造的.

import directory 应该在一个具有 "initialized data" 和  "readable" 属性的 section 中.
import directory 是一个 IMAGE_IMPORT_DESCRIPTOR 的数组, 每一项都使用 DLL. 整个链以一个全 0 的 IMAGE_IMPORT_DESCRIPTOR 项结尾.
IMAGE_IMPORT_DESCRIPTOR 是具有如下成员的结构:

    OriginalFirstThunk
        一个 RVA (32 bit), 到一个 0-terminated 数组, 这个数组是到 IMAGE_THUNK_DATA 的 RVA 数组, 每一项描述了一个 imported function. 这个数组从不改变.
       
    TimeDateStamp
        一个 32-bit-timestamp, 有很多用途. 让我们假设 timestamp 是 0, 以后再处理更进一步的情况.
       
    ForwarderChain
        imported function 的列表中第一个传送装置 (译著: 原文是 first forwarder) 的 32-bit 索引. 传送装置也是下一步的内容.
       
    Name
        到 DLL 的 Name (一个 0-terminated ASCII 字符串) 的 32-bit RVA.
       
    Firstthunk
        一个 RVA (32 bit), 到一个 0-terminated 数组, 这个数组是到 IMAGE_THUNK_DATA 的 RVA 数组, 每一项描述了一个 imported function. 这个数组会改变.
       
数组中每个 IMAGE_IMPORT_DESCRIPTOR 给了你 exporting DLL 的 name, 和到 forwarder 的距离以及时间戳, 它给了你 2 个到 IMAGE_THUNK_DATA 的数组的 RVA, (译著: 每个 RVA) 使用的 32 bits. (每个数组的最后一个成员都是以全 0 填充的.)
每一个 IMAGE_THUNK_DATA 都是一个到 IMAGE_IMPORT_BY_NAME 的 RVA, 这个 IMAGE_IMPORT_BY_NAME 是描述 imported function 的.
有趣的是, 这些数组平行运行, 例如: 它们指向相同的 IMAGE_IMPORT_BY_NAME.

不要痛苦, 我将画另一个图. 这是 IMAGE_IMPORT_DESCRIPTOR 的本质:

    OriginalFirstThunk        FirstThunk
           |                    |
           |                    |
           |                    |
           V                    V
          
           0-->        fun1     <--0
           1-->        fun2     <--1
           2-->        fun3     <--2
           3-->        foo      <--3
           4-->        mumpitz     <--4
           5-->        knuff     <--5
           6-->                 <--6        /* 最后一个 RVA 是 全 0! */
          
中间的名字还需要讨论 IMAGE_IMPORT_BY_NAME. 每个成员是 16-bit 的数字 (import symbol 的 ordinal), 这些数据出现在在一个不定量的 byte 之后, 而这些 byte 是 imported symbol 的 0-terminated ASCII name.
请记住, 这个 ordinal 也许是错误的. 它仅仅提供了在 DLL 中 export table 的哪里开始寻找 symbol name; 你也许不能比较 ordinal 或者为这些 entry 对号入座, 但是你一定能够找到对应的 symbol name.

总结起来, 如果你想从 DLL "knurr" 中查找 imported function "foo" 的信息, 你首先要找到在 data directory 中找到 IMAGE_DIRECTORY_ENTRY_IMPORT, 获得一个 RVA, 在 raw section data 中找到那个地址, 现在你就有了一个 IMAGE_IMPORT_DESCRIPTOR 数组. 这个数组中, 通过 'Name' 所指向的字符串找到关于 DLL "knurr" 的成员. 当你找到正确的 IMAGE_IMPORT_DESCRIPTOR 后, 顺着它的 'OriginalFirstThunk' 找到数组 IMAGE_THUNK_DATA; 观察 RVA 并找到 "foo".

这是基本的结构, 是最简单的情况. 现在, 我们学习一些 import directory 中的 tweak.

首先,  如果没有 symbol-name 信息而这些 symbol 仅仅通过 ordinal 被 import 进来的话, 数组 IMAGE_THUNK_DATA 中的 IMAGE_ORDINAL_FLAG 位 (这就是: MSB) 能被置位.你通过观察 IMAGE_THUNK_DATA 的低 word 来获得 ordinal.
(译著: 算了, 看原文吧 "First, the bit IMAGE_ORDINAL_FLAG (that is: the MSB) of the IMAGE_THUNK_DATA in the arrays can be set, in which case there is no symbol-name-information in the list and the symbol is imported purely by ordinal. You get the ordinal by inspecting the lower word of the IMAGE_THUNK_DATA.")
显然, 这种情况下, 警告 ordinal 不可靠这种事是不适用的, 因为, 你所有知道的就只有 ordinal 了. (译著: 坐着的意思可能是 "这种情况下因为只有 ordinal 了, 所以即使警告这是不可靠的也没什么办法.", 原文如下: "Obviously, in this case the warning about unreliable ordinals doesn't apply because all you have *is* the ordinal.")

好了, 现在我想知道为什么有两个纸箱 IMAGE_IMPORT_BY_NAME 的列表呢? 因为应用程序在运行时不需要 imported function 的 name, 但是需要地址. 加载器将在 DLL 文件中 export-directory 的 imported symbol 中查找每一个 import symbol, 并且使用 DLL 入口的线性地址替换 'FirstThunk' 列表中的 IMAGE_THUNK_DATA. 还记得 "jmp dword ptr [0xdeadbeef]"; 这个地址 0xdeadbeef 就是 'FirstThunk' 列表中 IMAGE_THUNK_DATA 的精确地址.
'OriginalFirstThunk' 保留不懂, 所以你总是能够通过 'OriginalFirstThunk' 列表找到 imported name 的原始列表.

现在我们遇到了叫做 "bound imports".
请考虑加载器的任务: 如果一个二进制想执行, 但是需要一个 DLL 中的函数, 加载器加载 DLL, 找到它的 export directory, 查找函数的 RVA 并且计算函数的入口. 然后将找到的地址放入 'FirstThunk' 列表.
只要程序员提供不冲突的 DLL 以及唯一的加载地址, 我们就能假设函数的入口点总是相同的. 它们能在连接期间被计算且放入 'FirstThunk' 列表中, 这就是 "bound import". ("bind" 工具就是做这个的.)

当然, 有一点需要注意: 用户的 DLL 也许有很多个版本, 或者必须 relocate DLL, 因此有限植入 'FirstThunk' 就无效了; 这种情况下, 加载器将仍然能够找到 'OriginalFirstThunk' 列表, 找到 imported symbol 并且重新植入 'FirstThunk' 列表. 装载器知道这是必须的, 如果 a)exporting DLL 的版本不匹配 或者 b)exporting DLL 需要被 relocated.

决定是否发生了 reclocation 对于加载器来说不是问题, 但是如何找到版本的不同? 这就是 IMAGE_IMPORT_DESCRIPTOR 中 'TimeDateStamp' 的用意. 如果它是 0, import list 还没有被绑定, 那么加载器就必须每次都修复进入点. 否则, import list 被绑定, 'TimeDateStamp' 必须匹配 exporting DLL 中 'FileHeader' 的 'TimeDateStamp'; 如果比匹配, 加载器假设二进制文件的绑定是错误的并重新植入 import list.

接下来是 "old-style" binding.

在 import-list 中的 "forwarder" 有一种额外的怪相. 一个 DLL 能够 export 一个 symbol, 而这个 symbol 不在这个 DLL 中, 而是在另一个 DLL 中; 这样的 symbol 就叫是 forwardered. 现在, 你不能通过查找实际上不包含 symbol 的那个 DLL 中的 timestamp 就断言 symbol 的 entry 是否有效了. 所以为了安全起见, forwarded symbol 的 entry  必须总是被修复. 在二进制中的 import list, forwarded symbol 必须被发现, 以便加载器能够修复它们.

这是通过 'ForwarderChain' 完成的.它是 thunk list 中的一个索引; 在索引位置的 import 实际上是 forwarded export, 并且索引位置的 'FirstThunk' 内容实际上是下一个 forwarded import 的索引, 以此类推, 直到 "-1" 表示没有更多的 forwarded import 为止.

现在, 我们应该总结一下目前我们已有的东西了.

好吧, 我将假设你找到了 IMAGE_DIRECTORY_ENTRY_IMPORT 并且你沿着它找到了 import-directory, 它会在某一个 section 中. 现在你在数组 IMAGE_IMPORT_DESCRIPTOR 的开始, 这个数组的最后一个项是全 0 填充的.
为了解密 IMAGE-IMPORT_DESCRIPTOR, 你首先查看 'Name' 这个字段, 跟着 RVA 就会找到 export DLL 的 name. 然后你判断 import 是否被 bind 了; 如果被 bind, 那么 'TimeDateStamp' 将会是非 0, 并且如果 bind, 那现在就是检查 DLL 版本是否与你的相匹配的好时机, 通过 'TimeDateStamp' 比较.
现在你随着 'OriginalfirstThunk' RVA 去到 IMAGE_THUNK_DATA 数组; 沿着数组往下走 (这个数组是 0-terminated), 每一个成员都是 IMAGE_IMPORT_BY_NAME (除非是 hi-bit 被置位, 这种情况仅仅有 ordinal 而没有 name) 的 RVA. 沿着 RVA, 并跳过 2 bytes (这是 ordinal), 现在你就获得了一个到 0-terminated ASCII 字符串, 这个字符串就是 imported function 的 name.
为了找到在 bind 情况下的 entry point address, 沿着数组 'FirstThunk' 并且平行的沿着数组 'OriginalFirstThunk'; 数组成员就是 entry piont 的线性地址.

有一件事我至今没有提及: 显然这是连接器在奖励 import directory 时露出的一个 bug (我发现这个 bug 存在于 Borland C 连接器). 这些连接器设置 IMAGE_IMPORT_DESCRIPTOR 的 'OriginalFirstThunk' 为 0 并且仅仅创建了数组 'FirstThunk'. 显然这样的 import directory 不能够被 bind (此外, 再次修复 import 的必要信息业丢失了). 这种情况下, 你不得不跟着数组 'FirstThunk' 去获得 imported symbol name, 并且你将永远不能提前植入 entry piont address.

关于 import directory 的最后一个 tweak 叫做 "new style" binding (它在文献 [3] 中被讲述), 这中技术也能完成 bind 任务. 当使用这种技术时, 'TimeDateStamp' 被设置为全 1 bit 填充, 并且没有 forwarderchain; 所有的 imported symbol 都被其地址修补, 无论它们是否是 forwarded. 这仍然需要知道 DLL 的版本, 并且你需要区分 forwarded 和普通的 symbol. 为了这个目标, IMAGE_DIRECTORY_ENTRY_BOUND_MPORT directory 被创建. 据我发现, 这个 directory 不能存在于 section 中, 而只能存在于 header 中, 并且在 section header 后, 在第一个 section 前. (嗨, 不是我发现了它, 我仅仅是描述了它!)
这个 directory 告诉你, 对于每一个被使用的 DLL, 从哪个其它的 DLL 中寻找 forwarded export.
它的结构是 IMAGE_BOUND_IMPORT_DESCRIPTOR, 包括 (按照如下顺序):
一个 32-bit 数字, 给出了 DLL 的 'TimeDateStamp';
一个 16-bit 数字, 'OffsetModuleName', 是从 directory 的开始, 到 DLL 中 forwarded 的 0-terminated name 的偏移.
一个 16-bit 数字, 'NumberOfmoduleForwarderRefs' 给出了 DLL 中 forwarder 的数量.

马上接着这个结构的你能找大 'NumberOfNoduleForwarderRefs' 结构, 它告诉你 forwarder 所在的 DLL 的 name 以及 version. 这个结构是 'IMAGE_BOUND_FORWARDER_REF':
一个 32-bit 的数字, 'TimeDateStamp'.
一个 16-bit 数字, 'OffsetModuleName', 是从 directory 的开始, 到 DLL 中 forwarded 的 0-terminated name 的偏移.
一个 16-bit 未使用空间.

在 'IMAGE_BOUND_FORWARDER_REF' 后面是下一个 'IMAGE_BOUND_IMPORT_DESCRIPTOR', 以此类推; 结尾的那个是全 0 的 IMAGE_BOUND_IMPORT_DESCRIPTOR.

抱歉这么复杂, 但是这确实就是它的样子 :-)

现在你有了 new style 的 import directory, 你加载所有的 DLL, 用 directory pointer IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 找到 IMAGE_BOUND_IMPORT_DESCRIPTOR, 遍历它并检查 'TimeDateStamp' 是否与加载的 DLL 匹配. 如果不匹配则修复  import directory 中的数组 'FirstThunk'.

如果你知道它是如何工作的, 它就很简答 :-)

resources
---------
resource, 如同对话框, 菜单, 图标等等, 被存储在 IMAGE_DIRECTORY_ENTRY_RESOURCE 所指向的 data directory 中. 这个 section 至少有 'IMAGE_SCN_CNT_INITIALIZED_DATA' 和 'IMAGE_SCN_MEM_READ' 标志.

resouce 的基础是 'IMAGE_RESOURCE_DIRECTORY'; 它包含了很多个 'IMAGE_RESOURCE_DIRECTORY_ENTRY', 这个结构又指向了一个 'IMAGE_RESOURCE_DIRECTORY'. 如此一来, 你就得到了一个以 'IMAGE_RESOURCE_DIRECTORY_ENTRY' 为树叶的树结构, 这些树叶指向实际的资源数据.

实际上, 情况要稍微复杂一些. 通常你找不到回旋的不可排序的树. (译著: 原文 "Normally you won't find convoluted trees you can't possibly sort out.")
通常的层次是这样: 一个 root directory. 它指向几个 (译著: 一级) directory, 每一个是一种资源类型. 这些 directory 指向下一级 (译著: 二级) directory, 每个二级 directory 具有 name 和 ID, 并指向提供给这个 resource 的语言的 directory; 每种语言都有一个 resource entry, 这些 entry 最终指向资源数据.

上述的树, 没有指向数据的指针, 看上去像是这样:

                           (root)
                              |
             +----------------+------------------+
             |                |                  |
            menu            dialog             icon
             |                |                  |
       +-----+-----+        +-+----+           +-+----+----+
       |           |        |      |           |      |    |
    "main"      "popup"   0x10   "maindlg"    0x100 0x110 0x120
       |            |       |      |           |      |    |
   +---+-+          |       |      |           |      |    |
   |     |     default   english   default    def.   def.  def.
german english

IMAGE_RESOURCE_DIRECTORY 包含:
32 bit 未使用的标识位, 叫做 'Characteristics';
32 bit 'TimeDateStamp' (再一次使用了通用的 time_t), 给出了时间资源被创建 (如果 entry 被设置) 的时间.;
16 bit 的 'MajorVersion' 和 16 bit 的 'MinorVersion', 借此允许你维护多个版本的资源;
16 bit 'NumberOfNamedEntries' 和另一个 16 bit 'NumberOfIdEntries'.

紧接着是 'NumberOfNameEntries' + 'NumberOfIdEntries' 结构, 它是 'IMAGE_RESOURCE_DIRECTORY_ENTRY' 格式, 先是 name 后是 id.
它也许指向下一个 'IMAGE_RESOURCE_DIRECTORY' 或者指向实际的资源数据.
一个 IMAGE_RESOURCE_DIRECTORY_ENTRY 包含:
32 bit 资源或者它描述的 directory 的 id;
32 bit 到资源数据或者到下一级 directory 的偏移.

id 的意义依赖于在树中的层数; id 也许是一个数字 (如果 hi-bit 是 0) 或者一个 name (如果 hi-bit 被置位). 如果是 name, 低 31 bit 是从 resource section 的 raw data 到 name (包含 16 bit 长度, 和结尾字符, unicode 编码, 非 0-terminated) 的偏移.

如果在 root-directory, id 是一个数字的话, 就是如下的资源类型:
    1: cursor
    2: bitmap
    3: icon
    4: menu
    5: dialog
    6: string table
    7: font directory
    8: font
    9: accelerators
    10: unformatted resource data
    11: message table
    12: group cursor
    14: group icon
    16: version information
任何其它的数字都是用户定义的. 任何 name 表示的 resource-type 都是用户定义的.

如果你深入一层, id 就是 resource-id (或者 resource-name).

如果你在更深的层, id 必须是数字, 并且是特定资源实例的 language-id; 例如, 你可以在同一对话框中拥有英语的, 法语的, 德语的 from, 并且它们共享同一个 resource-id. 系统会根据进程的 locale 来选择加载哪个对话框, locale 通常反映了用户的 "regional setting".
(如果 resource 不能被进程 locale 匹配, 系统首先会尝试寻找中立的子语言; 如果仍然不能匹配, 就会使用最小的 id).
(译著: 原文 "If the resource cannot be found for the process locale, the system will first try to find a resource for the locale using a neutral sublanguage; if it still can't be found, the instance with the smallest language id will be used.")
为了解密 language id, 用宏 PRIMARYLANGID() 和 SUBLANGID() 将 id 分解为 primary language id 和 sublanguage id, 分别会给你 0-9 位和 10-15 位两部分. 这些值的含义在 "winresrc.h" 中.
language resource 只在 快捷键, 对话框, 菜单栏, rcdata 以及 字符串表中被支持, 其它的资源类型应该是 LANG_NEUTRAL/SUBLANG_NEUTRAL.

为了判断下一层 resource directory 是否是另一个 directory, 你需要看偏移值的 hi-bit. 如果被置位, 则剩下的 31 bit 就是从 section raw data 到下一个 directory 的偏移, 下一层 directory 一样也是 IMAGE_RESOURCE_DIRECTORY 结构, 并以 IMAGE_RESOURCE_DIRECTORY_ENTRY 结束.

如果 hi-bit 没有置位, 那么偏移就是从 section raw data 到 resource raw data description 的偏移, 这个 description 是一个 IMAGE_RESOURCE_DATA_ENTRY. 它包含 32 bit 'OffsetToData' (到 raw data 的偏移, 从 resource section raw data 开始计算), 32 bit 的数据的 'Size', 32 bit 'CodePage' 以及 32 bit 没有使用的空间.
(codepage 的不鼓励使用的, 你应该使用 'language' 功能区支持多语言.)

raw data 的格式依赖于资源类型; 能够在 MS SDK 文档中找到描述. 记住, 所有的资源字符串都是 UNICODE 编码.

relocation
----------
我将讲述的最后一个 data directory 是 base relocation directory. 它被 optional header 中的 data directory 的  IMAGE_DIRECTORY_ENTRY_BASERELO entry 指向. 典型情况下它被包含在自己的 section 中, 叫做 ".reloc", 并且具有 IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_DISCARDABLE 以及 IMAGE_SCN_MEM_READ 标记.

如果镜像不能够被加载到所希望的地址, 这个地址是 optional header 中的 'ImageBase', 那么就需要加载器的 relocation. 在这种情况下, 连接器所提供的地址就不再有效了, 加载器需要修补静态变量以及字符串等变量所使用的绝对地址.

relocation directory 是一个大块的序列. 每个大块包含了 4 KB 镜像的重定向信息. 一个大块以 'IMAGE_BASE_RELOCATION' 结构开始. 它包含 32 bit 'VirtualAddress' 和 32 bit 'SizeOfBlock'. 接着是实际的 relocation data, 每个 16 bit.
'VirtualAddress' 是这块 relocation 要被应用到的 base RVA; 'SizeOfBlock' 是整个块的大小, 单位是 byte.
relocation 的数量是 ('SizeOfBlock' - sizeof(IMAGE_BASE_RELOCATION)) / 2 (译著: bytes).
relocation 以一个 'VirtualAddress' 为 0 的 IMAGE-BASE_RELOCATION 结束.

每一个 16-bit-relocation 信息包含实际的 relocation position 在低 12 bits, relocation type 在高 4 bits. 为了获得 relocation RVA, 你需要将 'IMAGE_BASE_RELOCATION' 的 'VirtualAddress' 加上 12-bit-position. 类型是以下之一:
    IMAGE_REL_BASED_ABSOLUTE (0)
        这个事 no-op; 它用来是这个大块对齐到 32-bit-border.
    IMAGE_REL_BASED_HIGH (1)
        relocation 必须应用到上述 DWORD 中的高 16 bits.
        (译著: 原文 "The relocation must be applied to the high 16 bits of the DWORD in question.")
    IMAGE_REL_BASED_LOW (2)
        relocation 必须应用到上述 DWORD 中的低 16 bits.
        (译著: 原文 "The relocation must be applied to the low 16 bits of the DWORD in question.")
    IMAGE_REL_BASED_HIGHLOW (3)
        relocation 必须应用到上述的整个 32 bits.
        (译著: 原文 "The relocation must be applied to the entire 32 bits in question.")
    IMAGE_REL_BASED_HIGHADJ (4)
        未知
    IMAGE_REL_BASED_MIPS_JMPADDR (5)
        未知
    IMAGE_REL_BASED_SECTION (6)
        未知
    IMAGE_REL_BASED_REL32 (7)
        未知
       
例如, 如果你发现 relocation 信息如下
    0x00004000      (32 bits, 起始 RVA)
    0x00000010      (32 bits, chunk 大小)
    0x3012          (16 bits reloc data)
    0x3080          (16 bits reloc data)
    0x30f6          (16 bits reloc data)
    0x0000          (16 bits reloc data)
    0x00000000      (下一个 chunk's RVA)
    0xff341234
你知道第一个块描述了 relocation 开始于 RVA 0x4000 并且 16 byte 长. 因为 header 用掉了 8 bytes, 每个 relocation 是 2 bytes, 因此一共有 (16-8)/2 = 4 个 relocation.
第一个 relocation 是被应用于 0x4012, 第二个在 0x4080, 第三个在 0x40f6. 最后一个 relocation 是 no-op.
下一个块的 RVA 是 0, 表示这个列表结束了.

现在, 你觉得 relocation 如何?
你知道镜像被 relocated 到了期望的加载地址, 这个地址是 optional header 中的 'ImageBase'; 你也知道你加载镜像的 (译著: 实际) 地址. 如果它们匹配, 你不需要做任何事, 如果它们不匹配, 你需要计算差距 actual_base - preferred_base, 并且将这个值加到 relocation position, 使用上述的方法.

Copyright
---------
This text is copyright 1998 by B. Luevelsmeyer. It is freeware, and you
may use it for any purpose but on you own risk. It contains errors and
it is incomplete. You have been warned.

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 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

Appendix: hello world
---------------------
在 appendix 中, 我将如何手工制作一个 program. 这个例子使用 Intel-assembly, 因为我没说 DEC Alpha (译著: 原文 "bacause I don't speak DEC Alpha").

程序相当于

    #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:
                ; parameters for WriteConsole(), backwards
    6A 00                     push      0x00000000
    68 ?? ?? ?? ??            push      offset _written
    6A 0D                     push      0x0000000d
    68 ?? ?? ?? ??            push      offset hello
                ; parameter for GetStdHandle()
    6A F5                     push      0xfffffff5
    2E FF 15 ?? ?? ?? ??      call      dword ptr cs:__imp__GetStdHandle@4
                ; result is last parameter for 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" 中.

现在, 我能够开始制作可执行文件了. question mark 会取代尚未找到的值; 它们将会在后面被修复.

首先是 DOS-header, 开始于 0x0, 0x40 bytes 长.
    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 program. 它仅仅是具有 "MZ" 起始标志和 e-lfanew 指针的 header, 没有任何代码, 因为它并不打算运行在 DOS 中.

然后是 signature, 开始于 0x40, 0x4 bytes 长.
        50 45 00 00
       
现在是 file-header, 开始于 0x44, 0x14 bytes 长.
    Machine                     4c 01       ; i386
    NumberOfSections            02 00       ; code and data
    TimeDateStamp               00 00 00 00 ; who cares?
    PointerToSymbolTable        00 00 00 00 ; unused
    NumberOfSymbols             00 00 00 00 ; unused
    SizeOfOptionalHeader        e0 00       ; constant
    Characteristics             02 01       ; executable on 32-bit-machine

以及 optional header, 开始于 0x58, 0x60 bytes 长:
    Magic                       0b 01       ; constant
    MajorLinkerVersion          00          ; I'm version 0.0 :-)
    MinorLinkerVersion          00          ;
    SizeOfCode                  20 00 00 00 ; 32 bytes of code
    SizeOfInitializedData       ?? ?? ?? ?? ; yet to find out
    SizeOfUninitializedData     00 00 00 00 ; we don't have a BSS
    AddressOfEntryPoint         ?? ?? ?? ?? ; yet to find out
    BaseOfCode                  ?? ?? ?? ?? ; yet to find out
    BaseOfData                  ?? ?? ?? ?? ; yet to find out
    ImageBase                   00 00 10 00 ; 1 MB, chosen arbitrarily
    SectionAlignment            20 00 00 00 ; 32-bytes-alignment
    FileAlignment               20 00 00 00 ; 32-bytes-alignment
    MajorOperatingSystemVersion  04 00      ; NT 4.0
    MinorOperatingSystemVersion  00 00      ;
    MajorImageVersion           00 00       ; version 0.0
    MinorImageVersion           00 00       ;
    MajorSubsystemVersion       04 00       ; Win32 4.0
    MinorSubsystemVersion       00 00       ;
    Win32VersionValue           00 00 00 00 ; unused?
    SizeOfImage                 ?? ?? ?? ?? ; yet to find out
    SizeOfHeaders               ?? ?? ?? ?? ; yet to find out
    CheckSum                    00 00 00 00 ; not used for non-drivers
    Subsystem                   03 00       ; Win32 console
    DllCharacteristics          00 00       ; unused (not a DLL)
    SizeOfStackReserve          00 00 10 00 ; 1 MB stack
    SizeOfStackCommit           00 10 00 00 ; 4 KB to start with
    SizeOfHeapReserve           00 00 10 00 ; 1 MB heap
    SizeOfHeapCommit            00 10 00 00 ; 4 KB to start with
    LoaderFlags                 00 00 00 00 ; unknown
    NumberOfRvaAndSizes         10 00 00 00 ; constant
   
正如你所见, 我计划只有 2 个 section, 一个 code 另一个是余下的所有内容 (data, constant 以及 import directory). 这里没有 relocation 以及像 resource 之类的东西.
section alignment 在文件和在 RAM 中是相同的 (32 bytes);
这有助于保持任务简单.

现在我们建立 data directory, 在市域 0xb8, 0x80 bytes 长:
    Address        Size
    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
只有 import directory 在使用.

下一个是 section header. 首先我们做 code section (译著: 作者这里的意思是制作 code section 的 header), 它包含上述的汇编指令. 它 32 bytes 长, 所以 code section 就是 32 bytes. header 开始于 0x138, 0x28 bytes 长:
    Name                    2e 63 6f 64 65 00 00 00     ; ".code"
    VirtualSize             00 00 00 00                 ; unused
    VirtualAddress          ?? ?? ?? ??                 ; yet to find out
    SizeOfRawData           20 00 00 00                 ; size of code
    PointerToRawData        ?? ?? ?? ??                 ; yet to find out
    PointerToRelocations     00 00 00 00                    ; unused
    PointerToLinenumbers     00 00 00 00                    ; unused
    NumberOfRelocations      00 00                          ; unused
    NumberOfLinenumbers      00 00                          ; unused
    Characteristics         20 00 00 60                 ; code, executable, readable
   
第二个 section 包含 data. header 开始于 0x160, 0x28 bytes 长:
    Name                    2e 64 61 74 61 00 00 00     ; ".data"
    VirtualSize             00 00 00 00                 ; unused
    VirtualAddress          ?? ?? ?? ??                 ; yet to find out
    SizeOfRawData           ?? ?? ?? ??                 ; yet to find out
    PointerToRawData        ?? ?? ?? ??                 ; yet to find out
    PointerToRelocations     00 00 00 00                    ; unused
    PointerToLinenumbers     00 00 00 00                    ; unused
    NumberOfRelocations      00 00                          ; unused
    NumberOfLinenumbers      00 00                       ; unused
    Characteristics         40 00 00 c0                 ; initialized, readable, writeable
   
下一个 byte 应该开始于 0x188, 但是 section 需要对齐到 32 bytes (因为我这样选择的), 所以我们需要填充到 0x1a0.
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
   
现在是第一个 section, 内容是上述汇编指令的 code section. 它开始于 0x1a0, 0x20 bytes 长:
    6A 00                    ; push      0x00000000
    68 ?? ?? ?? ??           ; push      offset _written
    6A 0D                    ; push      0x0000000d
    68 ?? ?? ?? ??           ; push      offset hello_string
    6A F5                    ; push      0xfffffff5
    2E FF 15 ?? ?? ?? ??     ; call      dword ptr cs:__imp__GetStdHandle@4
    50                       ; push      eax
    2E FF 15 ?? ?? ?? ??     ; call      dword ptr cs:__imp__WriteConsoleA@20
    C3                       ; ret      

因为之前的 section 正好符合对齐, 所以在接下来的 data section 我们不需要任何的填充, 开始于 0x1c0:
    68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A  ; "hello, world\n"
    00 00 00                                ; padding to align _written
    00 00 00 00                             ; _written
   
现在, 所有剩下的工作就是 import directory. 它将从 "kernel32.dll" 导入两个函数, 它在相同的 section 中紧接着变量出现. 首先,我们对齐到 32 bytes:
    00 00 00 00 00 00 00 00 00 00 00 00     ; padding
   
它开始于 0x1e, 结构是 IMAGE_IMPORT_DESCRIPTOR:
    OriginalFirstThunk      ?? ?? ?? ??     ; yet to find out
    TimeDateStamp           00 00 00 00     ; unbound
    ForwarderChain          ff ff ff ff     ; no forwarders
    Name                    ?? ?? ?? ??     ; yet to find out
    FirstThunk              ?? ?? ?? ??     ; yet to find out
   
我们需要用全 0 的结构结束 import-directory (我们在 0x1f4):
    OriginalFirstThunk      00 00 00 00     ; terminator
    TimeDateStamp           00 00 00 00     ;
    ForwarderChain          00 00 00 00     ;
    Name                    00 00 00 00     ;
    FirstThunk              00 00 00 00     ;
   
现在只剩下 DLL name, 2 个 thunks, thunk-data 以及函数 name 了. 但是我们马上就要完成了!

DLL name, 是 0-terminated, 开始于 0x208:
    6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00  ; "kernel32.dll"
    00 00 00                                ; padding to 32-bit-boundary
   
original first thunk, 开始于 0x218:
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "WriteConsoleA"
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator
                   
first tunk 是相通的列表, 开始于 0x224:
(__imp__WriteConsoleA@20, at 0x224)
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "WriteConsoleA"
(__imp__GetStdHandle@4, at 0x228)
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator

现在剩下的就只有两个函数 name 了, 它们在 IMAGE_IMPORT_BY_NAME 结构中. 我们现在在 0x230.
    01 00                                      ; ordinal, need not be correct
    57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00  ; "WriteConsoleA"
    02 00                                      ; ordinal, need not be correct
    47 65 74 53 74 64 48 61 6e 64 6c 65 00     ; "GetStdHandle"
   
好了, 全部完成. 接下来的 byte, 我们并不需要, 开始于 0x24f. 就是填充 section 到 0x260:
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ; padding
   
--------------

我们完成了. 现在我们知道了所有 byte 的偏移, 我们能够修复这些未知的, '??' 标记的地址了.
我不会强迫你去一个一个的读 (它很直观), 简单的展现这些结果:

--------------

DOS-header, 开始于 0x0:
    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
   
signature, 开始于 0x40:
        50 45 00 00
       
file-header, 开始于 0x44:
    Machine                         4c 01           ; i386
    NumberOfSections                02 00           ; code and data
    TimeDateStamp                   00 00 00 00     ; who cares?
    PointerToSymbolTable            00 00 00 00     ; unused
    NumberOfSymbols                 00 00 00 00     ; unused
    SizeOfOptionalHeader            e0 00           ; constant
    Characteristics                 02 01           ; executable on 32-bit-machine
   
optiona header, 开始于 0x58:
    Magic                           0b 01           ; constant
    MajorLinkerVersion              00              ; I'm version 0.0 :-)
    MinorLinkerVersion              00              ;
    SizeOfCode                      20 00 00 00     ; 32 bytes of code
    SizeOfInitializedData           a0 00 00 00     ; data section size
    SizeOfUninitializedData         00 00 00 00     ; we don't have a BSS
    AddressOfEntryPoint             a0 01 00 00     ; beginning of code section
    BaseOfCode                      a0 01 00 00     ; RVA to code section
    BaseOfData                      c0 01 00 00     ; RVA to data section
    ImageBase                       00 00 10 00     ; 1 MB, chosen arbitrarily
    SectionAlignment                20 00 00 00     ; 32-bytes-alignment
    FileAlignment                   20 00 00 00     ; 32-bytes-alignment
    MajorOperatingSystemVersion      04 00              ; NT 4.0
    MinorOperatingSystemVersion      00 00              ;
    MajorImageVersion               00 00           ; version 0.0
    MinorImageVersion               00 00           ;
    MajorSubsystemVersion           04 00           ; Win32 4.0
    MinorSubsystemVersion           00 00           ;
    Win32VersionValue               00 00 00 00     ; unused?
    SizeOfImage                     c0 00 00 00     ; sum of all section sizes
    SizeOfHeaders                   a0 01 00 00     ; offset to 1st section
    CheckSum                        00 00 00 00     ; not used for non-drivers
    Subsystem                       03 00           ; Win32 console
    DllCharacteristics              00 00           ; unused (not a DLL)
    SizeOfStackReserve              00 00 10 00     ; 1 MB stack
    SizeOfStackCommit               00 10 00 00     ; 4 KB to start with
    SizeOfHeapReserve               00 00 10 00     ; 1 MB heap
    SizeOfHeapCommit                00 10 00 00     ; 4 KB to start with
    LoaderFlags                     00 00 00 00     ; unknown
    NumberOfRvaAndSizes             10 00 00 00     ; constant
   
data directory, 开始于 0xb8:
    Address        Size
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
    e0 01 00 00    6f 00 00 00         ; 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
   
section header (code), 开始于 0x138:
    Name            2e 63 6f 64 65 00 00 00     ; ".code"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      a0 01 00 00             ; RVA to code section
    SizeOfRawData       20 00 00 00             ; size of code
    PointerToRawData    a0 01 00 00             ; file offset to code section
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     20 00 00 60             ; code, executable, readable
   
section header (data), 开始于 ox160:
    Name            2e 64 61 74 61 00 00 00     ; ".data"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      c0 01 00 00             ; RVA to data section
    SizeOfRawData       a0 00 00 00             ; size of data section
    PointerToRawData    c0 01 00 00             ; file offset to data section
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     40 00 00 c0             ; initialized, readable, writeable
   
(填充)
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00       ; padding
   
code section, 开始于 0x1a0:
    6A 00                    ; push      0x00000000
    68 d0 01 10 00           ; push      offset _written
    6A 0D                    ; push      0x0000000d
    68 c0 01 10 00           ; push      offset hello_string
    6A F5                    ; push      0xfffffff5
    2E FF 15 28 02 10 00     ; call      dword ptr cs:__imp__GetStdHandle@4
    50                       ; push      eax
    2E FF 15 24 02 10 00     ; call      dword ptr cs:__imp__WriteConsoleA@20
    C3                       ; ret
   
data section, 开始于 0x1c:
    68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A  ; "hello, world\n"
    00 00 00                                ; padding to align _written
    00 00 00 00                             ; _written
   
填充:
    00 00 00 00 00 00 00 00 00 00 00 00     ; padding
   
IMAGE_IMPORT_DESCRIPTOR, 开始于 0x1e0:
    OriginalFirstThunk      18 02 00 00     ; RVA to orig. 1st thunk
    TimeDateStamp           00 00 00 00     ; unbound
    ForwarderChain          ff ff ff ff     ; no forwarders
    Name                    08 02 00 00     ; RVA to DLL name
    FirstThunk              24 02 00 00     ; RVA to 1st thunk
   
结束 (0x1f4):
    OriginalFirstThunk      00 00 00 00     ; terminator
    TimeDateStamp           00 00 00 00     ;
    ForwarderChain          00 00 00 00     ;
    Name                    00 00 00 00     ;
    FirstThunk              00 00 00 00     ;
   
DLL name, 开始于 0x208:
    6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00  ; "kernel32.dll"
    00 00 00                                ; padding to 32-bit-boundary
   
original first thunk, 开始于 0x218:
    AddressOfData   30 02 00 00             ; RVA to function name "WriteConsoleA"
    AddressOfData   40 02 00 00             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator
first thunk, 开始于 0x224:
    AddressOfData   30 02 00 00             ; RVA to function name "WriteConsoleA"
    AddressOfData   40 02 00 00             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator
IMAGE_IMPORT_BY_NAME, 在 0x230:
    01 00                                      ; ordinal, need not be correct
    57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00  ; "WriteConsoleA"
IMAGE_IMPORT_BY_NAME, 在 0x240:
    02 00                                      ; ordinal, need not be correct
    47 65 74 53 74 64 48 61 6e 64 6c 65 00     ; "GetStdHandle"
   
(填充)
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; padding
    00
   
第一个未使用的 byte: 0x260

哎, 它能工作在 NT 但是不能工作在 windows 95 上. windows 95 不能运行 section 对齐到 32-bit 的应用程序, 他需要对齐到 4 KB, 文件对齐到 512 bytes. 所以在 windows 95 上你需要插入一大堆的 0-bytes (用于填充) 并且调节 RVA. 感谢 D. Binette 在 windows 95 上的测试.

        -- 全文完 --

posted @ 2012-07-25 11:56  walfud  阅读(1455)  评论(0编辑  收藏  举报