咨询电话 027-87678580 金老师 QQ 2056813683倪老师 QQ 1564396558

在线留言 提交作业 联系我们

在VC6.0中虚函数的实现方法

admin

围绕 VC++ 6.0 中虚函数的实现,结合内存布局与反汇编观察后期联编机制,解释虚函数调用背后的底层处理方式。

虚函数?!

虚拟+函数?!听起来很恐怖的样子。如果你知道使用微波 炉煎鸡蛋和使用煤气炉煎鸡蛋的方法和步骤绝对不一样的话,就应该知道为什么需要虚拟函数和在什么情况下使用虚拟函数,这些都是课堂上的内容,相信大家已经滚瓜烂熟,我也就不罗嗦了。我在这里是要和大家讨论虚拟函数的技术内幕——后期联编(Late binding),但是,如果你对以上我所提到的还没有完全的领悟的话,则千万不要阅读本帖,以防走火入魔,切记切忌。

一,让我们进入内存——I will come back(阿诺的口头禅)

首先,我觉得了解一个含有虚拟函数的类在内存中的结构是有必要的。

假设一个这样的类:

class CShape
{
       int b1
public:
       void MyTest()
       {
              cout << "CShape::MyTest \n"
       }
}

在栈区,它仅仅只是占据了四个字节,用于存放成员数据——b1。

奇 怪,那么它的成员函数在那里呢?对于普通的成员函数,编译器采取的是“名字粉碎法”,对于VC++6.0,它将CShape::MyTest()编修改 为:“?MyTest@CTestA@@QAEXXZ”,真是个奇怪的名字,但是在这个名字中却保存了重要的信息,比如所属类,参数类型等,具体的大家可 以查查相关资料,不好意思,我忘记了。

现在我们讨论虚拟函数, 假设另外的一个类:

class CShape_V
{
       int b1
public:
       virtual void play() 
       { 
              cout << "CShape::play \n" 
       }
       virtual void display() 
       { 
              cout <<b1<< "Shape \n" 
       }
}

在栈区,它占据了八个字节,用于存放成员数据b1和——。。。。。。。。。。。。。。。。。。。。。。。。。。。。。指向一个一维数组首地址的指针,这是个什么东东咧?且听我慢慢到来。

三,现在可以下手了,做掉它。

先看阅读以下一段小程序,请务必保证能看懂,并能分析出正确结果:

//vTest.cpp
#include <iostream.h>
//
class CShape
{
       int b1
public:
       CShape():b1(1){}
       void MyTest()
       {
              cout << "CShape::MyTest \n"
       }
       virtual void play() 
       { 
              cout << "CShape::play \n" 
       }
       virtual void display() 
       { 
              cout <<b1<< "Shape \n" 
       }
}
//
class CRect : public CShape
{       int b2
public:
       CRect():b2(2){}
       void MyTest()
       {
              cout << "CRect::MyTest \n"       }
       void display() 
       { 
              cout <<b2<< "Rectangle \n" 
       }
}//
class CSquare : public CRect
{
       int b3
public:
       CSquare():b3(3){}
       void MyTest()
       {
              cout << "CSquare::MyTest \n"
       }
       void display() 
       {
              cout <<b3<< "Square \n" 
       }
}
//
void main()
{
       CShape          aShape
       CRect            aRect
       CSquare         aSquare
       CShape* pShape[3] = { &aShape,
                                            &aRect,
                                            &aSquare }
       for (int i=0 i< 3 i++)
       {
              pShape[i]->display()
              pShape[i]->MyTest()
       }
}

以下是上面那个程序(vTest.cpp)里for循环和循环体中的内存结构,代码的汇编,和一些注释,我能证明的只有这些了(相信我的同志就可以不用看啦^_^)。

以下是栈:

0012FF4C>   00000000              int                i     //(循环体内的定义)
	0012FF50>   0012FF78              CShape*       pShape[0]
	0012FF54>   0012FF6C                              pShape[1]
	0012FF58>   0012FF5C                              pShape[2]
	0012FF5C>   00426064              CSquare              aSquare
	0012FF60>   00000001              b1
	0012FF64>   00000002              b2
	0012FF68>   00000003              b3
	0012FF6C>   00426048              CRect          aRect
	0012FF70>   00000001              b1
	0012FF74>   00000002              b2
	0012FF78>   0042601C              CShape        aShape
	0012FF7C>   00000001              b1

以下是三个对象vtable的内容(前面是virtual void play()的地址,后面是virtual void display()的地址):

	00426064>     37 10 40 00   50 10 40 00
	00426048>     37 10 40 00   55 10 40 00
	0042601C>     37 10 40 00   5F 10 40 00

以下是代码,for循环和循环体内的反汇编:

004010E9>    JMP SHORT SHAPE.004010F4
	004010EB>     MOV EAX,DWORD PTR SS:[EBP-34]
	004010EE>     ADD EAX,1
	004010F1>     MOV DWORD PTR SS:[EBP-34],EAX  i++
	004010F4>     CMP DWORD PTR SS:[EBP-34],3         循环次数的控制,i<3
	004010F8>     JGE SHORT SHAPE.00401124                   

关键,寻址得到 &pShape

	004010FA>     MOV ECX,DWORD PTR SS:[EBP-34]
	004010FD>     MOV ECX,DWORD PTR SS:[EBP+ECX*4-30]    
	00401101>     MOV EDX,DWORD PTR SS:[EBP-34]
	00401104>     MOV EAX,DWORD PTR SS:[EBP+EDX*4-30]

关键,得到函数表的首地址

	00401108>     MOV EDX,DWORD PTR DS:[EAX]
	0040110A>     MOV ESI,ESP
	0040110C>     CALL DWORD PTR DS:[EDX+4]            调用虚拟的成员函数
	0040110F>     CMP ESI,ESP
	00401111>     CALL SHAPE.__chkesp                           收拾残局^_^

寻址得到 &pShape

	00401116>     MOV EAX,DWORD PTR SS:[EBP-34]
	00401119>     MOV ECX,DWORD PTR SS:[EBP+EAX*4-30]
	0040111D>     CALL SHAPE.00401073                             调用普通的成员函数
	00401122>     JMP SHORT SHAPE.004010EB

返回技术交流