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.cpp 的 DLL_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.cpp的DLL_PROCESS_ATTACH最開頭就被設定(OpenThread(...)),那時MainMain還沒有建立實例,沒有this可以用。設成 static 讓它能在沒有實例的情況下被存取和設定。
Q:LoadConfig 如果 dinput8.ini 不存在會怎樣?
INIReader的所有 Get 函式都有「預設值」參數。如果檔案不存在或對應欄位缺失,就使用預設值(127.0.0.1:8484、1280x720)。登入器不會因為缺少設定檔而崩潰,只是用預設配置運行。
延伸閱讀
- 02_dllmain.cpp_DLL入口點 — CreateInstance 的呼叫起點
- 01_config.ini設定系統 — Phase 7:設定檔的格式與所有可用選項
- 02_INIReader.h解析 — Phase 7:INIReader 的實作細節
- 01_Client.cpp整體架構導覽 — MainMain 把設定同步給 Client 的流程