现在的位置: 首页 > 综合 > 正文

深入探索c/c++函数(2)—普通成员函数调用的基本过程

2018年07月05日 ⁄ 综合 ⁄ 共 7562字 ⁄ 字号 评论关闭

c++的成员函数根据其调用的不同,大致可以分为4类:内联成员函数,静态成员函数,虚成员函数和上述3种以外的普通成员函数。从本质来说类成员函数和全局函数在调用上并没有差别,非内联函数的在调用时,基本上都包括如下的过程:函数的参数入栈,eip指针值入栈,然后跳到函数体的地址,执行函数体对应的代码,执行完毕调整栈帧。下面就按照上述4个分类进行分析,先来说一下普通的成员函数:

 

普通的成员函数在被调用时有两大特征:

1 普通的成员函数是静态绑定的,
2 普通的成员函数调用时编译器隐式传入this指针的值。

 

通过代码分析一下:

 上面Print函数符合上面所说4类的中的普通成员,所谓的静态绑定实质是c++源代码编译时,编编译器在p->Print();处翻译成直接调用Test类中Print()的汇编代码,也就是编译期编译器就确定了被调函数的相对地址。而所谓的动态绑定实质是,源码在编译的时候,编译器不是翻译成直接调用Test类中Print()的汇编代码,而是翻译成一个查找虚表,得到到函数的相对地址的过程。看一下上面生成的汇编代码:

 

 

编译器调用Print()时是根据p类型来确定调用哪个类的Print()函数时,也就是说根据->(或者.)左边对象的类型来确定调用的函数,同时编译器也是根据对象的类型来确定该成员函数是否能够被合法的调用,而这个校验是发生在编译期的类型静态检查的,也就是只是一个代码级的检查的。不管对象的真正类型是什么,只要被强制转化成了Test类型,编译器就会接受p->Print(2);的调用,从而翻译成调用Print的代码。

 

[Note: the interpretation of the call of a virtual function depends on the type of the object for which it is called (the dynamic type), whereas the interpretation of a call of a nonvirtual member function depends only on the
type of the pointer or reference denoting that object (the static type) (5.2.2). ](ISO/IEC 14882:2003(E)//10.3.6 Virtual functions)

 

Print函数部分反汇编代码: 

<<<说的形象些如果Print是某一个山寨,山寨一般都有一个暗号(天王盖地虎?),而p的类型则是一个暗号,在这指的是Test类型,编译器此时就是一个守山寨入口的喽啰(纯打比喻),守山寨的喽啰(编译器)看见有人(p这个对象)进山寨(调用Test类的函数),喽啰喊了一句 :土豆土豆我是地瓜,(进行类型静态检查),那人回了句臭鱼臭鱼我是烂虾,喽啰一听密码正确(检查了p的类型是Test型的),登录中。。,只要暗号正确,喽啰就会放行,他不管这个人是不是真的寨子里的人(真的Test类型的对象),只要暗号正确(类型正确),哪怕这个人不是山寨的,而是来卧底的(不是Test类型的对象),只要把暗号整正确(强制转换成Test型),也会进入山寨的,这也给以后山寨留下了悲剧,欲知后事如何,请听下回分解>>>

如下的代码也是没有错误的:

 int i=0;//华丽的卧底
 ((Test*)&i)->Print(2);//得到了个暗号,进了山寨

((Test*)0)->Print(2);//进了山寨的不是人,而是寂寞

再说第二点,函数参数入栈后,this指针的值也会入栈或者存入ecx寄存器。而this指针的值可以认为是p的值,也就是->左边对象的值。传入this值的目的是为了操作对象里的数据,通过类的声明,编译器可以确定对象内成员变量的相对于类对象起始地址的偏移,即相对this值的偏移。而成员函数调用时隐式传入的this值,编译器是不对this值进行检查,编译器只是简单生成this+偏移操作对象的汇编代码,所以->左边对象的类型正确,编译器就会找到相应的成员函数,不管传入this值是否正确,只要this+偏移访问的地址是合法的,os也不会抱怨,一旦this+偏移不合法,激活os的异常机制,程序才会宕了。

If the function is a nonstatic member function, the “this” parameter of the function (9.3.2)shall be initialized with a pointer to the object of the call, converted as if by an explicit type conversion.[Note: There is no access checking on this conversion;
the access checking is done as part of the (possibly implicit) class member access operator. See 11.2. ]

(ISO/IEC 14882:2003(E)//5.2.2 Function call 4)

现在我们往Test类中装一下数据:

现在主要看看现在的Print函数 汇编代码:

 

 
 

 从上面Pint函数对应的汇编代码可以看到,当成员函数在访问成员变量时,伴随着一个通过this指针寻址的过程, mov ecx,dword ptr [this];mov edx,dword ptr [ecx+4],就因为这两句代码,许多进入Print函数这个山寨的无间道,就有可能被山大王(触发OS的异常机制)发现,mov edx,dword ptr [ecx+4]这句是一个内存访问语句,我们都知道对于指针int
*p;如果指向一个非法的地址,那么会触发os的异常机制的,比如p=0;*p=1,同样的ecx+4值不是一个合法的值,也会触发os异常的,所以像((Test*)0)->Print();语句触发异常了,显然访问了地址为0x00000004的内存.而win32位每个进程的地址空间里,开始内存地址空间里设置了一个分区,范围是0x00000000~0x0000ffff,如果进程中有线程试图读写这段区域,cpu就会引发
非法访问的。

 int a[2]={2,1111};((Test*)a)->Print();,根据分析,输出1111的,华丽的卧底~~~

 

最后要强调一点,c++标准规定,If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.(ISO/IEC 14882:2003(E)//9.3.1 Nonstatic member functions),虽然上述 int
a[2]={2,1111};((Test*)a)->Print()代码在一些主流的编译器vc,gcc编译执行通过,但是并不保证所有平台都没有问题的。实际编程中无论如何也不要写类似的代码。

O(∩_∩)O

 

 

 

 

抱歉!评论已关闭.