您的位置:首页 > 编程语言 > C语言/C++

c++下类成员函数的调用发生了啥(从汇编看)

2014-06-29 17:03 495 查看
昨天使用了下这样一段代码

打印结果为:

是两个一样的地址。。略震惊。
后来在网上参考了这几篇博客


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

/article/2252080.html

要点:
编译器调用Print()时是根据p类型来确定调用哪个类的Print()函数时,也就是说根据->(或者.)左边对象的类型来确定调用的函数,同时编译器也是根据对象的类型来确定该成员函数是否能够被合法的调用,而这个校验是发生在编译期的类型静态检查的,也就是只是一个代码级的检查的。

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


VC虚函数布局引发的问题


/article/8488619.html


几个要点:vc对同一类中的不同虚函数的解决方法是由编译器加入了一系列的内部函数"vcall". 一个类中的每个虚函数都有一个唯一与之对应的vcall函数,通过特定的vcall函数跳转到虚函数表中特定的表项。






虚函数表获取的函数地址和函数实际地址一样吗?


/article/8843347.html


要点:1符号表段是程序编译连接后产生的一个段,用于标识程序中全局静态变量、函数等符号和其真实地址之间的映射,就像代码段、数据段一样是程序编译连接后的一部分!

2.类的成员函数指针和普通函数指针不一样,成员函数指针是一个结构体指针,里面包含了偏移量,标志(是否是虚函数),真实地址等。虚函数表中所指向的函数地址和函数指针所指向的地址都不是该函数的真正入口地址,虚函数表所指向的函数地址是该函数的符号地址;而函数指针所指向的地址则是编译器为了满足函数指针类型定义而生成的函数指针的调用地址。总之,他们是无法进行比较的!




然后又不死心地非要用汇编看一遍它的流程,使用的代码如下

在调试过程中通过Ctrl+alt+d可以跳转到汇编页面,在vs2005中调试时通常在运行代码地址更低一点的位置就是符号表,在上面第三篇博文里提到过。

//通过父类函数指针调用父类虚函数void (A::* p_v_A)(void) = &A::vitualFun;(A().*p_v_A)();
这一段的执行汇编为:

在OO411BF7处的call语句跳转到了符号表中的这一段

上图中可以看到是调用了A中vcall函数,跟虚函数代码的实际存放地址还没有扯上半毛钱关系

在A类的vcall函数中,强行执行了jump到eax寄存器中所指地址处,执行该处的代码,据说ecx是this指针的存放位置。但是F10之后,后续的跳转有点匪夷所思,中间好像有汇编语句缺失,不知道该到哪里打断点来查看。因为4311128的16进制表示并不是0x00411136啊

但反正还是跳转到了符号表里面A类的虚函数对应的真正代码起始地址0x00412310处,挺曲折的。

//通过父类函数指针调用父类普通函数void (A::* p_d_A)(void) = &A::dynamicFun;(A().*p_d_A)();
汇编代码如下:
在00411C17  call        dword ptr [ebp-70h] 处跳转到了符号表中
接着就跳转到了

审查跳转的目标地址和函数代码实际地址,可见dynamicFun这个符号是对应了dynamicFun函数代码的真实地址。
//通过子类函数指针调用子类虚函数void (B::* p_v_B)(void) = &B::vitualFun;(B().*p_v_B)();
以上两行对应的汇编代码为
180: void (B::* p_v_B)(void) = &B::vitualFun;
00411C21 mov dword ptr [ebp-7Ch],offset A::`vcall'{0}' (4114FBh)
181: (B().*p_v_B)();
00411C28 lea ecx,[ebp-214h]
00411C2E call B::B (411271h)
00411C33 mov esi,esp
00411C35 mov ecx,eax
00411C37 call dword ptr [ebp-7Ch]
00411C3A cmp esi,esp
00411C3C call @ILT+895(__RTC_CheckEsp) (411384h)

在00411C37 call dword ptr [ebp-7Ch] 处还是跳到了A的vcall函数里
这应该也就是上面打印出来的两个虚函数地址一样的缘故,其实打印出来的都是A的vcall函数地址。
直接用&A::virtualFun()得到的并不是虚函数的真实地址按照百度百科里虚函数的相关内容叙述:编译器在编译时,会提供类似于CallVirtualFun这样的代码,当你调用虚函数时,其实就是先调用的那段类似CallVirtualFun的代码,通过这段代码,获得虚函数地址后,最后调用虚函数,这样就真正保证了多态性。同时大家都说虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。

接着是

但是可以发现此时eax寄存器里的值跟上面调用A类里面的virtualFun函数的值不一样咯。接着就跳到了下面这段
上图可见,跳转到了符号表中B::virtualFun处,同样的vcall函数,寄存器一个值不一样就调到别的地方了。接着就按照符号表后面的地址执行函数代码吧。


//通过向上转换的对象指针调用子类虚函数A *p1 = new B;p1->vitualFun();
在00411C95 call eax跳转到了如下位置:并没有经过vcall函数。
静态函数的调用:也是使用了符号表,然后直接跳转到函数代码的实际存放位置。

综上:  //通过父类函数指针调用父类虚函数:

调用处代码-> 符号表父类的vcall函数对应项 -> 父类vcall函数-> 可能存在的不明过程(上文有详述)-> 父类virtual函数实际代码存放处;//通过父类函数指针调用父类普通函数:
调用处代码-> 符号表父类dynamicFun函数对应项-> 真实代码//通过子类函数指针调用子类虚函数
调用处代码-> 符号表父类vcall-> 父类vcall函数-> 符号表子类virtual函数对应项-> 子类virtual函数真实代码

//通过向上转换的对象指针调用子类虚函数(好像比想象中少一点,可能漏掉了偏移计算过程)
调用处代码-> 符号表子类virtual函数对应项—> 子类virtual函数真实代码

//普通静态函数调用
调用处代码-> 符号表staticFun函数名对应项-> staticFun函数真实代码




内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: