对于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操作变化。