AddyLocations.h 解析:GMS v83 地址表

本篇定位

這是 Phase 3 的第 3 篇。AddyLocations.h 是整個登入器的「地圖」:所有需要修改的遊戲函式和資料,都在這裡以記憶體地址的形式登記。 讀完這篇,你會明白:為什麼地址是硬式編碼的,以及如何讀懂這份地址清單。


一、這個檔案的角色

MapleEzorsia-v2 修改遊戲行為的方式,是直接在記憶體裡找到目標位置並改動它。但每次操作都把地址寫死在程式碼裡,會讓程式碼變得混亂且難以維護:

// 沒有地址表時 — 四散各處的魔法數字
Memory::WriteProtected<BYTE>(0x009F5239, 0xE9);  // 這是什麼?為什麼是這個地址?
Memory::WriteProtected<BYTE>(0x009F4E54, 0x90);  // 和上面有什麼關係?

AddyLocations.h 把所有地址集中在一處,並給每個地址一個有意義的名稱:

// 有地址表時 — 清楚的意圖
Memory::WriteProtected<BYTE>(addys::CWvsApp_SetUp, 0xE9);
Memory::WriteProtected<BYTE>(addys::Crc32_Check,   0x90);

二、檔案結構:命名空間 + 常數

#pragma once
#include <cstdint>
 
namespace addys {  // 用命名空間避免和其他地址定義衝突
 
    // ── Windows API Hook 目標 ────────────────────────────────────────────
    // 這些是遊戲呼叫的 Windows API,我們在 Import Address Table 裡攔截
 
    // ── 遊戲初始化流程 ───────────────────────────────────────────────────
    constexpr uintptr_t CWvsApp_Ctor          = 0x009F4FDA;  // CWvsApp 建構子
    constexpr uintptr_t CWvsApp_SetUp         = 0x009F5239;  // CWvsApp::SetUp
    constexpr uintptr_t CWvsApp_Run           = 0x009F5C50;  // CWvsApp::Run(主迴圈)
    constexpr uintptr_t CWvsApp_InitInput     = 0x009F7CE1;  // CWvsApp::InitializeInput
    constexpr uintptr_t CWvsApp_CallUpdate    = 0x009F84D0;  // CWvsApp::CallUpdate
 
    // ── 網路連線 ─────────────────────────────────────────────────────────
    constexpr uintptr_t CClientSocket_Connect1 = 0x00494CA3; // CClientSocket::Connect
    constexpr uintptr_t CClientSocket_Connect2 = 0x00494D07; // CClientSocket::Connect(部分)
    constexpr uintptr_t CClientSocket_Connect3 = 0x00494D2F; // CClientSocket::Connect (sockaddr_in)
 
    // ── 資源系統 ─────────────────────────────────────────────────────────
    constexpr uintptr_t IWzFileSystem_Init    = 0x009F7964;  // IWzFileSystem::Init
    constexpr uintptr_t CWvsApp_InitResMan    = 0x009F7159;  // CWvsApp::InitializeResMan
    constexpr uintptr_t StringPool_GetString  = 0x0077A230;  // StringPool::GetString
 
    // ── 安全性 / 反作弊 ──────────────────────────────────────────────────
    constexpr uintptr_t Crc32_Check           = 0x009F4E54;  // CRC32 完整性檢查
    constexpr uintptr_t MyGetProcAddress      = 0x0044E88E;  // 自訂 GetProcAddress
 
    // ── 解析度相關資料地址 ────────────────────────────────────────────────
    constexpr uintptr_t GameWidth_Addr        = 0x00C4FCA4;  // 遊戲視窗寬度(int)
    constexpr uintptr_t GameHeight_Addr       = 0x00C4FCA8;  // 遊戲視窗高度(int)
    constexpr uintptr_t BackBuffer_Width      = 0x00C4FCC0;  // 後緩衝區寬度(Direct3D)
    constexpr uintptr_t BackBuffer_Height     = 0x00C4FCC4;  // 後緩衝區高度(Direct3D)
 
    // ── EXP 表 ───────────────────────────────────────────────────────────
    constexpr uintptr_t ExpTable_Addr         = 0x0078C8A6;  // 自訂 EXP 曲線起始地址
 
} // namespace addys

三、為什麼用 constexpr uintptr_t

每個關鍵字都有理由:

關鍵字意思為什麼用它
constexpr編譯期常數讓編譯器在建置時就把數字寫死,零執行期開銷;也防止地址被意外修改
uintptr_t無符號整數,大小和指標相同在 32 位元程式中等同 uint32_t(4 byte),足以表示任何記憶體地址;在 64 位元自動升級為 8 byte

為什麼不用 #define

