讀書筆記: Inside the C++ Object Model (1)
February 9th, 2011
最近重讀了一下 Stanley Lippman 的 Inside the C++ Object Model (有中譯本,侯俊傑譯的 C++ 對象模型),這書有點歷史了,所以裡頭很多東西其實不能全信,要做過實驗才知道。這些實驗還蠻有價值的,我會把我做的實驗整理一下貼上來,以免忘記 😀 (這是中年大叔的通病) 這一系列的文章都是以 Microsoft Visual C++ 2010 和 Debugging Tools for Windows x86 做出來的。
首先是對原書圖 1.3 的實驗,程式碼如下
point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// point.h #include <iostream> class Point { public: Point(float xval); virtual ~Point(); float x() const; static int PointCount(); protected: virtual std::ostream& print(std::ostream& os) const; float _x; static int _point_count; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// point.cpp #include "point.h" Point::Point(float xval) : _x(xval) { ++_point_count; } Point::~Point() { --_point_count; } float Point::x() const { return _x; } int Point::PointCount() { return _point_count; } std::ostream& Point::print(std::ostream& os) const { os << _x; return os; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include "point.h" using namespace std; // instantiate the global static var int Point::_point_count; int main() { Point* pPoint = new Point(2); cout << Point::PointCount() << endl; cout << sizeof(Point) << endl; cout << pPoint->x() << endl; delete pPoint; return 0; } |
不想太麻煩所以直接用命令列來編譯,我的環境可以直接調用 cl 和 windbg
1 2 |
cl /EHsc /Zi /Fd test1.cpp point.cpp windbg test1.exe |
在 windbg 下可以直接打開 test1.cpp,在第 12 行設中斷點,然後執行這個程式。停下來時我們就可以好好觀察一下 pPoint 的結構了
1 2 3 4 5 6 |
0:000> dt pPoint Local var @ 0x3cfe28 Type Point* 0x00112008 +0x000 __VFN_table : 0x012a84f4 +0x004 _x : 2 =012b1e00 Point::_point_count : 1 |
和原文圖 1.3 不同的是,vtable 是第一個而不是最後一個,這是 Microsoft 發明的做法,有興趣可觀察一下,似乎 gcc 也是這麼做的。接下來看看 vtable 裡有什麼
1 2 3 4 5 |
0:000> dds 0x012a84f4 012a84f4 012710f5 test1!ILT+240(??_GPointUAEPAXIZ) 012a84f8 01271258 test1!ILT+595(?printPointMBEAAV?$basic_ostreamDU?$char_traitsDstdstdAAV23Z) 012a84fc 00000000 ... |
ILT? 這是什麼東西呢?其實是 Incremental Link Table 的縮寫,它加了一層 indirection 把真正的 function 放到 ILT 去,這樣 linker 就不必每次都重新安排函數的位置,但這對我們做實驗是很不利的。所以呢,還是得乖乖的用 make file 或 vcproj。身為老派硬漢,當然要用硬漢用的 nmake
cppom.mak
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
CC = cl CFLAGS = /EHsc /Zi /Fd /c LD = link LDARGS = /INCREMENTAL:NO /DEBUG RM = del /q .cpp.obj:: $(CC) $(CFLAGS) $< test1.exe: test1.obj point.obj $(LD) test1.obj point.obj $(LDARGS) /PDB:test1.pdb clean: $(RM) *.exe *.obj *.pdb *.idb *.ilk *.bak all: test1.exe |
用 nmake 造出新的 test1.exe 之後,重複以上的步驟,再來看看我們的 vtable
1 2 3 4 5 6 7 8 9 10 11 |
0:000> dt pPoint Local var @ 0x37fef0 Type Point* 0x001e2008 +0x000 __VFN_table : 0x00dd95c8 +0x004 _x : 2 =00de0820 Point::_point_count : 1 0:000> dds 0x00dd95c8 00dd95c8 00db5a50 test1!Point::`scalar deleting destructor' 00dd95cc 00db5a20 test1!Point::print [c:\users\arthur\desktop\research\point.cpp @ 24] ... |
看起來順眼多了,不過啥是
圖 1.3 中提到,vtable 的第 0 個 entry 應指向 type info,但我們看的結果似乎不是如此,如果往上捲個 4 byte 會如何呢?
scalar deleting destructor' 啊?其實這是 VC 自動幫我們的 destructor 包的一層殼。
RTTI Complete Object Locator’ 應是我們要找的東西,那又是另一個故事了,拜了一下咕狗大神,07 年的黑帽有專文討論,這裡就不嚕囌。所以到目前為止,我們的實驗證明了幾件事實:
反組譯實驗
- VC 就如圖 1.3 中所畫的,會自己安排 static variable, static member function 以及 non-virtual member function 的位置
- 每個 object instance 都內含指向 vtable 的指標 (如果有 vtable 的話)
- vtable 的位置通常與圖 1.3 中畫的相反,它在最開始的位置。RTTI 資訊事實上也不一定放在 vtable 的第一個指向位置,只要 compiler 推的出來就可以