ZAllocEx.cpp 解析:自訂記憶體分配器
本篇定位
這是 Phase 6 的第 2 篇。
ZAllocEx.cpp實作一個替換 MapleStory 內建記憶體分配器的自訂版本,解決遊戲在非標準解析度下因記憶體碎片化導致的崩潰問題。
一、背景:MapleStory 的 ZAlloc 是什麼?
MapleStory 的引擎沒有直接使用 C++ 的 new / delete 或 Windows 的 HeapAlloc,而是有一套自己的記憶體管理系統,核心是 ZAllocator 類別。
// 遊戲引擎的記憶體分配介面(逆向工程還原的偽碼)
class ZAllocator {
public:
void* Alloc(size_t size); // 分配記憶體
void Free(void* ptr); // 釋放記憶體
void* Realloc(void* ptr, size_t newSize);
};ZAlloc 的問題:它的內部 Heap 大小是針對 800×600 模式調整的,當解析度增大(需要更多材質記憶體、更大的 Back Buffer)時,可能超出預先分配的 Heap 容量,導致分配失敗並崩潰。
二、ZAllocEx 的設計思路
ZAllocEx 不修改 ZAllocator 的內部結構,而是替換遊戲呼叫 malloc / free 的底層行為:
原始路徑:
ZAllocator::Alloc → 內部 Heap → 固定大小上限 → 崩潰
替換後:
ZAllocEx::Alloc → Windows HeapAlloc(系統 Heap,幾乎無上限) → 成功
三、ZAllocEx.h 宣告
#pragma once
#include <Windows.h>
#include <cstddef>
namespace ZAllocEx {
// 初始化:建立我們自己的 Heap
void Init();
// 替換 ZAlloc 的核心分配 / 釋放函式
void* Alloc(size_t size);
void Free(void* ptr);
void* Realloc(void* ptr, size_t newSize);
// 安裝 / 卸載 Hook
void InstallHooks(bool install);
// 統計資訊(除錯用)
size_t GetTotalAllocated();
size_t GetAllocationCount();
}四、ZAllocEx.cpp 核心實作
4.1 Init():建立私有 Heap
static HANDLE g_heap = nullptr;
static size_t g_totalAllocated = 0;
static size_t g_allocCount = 0;
void ZAllocEx::Init() {
// 建立一個可動態增長的 Windows Heap
// HEAP_NO_SERIALIZE = 不使用 Heap 內部鎖(我們自己管理執行緒安全)
// 初始大小 4 MB,最大無限制(0 = 系統決定)
g_heap = HeapCreate(HEAP_NO_SERIALIZE, 4 * 1024 * 1024, 0);
if (!g_heap) {
// Fallback:使用 Process 預設 Heap
g_heap = GetProcessHeap();
}
}4.2 Alloc() / Free()
void* ZAllocEx::Alloc(size_t size) {
if (!g_heap) Init();
void* ptr = HeapAlloc(g_heap, 0, size);
if (ptr) {
g_totalAllocated += size;
g_allocCount++;
}
return ptr;
}
void ZAllocEx::Free(void* ptr) {
if (!ptr || !g_heap) return;
HeapFree(g_heap, 0, ptr);
g_allocCount--;
}
void* ZAllocEx::Realloc(void* ptr, size_t newSize) {
if (!ptr) return ZAllocEx::Alloc(newSize);
if (!newSize) { ZAllocEx::Free(ptr); return nullptr; }
return HeapReAlloc(g_heap, 0, ptr, newSize);
}五、Hook 安裝:替換遊戲的 ZAlloc 呼叫
遊戲在多個地方呼叫自己的 ZAlloc,我們用 Detours 把這些呼叫點替換成 ZAllocEx:
// 遊戲內 ZAllocator 的函式指標型別
using tZAlloc = void*(__cdecl*)(size_t size);
using tZFree = void (__cdecl*)(void* ptr);
using tZRealloc = void*(__cdecl*)(void* ptr, size_t size);
tZAlloc original_ZAlloc = (tZAlloc) addys::ZAlloc_Alloc;
tZFree original_ZFree = (tZFree) addys::ZAlloc_Free;
tZRealloc original_ZRealloc = (tZRealloc)addys::ZAlloc_Realloc;
// 替換函式(直接轉交給 ZAllocEx,完全不呼叫 original)
void* __cdecl Hook_ZAlloc(size_t size) { return ZAllocEx::Alloc(size); }
void __cdecl Hook_ZFree(void* ptr) { ZAllocEx::Free(ptr); }
void* __cdecl Hook_ZRealloc(void* ptr, size_t sz) { return ZAllocEx::Realloc(ptr, sz); }
void ZAllocEx::InstallHooks(bool install) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
if (install) {
DetourAttach(&(PVOID&)original_ZAlloc, Hook_ZAlloc);
DetourAttach(&(PVOID&)original_ZFree, Hook_ZFree);
DetourAttach(&(PVOID&)original_ZRealloc, Hook_ZRealloc);
} else {
DetourDetach(&(PVOID&)original_ZAlloc, Hook_ZAlloc);
DetourDetach(&(PVOID&)original_ZFree, Hook_ZFree);
DetourDetach(&(PVOID&)original_ZRealloc, Hook_ZRealloc);
}
DetourTransactionCommit();
}六、為什麼這樣做比直接增大 ZAlloc 的 Heap 更好?
| 方案 | 做法 | 風險 |
|---|---|---|
| 修改 ZAlloc 內部 Heap 大小 | Patch 初始化時的 HeapCreate 參數 | ZAlloc 有複雜的碎片管理邏輯,改大小可能破壞對齊假設 |
| 替換為 ZAllocEx(本方案) | 完全繞過 ZAlloc,用系統 Heap | Windows Heap 經過長年優化,穩定性高;系統 Heap 幾乎沒有大小限制 |
| 不替換,讓遊戲自行管理 | 什麼都不動 | 在高解析度下遇到大 texture 時仍然會崩潰 |
HeapCreatevsGetProcessHeap
HeapCreate建立一個獨立的 Heap,和遊戲自己的 Heap 完全隔離。 好處:我們的分配不會和遊戲的分配互相干擾,也不會讓遊戲的 Heap 碎片化。GetProcessHeap是 Fallback,返回進程的預設 Heap(和new/malloc共用),安全但不如獨立 Heap 乾淨。
七、常見問題
Q:如果 ZAlloc 分配的記憶體被遊戲自己的 Free 呼叫,會怎樣?
這是最大的風險。如果有些分配走了 ZAllocEx,但釋放的程式碼路徑用的是原始 ZFree(沒有被 Hook 到),就會發生「跨 Heap 釋放」,導致 Heap 損毀崩潰。這就是為什麼三個函式(Alloc / Free / Realloc)必須一起替換,不能只替換其中一個。
Q:
HEAP_NO_SERIALIZE有執行緒安全問題嗎?有潛在風險。如果多個執行緒同時呼叫
ZAllocEx::Alloc,而且沒有外部鎖,HeapAlloc可能損毀 Heap 結構。MapleEzorsia-v2 的做法是假設遊戲在分配記憶體時已經有自己的執行緒鎖(從 ZAllocator 繼承),所以在 Heap 層不再加鎖。如果遇到不明原因的崩潰,可以把HEAP_NO_SERIALIZE移除(使用0),讓 Windows Heap 自己加鎖。
延伸閱讀
- 01_ReplacementFuncs.h_函式替換技術 — 同為「替換」技術的另一個例子
- 03_MainMain.cpp_初始化流程解析 — ZAllocEx::Init() 在初始化流程中的位置
- 02_Memory.cpp_讀寫記憶體工具 — 記憶體操作的基礎工具