您的位置:首页 > 大数据 > 人工智能

浅析C++中虚函数的调用及对象的内部布局http://blog.csdn.net/starlee/article/details/2089358

2015-12-12 17:17 603 查看


浅析C++中虚函数的调用及对象的内部布局

标签: c++汇编fun编译器classobject
2008-02-13 09:26 11547人阅读 评论(22) 收藏 举报


分类:

C++(24)


版权声明:本文为博主原创文章,未经博主允许不得转载。

在我那篇《浅析C++中的this指针》中,我通过分析C++代码编译后生成的汇编代码来分析this指针的实现方法。这次我依然用分析C++代码编译后生成的汇编代码来说明C++中虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布局也不尽相同;但是,只要是符合C++标准的编译器,编译结果和对象的内部布局应该是大同小异。

首先,是一个有着简单继承关系的两个类:

class CBase

{

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;

}
现在用下面的代码来调用成员函数:

CDerived derived;

// 用对象调用虚函数

derived.VFun1();

derived.VFun2();

// 用对象调用非虚函数

derived.Fun1();

derived.Fun2();

// 用指向派生类的基类的指针调用虚函数,实现多态

CBase *pTest = &derived;

pTest->VFun1();

pTest->VFun2();
下面就是用VC2005编译上面的代码后生成的汇编代码:

CDerived derived;

0041195E lea ecx,[derived]

00411961 call CDerived::CDerived (411177h)

// 代码段1

derived.VFun1();

00411966 lea ecx,[derived]

00411969 call CDerived::VFun1 (411078h)

derived.VFun2();

0041196E lea ecx,[derived]

00411971 call CDerived::VFun2 (4111B8h)

derived.Fun1();

00411976 lea ecx,[derived]

00411979 call CBase::Fun1 (411249h)

derived.Fun2();

0041197E lea ecx,[derived]

00411981 call CDerived::Fun2 (4111BDh)

// 代码段2

CBase *pTest = &derived;

00411986 lea eax,[derived]

00411989 mov dword ptr [pTest],eax

pTest->VFun1();

0041198C mov eax,dword ptr [pTest] // 行1

0041198F mov edx,dword ptr [eax] // 行2

00411991 mov esi,esp

00411993 mov ecx,dword ptr [pTest]

00411996 mov eax,dword ptr [edx] // 行3

00411998 call eax // 行4

0041199A cmp esi,esp

0041199C call @ILT+495(__RTC_CheckEsp) (4111F4h)

pTest->VFun2();

004119A1 mov eax,dword ptr [pTest]

004119A4 mov edx,dword ptr [eax]

004119A6 mov esi,esp

004119A8 mov ecx,dword ptr [pTest]

004119AB mov eax,dword ptr [edx+4] // 行5

004119AE call eax

004119B0 cmp esi,esp

004119B2 call @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()来证明上面图中虚函数表指针的存在。

void CDerived::Fun2()

{

004118F0 push ebp

004118F1 mov ebp,esp

004118F3 sub esp,0CCh

004118F9 push ebx

004118FA push esi

004118FB push edi

004118FC push ecx

004118FD lea edi,[ebp-0CCh]

00411903 mov ecx,33h

00411908 mov eax,0CCCCCCCCh

0041190D rep stos dword ptr es:[edi]

0041190F pop ecx

00411910 mov dword ptr [ebp-8],ecx

m_iValue1 = 13;

00411913 mov eax,dword ptr [this] // 行6

00411916 mov dword ptr [eax+4],0Dh // 行7

m_iValue2 = 13;

0041191D mov eax,dword ptr [this]

00411920 mov dword ptr [eax+8],0Dh

}

00411927 pop edi

00411928 pop esi

00411929 pop ebx

0041192A mov esp,ebp

0041192C pop ebp

0041192D ret
上面是类CDerived的非虚成员函数Fun2()的汇编代码。可以看到,行6是将this指向的地址放入eax寄存器,而行7是给this指针指向的地址加4的地址赋值(具体的分析,可以看《浅析C++中的this指针》),而这个地址里面存放的是类CDerived的第一个成员变量。我们知道this指针是指向对象首地址的,那么为什么要给第一个成员变量赋值的时候要向后移动4个字节?答案是因为对象的前4个字节是用来存放虚表指针的。

下面的代码是《浅析C++中的this指针》一文中的不含虚函数的类的C++代码和编译后的汇编代码:

class CTest

{

public:

void SetValue();

private:

int m_iValue1;

int m_iValue2;

};

void CTest::SetValue()

{

m_iValue1 = 13;

m_iValue2 = 13;

}

void CTest::SetValue()

{

004117E0 push ebp

004117E1 mov ebp,esp

004117E3 sub esp,0CCh

004117E9 push ebx

004117EA push esi

004117EB push edi

004117EC push ecx

004117ED lea edi,[ebp-0CCh]

004117F3 mov ecx,33h

004117F8 mov eax,0CCCCCCCCh

004117FD rep stos dword ptr es:[edi]

004117FF pop ecx

00411800 mov dword ptr [ebp-8],ecx

m_iValue1 = 13;

00411803 mov eax,dword ptr [this] // 行8

00411806 mov dword ptr [eax],0Dh // 行9

m_iValue2 = 13;

0041180C mov eax,dword ptr [this]

0041180F mov dword ptr [eax+4],0Dh

}

00411816 pop edi

00411817 pop esi

00411818 pop ebx

00411819 mov esp,ebp

0041181B pop ebp

0041181C ret
通过行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++的底层实现加深了了解。如果网友们看了觉得有收获,那我就心满意足了。呵呵~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: