class CNullPointCall {
public:
static void Test1(); // 靜態成員函數
void Test2(); // 非靜態,不存取成員
void Test3(int i); // 非靜態,只用參數
void Test4(); // 非靜態,存取成員變數 m_iTest
private:
static int m_iStatic;
int m_iTest;
};
CNullPointCall* pNull = NULL;
pNull->Test1(); // 成功:靜態函數無 this
pNull->Test2(); // 成功:不使用 this
pNull->Test3(13); // 成功:不使用 this
pNull->Test4(); // 崩潰:存取 m_iTest → 解參考空指標
ECX 寄存器,`__thiscall` 慣例)。this->member (即 `*(this + offset)`)。this == NULL 且程式碼嘗試解參考 → 存取違規 (0xC0000005)。
; pNull = NULL
mov dword ptr [pNull], 0
; pNull->Test1() -- 靜態函數,直接 CALL
call CNullPointCall::Test1
; pNull->Test2() -- 非靜態,將 this (ECX) = pNull
mov ecx, dword ptr [pNull] ; ECX = 0
call CNullPointCall::Test2
; pNull->Test3(13) -- 參數入堆疊,this 在 ECX
push 0Dh
mov ecx, dword ptr [pNull]
call CNullPointCall::Test3
; pNull->Test4() -- 同樣傳 this=0
mov ecx, dword ptr [pNull]
call CNullPointCall::Test4
; 函數序言
push ebp
mov ebp, esp
sub esp, 0CCh
push ebx; push esi; push edi
push ecx ; 保存 this (ECX) 到堆疊
lea edi, [ebp-0CCh]
mov ecx, 33h
mov eax, 0CCCCCCCCh
rep stos dword ptr es:[edi] ; 填充 0xCC (Debug 模式偵測未初始化)
pop ecx ; 還原 this 到 ECX
mov dword ptr [ebp-8], ecx ; 將 this 存入區域變數槽 [ebp-8]
; m_iTest = 13 → this->m_iTest (offset 0)
mov eax, dword ptr [ebp-8] ; eax = this (0)
mov dword ptr [eax], 0Dh ; **寫入記憶體位址 0 → 崩潰!**
ECX,而非堆疊 (一般參數走堆疊)。m_iTest 轉為 [this + 0],m_iValue2 轉為 [this + 4] (假設 int 4 位元組)。[this] → 空指標即崩潰。| 情境 | 風險 | 對策 |
|---|---|---|
| 公開 API 接收物件指標 | 呼叫端傳 NULL | 斷言/檢查 assert(pObj); if (!pObj) return E_INVALIDARG; |
| 智慧指標管理物件 | 釋放後誤用 | 使用 std::shared_ptr/unique_ptr,避免裸指標 |
| 非靜態成員函數不存取成員 | 誤以為安全 | 若邏輯上不需 this,改為 static 或 非成員函數 |
| 多重繼承/虛繼承 | this 指標調整 (thunk) | 理解編譯器調整 this 邏輯,勿假設 this == 物件起始位址 |
std::invoke / std::bind 統一呼叫語法,編譯器仍生成相同 this 傳遞機制。[[nodiscard]]、gsl::not_null<T*> (Guideline Support Library) 靜態分析攔截空指標傳遞。this 指標是 C++ 非靜態成員函數的隱含第一參數,以寄存器傳遞 (`__thiscall`)。空指標呼叫「不崩潰」僅因函數體未解參考 this,屬於未定義行為 (UB),極度危險且不可移植。深入理解組合語言層面的傳遞與存取機制,有助於撰寫健壯程式碼、除錯疑難崩潰、逆向分析二進位檔。永遠記得:檢查指標有效性、善用智慧指標、將不需 this 的函數設計為 static。