ZAllocEx 完整解析(標頭與實作)
前情提要
02 介紹了 ZAllocEx.cpp 的實作邏輯。但那只是「實作檔」——MapleStory 的記憶體分配系統還有一整套標頭體系。本篇補全這個架構。
分配器繼承鏈
ZAllocBase ← 定義 Block 大小 enum 與 AllocRawBlocks()
↑
ZAllocAnonSelector ← 一般物件的 GetBlockSize() 策略
ZAllocStrSelector<T> ← 字串物件的 GetBlockSize() 策略(考慮 ZXStringData 開銷)
↑
ZAllocEx<ZAllocAnonSelector> ← 一般物件記憶體池(單例)
ZAllocEx<ZAllocStrSelector<T>> ← 字串記憶體池(單例)
ZAllocBase — 原始記憶體區塊分配
ZAllocBase.h 只做一件事:向作業系統申請一整排記憶體區塊,並把它們串成鏈結串列。
enum BLOCK_SIZE {
BLOCK16 = 0, // 16 bytes
BLOCK32 = 1, // 32 bytes
BLOCK64 = 2, // 64 bytes
BLOCK128 = 3, // 128 bytes
};
static PVOID* AllocRawBlocks(UINT uBlockSize, UINT uNumberOfBlocks);AllocRawBlocks 的記憶體版面:
[前置大小] [鏈結串列頭] [第0塊大小] [第0塊資料] [第1塊大小] [第1塊資料] ...
每個區塊的「前一個地址」存放本區塊大小,這是 Free() 能正確識別大小的關鍵。
ZAllocAnonSelector vs ZAllocStrSelector
兩者都提供 GetBlockSize(nIndex, &nAllocBlocks) 但策略不同:
| 分配器 | BLOCK16 的實際大小 | 用途 |
|---|---|---|
ZAllocAnonSelector | 16 bytes | 一般物件 |
ZAllocStrSelector<char> | 16 + sizeof(_ZXStringData) + 1 bytes | char 字串 |
ZAllocStrSelector<wchar_t> | 32 + sizeof(_ZXStringData) + 2 bytes | wchar_t 字串 |
字串分配器多算了 _ZXStringData(含 nRef、nCap、nByteLen 三個欄位)的開銷,因為字串的記憶體版面是:
[_ZXStringData header][actual string data][null terminator]
ZAllocEx 標頭完整結構
ZAllocEx.h 定義了兩個特化版本,都是單例(Singleton):
// 取得一般物件分配器實例
ZAllocEx<ZAllocAnonSelector>::GetInstance()->Alloc(size);
ZAllocEx<ZAllocAnonSelector>::GetInstance()->Free((void**)ptr);
// 取得字串分配器實例
ZAllocEx<ZAllocStrSelector<char>>::GetInstance()->Alloc(size);
ZAllocEx<ZAllocStrSelector<wchar_t>>::GetInstance()->Alloc(size);成員變數(記憶體版面)
BYTE gap0[1]; // 對齊 padding(保持與 MapleStory 原版 struct 一致)
ZFatalSection m_lock; // 執行緒鎖(僅保留版面,實際用 std::mutex)
LPVOID m_apBuff[4]; // 4 個大小等級各自的可用記憶體頂端指標
LPVOID m_apBlockHead[4]; // 4 個大小等級各自已分配區塊鏈結串列頭四格記憶體池運作方式
m_apBuff[BLOCK16] ──→ [可用區塊] ──→ [可用區塊] ──→ nullptr
m_apBuff[BLOCK32] ──→ [可用區塊] ──→ nullptr
m_apBuff[BLOCK64] ──→ nullptr ← 此等級已滿,下次 Alloc 會呼叫 AllocRawBlocks
m_apBuff[BLOCK128]──→ [可用區塊]
Alloc 流程(4 個步驟):
- 判斷請求大小屬於哪個等級(16/32/64/128)
- 若超過 128 bytes → 直接
HeapAlloc(大物件不走池子) - 若
m_apBuff[等級]為 null → 呼叫AllocRawBlocks補充 - 從堆頂取出一個可用區塊,更新
m_apBuff為下一個
Free 流程:
- 讀取
*(ptr - 1)得知原始大小(MSB 為 1 時取反得真實大小) - 若大於 128 →
HeapFree直接釋放 - 否則把該區塊推回對應等級的
m_apBuff堆頂
為什麼不真正釋放記憶體?
記憶體池的設計哲學:Free 不歸還給 OS,而是放回自己的可用堆。下次 Alloc 同大小物件時直接從堆頂取,速度比 HeapAlloc 快很多。這就是 MapleStory 高頻物件(如封包、字串)能高效運作的原因。
全域 new/delete 覆蓋
ZAllocEx.h 末尾宣告了全域覆蓋:
void* operator new(size_t uSize);
void* operator new[](size_t uSize);
void operator delete(void* p);
void operator delete[](void* p);實作在 ZAllocEx.cpp(Phase 6 已介紹)。這意味著:專案內所有 new 都走 ZAllocEx<ZAllocAnonSelector>,讓 DLL 分配的物件與 MapleStory 的記憶體管理系統相容。
ZFatalSection — 執行緒鎖
struct ZFatalSectionData {
void* _m_pTIB; // Thread Information Block 指標
int _m_nRef; // 引用計數
};
struct ZFatalSection : ZFatalSectionData { /* TODO emulate */ };MapleStory 原版用 ZFatalSection 做執行緒同步。Ezorsia 保留這個結構只是為了維持正確的記憶體版面大小(0x8 bytes),實際鎖定功能改用 std::mutex。
struct 大小驗證
assert_size(sizeof(ZAllocEx<ZAllocAnonSelector>), 0x2C)— 整個分配器結構必須是 44 bytes,才能與 MapleStory 的原版結構對齊。