dllmain.cpp 解析:DLL 的入口點
本篇定位
這篇筆記對應專案檔
dllmain.cpp,是整個登入器 DLL 的「大門」。 讀完這篇,你會明白:遊戲啟動後第一行屬於我們的程式碼是從哪裡開始執行的。
一、什麼是 DllMain?
每一個 Windows DLL 都必須有一個叫做 DllMain 的函式。
你可以把它想像成百貨公司的服務台:
- 有顧客(進程)進門——服務台馬上通知你(
DLL_PROCESS_ATTACH) - 有顧客(進程)離開——服務台馬上通知你(
DLL_PROCESS_DETACH)
Windows 在載入或卸載 DLL 時,會自動呼叫這個函式,並用 ul_reason_for_call 這個參數告訴你「現在是什麼情況」。
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; // 回傳 TRUE 表示初始化成功
}白話翻譯
BOOL APIENTRY是 Windows 規定的函式格式,不用在意細節。return TRUE告訴 Windows「我初始化成功,沒問題」。若回傳FALSE,DLL 會被立刻卸載。
二、DLL_PROCESS_ATTACH:我們能做事的第一個機會
遊戲啟動時,Windows 會把所有 DLL 依序載入。我們的假 dinput8.dll 就在這個時候被載入。
載入順序:
- 玩家點擊
MapleStory.exe - Windows 找到遊戲需要的
dinput8.dll(我們放的假版本) - Windows 立刻呼叫
DllMain(... DLL_PROCESS_ATTACH ...) - 我們的程式開始執行
這是整個登入流程的起點。
三、為什麼不能在 DllMain 裡做太多事?
Loader Lock 限制
Windows 在呼叫
DllMain時,內部有一把「Loader Lock(載入器鎖)」。 這把鎖的規定是:
- 不能在 DllMain 裡面載入其他 DLL
- 不能呼叫大多數 Win32 API
- 不能做複雜的初始化操作
如果違反這些規定,程式會死鎖(Deadlock)——就是兩個人互相等待對方,永遠卡住。
所以我們的 DllMain 只做兩件事:
- 安裝幾個「最早期」的 Windows API Hook
- 開一條新執行緒(
CreateThread),讓真正的初始化工作在新執行緒裡進行
case DLL_PROCESS_ATTACH:
{
// 取得目前執行緒的 Handle,供後續暫停/恢復使用
MainMain::mainTHread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, GetCurrentThreadId());
// 只安裝最基礎的 Windows API Hook
Hook_CreateMutexA(true);
Hook_WSPStartup(true);
// ...(其餘 Hook)...
DisableThreadLibraryCalls(hModule); // 關掉執行緒通知,減少不必要呼叫
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&MainProc, NULL, 0, 0); // 開新執行緒
break;
}四、Windows API Hook 區段解析
這些 Hook 在 DLL_PROCESS_ATTACH 最早期安裝,目的是「攔截遊戲對 Windows 系統的呼叫」,在遊戲真正啟動前就改變它的行為。
| Hook 函式 | 攔截的 Windows API | 目的 |
|---|---|---|
Hook_CreateMutexA | CreateMutexA | 讓遊戲可以多開視窗(原版只允許一個客戶端) |
Hook_WSPStartup | WSPStartup | 讓遊戲連線到我們的自訂 IP(私服伺服器) |
Hook_CreateWindowExA | CreateWindowExA | 替遊戲視窗加上最小化按鈕 |
Hook_FindFirstFileA | FindFirstFileA | 阻止遊戲偵測到資料夾裡的假 DLL(防 DLL 衝突偵測) |
Hook_GetACP | GetACP | 處理語系問題(避免文字亂碼) |
Hook_GetModuleFileNameW | GetModuleFileNameW | 修正遊戲讀取自身路徑的結果 |
Hook_GetLastError | GetLastError | 目前停用(false),因為啟用後會讓遊戲 crash |
為什麼 Hook_GetLastError 是 false?
GetLastError是遊戲用來檢查「上一個操作有沒有出錯」的 API。 若我們攔截它並回傳錯誤碼,遊戲可能誤以為某個初始化失敗,直接崩潰。 所以開發者先把它關掉,等找到正確的回傳值再啟用。
五、MainProc 與 MainFunc 的分工
CreateThread 開啟的新執行緒會執行 MainProc,形成以下三層結構:
DllMain (入口點)
└── CreateThread → MainProc (執行緒啟動函式)
└── MainMain::CreateInstance(MainFunc)
└── MainFunc (真正安裝 Hook 的地方)
MainProc — 執行緒的起點
void MainProc() { MainMain::CreateInstance(MainFunc); }非常簡短。它只是呼叫 MainMain::CreateInstance,把 MainFunc 當作參數傳進去。
MainMain 負責:
- 讀取設定檔(私服 IP、解析度等)
- 等待遊戲的 Themida 保護層解壓縮完畢(內部有
Sleep迴圈) - 一切就緒後,才呼叫
MainFunc
為什麼要「等」?
Themida 是一種程式保護機制,遊戲執行初期會把程式碼壓縮起來,啟動後才即時解壓縮。 如果我們的 Hook 太早安裝,那段記憶體還不存在——Hook 會打在錯誤的位置,遊戲直接崩潰。
MainMain裡的等待邏輯就是在解決這個問題。
MainFunc — 真正安裝遊戲內部 Hook 的地方
void MainFunc() {
// 虛擬化段落的 Hook(v83)
Hook_sub_44E88E(true); // MyGetProcAddress
Hook_sub_494CA3(true); // CClientSocket::Connect
Hook_sub_494D07(true); // CClientSocket::Connect (部分)
Hook_sub_494D2F(true); // CClientSocket::Connect (sockaddr_in)
Hook_sub_9F4E54(true); // Crc32 非檢查器
Hook_sub_9F4FDA(true); // CWvsApp::CWvsApp (建構子)
Hook_sub_9F5239(true); // CWvsApp::SetUp
Hook_sub_9F5C50(true); // CWvsApp::Run
Hook_sub_9F7CE1(true); // CWvsApp::InitializeInput
Hook_sub_9F84D0(true); // CWvsApp::CallUpdate
HookCWvsApp__Dir_BackSlashToSlash(true);
Hook_sub_9F7964(true); // IWzFileSystem::Init
Hook_sub_9F7159(true); // CWvsApp::InitializeResMan
Hook_StringPool__GetString(true);
Hook_sub_78C8A6(true); // 自訂 EXP 表
// ...(其他 Hook)...
std::cout << " Applying updated startup routines" << std::endl;
Client::UpdateGameStartup();
std::cout << "Applying resolution " << Client::m_nGameWidth << "x" << Client::m_nGameHeight << std::endl;
Client::UpdateResolution();
dinput8::CreateHook();
std::cout << "dinput8 hook initialized" << std::endl;
}這裡 Hook 的是遊戲自己的函式(以記憶體位址識別,例如 sub_9F5239),而不是 Windows API。
| 代號 | 對應函式 | 作用 |
|---|---|---|
sub_494CA3 / 494D07 / 494D2F | CClientSocket::Connect | 讓連線指向私服伺服器 |
sub_9F4E54 | Crc32 檢查器 | 關閉 CRC 完整性驗證(避免我們的修改被偵測) |
sub_9F4FDA | CWvsApp::CWvsApp | 攔截遊戲 App 建構子 |
sub_9F5239 | CWvsApp::SetUp | 攔截遊戲初始化設定 |
sub_9F5C50 | CWvsApp::Run | 攔截遊戲主迴圈 |
sub_9F7CE1 | CWvsApp::InitializeInput | 攔截輸入初始化 |
sub_9F84D0 | CWvsApp::CallUpdate | 攔截每幀更新呼叫 |
sub_9F7964 | IWzFileSystem::Init | 攔截資源檔案系統初始化 |
sub_78C8A6 | 自訂 EXP 表 | 讓升級 EXP 曲線符合私服設定 |
最後三行是套用解析度設定,並初始化 dinput8 的 Hook 鏈。
六、DLL_PROCESS_DETACH:善後清理
case DLL_PROCESS_DETACH:
{
MainMain::GetInstance()->~MainMain();
break;
}當遊戲關閉(或 DLL 被卸載),Windows 會觸發 DLL_PROCESS_DETACH。
我們在這裡手動呼叫 MainMain 的解構子,目的是:
- 關閉 Socket 連線
- 釋放佔用的記憶體
- 清理 Hook(避免殘留指標造成問題)
為什麼要手動呼叫解構子?
C++ 的全域或 Singleton 物件在某些情況下不會自動清理。 手動呼叫
~MainMain()確保資源在 DLL 卸載前被正確釋放。
七、整體執行時序圖
sequenceDiagram participant Win as Windows 載入器 participant DLL as dinput8.dll (我們) participant Thread as 新執行緒 (MainProc) participant Main as MainMain participant Game as MapleStory.exe Game->>Win: 請求載入 dinput8.dll Win->>DLL: DllMain(DLL_PROCESS_ATTACH) DLL->>DLL: 安裝 Windows API Hook<br/>(CreateMutexA / WSPStartup 等) DLL->>Thread: CreateThread(MainProc) Win-->>Game: DLL 載入完成,繼續啟動 Note over Game: Themida 開始解壓縮遊戲程式碼 Thread->>Main: MainMain::CreateInstance(MainFunc) Main->>Main: 讀取設定檔(IP、解析度) Main->>Main: 等待 Themida 解壓縮完成 Note over Main: 輪詢記憶體特徵,確認解壓完畢 Main->>DLL: 呼叫 MainFunc() DLL->>DLL: 安裝遊戲內部 Hook<br/>(CWvsApp / CClientSocket 等) DLL->>Game: Client::UpdateGameStartup() DLL->>Game: Client::UpdateResolution() DLL->>DLL: dinput8::CreateHook() Note over Game: 遊戲正常運行(已套用所有修改) Game->>Win: 遊戲關閉 Win->>DLL: DllMain(DLL_PROCESS_DETACH) DLL->>Main: ~MainMain()(清理資源)
八、常見問題
Q:為什麼 Hook 要分兩批?
第一批(DllMain 裡):攔截 Windows API,這些 API 遊戲一啟動就會呼叫,必須最早安裝。 第二批(MainFunc 裡):攔截遊戲內部函式,這些函式的記憶體位址在 Themida 解壓縮前是無效的,必須等待。
Q:
sub_9F5239這種名字是什麼意思?這是逆向工程工具(如 IDA Pro)在看不到原始碼時,自動以記憶體位址命名的函式代號。
sub是 subroutine(子程式),9F5239是十六進位的記憶體位址。 旁邊的注解(如CWvsApp::SetUp)是開發者手動分析後補上的真實名稱。
Q:我可以直接在 DllMain 裡安裝所有 Hook 嗎?
不行。Loader Lock 限制 + Themida 解壓縮時機問題,會讓遊戲在啟動階段崩潰。 必須用
CreateThread分離時機。
延伸閱讀
- 01_專案總覽(.sln與.vcxproj) — 了解 dllmain.cpp 在整個專案中的位置
- 01_ReplacementFuncs.h_函式替換技術 — 深入了解
Hook_sub_XXXXXX函式如何實作 - 03_dinput8代理DLL原理 — 了解為什麼用 dinput8.dll 作為注入載體
- 03_MainMain.cpp_初始化流程解析 — MainMain 如何等待 Themida 解壓縮完畢
- 03_dinput8代理DLL原理 — 複習 DLL 與 Proxy 的基本原理