对于16位DOS系统而言,PE文件被划分为DOS头和冗余数据,冗余数据常常包括PE文件头和PE数据等,这些16位系统下的冗余数据正是32位或64位系统使用的主要数据,在32位系统下运行时,DOS头实际上就成为了冗余数据。但也不完全正确,因为DOS头在非16位系统下依然是必须的,没有MZ头的文件在非16位系统下是不会被认为是符合规范的PE文件的,因为其中某些字段对于文件十分必要。

DOS MZ头结构

关键的部分是MZ头,结构如下:

DOS MZ 头 IMAGE_DOS_HEADER(总共是64byte)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IMAGE_DOS_HEADER STRUCT
e_magic WORD ? ; 0000h - Magic number ; EXE标志 MZ
e_cblp WORD ? ; 0002h - Bytes on last page of file ; 最后(部分)页中的字节数
e_cp WORD ? ; 0004h - Pages in file ; 文件中的全部和部分页数
e_crlc WORD ? ; 0006h - Relocations ; 重定位表中的指针数
e_cparhdr WORD ? ; 0008h - Size of header in paragraphs ; 头部尺寸,以段落为单位
e_minalloc WORD ? ; 000ah - Minimum extra paragraphs needed ; 所需的最小附加段
e_maxalloc WORD ? ; 000ch - Maximum extra paragraphs needed ; 所需的最大附加段
e_ss WORD ? ; 000eh - Initial (relative) SS value ; 初始的SS值(相对偏移量)
e_sp WORD ? ; 0010h - Initial SP value ; 初始的SP值 stack segment
e_csum WORD ? ; 0012h - Checksum ; 补码校验值
e_ip WORD ? ; 0014h - Initial IP value ; 初始的IP值 instruction pointer
e_cs WORD ? ; 0016h - Initial (relative) CS value ; 初始的CS值 code segment
e_lfarlc WORD ? ; 0018h - File address of relocation table ; 重定位表中的字节偏移量
e_ovno WORD ? ; 001ah - Overlay number ; 覆盖号

e_res WORD 4 dup(?) ; 001ch - Reserved words ; 保留字
e_oemid WORD ? ; 0024h - OEM identifier (for e_oeminfo) ; OEM标识符(相对e_oeminfo)
e_oeminfo WORD ? ; 0026h - OEM information; e_oemid specific ; OEM信息
e_res2 WORD 10 dup(?) ; 0028h - Reserved words ; 保留字
e_lfanew DWORD ? ; 003ch - 指向PE文件头的位置为中的PE文件头标志的地址 ; PE头相对于文件的偏移地址
IMAGE_DOS_HEADER ENDS

注:最后5项在16位系统下不存在

PE文件头寻址

其中e_lfanew字段指向PE文件头的偏移量,计算方法为:

PE文件头位置 = MZ头基址 + e_lfanew

代码寻址

Code Segment

CS寄存器,即代码段寄存器。

对应的是内存中用来存放代码的内存段,指向代码段的基址

Instruction Pointer

IP寄存器,即指令指针寄存器。

指向下一条要执行的指令地址

计算机在执行程序时通过 CS : IP 来寻址下一条待执行的指令,计算方式一般是:

Address = ( CS << 4 ) + IP

(视具体情况,总之这两者是必需的)

这两个寄存器的初始值直接决定一切程序的起点

堆栈的处理

Stack Segment

堆栈段寄存器

段寄存器的产生源于Intel 8086 CPU体系结构中数据总线与地址总线的宽度不一致.

当时使用的是16位处理器,但是采用了20位地址总线,地址总线的宽度直接决定可寻址的地址范围,若将地址总线设计为跟当时的ALU一致的16位是非常合理的设计方法,但是普遍的看法是16位地址总线是不够的,所以Intel决定将地址总线设计为20位,可寻址范围增大到1M内存。带来的问题是ALU无法在单个时钟周期内完成地址计算。

因此Intel设计了段,将内存分为Code Segment, Data Segment, Extra Segment, Stack Segment四个段。

段寄存器保存了对应段基址的高16位,拼接上低位的0000就构成20位的段基址。

现在一个完整的内存地址就变成了16位的段寄存器值(高)加上16位的偏移地址(低),当然中间有12位重叠部分。

Base Pointer

基址指针寄存器,保存一个栈帧的基址值,在一个过程中是恒定的,一般用来寻址参数。

Stack Pointer

栈指针寄存器,指向当前栈顶,根据堆栈的push和pop操作变化。