大型 C++ 專案動輒數千個類別,繼承關係錯綜複雜。靜態分析時,虛函數呼叫的目標函數在編譯期無法確定,必須在執行期透過虛表(vtable)與虛指標(vptr)動態綁定。本文分享在 IDA Pro 等反編譯環境下,如何系統性處理虛函數逆向,縮小候選函數範圍,還原真實呼叫關係。
g++ -m32 -fno-rtti -fno-exceptions -O1strip(移除符號表)walk()struct Mammal {
virtual ~Mammal() {}
virtual void run() = 0;
virtual void walk() = 0;
virtual void move() { walk(); }
};
struct Cat : Mammal { void run() {} void walk() {} };
struct Dog : Mammal { void run() {} void walk() {} };
int main() {
Mammal* m = (rand() % 2) ? new Cat() : new Dog();
m->walk();
delete m;
}
編譯器為每個含虛函數的類別在 .rodata 段建立虛表(函數指標陣列),物件實例化時在開頭插入 vptr 指向對應虛表。GCC 虛表佈局(以本例為例):
| 偏移 | 內容 | 說明 |
|---|---|---|
| 0 | Destructor1 | 完整解構函數(僅清理成員) |
| 4 | Destructor2 | 刪除解構函數(含 operator delete) |
| 8 | run | 純虛函數 → __cxa_pure_virtual |
| 12 | walk | Cat::walk / Dog::walk / 純虛 |
| 16 | move | Mammal::move(子類未覆寫) |
注意:新版 GCC 針對含純虛函數的類別,將前兩個解構函數槽位設為空指標。
vptr 指標void (*)(Cat*))Cat 的 vptr 設為 CatVtable*Cat__walk、Dog__runm 設為 Cat* 或 Dog*,反編譯器即顯示具體呼叫目標原始碼中 m 宣告為 Mammal*(靜態型別),但執行時只能是 Cat* 或 Dog*(動態型別)。動態型別永遠不會是抽象類別。若虛表含 __cxa_pure_virtual,該型別不可能為運行時型別,可直接排除。此為虛函數逆向的核心判斷依據。
本文建立基礎流程:定位虛表 → 建立結構體 → 標記動態型別 → 還原呼叫。下篇將處理:多重繼承、虛繼承、大型類別體系的自動化腳本技巧、RTTI 缺失時的型別推導進階策略。