首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

誰動了我的指標?:重載 new/delete 追蹤野指標與記憶體洩漏的偵錯技巧

     編輯:關於VC++

前言:野指標的噩夢

指標指向已釋放記憶體 (Dangling Pointer) 是 C++ 最難除錯的問題之一。症狀:指標非 NULL、位址對齊、記憶體內容為 `0xFEEEFEEE` (MSVC Debug 填充模式)。本文介紹一種**僅限偵錯期**的技巧:重載全域 `operator new/delete`,在配置/釋放前後記錄呼叫堆疊位址,事後透過記憶體視窗追溯來源。

核心原理

  • `new`:在實際配置前額外配置 16 位元組,**記錄呼叫者返回位址 + 配置大小**
  • `delete`:不真正釋放,改寫記錄區:**填入呼叫者返回位址 + 0xDEADBEEF 標記 + 0x77 填充**
  • 崩潰時:在指標位址**前 16 位元組**可見兩個返回位址,對應原始碼 `new`/`delete` 呼叫點

實作代碼 (僅 Debug 版編譯)


#ifdef _DEBUG
#include 

void* operator new(size_t size) {
    int stackVar; // 區域變數,用於計算堆疊位址
    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&size;
    // 返回位址通常在 (stackVarAddr + argAddr)/2 + 2 處 (視編譯器/呼叫慣例調整)
    void** retAddrAddr = (void**)(stackVarAddr/2 + argAddr/2 + 2);
    void* retAddr = *retAddrAddr;

    unsigned char* buf = (unsigned char*)malloc(size + 16);
    memset(buf, 0, 16);
    memcpy(buf, &retAddr, sizeof(retAddr));     // 0~3: new 呼叫者位址
    memcpy(buf + 4, &size, sizeof(size));       // 4~7: 配置大小
    return buf + 16; // 返回使用者區塊
}

void operator delete(void* ptr) {
    if (!ptr) return;
    int stackVar;
    unsigned long stackVarAddr = (unsigned long)&stackVar;
    unsigned long argAddr = (unsigned long)&ptr;
    void** retAddrAddr = (void**)(stackVarAddr/2 + argAddr/2 + 2);
    void* retAddr = *retAddrAddr;

    unsigned char* buf = (unsigned char*)ptr;
    buf -= 8; // 指向大小欄位
    size_t size;
    memcpy(&size, buf, sizeof(size)); // 讀取原始大小
    buf += 8; // 指向使用者資料起始 (即原 ptr)
    memcpy(buf, &retAddr, sizeof(retAddr)); // 8~11: delete 呼叫者位址
    buf[4] = 0xDE; buf[5] = 0xAD; buf[6] = 0xBE; buf[7] = 0xEF; // 12~15: DEADBEEF
    memset(buf + 8, 0x77, size); // 填充使用者區塊
    // 故意不 free,保留記錄供事後分析 → 偵錯期記憶體洩漏可接受
}
#endif

事後分析步驟

  1. 程式因野指標崩潰 (Access Violation)
  2. 開啟 **Debug → Windows → Memory**,輸入錯誤指標位址
  3. 向上捲動 16 位元組 (0x10),觀察結構:

Address     Value (Little Endian)     Meaning
ptr-10      XX XX XX XX               new 呼叫者返回位址 (需位元組反轉)
ptr-C       YY YY YY YY               配置大小
ptr-8       ZZ ZZ ZZ ZZ               delete 呼叫者返回位址 (需反轉)
ptr-4       DE AD BE EF               釋放標記
ptr+0       77 77 77 77 ...           已釋放資料區填充
  1. 取得兩個位址 (如 `0x00401234`),在 **Disassembly 視窗** `Ctrl+G` 輸入位址
  2. 右鍵 **Go To Source Code** 定位到原始碼 `new`/`delete` 行
  3. 分析為何在該處 `delete` 而其他地方仍持有指標

進階應用:記憶體洩漏報表

修改 `delete` 真正 `free(buf - 16)`,程式結束前遍歷堆疊 (`_heapwalk`),收集所有未釋放區塊的 `new` 位址,產生洩漏報表。

⚠️ 重要限制與警告

  • 嚴禁用於 Release/產品版:不釋放記憶體會導致嚴重洩漏、效能暴跌
  • 僅適用 MSVC x86:堆疊布局假設在 x64、其他編譯器、最佳化下失效
  • 多執行緒不安全:`malloc`/`free` 未加鎖
  • MFC 專案優先用 `DEBUG_NEW`**:已內建檔案/行號追蹤、客戶區塊標記、洩漏報告,功能更完善

MFC DEBUG_NEW 使用方式


// stdafx.h 或專案屬性 C/C++ → Preprocessor → _DEBUG 定義下
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

輸出範例:


Detected memory leaks!
D:\Project\MyClass.cpp(42) : {123} client block at 0x004A3B20, 40 bytes long.
Data:  4D 79 43 6C 61 73 73 00 CD CD CD CD CD CD CD CD

直接顯示**檔案名、行號、類別名**,無需手動反組譯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved