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 就在這個時候被載入。

載入順序:

  1. 玩家點擊 MapleStory.exe
  2. Windows 找到遊戲需要的 dinput8.dll(我們放的假版本)
  3. Windows 立刻呼叫 DllMain(... DLL_PROCESS_ATTACH ...)
  4. 我們的程式開始執行

這是整個登入流程的起點。


三、為什麼不能在 DllMain 裡做太多事?

Loader Lock 限制

Windows 在呼叫 DllMain 時,內部有一把「Loader Lock(載入器鎖)」。 這把鎖的規定是:

  • 不能在 DllMain 裡面載入其他 DLL
  • 不能呼叫大多數 Win32 API
  • 不能做複雜的初始化操作

如果違反這些規定,程式會死鎖(Deadlock)——就是兩個人互相等待對方,永遠卡住。

所以我們的 DllMain 只做兩件事:

  1. 安裝幾個「最早期」的 Windows API Hook
  2. 開一條新執行緒(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_CreateMutexACreateMutexA讓遊戲可以多開視窗(原版只允許一個客戶端)
Hook_WSPStartupWSPStartup讓遊戲連線到我們的自訂 IP(私服伺服器)
Hook_CreateWindowExACreateWindowExA替遊戲視窗加上最小化按鈕
Hook_FindFirstFileAFindFirstFileA阻止遊戲偵測到資料夾裡的假 DLL(防 DLL 衝突偵測)
Hook_GetACPGetACP處理語系問題(避免文字亂碼)
Hook_GetModuleFileNameWGetModuleFileNameW修正遊戲讀取自身路徑的結果
Hook_GetLastErrorGetLastError目前停用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 / 494D2FCClientSocket::Connect讓連線指向私服伺服器
sub_9F4E54Crc32 檢查器關閉 CRC 完整性驗證(避免我們的修改被偵測)
sub_9F4FDACWvsApp::CWvsApp攔截遊戲 App 建構子
sub_9F5239CWvsApp::SetUp攔截遊戲初始化設定
sub_9F5C50CWvsApp::Run攔截遊戲主迴圈
sub_9F7CE1CWvsApp::InitializeInput攔截輸入初始化
sub_9F84D0CWvsApp::CallUpdate攔截每幀更新呼叫
sub_9F7964IWzFileSystem::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 分離時機。


延伸閱讀