EXE的文件头

小端格式:高位存在高地址

EXE文件头的大小一般为200H,大致的格式如下:

00000000: 4d5a 5001 0200 0200 2000 0000 ffff 0500  MZP..... .......
00000010: 0001 e80e 2800 0200 1e00 0000 0100 0100  ....(...........
00000020: 0200 0d00 0200 0000 0000 0000 0000 0000  ................

接下来,我们来解释文件头中每一个字节的具体含义:

00000000: 4d5a 5001 0200 0200 2000 0000 ffff 0500  MZP..... .......
            |	|     |	   |	|   |	 |    |
	    |	|     |	   |	|   |	 |    `---[e-f]堆栈段的段值ss(相对值)=该段(stack)段地址-第一个段地址的差		
	    |	|     |	   |	|   |	 `--------[c-d]至多可以为EXE分配多少字节,几乎都是ffffh
	    |	|     |	   |	|   `-------------[a-b]至少要为EXE分配多少字节,几乎都是0000h
	    |	|     |	   |	`-----------------[8-9]文件头的节长度,一节=10h,文件头的字节长度=节长度*10H;该文件头长度为0020*10h=200h										 
	    |	|     |	   `----------------------[6-7]重定位项的个数(后续说明)
	    |	|     `---------------------------[4-5]小端格式表示当前exe在硬盘中占几个扇区,一个扇区200h=512 bytes, 0002h个扇区															 
	    |	`---------------------------------[2-3]小端格式表示最后一个扇区的字节数,如果为0,表示最后一个扇区是满的,
            |                                          此时最后一个扇区的字节数是0150h   	
	    `-------------------------------------[0-1]标志“MZ”,操作系统在运行exe文件时,会检查这两个字节,判断是否为exe文件---防止死机
									   
						
00000010: 0001 e80e 2800 0200 1e00 0000 0100 0100  ....(...........
            |	|     |	   |	|   |	 |    |
	    |	|     |	   |	|   |	 |    `---[1Eh-1Fh] 重定位的偏移地址=0001h		
	    |	|     |	   |	|   |	 `
	    |	|     |	   |	|   `
	    |	|     |	   |	`-----------------[18h-19h] 重定位表的偏移位置001eh(后续说明)										 
	    |	|     |	   `----------------------[16h-17h] delta CS,代码段的段地址=该段(.text)-第一个段地址的差
	    |	|     `---------------------------[14h-15h]IP的值0028h,程序在载入运行时第一条指令地址,病毒必须修改此处															 
	    |	`---------------------------------[12h-13h] EXE文件头的校验值,在DOS中没什么用,windows里的驱动文件.sys(格式其实上是EXE)中使用
	    `-------------------------------------[10h-11h] sp寄存器的值(绝对值)0100h
00000020: 0200 0d00 0200 0000 0000 0000 0000 0000  ................
	    |	|    |
	    |	|    |
	    |	|    |
	    |	|    |
	    |	|    `-------------------------------[24h-25h] Δ=0002h 首段地址+Δ=重定位的段地址
	    |	`------------------------------------[22h-23h]重定位的偏移地址=000Dh
	    `----------------------------------------[20h-21h]重定位Δ=0002h,首段地址+Δ=重定位的																									   段地址

了解完文件头各个字节的含义之后,为了加深理解,我们来看如下的几个问题:

如何根据文件头计算文件大小?

  1. 从文件头[2-3]字节中可得出EXE文件在硬盘中的最后一个扇区的大小

    从文件头[4-5]字节中可得出EXE文件在硬盘中所占的扇区数

  2. 文件大小的计算公式如下:

    if 最后一个扇区的大小==0
    	文件大小=扇区数*200h bytes
    else if 最后一个扇区大小>0
      文件大小=(扇区数-1)*200h + 最后一个扇区的大小
    
  3. 具体的C语言代码实现如下(copy from blackwhite):

     // 设ExeHead的类型是char *, 它指向文件头的内容。
     if(*(short int *)(ExeHead+2) != 0)
          FileSize = (*(short int *)(ExeHead+4)-1)*0x200+ *(short int *)(ExeHead+2);
     else
          FileSize = *(short int *)(ExeHead+4) * 0x200;
    
  4. 根据此公式算出来的文件大小严格的讲是EXE文件载入内存的部分长度

为什么要在文件内部对文件的长度进行记录?

任何一个文件系统EXT,FAT...在储存文件的时候会保存文件目录+文件内容,文件目录里面包含文件名,文件长度,创建时间,文件第一块内容保存在何处等等,那么在EXE的文件头中为什么还需要记录文件大小呢?

  • 文件内部的文件长度可能小于文件目录里面所记录的长度:EXE在运行时末尾的一部分可以暂时不载入内存,这一部分代码叫做覆盖(overlay)

  • 当exe文件中存在覆盖时,根据文件头计算得出的exe长度(exe载入内存的长度)<在硬盘上存储的物理长度

    overlay技术的作用(与demand paging相似):

    1. 在DOS中,把常用的函数变量载入内存,把不常用的数据/函数放在overlay中,在使用时再放入内存

    2. 利用覆盖技术实现反跟踪,把dll动态库文件粘在文件末尾,当作覆盖,在程序运行时动态载入函数

验证文件头ss, cs中记录的是相对值

  1. 通过masmlink编译hello2.asm文件,得到hello2.exe

    hello2.asm的具体代码(from blackwhite)如下:

    data segment; 1000:0000
    abc db "Hello, abc!$"; 12字节=0Ch字节 
        db 10h dup(0); 16字节
    data ends
    
    code segment; 
    assume cs:code, ds:data, ss:stk
    begin:
       mov ax, data
       mov ds, ax
       mov ah, 9
       mov dx, offset abc
       int 21h
       mov ax, code; 编译时code被设成以下值
                   ; (code段-首个段的距离)/10h
       mov ds, ax
       mov ah, 9
       mov dx, offset xyz
       int 21h
       mov ah, 4Ch
       int 21h
    xyz db "Hello, xyz!$"
    main:
       jmp begin
    code ends
    
    stk segment stack
    db 100h dup('$'); 为了方便查看,将堆栈中的初始值改为'$'
    stk ends
    end main
    
  2. 在xp环境下,通过qv打开exe文件,跳过前200h字节(文件头),来到程序开始的地方

    • 数据段(data segment)位于的区域[200h-21Fh],数据段为该程序的第一个段

      abc db Hello, abc!$ 占0Ch个字节, db 10h dup(0); 占10h个字节

      由于下一个段开始的段地址需要合法(被10h整除),所以前一个段的段长度也需要被10h整除,

      在这个例子中,数据段最后的4个byte的作用是扩充数据段,使其长度可以被10h整除的

    • 代码段(code segment)位于的区域[220h-24Fh]

    • 堆栈段的起始地址是250h, 长度为100h

    验证文件头中ss和cs的值

    数据段的开始是在00000200, 代码段是在00000220, 堆栈段是在00000250

    • 在文件头中 [e-f]位保存的是ss的段相对地址=(250-200)/10h=5=0005

    • 在文件头中[10h-11h]位保存的是堆栈段的大小100h,sp的绝对值

    • 在文件头中 [16h-17h]位保存的是cs的段相对地址=(220-200)/10h=2=0002

    为什么ss, cs是相对值,sp可以是绝对值?

    stack的大小是绝对的,但是stk的实际值在编译的时候是不知道的

    编译器在编译时表示data,code,stk的时都采用相对值的形式,用该段的段地址-第一个段地址的差来表示

  3. 在Qview中使用Tab键,切换到assembler模式,观察代码段

以上内容是个人学习总结,如有错误,欢迎指出!