`
weiyinchao88
  • 浏览: 1186992 次
文章分类
社区版块
存档分类
最新评论

C++对象布局及多态实现的探索(三)

 
阅读更多
  普通成员函数的调用

  从这部分开始我们除了利用内存的信息打印来进行探索外,更多的会通过跟踪和观察编译器产生的汇编代码来理解编译器对这些语言特性的实现方式。汇编方面知识的讨论超出了本文的范围,我只对和我们讨论相关的汇编代码进行解析。理解本文要讨论的知识并不需要有很完整的汇编知识,但必须了解起码的概念。
  下面我们看看引入虚继承后的影响。为了有所对比我们首先看看普通成员函数的调用情况。
  执行如下代码,它包括了对象的普通成员函数调用,类的静态成员函数调用、通过指针调用普通成员函数:
C010 obj;
PRINT_OBJ_ADR(obj)
obj.foo();
C012::sfoo();
C010 * pt = &obj;
pt->foo();
  结果如下:
obj's address is : 0012F843
  这是obj对象的内存地址。
  首先我们看看对象的普通成员函数调用,obj.foo();,对应的汇编代码为:
00422E09 lea ecx,[ebp+FFFFF967h]
00422E0F call 0041E289
  第1行把对象的地址存入ecx寄存器,执行完这行指令后,我们要以看到ecx中的值为0x0012F843,就是前面打印出的值。如果函数需要传递参数,我们还会在前面看到一些push指令。在第2行我们可以看到call的是一个直接的地址,这也就是静态绑定。即函数的调用地址在编译时已经被编译器决议。
  跟踪进去我们要以看到是一条跳转指令,继续执行可以看到真正的函数代码部分,如下(注:为了讨论方便我在第行前面加了一个行号):
01 00425FE0 push ebp
02 00425FE1 mov ebp,esp
03 00425FE3 sub esp,0CCh
04 00425FE9 push ebx
05 00425FEA push esi
06 00425FEB push edi
07 00425FEC push ecx
08 00425FED lea edi,[ebp+FFFFFF34h]
09 00425FF3 mov ecx,33h
10 00425FF8 mov eax,0CCCCCCCCh
11 00425FFD rep stos dword ptr [edi]
12 00425FFF pop ecx
13 00426000 mov dword ptr [ebp-8],ecx
14 00426003 mov eax,dword ptr [ebp-8]
15 00426006 mov byte ptr [eax],2
16 00426009 pop edi
17 0042600A pop esi
18 0042600B pop ebx
19 0042600C mov esp,ebp
20 0042600E pop ebp
21 0042600F ret
  我们看看第7行,把ecx寄存器入栈,后面4行初始化了函数的堆栈中的保存局部变量的部分。第12行弹出ecx值,到这里时ecx的值保持为在函数调用前存入的对象内存地址,第13行就是保存this指针的值,作为一个局部变量。这样我们就知道了VC7.1不是象传递普通函数那样通过压栈来传递this指针,而是通过ecx寄存器来传递。第14、15行利用这个this指针给对象的成员变量进行了赋值。
  再看看静态成员函数调用的汇编代码:
00422E14 call 0041DD84
  非常直接,因为它不需要处理this指针,跟踪到函数的汇编代码,可以看到同样不需要处理this指针。具体的代码这里就不列出来了。
  再看看通过指针调用普通成员函数pt->foo();,产生的汇编代码如下:
00422E25 mov ecx,dword ptr [ebp+FFFFF958h]
00422E2B call 0041E289
  和通过对象调用普通成员函数的代码差不多。不过存对象地址到ecx寄存器地,是通过解引用pt指针来找到对象地址的。

 虚函数调用

  我们再看看虚成员函数的调用。类C041中含有虚成员函数,它的定义如下:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
  执行如下代码:
C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
obj.foo();
C041 * pt = &obj;
pt->foo();
  结果如下:
The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
  我们打印出了C041的对象内存布局及它的虚表信息。
  先看看obj.foo();的汇编代码:
004230DF lea ecx,[ebp+FFFFF948h]
004230E5 call 0041DF1E
  和前面第五篇中看过的普通的成员函数调用产生的汇编代码一样。这说明了通过对象进行函数调用,即使被调用的函数是虚函数也是静态绑定,即在编译时决议出函数的地址。不会有多态的行为发生。
  我们跟踪进去看看函数的汇编代码。
01 004263F0 push ebp
02 004263F1 mov ebp,esp
03 004263F3 sub esp,0CCh
04 004263F9 push ebx
05 004263FA push esi
06 004263FB push edi
07 004263FC push ecx
08 004263FD lea edi,[ebp+FFFFFF34h]
09 00426403 mov ecx,33h
10 00426408 mov eax,0CCCCCCCCh
11 0042640D rep stos dword ptr [edi]
12 0042640F pop ecx
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax+4],2
16 0042641A pop edi
17 0042641B pop esi
18 0042641C pop ebx
19 0042641D mov esp,ebp
20 0042641F pop ebp
21 00426420 ret
  值得注意的是第14、15行。第14行把this指针的值移到eax寄存器中,第15行给类的第一个成员变量赋值,这时我们可以看到在取变量的地址时用的是[eax+4],即跳过了对象布局最前面的4字节的虚表指针。

  接下来我们看看通过指针进行的虚函数调用pt->foo();,产生的汇编代码如下:
01 004230F6 mov eax,dword ptr [ebp+FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp+FFFFF900h]
05 00423106 call dword ptr [edx]
  第1行把pt指向的地址移入eax寄存器,这样eax中就保存了对象的内存地址,同时也是类的虚表指针的地址。第2行取eax中指针指向的值(注意不是eax的值)到edx寄存器中,实际上也就是虚表的地址。执行完这两条指令后,我们看看eax和edx中的值,果然和我们前面打印的obj的虚表信息中的vpadr和vtadr的值是一样的,分别为0x0012F824
和0x0045B314。第4行同样用ecx寄存器来保存并传递对象地址,即this指针的值。第5行的call指令,我们可以看到目的地址不象通过对象来调用那样,是一个直接的函数地址。而是将edx中的值做为指针来进行间接调用。前面我们已经知道edx中存放的实际是虚表的地址,我们也知道虚表实际是一个指针数组。这样第5行的调用实际就是取到虚表中的第一个条目的值,即C041::foo()函数的地址。如果被调用的虚函数对应的虚表条目的索引不是0,将会看到edx后加上一个索引号乘4后的偏移值。继承跟踪可以发现,ptr[edx]的值为0x0041DF1E,也和我们打印的vtival(0)的值相同。前面已经提到过,这个地址实际也不是真正的函数地址,是一个跳转指令,继续执行就到了真正的函数代码部分(即前面列出的代码)。
  我们在上面看到的这个过程,就是动态绑定的过程。因为我们是通过指针来调用虚成员函数,所以会产生动态绑定,即使指针的类型和对象的类型是一样的。为了保证多态的语义,编译器在产生call指令时,不象静态绑定时那样,是在编译时决议出一个确定的地址值。相反它是通过用发出调用的指针指向的对象中的虚指针,来迂回的找到对象所对应类型的虚表,及虚表中相应条目中存放的函数地址。这样具体调用哪个函数就与指针的类型是无关的,只与具体的对象相关,因为虚指针是存放在具体的对象中,而虚表只和对象的类型相关。这也就是多态会发生的原因。
  请回忆一下前面(第二篇中)讨论过的C071类,当子类重写从父类继承的虚函数时,子类的虚表内容的变化,及和父类虚表内容的区别(请参照第二篇中打印的子类和父类的虚表信息)。具体的通过指向子类对象的父类指针来调用被子类重写过的虚函数时的调用过程,请有兴趣的朋友自己调试一下,这里不再列出。
  另外前面在第四篇中我们讨论了指针的类型动态转换。我们在这里再利用C041、C042及C051类,来看看指针的类型动态转换。这几个类的定义请参见第三篇。类C051从C041和C042多重继承而来,且后两个类都有虚函数。执行如下代码:
C051 obj;
C041 * pt1 = dynamic_cast<C041*>(&obj);
C042 * pt2 = dynamic_cast<C042*>(&obj);
pt1->foo();
pt2->foo2();
  第一个动态转型对应的汇编代码为:
00404B59 lea eax,[ebp+FFFFF8ECh]
00404B5F mov dword ptr [ebp+FFFFF8E0h],eax
  因为不需要调整指针位置,所以很直接,取出对象的地址后直接赋给了指针。
  第二个动态转型牵涉到了指针位置的调整,我们来看看它的汇编代码:
01 00404B65 lea eax,[ebp+FFFFF8ECh]
02 00404B6B test eax,eax
03 00404B6D je 00404B7D
04 00404B6F lea ecx,[ebp+FFFFF8F1h]
05 00404B75 mov dword ptr [ebp+FFFFF04Ch],ecx
06 00404B7B jmp 00404B87
07 00404B7D mov dword ptr [ebp+FFFFF04Ch],0
08 00404B87 mov edx,dword ptr [ebp+FFFFF04Ch]
09 00404B8D mov dword ptr [ebp+FFFFF8D4h],edx
  代码要复杂的多。&obj运算后得到的是一个指针,前三行指令就是判断这个指针是否为NULL。奇怪的是第4行并没有根据eax中的地址(即对象的起始地址)来进行指针的位置调整,而是直接把[ebp+FFFFF8F1h]的地址取到ecx寄存器中。第1行指令中的[ebp+FFFFF8ECh]实际是得到对象的地址,ebp所加的那个数实际是个负数(补码)也就是对象的偏移地址。对比两个数发现相差5字节,这样实际上第4行是直接得到了指针调整后的地址,即将指针指向了对象中的属于C042的部分。后面的代码又通过一个临时变量及edx寄存器把调整后的指针值最终存到了pt2指针中。
  这些代码实际可以优化成二行:
lea eax, [ebp+FFFFF8F1h]
mov dword ptr [ebp+FFFFF8d4h], eax
  在第三篇中我们提到C051类有两个虚表,相应对象中有也两个虚表指针,之所以不合并为一个,就是为了处理指针的类型动态转换。结合前面对于多态的讨论,我们就可以理解得更清楚了。pt2->foo2();调用时,对象的类型还是C051,但经过指针动态转换pt2指向了对象中属于C042的部分的起始,也就是第二个虚表指针。这样在进行函数调用时就不需要再做额外的处理了。我们看看pt1->foo();及pt2->foo2();产生的汇编码即知。
01 00404B93 mov eax,dword ptr [ebp+FFFFF8E0h]
02 00404B99 mov edx,dword ptr [eax]
03 00404B9B mov esi,esp
04 00404B9D mov ecx,dword ptr [ebp+FFFFF8E0h]
05 00404BA3 call dword ptr [edx]
06 00404BA5 cmp esi,esp
07 00404BA7 call 0041DDDE
08 00404BAC mov eax,dword ptr [ebp+FFFFF8D4h]
09 00404BB2 mov edx,dword ptr [eax]
10 00404BB4 mov esi,esp
11 00404BB6 mov ecx,dword ptr [ebp+FFFFF8D4h]
12 00404BBC call dword ptr [edx]
13 00404BBE cmp esi,esp
14 00404BC0 call 0041DDDE
  前7行为pt1->foo();,后7行为pt2->foo2();。唯一不同的是指针指向的地址不同,调用机制是一样的。

  (未完待继)
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics