什麼是記憶體位址與指標?

本篇定位

這是 Phase 3「記憶體操作基礎」的第一篇,也是整個 Phase 3 的地基。 讀完這篇,你會明白:為什麼登入器能夠「修改」遊戲行為,底層的核心機制是什麼。


一、記憶體的物理比喻:巨大的格子陣列

程式執行時,作業系統會給它一塊記憶體(RAM)使用。你可以把這塊記憶體想像成一個超長的信箱走廊

地址:  0x00000000  0x00000001  0x00000002  ...  0xFFFFFFFF
       ┌─────────┐ ┌─────────┐ ┌─────────┐     ┌─────────┐
信箱:  │  0x4A   │ │  0x00   │ │  0x1F   │ ... │  0xFF   │
       └─────────┘ └─────────┘ └─────────┘     └─────────┘
  • 每個信箱存放 1 個位元組(byte)(0x00 ~ 0xFF,共 256 種值)
  • 每個信箱都有唯一的「門牌號碼」,就是記憶體位址(Memory Address)
  • 在 32 位元 Windows(MapleStory v83 是 32 位元)中,地址範圍是 0x000000000xFFFFFFFF(4 GB 的地址空間)

為什麼用十六進位?

記憶體地址習慣用**十六進位(Hex)**表示,因為每個 byte 的範圍(0~255)剛好用兩個十六進位數字表達(00~FF),比十進位的 0~255 更緊湊。 開頭的 0x 是「這是十六進位數字」的標記,例如 0x9F5239 就是一個記憶體地址。


二、指標(Pointer):存放地址的變數

一般變數存的是「值」,指標存的是「地址」:

int hp = 9999;        // hp 這個格子裡面裝的是數字 9999
int* pHp = &hp;       // pHp 這個格子裡面裝的是「hp 住在哪裡」(hp 的地址)
 
// &hp  → 取出 hp 的地址(Address-of operator)
// *pHp → 讀出 pHp 所指向地址裡面的值(Dereference operator)

用信箱比喻:

  • hp 是信箱 0x00ABCD00,裡面放了數字 9999
  • pHp 是另一個信箱,裡面放的是 0x00ABCD00(那個地址)
  • *pHp 就是「按照便條紙上的地址,去那個信箱看看裡面是什麼」
       地址:  0x00ABCD00   0x00ABCD04
              ┌──────────┐ ┌──────────┐
              │   9999   │ │0x00ABCD00│
              └──────────┘ └──────────┘
                   hp           pHp
                               (指向 hp 的指標)

在 32 位元程式中,指標本身是 4 個 byte(存放一個 32 位元地址)


三、遊戲的記憶體長什麼樣子?

MapleStory.exe 執行時,它的程式碼、資料、物件全部住在同一塊記憶體裡。

舉例,遊戲可能在某個位置儲存角色的資料:

地址           值(十六進位)  說明
0x009F0000     0F 27 00 00   角色目前 HP = 9999(小端序 = 0x0000270F = 9999)
0x009F0004     0F 27 00 00   角色最大 HP = 9999
0x009F0008     E8 03 00 00   角色目前 MP = 1000
...

登入器要做的事,就是找到這些地址,然後讀取或寫入新的值


四、靜態地址 vs 動態地址

這是登入器開發中最關鍵的概念之一

靜態地址(Static Address)

程式碼段(.text section)裡的函式地址,每次開啟同一個 EXE 通常在固定位置

// MapleStory v83,每次啟動 CWvsApp::SetUp 都在這個地址
#define CWvsApp_SetUp   0x009F5239

這種地址可以直接寫死在程式碼裡(Hardcode),因為只要 EXE 版本不變,地址就不變。AddyLocations.h 裡存的就是這種靜態地址。

為什麼 v83 的地址不會隨機?

現代 Windows 有 ASLR(Address Space Layout Randomization),每次啟動地址都不同。但 MapleStory v83 是 2007 年的舊遊戲,沒有啟用 ASLR,所以地址每次都一樣,可以直接使用。

動態地址(Dynamic Address)

堆積(Heap)上的物件,例如角色的 HP,每次進遊戲可能在不同的記憶體位置

第一次啟動:角色物件在 0x02A5F000
第二次啟動:角色物件在 0x038D2000

要找動態物件,必須從已知的靜態地址出發,按照**指標鏈(Pointer Chain)**一步步走過去:

靜態基址 → 第一層指標 → 第二層指標 → ... → 目標值
0x009F0000 → [0x009F0000] = 0x02A5F000 → [0x02A5F000 + 0x0C] = HP值

五、登入器如何在 DLL 內修改記憶體

因為我們的登入器是以 DLL 注入形式存在於遊戲進程內部,可以直接使用 C++ 指標操作記憶體,不需要跨進程的 ReadProcessMemory/WriteProcessMemory:

// 直接讀取遊戲靜態地址的值(我們的 DLL 和遊戲在同一進程)
DWORD value = *(DWORD*)0x009F0000;
 
// 直接寫入新值
*(DWORD*)0x009F0000 = 12345;

白話翻譯

*(DWORD*)0x009F0000 分三步理解:

  1. 0x009F0000 — 這是一個數字(記憶體地址)
  2. (DWORD*) — 把這個數字「當成指標」來看待(告訴 C++ 這個地址指向的是 DWORD 型別的資料)
  3. 最前面的 * — 去那個地址把值讀出來

六、為什麼要理解這些?

Phase 3 接下來的三篇都建立在這個基礎上:

篇目依賴的概念
02_Memory.cpp指標、直接記憶體讀寫
03_AddyLocations.h靜態地址、EXE 版本綁定
04_AutoTypes.h型別定義、讓指標有意義

Phase 4 的 Hook 技術更直接依賴:我們要把函式的「入口地址」那幾個 byte 改掉,讓 CPU 跳到我們的程式碼去執行。


七、常見問題

Q:地址 0x009F5239 是十進位的幾?

9 × 16^6 + 15 × 16^5 + 5 × 16^4 + 2 × 16^3 + 3 × 16^2 + 9 × 16 = 10,440,249(十進位)。實際上不需要換算,在記憶體操作中我們都用十六進位。

Q:指標和參考(reference)有什麼差別?

參考(int& ref = hp)是指標的語法糖衣,一旦綁定就不能更改指向。指標(int* ptr = &hp)可以隨時改變指向、或設為 nullptr。在登入器中大量使用指標,因為我們需要操作原始地址。

Q:直接寫入遊戲記憶體安全嗎?

需要謹慎。寫錯地址、寫錯大小,都可能造成遊戲立刻崩潰(Access Violation)。這就是為什麼 Memory.cpp 會封裝讀寫操作,加入邊界和錯誤處理。


延伸閱讀