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,用系統 HeapWindows Heap 經過長年優化,穩定性高;系統 Heap 幾乎沒有大小限制
不替換,讓遊戲自行管理什麼都不動在高解析度下遇到大 texture 時仍然會崩潰

HeapCreate vs GetProcessHeap

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 自己加鎖。


延伸閱讀