#define CWvsApp_SetUp 0x009F5239 是純文字替換,沒有型別資訊,編譯器無法幫你做型別檢查。constexpr 是真正的 C++ 常數,有型別、有作用域(命名空間限制),不會污染全域命名空間。


四、地址來自哪裡?逆向工程流程

這些地址不是憑空想出來的,是逆向工程師分析 MapleStory.exe 後得到的:

flowchart TD
    A[MapleStory.exe] --> B[IDA Pro / Ghidra 載入]
    B --> C[反組譯:機器碼 → 組合語言]
    C --> D[識別函式邊界]
    D --> E[分析函式行為<br/>如:這個函式初始化遊戲視窗]
    E --> F[命名函式<br/>CWvsApp::SetUp]
    F --> G[記錄函式起始地址<br/>0x009F5239]
    G --> H[寫入 AddyLocations.h]

什麼是 IDA Pro?

Industry-standard 的反組譯工具。它把 EXE 的機器碼(CPU 直接執行的 0/1)翻譯回可讀的組合語言,並自動分析函式邊界、交叉引用、字串等。費用昂貴,但有免費版 IDA Free 和開源替代品 Ghidra(NSA 開發)。


五、地址解讀指南:每個地址代表什麼?

5.1 函式起始地址

constexpr uintptr_t CWvsApp_SetUp = 0x009F5239;

這個地址指向 CWvsApp::SetUp 函式在記憶體中的第一個 byte,也就是函式的機器碼開頭。

記憶體:
0x009F5239: 55           PUSH EBP          ← 函式開頭(Prologue)
0x009F523A: 8B EC        MOV  EBP, ESP
0x009F523C: 83 EC 14     SUB  ESP, 0x14
0x009F523F: ...          ...               ← 函式主體

安裝 Hook 時,我們把開頭幾個 byte 換成跳轉指令(JMP),讓 CPU 執行到這裡就跳到我們的程式碼。

5.2 資料地址

constexpr uintptr_t GameWidth_Addr = 0x00C4FCA4;

這個地址指向一個 int 型別的資料,裡面儲存遊戲當前的視窗寬度。

記憶體:
0x00C4FCA4: 00 05 00 00  → 0x00000500 = 1280(預設解析度寬度)

修改解析度時,我們直接向這個地址寫入新值:

Memory::Write<int>(addys::GameWidth_Addr, 1920);

六、版本綁定:地址和 EXE 版本強耦合

這是使用地址表最重要的注意事項

AddyLocations.h 裡的所有地址只對特定版本的 MapleStory.exe 有效。如果 EXE 換了一個版本(即使只是重新編譯,未改動任何功能),所有地址都可能失效:

MapleStory.exe v83.1(北美正式服):CWvsApp::SetUp = 0x009F5239  ← 地址表用的
MapleStory.exe v83.1(重編譯版):  CWvsApp::SetUp = 0x009F6040  ← 完全不同

這就是為什麼:

  1. 登入器開發者必須明確指定「只支援哪個確切版本的 EXE」
  2. MapleEzorsia-v2 在 README 中會列出相容的客戶端 MD5 或版本號
  3. 如果私服更新了客戶端,登入器的地址表也必須重新逆向工程

七、如何找到一個新地址?(實務流程)

假設你想讓遊戲的聊天框字體變大,需要找到控制字體大小的地址:

1. 在 Cheat Engine 中附加到 MapleStory.exe
2. 搜尋「聊天字體大小」的值(例如 16)
3. 改變字體大小,再搜尋新值
4. 縮小到單一地址
5. 在 IDA Pro 中找到這個地址
6. 查看哪些函式讀寫這個地址(交叉引用分析)
7. 確認這是靜態地址(不是堆積上的動態地址)
8. 加入 AddyLocations.h

Cheat Engine 是什麼?

一個開源的遊戲記憶體掃描工具,可以搜尋特定值、追蹤值的變化、找到動態指標鏈。是找到遊戲記憶體地址的入門工具。找到地址後,再用 IDA Pro 分析周邊程式碼確認其意義。


八、常見問題

Q: addys:: 前綴有什麼作用?

namespace addys { ... } 讓所有地址常數都需要加 addys:: 前綴才能使用,避免和其他檔案的變數名稱衝突。例如,如果另一個地方也有一個叫 Crc32_Check 的變數,它和 addys::Crc32_Check 是完全不同的東西,不會互相干擾。

Q:地址 0x009F5239,EXE 載入時真的在那個位置嗎?

是的,前提是 EXE 的**映像基址(Image Base)**是 0x00400000(這是 v83 MapleStory.exe 的預設基址,可在 PE 標頭中確認)。如果有 ASLR,映像基址會隨機化,地址就需要加上基址偏移。v83 沒有 ASLR,所以直接用就行。


延伸閱讀