主要是PE文件头部分。来再看雪 《加密与解密》

//PE文件格式小部分资料,摘自《加密与解密》-看雪
typedef struct _IMAGE_DOS_HEADER{       // DOS .EXE header
+0h     WORD    e_magic;                      // Magic number
+2h     WORD    e_cblp;                       // Bytes on last page of file
+4h     WORD    e_cp;                         // Pages in file
+6h     WORD    e_crlc;                       // Relocations
+8h     WORD    e_cparhdr;                    // Size of header in paragraphs
+Ah     WORD    e_minalloc;                   // Minimum extra paragraphs needed
+Ch     WORD    e_maxalloc;                   // Maximum extra paragraphs needed
+Eh     WORD    e_ss;                         // Initial (relative) SS value
+10h     WORD    e_sp;                         // Initial SP value
+12h     WORD    e_csum;                       // Checksum
+14h     WORD    e_ip;                         // Initial IP value
+16h     WORD    e_cs;                         // Initial (relative) CS value
+18h     WORD    e_lfarlc;                     // File address of relocation table
+1Ah     WORD    e_ovno;                       // Overlay number
         WORD    e_res[4];                     // Reserved words
         WORD    e_oemid;                      // OEM identifier (for e_oeminfo)
         WORD    e_oeminfo;                    // OEM information; e_oemid specific
         WORD    e_res2[10];                   // Reserved words
+3Ch     LONG    e_lfanew;                     // File address of new exe header   新exe头文件地址
   } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

对于PE文件来说, 最有用的是e_lfanew字段,   这个字段指出了真正的PE文件头,它占用4个字节,
位于文件开始偏移3Ch字节中。

PE文件头, PE装载器将从DOS MZ header的e_lfanew字段里找到PE Header的起始偏移量,跳到真正的
的PE文件头处。

IMAGE_NT_HEADERS的数据结构如下所示(左边的数字是到PE文件头的偏移量)

