MainMain.cpp 解析:初始化流程的指揮官

本篇定位

這是 Phase 6 的第 3 篇,也是整個登入器「啟動流程」的最終拼圖。 MainMain.cpp 是一個 Singleton 類別,負責統籌所有模組的初始化順序:讀設定檔、等待 Themida 解壓縮、分配記憶體、呼叫 Client 和其他 Hook 的安裝。


一、什麼是 Singleton?

MainMain 使用 Singleton 設計模式:整個程式生命週期內只存在一個實例,所有模組都透過同一個入口取得它。

class MainMain {
public:
    // 取得唯一實例(不存在則建立)
    static MainMain* GetInstance();
 
    // 建立實例並執行初始化(由 dllmain.cpp 的 MainProc 呼叫)
    static void CreateInstance(void(*mainFunc)());
 
    // 解構子:清理所有資源
    ~MainMain();
 
    // ── 設定值(從 config.ini 讀入)─────────────────────────────────────
    const char* GetServerIp()   const { return m_serverIp; }
    uint16_t    GetServerPort() const { return m_serverPort; }
    int         GetGameWidth()  const { return m_gameWidth; }
    int         GetGameHeight() const { return m_gameHeight; }
 
    // ── 執行緒管理 ────────────────────────────────────────────────────────
    static HANDLE mainTHread;   // 主執行緒 Handle(dllmain.cpp 設定)
 
private:
    MainMain();   // 建構子私有,防止外部直接 new
 
    static MainMain* s_instance;
 
    // 設定值
    char     m_serverIp[64];
    uint16_t m_serverPort;
    int      m_gameWidth;
    int      m_gameHeight;
    bool     m_bWindowed;
};

二、CreateInstance() 的完整流程

這是整個登入器初始化的指揮中心:

void MainMain::CreateInstance(void(*mainFunc)()) {
    // ── 步驟一:建立 Singleton 實例 ─────────────────────────────────────
    s_instance = new MainMain();
 
    // ── 步驟二:初始化自訂記憶體分配器 ─────────────────────────────────
    ZAllocEx::Init();
    ZAllocEx::InstallHooks(true);
 
    // ── 步驟三:讀取設定檔 ───────────────────────────────────────────────
    s_instance->LoadConfig();
 
    // 把解析度設定同步到 Client 模組
    Client::m_nGameWidth  = s_instance->m_gameWidth;
    Client::m_nGameHeight = s_instance->m_gameHeight;
    Client::m_bWindowed   = s_instance->m_bWindowed;
 
    // ── 步驟四:等待 Themida 解壓縮完成 ─────────────────────────────────
    WaitForThemida();
 
    // ── 步驟五:呼叫主函式(安裝所有 Hook + Client 修改)────────────────
    mainFunc();  // 這裡呼叫的是 dllmain.cpp 的 MainFunc()
}

三、LoadConfig():讀取設定檔

void MainMain::LoadConfig() {
    INIReader reader("dinput8.ini");   // 和 DLL 同目錄的設定檔
 
    // 讀取伺服器 IP(預設 127.0.0.1)
    std::string ip = reader.Get("Connection", "ServerIP", "127.0.0.1");
    strncpy(m_serverIp, ip.c_str(), sizeof(m_serverIp) - 1);
 
    // 讀取 Port(預設 8484)
    m_serverPort = (uint16_t)reader.GetInteger("Connection", "Port", 8484);
 
    // 讀取解析度
    m_gameWidth  = reader.GetInteger("Display", "Width",  1280);
    m_gameHeight = reader.GetInteger("Display", "Height", 720);
    m_bWindowed  = reader.GetBoolean("Display", "Windowed", true);
 
    std::cout << "[Config] Server: " << m_serverIp << ":" << m_serverPort << std::endl;
    std::cout << "[Config] Resolution: " << m_gameWidth << "x" << m_gameHeight << std::endl;
}

四、WaitForThemida():輪詢等待解壓縮

Themida 是 MapleStory 使用的程式碼保護工具。它把遊戲的機器碼加密壓縮,啟動後才在記憶體中即時解壓縮。如果我們的 Hook 太早安裝,那段記憶體還是加密狀態,Hook 會打在錯誤的 byte 上,導致崩潰。

