您的位置:首页 > 其它

通过反汇编理解 C++语言实质探讨

2007-06-09 22:49 204 查看
1 前言
和传统的程序设计方法相比较,面向对象的程序设计方法的最显著的特点是它更接近于人们通常的思维规律,因而设计出的软件系统能够更直接地、自然地反映客观现实中的问题。
面向对象的程序设计方法起源于信息隐藏和抽象数据类型概念。它的基本思想是将要构造的软件系统表现为对象集,其中每个对象是将一组数据和使用它的一组基本操作或过程封装在一起而组成的实体,对象之间的联系主要是通过消息的传递实现的。
然而正是面向对象程序设计的这些特点,长期以来,那些对标准C语言有较好了解的广大技术人员一直难于顺利地过渡到面向对象的编程方法上来,主要表现于对其实质性的技术概念缺乏真正的了解。比如对象模型的构造、存储、数据与成员函数之间的联系,以及多态、继承、封装等,对其实质性技术觉得既抽象又具迷惑性。以致在使用中连连碰壁,结果是不得已而求其次,纷纷转入使用其它可视化编程工具。
本文从C++语言模型入手,探讨对象模型的塑造过程。从一个具体的例子着手,针对VS.NET集成的C++编译器的编译结果,对其进行反汇编,从而揭示出代码后面编译器所作的许多工作,以使读者对类的构造、存储、数据成员与成员函数之间的联系等有一个较为深入的理解。
2 对象的存储及虚函数表
类和对象是面向程序数据的两个最基本的概念。类是将一组数据和作用在它上面的一组操作或过程封装在一起组成的数据类型,在C++中,类中定义的数据称为数据成员,操作称为成员函数。而对象则是类的实例,是面向对象的程序设计中最基本的单元。
对象作为一个整体,从外面只能看见它的外部特性,即具备那些处理能力(操作或过程),而这些处理能力的具体实现和对象内部的状态对外是不可见的。正因为如此,才使得用户觉得既抽象又具迷惑性。下面以分析一个具体的代码为例,向读者展示对象的存储实质。
例子代码如下:
class Class1
{ public:
int mem_data1;
int mem_data2;
Class1(){ } //constructor
void mem_func1(){ cout<<"this is mem_func1"<<endl; this->mem_data1 =10;}
void virtual mem_vfun1(){ cout<<"this is mem_vfun1"<<endl; this->mem_data2 =20;}
void virtual mem_vfun2(){ cout<<"this is mem_vfun2"<<endl; this->mem_data2 =30;}
};
int _tmain(int argc, _TCHAR* argv[])
{ Class1 * ptemp=new Class1();
ptemp->mem_func1();
ptemp->mem_vfun1();
return 0;
}
上面的代码声明一个类Class1,它包括两个成员变量 mem_data1,mem_data2,一个成员函数 mem_func1(),两个成员虚函数mem_vfunc1()、mem_vfunc2()。那么当用户用Class1定义一个对象,即Class1被实例化后它在内存中是如何布局的呢?
如果不考虑虚函数的影响,可以发现一个Class 实例的内存布局,完全和一个Struct(标准C语言版)的实例的内存布局一样,成员按照声明的先后在内存中连续分配。如下图所示。
vtable
Vptr
Mem_data1
Mem_data2
(Class1::*mem_vfun1)()
(Class1::*mem_vfun2)()

图中(Class1::*mem_vfun1)()和(Class1::*mem_vfun2)()分别表示指向Class1成员函数mem_vfun1和mem_vfun1的指针,而vptr 是指向虚函数表的指针。
虚函数表是一个表格结构,里面放着类的成员虚函数的地址。上述类Class1的虚函数表用Microsoft Macro Assembler定义形式如下:
??_7Class1@@6B@ DD FLAT:?mem_vfun1@Class1@@UAEXXZ;Class1::‘vftable’
DD FLAT:?mem_vfun2@Class1@@UAEXXZ
读者可能对上面代码中出现的很多奇怪的字符感到陌生,“??_7Class1@@6B@”在代码中从没有出现过,那么它代表什么?其实“??_7Class1@@6B@”就等价于字符串“Class1::vftable”,
而 ”mem_vfun1@Class1@@UAEXXZ” 和“?mem_vfun2@Class1@@UAEXXZ”正是Class1中的两个虚函数mem_vfun1,mem_vfun2处理后的名字,“FLAT”表示内存用的线性模式。如果取消名字的处理,上述的定义也就等价如下:
Class1_vftable DD FLAT::Class1::mem_vfun1;
DD FLAT::Class1::mem_vfun2;
Class1_vftable 表示这是Class1的虚函数表。很明显虚函数表中存放的是类Class1的两个虚函数mem_vfunc1,mem_vfunc2的内存地址。
3 数据成员与成员函数间的联系
上面分析了类Class1的存储方式以及两个虚函数在虚函数表中的表达形式,那么为什么编译器要这么处理呢?请考虑下面的操作:

