C++ vtable的生成以及vptr的赋值跟踪实验

在看《Inside The C++ Object Model》时想到一个问题, vtable是在什么时候生成的?运行时对象的vptr又是什么时候赋值的?如何得知相应的函数的地址的?

为此写程序实验了一下,得出结论如下:
1. 编译的时候,编译器自动为每个有虚函数的类生成vtable,此vtable类似于静态常量数据,并编译到最终的可执行文件中。
2. 具体实例的vptr在构造函数中赋值,指向vtable地址。
3. 构造函数中如果调用虚函数,只会调用该类的虚函数。

程序如下:
#include <iostream> class B { public: B(int i) : _i(i) { vf(); } virtual void vf() { std::cout << "B::vf called" << std::endl; } virtual ~B() { vf(); } private: int _i; }; class D : public B { public: D(int i, int j) : B(i), _j(j) { vf(); } virtual void vf() { std::cout << "D:vf called" << std::endl; } virtual ~D() { vf(); } private: int _j; }; int main() { B* p = new D(1, 2); p->vf(); delete p; return 0; }

编译,进入Debug状态,打开Disassembly, 能够看到

27:       D(int i, int j) : B(i), _j(j)
28:       {
004012D0   push        ebp
004012D1   mov         ebp,esp
004012D3   push        0FFh
004012D5   push        offset __ehhandler$??0D@@QAE@HH@Z (0041d779)
004012DA   mov         eax,fs:[00000000]
004012E0   push        eax
004012E1   mov         dword ptr fs:[0],esp
004012E8   sub         esp,44h
004012EB   push        ebx
004012EC   push        esi
004012ED   push        edi
004012EE   push        ecx
004012EF   lea         edi,[ebp-50h]
004012F2   mov         ecx,11h
004012F7   mov         eax,0CCCCCCCCh
004012FC   rep stos    dword ptr [edi]
004012FE   pop         ecx
004012FF   mov         dword ptr [ebp-10h],ecx
00401302   mov         eax,dword ptr [ebp+8]
00401305   push        eax
00401306   mov         ecx,dword ptr [ebp-10h]
00401309   call        @ILT+125(B::B) (00401082)
0040130E   mov         dword ptr [ebp-4],0
00401315   mov         ecx,dword ptr [ebp-10h]
00401318   mov         edx,dword ptr [ebp+0Ch]
0040131B   mov         dword ptr [ecx+8],edx
0040131E   mov         eax,dword ptr [ebp-10h]
00401321   mov         dword ptr [eax],offset D::`vftable' (0042f01c)
29:           vf();
00401327   mov         ecx,dword ptr [ebp-10h]
0040132A   call        @ILT+155(D::vf) (004010a0)
30:       }

在其中,看到了对B::B()的调用, B::B()的汇编如下

 B(int i) : _i(i)
7:        {
00401380   push        ebp
00401381   mov         ebp,esp
00401383   sub         esp,44h
00401386   push        ebx
00401387   push        esi
00401388   push        edi
00401389   push        ecx
0040138A   lea         edi,[ebp-44h]
0040138D   mov         ecx,11h
00401392   mov         eax,0CCCCCCCCh
00401397   rep stos    dword ptr [edi]
00401399   pop         ecx
0040139A   mov         dword ptr [ebp-4],ecx
0040139D   mov         eax,dword ptr [ebp-4]
004013A0   mov         ecx,dword ptr [ebp+8]
004013A3   mov         dword ptr [eax+4],ecx
004013A6   mov         edx,dword ptr [ebp-4]
004013A9   mov         dword ptr [edx],offset B::`vftable' (0042f028)
8:            vf();
004013AF   mov         ecx,dword ptr [ebp-4]
004013B2   call        @ILT+210(B::vf) (004010d7)
9:        }

其中能够看到两处对vptr的赋值
B::B中的
00401321 mov dword ptr [eax],offset D::`vftable' (0042f01c)
和D::D中的
004013A9 mov dword ptr [edx],offset B::`vftable' (0042f028)

这是对instance的vptr赋值的语句, 通过计算,可以得知,此时的mov命令目标地址就是传入的this指针(通过ECX传入)所指的第一个单元,(这个要认真算一下,跳来跳去比较麻烦)

还能看到对_i和_j的赋值语句
    004013A0   mov         ecx,dword ptr [ebp+8]


在B::B执行完毕时对象的状态如下
-------------------
|  vptr (B:vftable)|
--------------------
|  _i (1)          |
 ------------------
| _j (未赋值)     |
--------------------

D::D执行完毕后对象状态如下:
-------------------
|  vptr (D:vftable)|
--------------------
|  _i (1)          |
 ------------------
| _j (2)          |
--------------------

由此可以得出结论
    构造函数中调用虚函数是不会产生“虚”效果的,编译的时候就写死了,只调用自己的虚函数。如上
0040132A   call        @ILT+155(D::vf) (004010a0)
至于原因,很多地方都有解释: 此时子类尚未构造,因此无法去调用子类的虚函数。

此外,我们看到了vtable的地址
B::`vftable' (0042f028)
D::`vftable' (0042f01c)
通过watch窗口能看到起内容,我们再使用dumpbin /all test.exe > test_all.txt,然后打开test_all.txt,我们能够看到同样的内容


SECTION HEADER #2
  .rdata name
    2DBF virtual size
   2F000 virtual address
    3000 size of raw data
   2F000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         Read Only

RAW DATA #2
  0042F000: 00 00 00 00 D1 2C 50 4A 00 00 00 00 02 00 00 00  .....,PJ........
  0042F010: 32 00 00 00 00 00 00 00 00 70 03 00 A0 10 40 00  2........p....@.
  0042F020: D2 10 40 00 00 00 00 00 D7 10 40 00 23 10 40 00  ..@.......@.#.@.
  0042F030: 00 00 00 00 42 3A 3A 76 66 20 63 61 6C 6C 65 64  ....B::vf called
  0042F040: 00 00 00 00 44 3A 76 66 20 63 61 6C 6C 65 64 00  ....D:vf called.

其中我们可以看到0042f01c处的几个地址A0 10 40 00 D2 10 40 00, 分别是0x004010A0, 和0x04010D2,
对照汇编以及Debug时的Variables窗口,可以看到这两个地址是D::vf和D::~D的地址, 这就是D类的vtable的内容,两个虚函数的地址
在看B的D7 10 40 00 23 10 40 00, 0x004010D7 和00401023是B::vf和B::~B的地址,可以在构造函数中设断点看到这些信息。

为此,我们可以认为编译的是很编译器做了这些工作:
1. 为有虚函数的类,生成虚函数表 ,可以认为插入到相应的C++代码中(C++编译器往用户代码中插入不少东西,比如构造函数、析构函数的调用)

可以认为如下形式:
static const void* _vtable_B[2] = { (void*)B::vf,
    (void*)B::~B}
static const void* _vtable_D[2] = { (void*)D::vf,
    (void*)D::~D}

2. 然后再在类的构造函数中插入对vptr的赋值
    this->vptr = _vtable_B[0];


3. 其中的vtable就像程序定义的其他全局变量一样,编译到最终的可执行文件中,在载入运行的时候载入

你可能感兴趣的:(C++,汇编,File,编译器,Numbers,variables)