WaitForThemida() 透過「輪詢特徵 byte」來等待解壓縮完成:

void MainMain::WaitForThemida() {
    // 選一個位於 Themida 保護段落中的地址
    // 加密時這個地址的值不固定;解壓縮後,第一個 byte 會變成 0x55(PUSH EBP,函式開頭)
    constexpr uintptr_t CHECK_ADDR = addys::CWvsApp_SetUp;  // 0x009F5239
 
    std::cout << "[MainMain] Waiting for Themida depack..." << std::endl;
 
    while (*(BYTE*)CHECK_ADDR != 0x55) {
        Sleep(100);  // 每 100ms 檢查一次
    }
 
    // 多等一點,讓整個解壓縮過程徹底完成
    Sleep(500);
 
    std::cout << "[MainMain] Themida depack complete." << std::endl;
}

為什麼檢查 0x55?

x86 函式開頭的標準 Prologue 是 PUSH EBP,機器碼 0x55。這是所有正常 C++ 函式在編譯後的第一個 byte,可以作為「函式已經被解壓縮回來」的特徵。

極端情況

如果 Themida 解壓縮速度很慢(老電腦或 Debug 模式),100ms 的 Sleep 間隔可能導致等太久。實務上 MapleEzorsia-v2 也有超時保護:如果等超過 30 秒還沒解完,就彈出錯誤訊息並退出。


五、解構子 ~MainMain():清理資源

MainMain::~MainMain() {
    std::cout << "[MainMain] Shutting down..." << std::endl;
 
    // 卸載所有 Hook(按安裝的反序)
    ZAllocEx::InstallHooks(false);
 
    // 關閉主執行緒 Handle
    if (mainTHread) {
        CloseHandle(mainTHread);
        mainTHread = nullptr;
    }
 
    s_instance = nullptr;
}

dllmain.cppDLL_PROCESS_DETACH 呼叫:

case DLL_PROCESS_DETACH:
    MainMain::GetInstance()->~MainMain();
    break;

六、整個登入器的啟動時序總覽

sequenceDiagram
    participant Win as Windows 載入器
    participant DLL as DllMain
    participant MM  as MainMain
    participant C   as Client
    participant Game as MapleStory.exe

    Game->>Win: 請求載入 dinput8.dll
    Win->>DLL: DllMain(DLL_PROCESS_ATTACH)
    DLL->>DLL: Hook_CreateMutexA / Hook_WSPStartup 等(Windows API Hook)
    DLL->>DLL: CreateThread → MainProc
    Win-->>Game: DLL 載入完成

    Note over Game: Themida 開始解壓縮

    DLL->>MM: MainMain::CreateInstance(MainFunc)
    MM->>MM: ZAllocEx::Init + InstallHooks
    MM->>MM: LoadConfig(讀 dinput8.ini)
    MM->>C: 同步解析度設定
    MM->>MM: WaitForThemida(輪詢 0x55)

    Note over Game: Themida 解壓縮完成

    MM->>DLL: 呼叫 MainFunc()
    DLL->>DLL: 安裝所有遊戲內部 Hook(Detours)
    DLL->>C: Client::UpdateGameStartup()
    DLL->>C: Client::UpdateResolution()
    DLL->>DLL: dinput8::CreateHook()

    Note over Game: 遊戲正常啟動,所有修改生效

七、常見問題

Q: mainTHread 為什麼是 public static?

因為它在 dllmain.cppDLL_PROCESS_ATTACH 最開頭就被設定(OpenThread(...)),那時 MainMain 還沒有建立實例,沒有 this 可以用。設成 static 讓它能在沒有實例的情況下被存取和設定。

Q:LoadConfig 如果 dinput8.ini 不存在會怎樣?

INIReader 的所有 Get 函式都有「預設值」參數。如果檔案不存在或對應欄位缺失,就使用預設值(127.0.0.1:84841280x720)。登入器不會因為缺少設定檔而崩潰,只是用預設配置運行。


延伸閱讀