typedef struct _IMAGE_NT_HEADERS{
     DWORD Signature;
     IMAGE_FILE_HEADER FileHeader;
     IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

1. 字串   "PE\0\0" 它是PE文件头的开始。 5045, 直接查找串 'PE', 或者 16进制 5045就可以找到
PE头。

2. 映像文件头 (IMAGE_FILE_HEADER)
PE文件头的开始后然后就是   映像文件头,映像文件头里包括这些东西,如CPU类型,块数目,时间日期
标记, COFF符号指针,就是调试信息, 符号数等。
由于16进制低位在前,所有 4C01实际上是 014Ch,表示的Intel i386系列的CPU。

还是将全部的IMAGE_NT_HEADERS 结构的成员全部列出。
;PE文件头结构定义
IMAGE_NT_HEADERS STRUC
;-----------------PE文件标识"PE"
+0h     Signature DWORD ? ;PE文件标识'PE'
;-----------------映像文件头(IMAGE_FILE_HEADER)
+04h Machine    WORD ? ;运行平台
+06h NumberOfSections     WORD     ? ;块数目
+08h TimeDateStamp   DWORD    ? ;文件创建日期和时间
+0Ch PointerToSymbolTable DWORD    ? ;指向符号表(用于测试)
+10h NumberOfSymbols   DWORD    ? ;符号表中符号个数(用于调试)
+14h SizeofOptionalHeader WORD ? ;IMAGE_OPTIONAL_HEADER32结构大小
+16h Characteristics   WORD ? ;文件属性
;-----------------可选映像头(IMAGE_OPTIONAL_HEADER32)
+18h Magic     WORD ? ;标志字(总是010bh)
+1Ah MajorLinkerVersion    BYTE ? ;链接器版本号
+1Bh MinorLinkerVersion    BYTE ? ;
+1Ch SizeOfCode     DWORD    ? ;代码段大小
+20h SizeOfInitializeData   DWORD ? ;已初始化数据块大小
+24h SizeOfOfUninitlaizeData   DWORD ? ;未初始化数据块大小
+28h AddressOfEntryPoint   DWORD ? ;程序执行入口RVA, 程序执行入口的相对虚拟地址,这个好
+2Ch BaseOfCode     DWORD ? ;代码段开始RVA
+30h BaseOfData     DWORD    ? ;数据段起始RVA
+34h ImageBase     DWORD ? ;程序默认装入的基址RVA
+38h SectionAlignment    DWORD ? ;内存中的块对齐粒度
+3Ch FileAlighment    DWORD ? ;文件中的快对器粒度
+40h MajorOperatingSystemVersion WORD ? ;操作系统主版本号
+42h MinorOperatingSystemVersion WORD ? ;操作系统次版本号
+44h MajorImageVersion    WORD ? ;用户自定义版本号
+46h MinorImageVersion    WORD ? ;用户自定义次版本号
+48h MajorSubsystemVersion   WORD ? ;所需自系统版本号
+4Ah MinorSubsystemVersion   WORD ? ;
+4Ch Reserved     DWORD ? ;保留
+50h SizeOfImage    DWORD ? ;内存中整个PE映像尺寸
+54h SizeOfHeaders    DWORD ? ;部首+块表大小
+58h CheckSum     DWORD ? ;校验和
+5Ch Subsystem     WORD ? ;文件子系统
+5Eh DllCharacteristics    WORD ?
+60h SizeOfStackReserve    DWORD ? ;初始化的堆栈大小
+64h SizeOfStackCommit    DWORD ? ;初始化实际提交的堆栈大小
+68h SizeOfHeapReserve    DWORD ? ;初始化时保留的堆大小
+6Ch SizeOfHeapCommit    DWORD ? ;初始化时实际提交的堆大小
+70h LoaderFlags    DWORD ?
+74h NumberOfRvaAndSizes   DWORD ? ;数据目录结构的数量
DataDirectory   IMAGE_DATA_DIRECTORY 16 DUP(<0>); 数据目录表
IMAGE_NT_HEADERS ENDS

Section Table(节表)

节表其实就是紧挨着 PE header的一结构数组。该数组成员的数目由file header (IMAGE_FILE_HEADER)结构中NumberOfSections域的域值来决定。节表结构又命名为IMAGE_SECTION_HEADER

#define IMAGE_SIZEOF_SHORT_NAME 8

typedef struct _IMAGE_SECTION_HEADER {

    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];   // 节表名称,如".text"

    union {

        DWORD   PhysicalAddress;        // 物理地址

        DWORD   VirtualSize;            // 真实长度

    } Misc;

    DWORD   VirtualAddress;                 // RVA

    DWORD   SizeOfRawData;                  // 物理长度

    DWORD   PointerToRawData;               // 节基于文件的偏移量

    DWORD   PointerToRelocations;           // 重定位的偏移

    DWORD   PointerToLinenumbers;           // 行号表的偏移

    WORD    NumberOfRelocations;         // 重定位项数目

    WORD    NumberOfLinenumbers;         // 行号表的数目

    DWORD   Characteristics;            // 节属性

} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

同样,不是所有成员都是很有用的,我们只关心那些真正重要的。

FieldMeanings
Name1事实上本域的名称是"name",只是"name"已被MASM用作关键字,所以我们只能用"Name1"代替。这儿的节名长不超过8字节。记住节名仅仅是个标记而已,我们选择任何名字甚至空着也行,注意这里不用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。
VirtualAddress本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h
SizeOfRawData经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。(译者注:假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长)。
PointerToRawData这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。
Characteristics包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。

现在我们已知晓 IMAGE_SECTION_HEADER结构,再来模拟一下PE装载器的工作吧:

  1. 读取 IMAGE_FILE_HEADERNumberOfSections域,知道文件的节数目。
  2. SizeOfHeaders域值作为节表的文件偏移量,并以此定位节表。
  3. 遍历整个结构数组检查各成员值。
  4. 对于每个结构,我们读取PointerToRawData域值并定位到该文件偏移量。然后再读取SizeOfRawData域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址。然后就准备把节映射进内存,并根据Characteristics域值设置属性。
  5. 遍历整个数组,直至所有节都已处理完毕。

注意我们并没有使用节名:这其实并不重要。


PE文件的框架结构: DOS首部 -> PE文件头 -> 块表(Section Table) -> 块(Section) -> 调试信息
不得不佩服美妙的设计。