上一篇認識了 DLL 劫持的原理和開發環境。這篇進入 MapleEzorsia-v2 的專案本身:.sln/.vcxproj 各自的角色、每個檔案的職責,以及 DLL 真正的入口點 dllmain.cpp 怎麼運作,最後完整走一次編譯到放進遊戲目錄的流程。
.sln 與 .vcxproj:工廠大門與建造藍圖
.sln(Solution)是 Visual Studio 的方案檔,本身不含任何程式碼,只是一份清單,告訴 Visual Studio「這個方案底下有哪些子專案」。永遠雙擊 .sln 開啟專案,不要直接點 .vcxproj,才能讓環境設定一起載入。
.vcxproj 才是真正的建造藍圖,記錄了要編譯哪些 .cpp、要連結哪些函式庫、輸出檔案叫什麼名字放哪裡。MapleEzorsia-v2 的關鍵設定:
| 設定 | 值 | 說明 |
|---|---|---|
ConfigurationType | DynamicLibrary | 這是DLL專案,不是exe |
TargetName | dinput8 | 輸出檔名叫dinput8.dll,不是ezorsia.dll |
OutDir | out\Release\ | 編譯完成的位置 |
LanguageStandard | stdcpp17 | 使用C++17標準 |
專案名稱叫
ezorsia,但輸出檔名故意設定成dinput8.dll——因為 MapleStory 啟動時會自動讀取遊戲資料夾裡的dinput8.dll,這正是上一篇介紹的 DLL Proxy 劫持手法。
連結的函式庫:Winmm.lib(多媒體/計時器)、Ws2_32.lib(Socket網路連線)、detours.lib(Hook核心,攔截並替換函式)。
各檔案職責一覽
| 檔案 | 職責 |
|---|---|
dllmain.cpp | DLL入口點,Windows載入DLL時第一個執行的地方 |
dinput8.cpp | Proxy轉發,把DirectInput呼叫轉給真正的系統DLL |
MainMain.cpp | 初始化主體,讀取config.ini、決定開啟哪些功能 |
Client.cpp | 客戶端修改核心,記憶體修改/封包攔截/功能注入都在這 |
Memory.cpp | 記憶體讀寫工具 |
ZAllocEx.cpp | 自訂記憶體分配器,讓程式碼相容MapleStory的記憶體管理方式 |
另外還有一個 MapleClientCollectionTypes\ 子資料夾,裡面是社群專案 MapleClientEditTemplate 提供的型別定義(ZArray、ZList、ZMap、ZXString、ZRef 等,命名都以 Z 開頭,對應 MapleStory 引擎自研的 ZLib 架構)。這些是「地基」,除非要追查特定內部資料結構,否則不需要修改這個資料夾。
dllmain.cpp:DLL 的入口點
每個 Windows DLL 都必須有一個 DllMain 函式,Windows 在載入或卸載 DLL 時會自動呼叫它:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: // 有人把我們載入了
...
case DLL_PROCESS_DETACH: // 有人要卸載我們了
...
}
return TRUE;
}
為什麼不能在 DllMain 裡做太多事
Windows 呼叫 DllMain 時內部有一把 Loader Lock(載入器鎖):不能在裡面載入其他DLL、不能呼叫大多數Win32 API、不能做複雜初始化,違反會導致死鎖。所以 DllMain 只做兩件事:安裝幾個最早期的 Windows API Hook,然後開一條新執行緒讓真正的初始化工作在裡面進行:
case DLL_PROCESS_ATTACH:
{
MainMain::mainTHread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, GetCurrentThreadId());
Hook_CreateMutexA(true);
Hook_WSPStartup(true);
// ...
DisableThreadLibraryCalls(hModule);
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&MainProc, NULL, 0, 0);
break;
}
這裡安裝的 Windows API Hook 攔截的是系統呼叫本身,目的是在遊戲真正啟動前就改變行為:
| Hook | 攔截的API | 目的 |
|---|---|---|
Hook_CreateMutexA | CreateMutexA | 讓遊戲可以多開視窗(原版只允許一個) |
Hook_WSPStartup | WSPStartup | 讓遊戲連到自訂IP(私服) |
Hook_CreateWindowExA | CreateWindowExA | 替遊戲視窗加上最小化按鈕 |
Hook_FindFirstFileA | FindFirstFileA | 阻止遊戲偵測到假DLL |
Hook_GetACP | GetACP | 處理語系問題,避免文字亂碼 |
MainProc / MainFunc:為什麼要分兩批 Hook
新執行緒執行 MainProc → MainMain::CreateInstance(MainFunc)。MainMain 負責讀取設定檔,並等待遊戲的 Themida 保護層解壓縮完畢才呼叫 MainFunc——因為遊戲內部函式的記憶體位址在解壓縮前是無效的,太早安裝 Hook 會打在錯誤位置導致崩潰。
MainFunc 才是真正安裝遊戲內部函式 Hook 的地方(以記憶體位址識別,例如 sub_9F5239):
| 代號 | 對應函式 | 作用 |
|---|---|---|
sub_494CA3 等 | CClientSocket::Connect | 讓連線指向私服 |
sub_9F4E54 | Crc32檢查器 | 關閉完整性驗證 |
sub_9F5239 | CWvsApp::SetUp | 攔截遊戲初始化設定 |
sub_9F5C50 | CWvsApp::Run | 攔截遊戲主迴圈 |
sub_9F7964 | IWzFileSystem::Init | 攔截資源檔案系統初始化 |
所有 Hook 裝完後,最後才呼叫 Client::UpdateResolution() 套用解析度設定,並呼叫 dinput8::CreateHook()(上一篇介紹過的Proxy轉發)初始化。順序很重要:如果Proxy先啟動,遊戲輸入系統開始運作,但Hook還沒裝完,會造成競爭條件或記憶體存取衝突。
DLL_PROCESS_DETACH(遊戲關閉時)則手動呼叫 MainMain 的解構子,清理Socket連線和記憶體,避免殘留指標造成問題。
完整編譯流程
1. 開啟並確認設定
雙擊 Ezorsia V2.sln 開啟專案(第一次載入可能要等 30 秒到 1 分鐘),確認頂端工具列兩個下拉選單:Configuration 選 Release,Platform 選 x86。Platform 選錯成 x64 的話,編譯出來的 DLL 放進遊戲目錄會完全沒有反應,不會有任何錯誤提示。
2. 建置
Ctrl+Shift+B 或選單 Build → Build Solution,Output 視窗最後出現:
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
3. 找到輸出檔案並複製到遊戲目錄
編譯成功後,out\Release\ 資料夾會出現三個檔案:
out\Release\
├── dinput8.dll ← 主要DLL
├── config.ini ← 設定檔(自動複製)
└── EzorsiaV2_UI.wz ← UI資源(自動複製)
把這三個檔案全部複製進 MapleStory 遊戲目錄——dinput8.dll 啟動時會讀取另外兩個檔案,缺任何一個都會導致閃退或功能失效。啟動遊戲後觀察畫面是否變成HD解析度,有變化就代表DLL已成功載入。
常見編譯錯誤
| 錯誤訊息 | 原因 | 解法 |
|---|---|---|
LNK1104: cannot open file 'detours.lib' | detours\ 資料夾沒跟 .sln 放同一層 | 確認路徑存在,必要時重新完整下載倉庫 |
C1010: unexpected end of file while looking for precompiled header | stdafx.cpp 沒設定為建立PCH | 右鍵stdafx.cpp → Properties → C/C++ → Precompiled Headers → 設為 Create (/Yc) |
C2065: 'xxx': undeclared identifier | 某個.h標頭檔遺失或Include路徑錯誤 | 確認所有原始碼檔案都完整下載 |
| 編譯成功但放進遊戲後沒效果 | Platform選了x64 | 改回x86重新編譯 |
LNK2019: unresolved external symbol | 函式庫連結問題 | 確認vcxproj未被修改,或重新下載原始碼 |
開啟.sln要求「Retarget」 | Toolset版本不符(需要v143) | 安裝Visual Studio 2022或v143 Build Tools |
下一篇進入記憶體操作與 Hook 技術:了解 Memory.cpp、AddyLocations.h 這些工具怎麼讀寫遊戲記憶體,以及 Detours 函式庫實際攔截函式呼叫的原理。