class Class2{public: int mem1;}; class Class3:public Class2{public: int mem1;};
上面的Class3从Class2派生出来,自然Class3将直接继承了Class2的mem1。可是Class3中也定义了一个mem1。为了使每个符号变量都能被唯一的区分出来,编译器都会对用户定义的符号名称施以“name-mangling”(微软的帮助文档中称这种操作叫“名字修饰”),让所有的符号名称得到独一无二的名称。Class3施以“name-mangling”后,其形式如下:

class Class3{public:int Class2-mem1; int Class3-mem1;};
一个类生成实例,就是在内存中构造出了它的成员函数和虚函数表指针(当类中声明虚函数的时候)。那么类的成员函数是如何操作类的成员数据的呢?其中的奥秘就在this 指针。每个成员函数都隐藏了一个指向本类实例的指针(this 指针)。因此
void mem_func1()
{ cout<<"this is mem_func1"<<endl; this->mem_data1 =10;}
其实等价于:
void mem_func1(Class1 * _this){ cout<<"this is mem_func1"<<endl; _this->mem_data1 =10;}
而int _tmain(int argc, _TCHAR* argv[])中的
ptemp->mem_func1();
也就等价如下:
mem_func1(ptemp);
实际执行的是:
cout<<"this is mem_func1"<<end;
ptemp->mem_data1 =10;
每个成员函数都是含有一个指向本类类型的指针,通过本指针成员函数也就和成员数据建立了联系。不过这一切用户并不知道,编译器为用户做了一切。
4 汇编结果
从下面的Class1::mem_func1()的汇编实现代码中,读者可以就会编译器的工作实质(分号后面带底纹的文字是作者加的注释)。
_TEXT SEGMENT
_this$ = -8 ;this 指针被函数保存在ebp-8处
?mem_func1@Class1@@QAEXXZ PROC NEAR ; Class1::mem_func1名字处理后的结果

; _this$ = ecx 函数用ecx来接收this指针
push ebp
mov ebp, esp
sub esp, 204 ; 000000ccH
push ebx
push esi
push edi
push ecx
lea edi, DWORD PTR [ebp-204]
mov ecx, 51 ; 00000033H
mov eax, -858993460 ; ccccccccH
rep stosd
pop ecx
;以上代码是堆栈的建立和初始化,开辟了204个字节的区域,并初始化为ccH。
mov DWORD PTR _this$[ebp], ecx ;构造this指针
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax+4], 10 ;使用间接寻址,等价操作:this->mem_data1 =10;因为vptr使用了4个字节,所以Class1::mem_data1的偏移地址为4,可推之Class1::mem_data2的偏移地址为8
pop edi
pop esi
pop ebx
add esp, 204
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp ;恢复堆栈并检验
ret 0
?mem_func1@Class1@@QAEXXZ ENDP ;End Class1::mem_func1
_TEXT ENDS
为了清晰起见,上述代码中去掉了cout<<"this is mem_func1"<<endl;的汇编实现。
从上述代码中,读者可以看到,数据成员和成员函数之间是如何通过this指针建立联系的。一旦类的数据成员和成员函数建立了关联,对象模型也就“基本”塑造出来了。那么模型中的vtable和编译器使用的“name-mangling”到底有什么其它作用呢?其实正是这两种技术成就了两种多态:运行时多态,编译期多态;限于篇幅,本文就不再讨论了。
5 结束语
以上只是通过对象的存储和数据成员和成员函数之间的联系两个方面,利用反汇编技术,向读者展示了对象模型的塑造过程,以期望对读者深入了解C++的精髓有所帮助。实际上,封装、继承、多态等机制是很复杂的,要探讨的东西还很多。读者可以采用本文所介绍的方法,获得更多更深刻的理解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: