讀書筆記: 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
// point.h
#include
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;
};
// 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;
}
#include
#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
cl /EHsc /Zi /Fd test1.cpp point.cpp
windbg test1.exe
在 windbg 下可以直接打開 test1.cpp,在第 12 行設中斷點,然後執行這個程式。停下來時我們就可以好好觀察一下 pPoint 的結構了
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 裡有什麼
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
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
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]
...
看起來順眼多了,不過啥是 `scalar deleting destructor' 啊?其實這是 VC 自動幫我們的 destructor 包的一層殼。
反組譯實驗
0:000> ln 00db5a50
(00db5a50) test1!Point::`scalar deleting destructor' | (00db5a7c) test1!std::_Iterator_base0::_Adopt
Exact matches:
test1!Point::`scalar deleting destructor' (void)
0:000> u 00db5a50 00db5a7b
test1!Point::`scalar deleting destructor':
00db5a50 55 push ebp
00db5a51 8bec mov ebp,esp
00db5a53 51 push ecx
00db5a54 894dfc mov dword ptr [ebp-4],ecx
00db5a57 8b4dfc mov ecx,dword ptr [ebp-4]
00db5a5a e861ffffff call test1!Point::~Point (00db59c0)
00db5a5f 8b4508 mov eax,dword ptr [ebp+8]
00db5a62 83e001 and eax,1
00db5a65 740c je test1!Point::`scalar deleting destructor'+0x23 (00db5a73)
00db5a67 8b4dfc mov ecx,dword ptr [ebp-4]
00db5a6a 51 push ecx
00db5a6b e854290000 call test1!operator delete (00db83c4)
00db5a70 83c404 add esp,4
00db5a73 8b45fc mov eax,dword ptr [ebp-4]
00db5a76 8be5 mov esp,ebp
00db5a78 5d pop ebp
00db5a79 c20400 ret 4
圖 1.3 中提到,vtable 的第 0 個 entry 應指向 type info,但我們看的結果似乎不是如此,如果往上捲個 4 byte 會如何呢?
0:000> dds 0x00dd95c4
00dd95c4 00ddcadc test1!Point::`RTTI Complete Object Locator'
00dd95c8 00db5a50 test1!Point::`scalar deleting destructor'
00dd95cc 00db5a20 test1!Point::print [c:\users\arthur\desktop\research\point.cpp @ 24]
...
`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 推的出來就可以