標籤:5.6 尾碼 [] 解析 type 文章 指令碼 new 方式
N久不來 於是不知道扔在哪兒
於是放這裡先
如果你覺得礙事的話 幫我扔到合適的版塊去..
導讀
這是一篇說明文 它介紹了標準冒險島更新檔案(*.patch;*.exe)的格式
文章的最後附了一段C#的參考代碼 你可以自由的下載 編譯 或改寫為其他語言
文章不附加任何有風險的可執行檔(*.exe) 對此沒有興趣的可以直接後退瀏覽其他文章
目錄
0 前言
1 檔案結構
1.1 patch檔案結構
1.1.1 檔案頭
1.1.2 zlib段
1.2 exe檔案結構
1.2.1 exe段
1.2.2 patch段
1.2.3 notice段
1.2.4 檔案尾部
2 編程實現
2.1 exe檔案的分離
2.2 校正和的演算法
2.3 patch檔案預先處理
2.4 patch資料區段的結構
2.4.1 新增/替換檔案指令
2.4.2 重構檔案指令
2.4.3 刪除檔案指令
2.5 patch結束
3 擴充
4 感謝
0 前言
我們經常要更新冒險島
一部分是很自然的與官方版本同步 以正常登入遊戲
一部分是可以隨時關注外服的最新更新資訊
但是更新冒險島有一個很麻煩的問題
首先 我們需要有至少大於一個用戶端大小的硬碟剩餘空間
而在這裡的很多人 硬碟裡都有3個以上的用戶端 多則10個...
我們眼中可以看到的更新過程?
我們下載了官方提供的exe補丁或者patch補丁
如果下載了後者還要使用一些工具 把它轉換成可以執行的exe檔案
雙擊它 會提示瀏覽檔案夾 選中自己的冒險島用戶端所在檔案夾
正確的選中以後 補丁程式會在自身檔案夾下建立一個log檔案 並且在冒險島檔案夾下建立一個隨機的檔案夾以存放臨時檔案
更新結束以後 補丁程式把臨時檔案剪下回原來的用戶端
如果發生意外 程式則在沒有任何提示的情況下中止
你需要自行關閉程式 並且自行銷毀臨時檔案
很容易推測 這是補丁程式在“重構”用戶端檔案 並且“簡單替換”回原檔案的過程
當補丁過程意外中止時 這不會對原用戶端造成任何影響
為什麼我們要研究patch檔案結構?
簡單的說 因為每次對比都要留3份用戶端大小(舊版用戶端+新版用戶端+臨時檔案預留空間)的硬碟我很頭痛
所以 瞭解patch檔案的結構 有利於我們對這個過程的控制 最佳化
當然 這對於普通的冒險島玩家毫無意義
它很複雜嗎?
打補丁的過程出奇的簡單 甚至我用三句話就可以描述了:
1) patch檔案是用zlib壓縮 使用crc32驗證檔案版本和完整性的
2) patch檔案實際記錄了修改過程 它包含三種操作:替換 重構 和刪除檔案
3) 重構檔案是一個基於位元組的過程 它包含三種操作:複製新區段 填充定長段 複製原區段
結果呢?
以後我打補丁只需要原用戶端+1G左右的額外空間就行了(其他盤符 甚至是記憶體中都可以)
順帶我還能輸出一份基於img內部節點粒度的wz對比報告來
就這樣?
啊 還不夠麼...
難道你還要我能產生降版本的補丁麼...(其實好像真的可以...)
1 檔案結構
1.1 patch檔案結構
完整的patch檔案是由16位元組檔案頭以及不定位元組的zlib壓縮段組成
1.1.1 檔案頭 (0x0000-0x000F)
前8個位元組(0x0000-0x0007)是固定的 "WzPatch\x1A" 為檔案標識符
繼續4個位元組(0x0008-0x000B)是固定的 02 00 00 00 大概為檔案版本 值為2
----------------------------------------
|W|z|P|a|t|c|h|\x1A|\x02|\x00|\x00|\x00|
----------------------------------------
繼續4個位元組(0x000C-0x000F) 為zlib壓縮段的校正和 在2.2節會給出校正和的實際演算法
1.1.2 zlib段 (0x0010-eof)
這一段包含著patch檔案的實際壓縮資料 它由2位元組的zlib頭和剩餘的壓縮部分組成
前2個位元組(0x0010-0x0011)是固定的 78 DE 詳細含義請參考文檔rfc1950
剩餘的部分(0x0012-eof)為deflate壓縮資料區段 可以用標準的zlib演算法進行解壓縮
注意 patch檔案沒有包含壓縮段的資料長度資訊
壓縮前的資料結構會在2.3詳述
1.2 exe檔案結構
標準的exe補丁結構是由exe段 patch段 notice段 檔案尾部標識組成
基本上全世界各個伺服器的exe補丁 第三方工具產生的補丁 都遵守了這個約定
1.2.1 exe段
這個區塊實際上是一個標準的exe可執行檔 又稱MZ檔案 它是以位元組 4D 5A 作為起始標識的
exe段有固定的位元組流格式 但是沒有解釋的意義
它的具體長度對於各個服都不一樣 不過執行的功能大同小異
在它和patch段之間會有一段位元組填充區段 也可以看做exe段本身的一部分
1.2.2 patch段
這個區塊實際上是一個完整的patch檔案 它的格式已在1.1描述
它的長度在檔案尾部標識有所體現 和exe段之間有一道明顯的壕溝用於位元組對齊(沒有也無所謂)
1.2.3 notice段
這個區塊是一段ansi編碼的文本 實際上記錄了一些補丁的文字資訊
不過目前大多數補丁檔案不顯示這個區段了...
它的位元組長度在檔案尾部標識有所體現 和patch段直接相連 沒有首部和尾部的標識
1.2.4 檔案尾部標識 (fileLen-12)-eof
這個區塊是exe檔案中唯一定長的 可區分的一段
前4位元組 一個int值 表示patch段的區塊長度
中間4位元組 一個int值 表示notice段的區塊長度
後4位元組 固定的 f3 fb f7 f2標識
---------------------------------------------
|-- -- -- --|-- -- -- --|\xf3|\xfb|\xf7|\xf2|
---------------------------------------------
patchLen noticeLen
2 編程實現
這個章節將會描述如何讀取patch檔案並對用戶端進行更新的技術
2.1 exe檔案的分離
在1.2節已經分析了exe補丁的結構 我們只要簡單的解析尾部12位元組 就可以從exe中提取出patch段和notice段了
基本步驟如下:
> 開啟檔案流
> 判斷頭雙位元組是否是"MZ"
> 移動讀寫指標到(-4,SeekEnd)
> 讀取4位元組 看看是否符合尾部標識
> 移動讀寫指標到(-12,SeekEnd)
> 連續讀取兩個int 作為patch段和notice段的長度
> 移動讀寫指標到(-noticeLen-12,SeekEnd)
> 讀取noticeLen個位元組 並且解析成ansi字串 這記錄著補丁的更新文字資訊
> 移動讀寫指標到(-patchLen-noticeLen-12,SeekEnd)
> 讀取patchLen個位元組 作為patch段進行下一步處理
2.2 校正和的演算法
patch檔案中全部的校正和都使用crc32演算法 多項式為0x04C11DB7
在程式中使用查表法就很OK~~
詳細的演算法實現見程式檔案 CheckSum類實現了這個演算法
2.3 patch檔案預先處理
當你輸入了一個patch檔案 或者從exe中提取出了patch區塊
要進行如下步驟的預先處理:
> 移動讀寫指標到(0,SeekBegin)
> 讀取8個位元組 判斷是否是"WzPatch\x1A"
> 讀取一個int 作為patch格式版本
> 讀取一個uint 作為zlib段的checksum
> 對剩餘的位元組使用crc32進行hash 並和checksum對比 檢查檔案完整性
> 移動讀寫指標到(16,SeekBegin)
> 讀取2位元組 作為zlib頭標識 並且判斷壓縮類型(也可以不判斷)
> 對剩餘的位元組使用inflate解壓縮演算法 獲得不定長度byte[] 作為patch資料區段進行下一步處理
> 建立一個臨時檔案夾 用於存放更新後的用戶端
順帶一提 補丁這玩意壓縮率極低... 剛試了下用KMST452to454(65.9Mb)的補丁解壓 真實資料區段長度也才75.6Mb 通過zlib頭還能看出來它使用的是3級壓縮的...
果然這種壓縮毫無意義啊-△-
另外臨時檔案夾的選擇 一般和冒險檔案夾在同一個盤符 在目前的檔案系統中 這種操作會使補丁完成後的檔案轉移更迅速 否則 這會是一個很大規模的I/O操作 並且伴隨著風險 兩種選擇取決於實際需要
2.4 patch資料區段的結構
經過解壓縮的資料區段包含著補丁操作的控制資訊 這一區塊使用流式讀寫
基本結構如下:
{
{ fileName }{ patchType }[ { patchData } ]
}
[..n]
fileName: 不定長度的ascii編碼字串 表示要進行操作的檔案名稱或檔案夾相對路徑
例:"MapleStoryT.exe" "HShield\\" "HShield\\AhnUpCtl.dll"
fileName沒有‘\0‘結尾標識 需要與patchType一起讀出來區分邊界
patchType: 1位元組的標識位 範圍可能為00 01 02
00:表示檔案建立操作 這將建立並替換用戶端同名檔案
01:表示檔案重構作業 這將從用戶端原始檔案和patchData中讀取段落 產生一個新的檔案替換原檔案
02:表示檔案刪除操作 此時沒有也不需要patchData
patchType不僅標識了更新類型 還可以作為fileName的結束標識 實際讀取fileName時可以逐位元組讀取 當下一位元組小於等於02時終止
patchData:可選段 對於patchType=01時一定存在 對於patchType=00時可能存在(當fileName為檔案夾時不存在) 對於patchType=02時不存在
這個區段的結構也取決於對應的patchType 這將在下面章節詳述
2.4.1 新增/替換檔案指令
當patchType=00時 需要判斷fileName是否為檔案夾
判斷方式只要簡單的判斷是否含有尾碼名 或者尾位元組是否為‘\\‘
如果是檔案夾 則在臨時檔案夾下面建立一個相同名稱的檔案夾 操作結束
如果是檔案 patchData的格式如下:
0 4 8
------------------------------------
|-- -- -- --|-- -- -- --| …… ……
------------------------------------
fileLen checksum fileData
fileLen:4位元組int 表示新檔案的長度
checksum:4位元組uint 表示新檔案的crc校正和
fileData:fileLen長度 表示新檔案的位元組資料
處理過程如下:
> 依次讀取4位元組檔案長度 以及4位元組校正和
> 記錄當前讀寫指標的位置pos
> 對後面(fileLen)位元組使用crc32進行hash 並和checksum對比 檢查檔案完整性
> 移動讀寫指標回到pos
> 對後面(fileLen)位元組轉存到檔案{fileName}
2.4.2 重構檔案指令
當patchType==01時 則使用重構檔案的操作 我們需要輸入原冒險檔案夾中的同名檔案 和補丁資料區塊一同讀取
此時patchData的格式如下:
{
{ oldChecksum }{ newChecksum }
{{ commandBlock }[...n]}
{ commandEnd }
}
oldChecksum:4位元組uint 表示原檔案的checksum
newChecksum:4位元組uint 表示新檔案的checksum
commandBlock:補丁操作命令區塊 這個區塊長度不定 總的來說有三種命令格式
{
{ commandHeader }[ otherData ]
}
commandHeader:4位元組長度的操作命令的頭部 包含了豐富的資訊
1>
|4bit| 28bit | ...
-------------------------
|1000| length | dataBlock
當高4位的值為0x08時 低28位則作為長度標識 這將進行如下操作:
從補丁中讀取length長度位元組資料 並寫入到新檔案中
2>
|4bit| 20bit | 8bit |
-----------------------
|1100| length | byte |
當高4位的值為0x0c時 中間20位作為長度標識 低8位作為一個byte的資訊 進行如下操作:
向新檔案中填充length長度的重複byte位元組
此時這個區段不包含otherData
3>
| 32bit | 32bit |
---------------------
| length | offset |
如果高4位並非以上值 則它的格式為這樣 header和otherData各佔4位元組 分別表示length和offset資訊 它將進行如下操作:
從舊檔案中offset位元組開始 讀取length長度資料 然後寫入到新檔案中
commandEnd:4位元組 固定的00 00 00 00 標識patchData的結束
當執行完檔案重構指令後 應當對新檔案進行crc32檢查完整性並再次比較 如果通過驗證 則關閉檔案流進行下個檔案的更新
2.4.3 刪除檔案指令
它除了檔案名稱沒有包含任何額外的資訊...當然大多數時候你很少處理它...或者簡單處理它即可
當年國際服好像有這樣一個故事...製作更新補丁的時候不小心打包進去一個mob1.wz 然後補丁中建立了這個多餘的檔案...理所當然的...下一個補丁把這個檔案移除了
檔案夾裡出現多餘檔案的情況很常見 經常在韓服用戶端裡發現程式猿不小心遺留的服務端指令碼...
2.5 patch結束
當你根據patch資料區段對原用戶端進行處理 產生新用戶端臨時檔案後 只需要按照更新類型對檔案剪下 即可以獲得一份完整的更新後用戶端來 其他的操作 如回收記憶體 刪除臨時檔案夾等操作也應一併執行如果臨時檔案夾和冒險用戶端檔案夾在同一盤符上 這是一個很簡單的操作 基本不會出現意外
如果檔案覆蓋過程中出錯 則會破壞整個用戶端完整性 這將造成很大的災難...只能通過手動更新才能實現用戶端恢複...否則只能重新下載完整用戶端
3 擴充
對整個補丁檔案結構和執行過程瞭解以後 就可以對冒險島打補丁的過程進行控制和擴充
比較容易想象 為了節省臨時檔案空間 可以對每個臨時檔案產生後 直接覆蓋原用戶端檔案
如果補丁過程因為異常中斷 下次執行patch的時候可以進行檔案hash對比 如果判定舊有檔案的hash符合舊檔案則執行更新 符合新檔案則跳過 這樣可以最大限度保證用戶端完整
另外比較實用的一種更新模式 即產生臨時檔案後可以直接與原檔案進行對比產生報告
當瞭解了patch檔案的結構後 你可以很容易預讀補丁相關檔案的大小 校正和等資訊 也可以自由的改變補丁執行順序
應該還會有其他的對於更新過程可以擴充的方式 暫不列舉
4 感謝
拖了整整半個月才成文 代碼的部分還是沒有整理的太完美 不過還是嘗試把自己的用戶端更新了 問題不大
特別感謝在我一頭霧水的時候發現的Fiel大神的文檔...太美好了...
你可以在southperry上找到原始碼 是用C編寫的 關鍵字為"NXPatcher"
附件裡包含著C#的原始碼和一個測試案例
代碼寫的略亂而且注釋很少 請配合上述參考資料一同閱讀
[轉]Patch檔案結構詳解