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,首段地址+Δ=重定位的 段地址
了解完文件頭各個字節的含義之后,為了加深理解,我們來看如下的幾個問題:
如何根據文件頭計算文件大小?
-
從文件頭[2-3]字節中可得出EXE文件在硬盤中的最后一個扇區的大小
從文件頭[4-5]字節中可得出EXE文件在硬盤中所占的扇區數
-
文件大小的計算公式如下:
if 最后一個扇區的大小==0 文件大小=扇區數*200h bytes else if 最后一個扇區大小>0 文件大小=(扇區數-1)*200h + 最后一個扇區的大小
-
具體的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;
-
根據此公式算出來的文件大小嚴格的講是EXE文件載入內存的部分長度
為什么要在文件內部對文件的長度進行記錄?
任何一個文件系統EXT,FAT...在儲存文件的時候會保存文件目錄+文件內容,文件目錄里面包含文件名,文件長度,創建時間,文件第一塊內容保存在何處等等,那么在EXE的文件頭中為什么還需要記錄文件大小呢?
-
文件內部的文件長度可能小於文件目錄里面所記錄的長度:EXE在運行時末尾的一部分可以暫時不載入內存,這一部分代碼叫做覆蓋(overlay)
-
當exe文件中存在覆蓋時,根據文件頭計算得出的exe長度(exe載入內存的長度)<在硬盤上存儲的物理長度
overlay技術的作用(與demand paging相似):
-
在DOS中,把常用的函數變量載入內存,把不常用的數據/函數放在overlay中,在使用時再放入內存
-
利用覆蓋技術實現反跟蹤,把dll動態庫文件粘在文件末尾,當作覆蓋,在程序運行時動態載入函數
-
驗證文件頭ss, cs中記錄的是相對值
-
通過
masm
,link
編譯hello2.asm文件,得到hello2.exehello2.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
-
在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的時都采用相對值的形式,用
該段的段地址-第一個段地址的差
來表示 -
-
在Qview中使用
Tab
鍵,切換到assembler模式,觀察代碼段
以上內容是個人學習總結,如有錯誤,歡迎指出!