EG(symbol_table) 是 Zend 引擎執行期全域符號表(Executor Globals 結構成員),儲存當前作用域所有變數($GLOBALS 即此表)。透過 GDB 直接檢視哈希桶結構,可除錯變數異常、記憶體洩漏、擴展開發時的符號表操作。
# 1. 編譯含除錯符號的 PHP
./configure --enable-debug --disable-optimization
make
# 2. 撰寫測試擴展 confirm_getGlobal_compiled
// 參考 Sara Golemon 文章,功能:傳入變數名,從 EG(symbol_table) 查找並回傳值
# 3. 測試腳本 a.php
$abc = 'string';
$def = 'string2';
var_dump(confirm_getGlobal_compiled('abc'));
gdb --args sapi/cli/php -c php.ini a.php
(gdb) b confirm_getGlobal_compiled.c:301 // 擴展原始碼第 301 行
(gdb) r
(gdb) n // 執行到 zend_hash_find 呼叫前
(gdb) step // 進入 zend_hash_find 內部
zend_hash_find (ht=0x82e3250, arKey=0x837a42c "abc", nKeyLength=4, pData=0xbfffc484)
(gdb) p *ht
$9 = {
nTableSize = 64,
nTableMask = 63,
nNumOfElements = 10, // 共 10 個元素
nNextFreeElement = 0,
pInternalPointer = 0x83edc98,
pListHead = 0x83edc98, // 雙向鏈表頭
pListTail = 0x837a3fc, // 雙向鏈表尾
arBuckets = 0x83705a8, // 雜湊桶陣列指標
pDestructor = 0x81923b0 <_zval_ptr_dtor>,
persistent = 0,
nApplyCount = 0,
bApplyProtection = 1
}
(gdb) p *ht.pListHead
$2 = { h = 2572561225, nKeyLength = 8, pData = 0x83edca4, pDataPtr = 0x83edc7c,
pListNext = 0x8378c4c, pListLast = 0x0, pNext = 0x0, pLast = 0x0,
arKey = "GLOBALS" } // 第一個元素永遠是 $GLOBALS
(gdb) p *ht.pListHead.pListNext
$3 = { h = 253399445, nKeyLength = 5, pData = 0x8378c58, pDataPtr = 0x8378b60,
pListNext = 0x8378c7c, pListLast = 0x83edc98, pNext = 0x0, pLast = 0x0,
arKey = "_POST" }
(gdb) p *ht.pListHead.pListNext.pListNext
$4 = { arKey = "_GET", ... }
... 繼續 .pListNext 直到 pListTail ...
// 最後兩個為測試腳本定義的變數
(gdb) p *ht.pListHead.pListNext... (第 9 個)
$10 = { h = 2090069483, nKeyLength = 4, pData = 0x837a3d8, pDataPtr = 0x8379ef8,
pListNext = 0x837a3fc, pListLast = 0x8379e8c, pNext = 0x0, pLast = 0x0,
arKey = "abc" }
(gdb) p *ht.pListHead.pListNext... (第 10 個)
$11 = { h = 2090180660, nKeyLength = 4, pData = 0x837a408, pDataPtr = 0x8379edc,
pListNext = 0x0, pListLast = 0x837a3cc, pNext = 0x0, pLast = 0x0,
arKey = "def" }
Bucket 結構中 pDataPtr 指向實際 zval*(PHP 5.3 為 pDataPtr,PHP 7+ 直接內嵌 zval)。
(gdb) p *(zval *)$10->pDataPtr
$30 = {
value = { lval = 138024112, dval = 1.28e-313,
str = { val = 0x83a14b0 "string", len = 6 },
ht = 0x83a14b0, obj = { handle = 138024112, handlers = 0x6 } },
refcount__gc = 1,
type = 6, // IS_STRING
is_ref__gc = 0
}
成功讀到 $abc = "string" 的字串值與長度。
(gdb) p &$10->pDataPtr
$46 = (void **) 0x839fe28
(gdb) p $10->pData
$47 = (void *) 0x839fe28
// 結論:pData 存放的是 pDataPtr 的位址 (void**),即 &pDataPtr
typedef struct bucket {
ulong h; // 雜湊值 (數字鍵) 或字串雜湊
uint nKeyLength; // 鍵名長度 (含結尾 \0),0 表示數字鍵
void *pData; // 指向 pDataPtr 的指標 (void**)
void *pDataPtr; // 實際資料指標 (zval*)
struct bucket *pListNext; // 全域雙向鏈表下一個
struct bucket *pListLast; // 全域雙向鏈表上一個
struct bucket *pNext; // 同一雜湊桶鏈表下一個
struct bucket *pLast; // 同一雜湊桶鏈表上一個
char arKey[1]; // 彈性陣列成員,存放鍵名字串
} Bucket;
| 命令 | 用途 |
|---|---|
p *ht | 印出 HashTable 結構體 |
p *ht.pListHead | 印出鏈表頭 Bucket |
p *ht.pListHead.pListNext | 印出下一個 Bucket (鏈式) |
p *(zval*)bucket->pDataPtr | 解引用取得 zval 內容 |
p bucket->arKey[0]@bucket->nKeyLength | 印出完整鍵名字串 |
set $bucket = ht.pListHeadwhile($bucket) { p *(zval*)$bucket->pDataPtr; set $bucket = $bucket->pListNext } | 腳本化遍歷所有變數 |
zval 不再分離指標,直接內嵌於 Bucket (zval val 成員取代 pDataPtr)zend_array + Bucket 陣列,無雙向鏈表,改用 nNextFreeElement 與 arData 陣列索引迭代實戰記錄整理自 xiaoq3406 專欄《gdb方式遍歷EG(symbol_table) 哈希表的key》