探索C++对象模型
类对象实例究竟包含哪些东西
我们的例子代码非常简单:
#include <iostream>
using namespace std;
class A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
int A::s_nCount = 0;
int main()
{
A* p = new A;
p->fun2();
system("pause");
return 0;
}
我们在main函数里 system("pause");的地方设置断点,然后让程序运行到这里。
输入WinDbg命令?? sizeof(*p)让他打印A对象的大小,输出如下:
0:000> ?? sizeof(*p)
unsigned int 0xc
可以看到A的实例对象大小是 0xc = 12 字节
接下来输入WinDbg命令dt p让他打印p所指下对象的内存布局, 输出如下:
0:000> dt p
Local var @ 0x13ff74 Type A*
0x00034600
+0x000 __VFN_table : 0x004161d8
+0x004 m_cA : 120 'x'
+0x008 m_nA : 0n0
=0041c3c0 A::s_nCount : 0n0
可以看到A的对象实例由虚表指针,m_cA, m_nA组成,正好是12字节(内部char作了4字节对齐)。
最后一个静态变量s_nCount的地址是0041c3c0, 我们可以通过命令!address 0041c3c0查看它所在地址的属性, 结果如下:
0:000> !address 0041c3c0
Usage: Image
Allocation Base: 00400000
Base Address: 0041b000
End Address: 0041f000
Region Size: 00004000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
More info: lmv m ConsoleTest
More info: !lmi ConsoleTest
More info: ln 0x41c3c0
可以看到类静态变量被编译在consoletest.exe可执行文件的 可读写数据节(.data)
结论: C++中类实例对象由虚表指针和成员变量组成(一般最开始的4个字节是虚表指针),而类静态变量分布在PE文件的.data节中,与类实例对象无关。
虚表位置和内容
根据+0x000 __VFN_table : 0x004161d8 继续上面的调试,我们看到虚表地址是在0x004161d8, 输入!address 0x004161d8, 查看虚表地址的属性:
0:000> !address 0x004161d8
Usage: Image
Allocation Base: 00400000
Base Address: 00416000
End Address: 0041b000
Region Size: 00005000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
More info: lmv m ConsoleTest
More info: !lmi ConsoleTest
More info: ln 0x4161d8
可以看到类虚表被编译在consoletest.exe可执行文件的 只读数据节(.rdata)
接下来我们看下虚表中有哪些内容, 输入dps 0x004161d8 查看虚表所在地址的符号,输出如下:
0:000> dps 0x004161d8
004161d8 00401080 ConsoleTest!A::fun2 [f:\test\consoletest\consoletest\consoletest.cpp @ 13]
004161dc 004010a0 ConsoleTest!A::`scalar deleting destructor'
004161e0 326e7566
004161e4 00000000
我们可以看到虚表里正好包含了我们的2个虚函数fun2()和~A().
另外我们也可以多new几个A的实例试下,我们可以看到他们的虚表地址都是 0x004161d8。
结论: C++中类的虚表内容由虚函数地址组成,虚表分布在PE文件的.rdata节,并且同一类的所有实例共享同一个虚表。
禁止生成虚表会怎样
我们可以通过__declspec(novtable)来告诉编译器不要生成虚表,ATL中大量应用这种技术来减小虚表的内存开销,我们原来的代码改成
class __declspec(novtable) A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
继续原来的方法调试,我们会看到一运行到p->fun2(), 程序就会Crash, 究竟是什么原因?
用原来的?? sizeof(*p)命令,可以看到对象大小依然是12 字节, 输入dt p, 输出:
0:000> dt p
Local var @ 0x13ff74 Type A*
0x00033e58
+0x000 __VFN_table : 0x00030328
+0x004 m_cA : 40 '('
+0x008 m_nA : 0n0
=0040dce0 A::s_nCount : 0n0
从上面可以看到虚表似乎依然存在, 但是再输入dps 0x00030328 查看虚表内容, 你就会发现现在虚表内容果然已经不存在了:
0:000> dps 0x00030328
00030328 00030328
0003032c 00030328
00030330 00030330
但是我们的程序还是通过虚表去调用虚函数fun2, 难怪会Crash了。
结论: 通过__declspec(novtable),我们只能禁止编译器生成虚表,但是不能阻止对象仍包含虚表指针(不能减小对象的大小),也不能阻止程序对虚表的访问(尽管实际虚表不存在),所以禁止生成虚表只适用于永远不会实例化的类(基类)
单继承对象内存模型
下面我们简单的将上面的代码改下下,让B继承A,并且重写原来的虚函数fun2:
#include <iostream>
using namespace std;
class A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
int A::s_nCount = 0;
class B: public A
{
public:
virtual void fun2() { cout << "fun2 in B"; }
virtual void fun3() { cout << "fun3 in B"; }
public:
int m_nB;
};
int main()
{
B* p = new B;
A* p1 = p;
&nb
补充:软件开发 , C++ ,