什麼是記憶體位址與指標?
本篇定位
這是 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 位元)中,地址範圍是
0x00000000到0xFFFFFFFF(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,裡面放了數字 9999pHp是另一個信箱,裡面放的是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分三步理解:
0x009F0000— 這是一個數字(記憶體地址)(DWORD*)— 把這個數字「當成指標」來看待(告訴 C++ 這個地址指向的是 DWORD 型別的資料)- 最前面的
*— 去那個地址把值讀出來
六、為什麼要理解這些?
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會封裝讀寫操作,加入邊界和錯誤處理。
延伸閱讀
- 02_Memory.cpp_讀寫記憶體工具 — 下一篇:封裝記憶體操作的工具函式
- 03_AddyLocations.h_地址表解析 — 所有硬式編碼地址的清單
- 04_AutoTypes.h_MapleStory型別定義 — 讓指標有意義的型別系統
- 01_什麼是函式掛鉤(Hooking) — 記憶體操作的終極應用:修改 CPU 執行流程