在我那篇
《浅析C++中的this指针》
中,我通过分析C++代码编译后生成的汇编代码来分析this指针的实现方法。这次我依然用分析C++代码编译后生成的汇编代码来说明C++中虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布局也不尽相同;但是,只要是符合C++标准的编译器,编译结果和对象的内部布局应该是大同小异。
首先,是一个有着简单继承关系的两个类:
{
public :
virtual void VFun1() = 0 ;
virtual void VFun2() = 0 ;
void Fun1();
};
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CBase::Fun1()
{
}
class CDerived: public CBase
{
public :
virtual void VFun1();
virtual void VFun2();
void Fun2();
private :
int m_iValue1;
int m_iValue2;
};
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun1()
{
}
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun2()
{
}
// 这里是为了分析对象的内部布局,因此仅仅是给成员变量赋值
void CDerived::Fun2()
{
m_iValue1 = 13 ;
m_iValue2 = 13 ;
}
现在用下面的代码来调用成员函数:
// 用对象调用虚函数
derived.VFun1();
derived.VFun2();
// 用对象调用非虚函数
derived.Fun1();
derived.Fun2();
// 用指向派生类的基类的指针调用虚函数,实现多态
CBase * pTest = & derived;
pTest -> VFun1();
pTest -> VFun2();
下面就是用VC2005编译上面的代码后生成的汇编代码:
0041195Eleaecx,[derived]
00411961 callCDerived::CDerived(411177h)
// 代码段1
derived.VFun1();
00411966 leaecx,[derived]
00411969 callCDerived::VFun1(411078h)
derived.VFun2();
0041196Eleaecx,[derived]
00411971 callCDerived::VFun2(4111B8h)
derived.Fun1();
00411976 leaecx,[derived]
00411979 callCBase::Fun1(411249h)
derived.Fun2();
0041197Eleaecx,[derived]
00411981 callCDerived::Fun2(4111BDh)
// 代码段2
CBase * pTest = & derived;
00411986 leaeax,[derived]
00411989 movdwordptr[pTest],eax
pTest -> VFun1();
0041198Cmoveax,dwordptr[pTest] // 行1
0041198Fmovedx,dwordptr[eax] // 行2
00411991 movesi,esp
00411993 movecx,dwordptr[pTest]
00411996 moveax,dwordptr[edx] // 行3
00411998 calleax // 行4
0041199Acmpesi,esp
0041199Ccall@ILT + 495 (__RTC_CheckEsp)(4111F4h)
pTest -> VFun2();
004119A1moveax,dwordptr[pTest]
004119A4movedx,dwordptr[eax]
004119A6movesi,esp
004119A8movecx,dwordptr[pTest]
004119ABmoveax,dwordptr[edx + 4 ] // 行5
004119AEcalleax
004119B0cmpesi,esp
004119B2call@ILT + 495 (__RTC_CheckEsp)(4111F4h)
通过对代码段1的观察我们可以发现:通过对象调用类的虚成员函数和调用非虚成员函数是相同的(对调用成员函数的汇编代码的分析可以看我的那篇
《浅析C++中的this指针》
)。也就是说,用对象是无法实现多态的。
下面主要来分析实现多态的代码段2。
行1、将pTest指针指向的地址前2个字(4个字节,也就是32位系统中一个指针的大小)的内容当成一个指针放到eax寄存器中
行2、将eax寄存器中的指针的值放入edx寄存器
行3、将dex寄存器中的指针的值放入eax寄存器
行4、调用eax寄存器指向的函数
这样分析似乎对怎样调用对象derived的虚函数VFun1()并不是很清楚。那么我们先来看下面的这张图:
这张图是一个假设的对象derived在内存中的内部布局图。指针pTest指向对象derived,而对象derived的前4个字节是一个虚表指针,指向虚函数表。
看着这张图再来分析上面的汇编代码就会清晰很多:
行1、取得虚表指针值放入eax寄存器中
行2、取得虚表指针的值放入edx寄存器中
行3、取得虚表指针指向的地址的值(也就是VFun1)放入eax寄存器中
行4、调用eax寄存器指向的函数
行5证明了上面图中对虚函数表的假设。第二个虚函数VFun2()的地址就是通过在第一虚函数VFun1()的地址加4(32位系统中一个指针的大小)而得到的。
通过上面的分析,可以得出C++中虚函数的调用方法:首先,取得对象中的虚表指针;然后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调用。
下面通过分析类CDerived的非虚成员函数Fun2()来证明上面图中虚函数表指针的存在。
{
004118F0pushebp
004118F1movebp,esp
004118F3subesp,0CCh
004118F9pushebx
004118FApushesi
004118FBpushedi
004118FCpushecx
004118FDleaedi,[ebp - 0CCh]
00411903 movecx,33h
00411908 moveax,0CCCCCCCCh
0041190Drepstosdwordptres:[edi]
0041190Fpopecx
00411910 movdwordptr[ebp - 8 ],ecx
m_iValue1 = 13 ;
00411913 moveax,dwordptr[ this ] // 行6
00411916 movdwordptr[eax + 4 ],0Dh // 行7
m_iValue2 = 13 ;
0041191Dmoveax,dwordptr[ this ]
00411920 movdwordptr[eax + 8 ],0Dh
}
00411927 popedi
00411928 popesi
00411929 popebx
0041192Amovesp,ebp
0041192Cpopebp
0041192Dret
上面是类CDerived的非虚成员函数Fun2()的汇编代码。可以看到,行6是将this指向的地址放入eax寄存器,而行7是给this指针指向的地址加4的地址赋值(具体的分析,可以看
《浅析C++中的this指针》
),而这个地址里面存放的是类CDerived的第一个成员变量。我们知道this指针是指向对象首地址的,那么为什么要给第一个成员变量赋值的时候要向后移动4个字节?答案是因为对象的前4个字节是用来存放虚表指针的。
下面的代码是
《浅析C++中的this指针》
一文中的不含虚函数的类的C++代码和编译后的汇编代码:
{
public :
void SetValue();
private :
int m_iValue1;
int m_iValue2;
};
void CTest::SetValue()
{
m_iValue1 = 13 ;
m_iValue2 = 13 ;
}
void CTest::SetValue()
{
004117E0pushebp
004117E1movebp,esp
004117E3subesp,0CCh
004117E9pushebx
004117EApushesi
004117EBpushedi
004117ECpushecx
004117EDleaedi,[ebp - 0CCh]
004117F3movecx,33h
004117F8moveax,0CCCCCCCCh
004117FDrepstosdwordptres:[edi]
004117FFpopecx
00411800 movdwordptr[ebp - 8 ],ecx
m_iValue1 = 13 ;
00411803 moveax,dwordptr[ this ] // 行8
00411806 movdwordptr[eax],0Dh // 行9
m_iValue2 = 13 ;
0041180Cmoveax,dwordptr[ this ]
0041180Fmovdwordptr[eax + 4 ],0Dh
}
00411816 popedi
00411817 popesi
00411818 popebx
00411819 movesp,ebp
0041181Bpopebp
0041181Cret
通过行8、行9和行6、行7的比较就可以看出:类CTest的对象前4个字节存放的是自己的第一个成员变量;而类CDerived的对象从第5个字节开始才是存放的自己的第一个成员变量,它的前4个字节是用来存放虚表指针的。这再一次证明了上面图中对象内部布局的正确性。
PS:
这篇文章可以说是
《浅析C++中的this指针》
的续篇,最后我说说我为什么会用这种方法来分析C++,也算是对
《浅析C++中的this指针》
一文中网友评论的回复吧。
dch4890164建议我看inside the c++ object model;而hacker47却说了风凉话:“孔乙己说:回字有三种写法,你们知道么?”;最直接的是wengch,直接反问我:“用汇编分析C++.....有意义么?”。而我要说的是,《Inside The C++ Object Model》这本书我看过,确实是一本非常好的讲解C++底层的书。可是由于平时写C++代码的时候,很少会关心底层的实现,所以那本书看过之后留下的印象并不深刻。而用汇编代码来分析C++也是源于一个很偶然的事件:就是
《浅析C++中的this指针》
一文中提到的可以用一个类的空指针来调用成员函数。我发现我的C++知识不能解释那种现象,在Debug代码的时候,我转到了汇编代码中来寻找答案。后来就把我的分析结果写成了那篇
《浅析C++中的this指针》
。说实话,这也是我第一次接触Windows下的汇编语言,文章中的分析都是边看资料边揣摩得出的。也许会有人觉得我这种方法不值一提,但是我却通过这种方法对C++的底层实现加深了了解。如果网友们看了觉得有收获,那我就心满意足了。呵呵~~