多态的实现原理
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); }protected:private:};class B:public A{public: int x; void Test() { printf("B /n"); }protected:private:};void Fun(A* p){ p->Test();}int main(int argc, char* argv[]){ A a; B b; Fun(&b); return 0;}//我们发现在这里 调用的test函数 是b的 因为fun方法传入的对象是b b继承自a 这里体现了多态//反编译31: void Fun(A* p)32: {00401050 push ebp00401051 mov ebp,esp00401053 sub esp,40h00401056 push ebx00401057 push esi00401058 push edi00401059 lea edi,[ebp-40h]0040105C mov ecx,10h00401061 mov eax,0CCCCCCCCh00401066 rep stos dword ptr [edi]33: p->Test();00401068 mov eax,dword ptr [ebp+8]0040106B mov edx,dword ptr [eax]0040106D mov esi,esp0040106F mov ecx,dword ptr [ebp+8]00401072 call dword ptr [edx] //间接调用 + 虚表00401074 cmp esi,esp00401076 call __chkesp (00401240)34: }@ILT+0(?Fun@@YAXPAVA@@@Z):00401005 jmp Fun (00401050)@ILT+5(??0B@@QAE@XZ):0040100A jmp B
A (00401100)@ILT+15(?Test@B@@UAEXXZ):00401014 jmp B
Test (00401140)@ILT+25(_main):0040101E jmp main (004010a0)
总结:
1. 当我们在类中定义虚函数时,就会产生虚表2. 多态的实现 间接调用+虚表
虚表
观察带有虚函数的对象大小
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; void Test() { printf("A /n"); }protected:private:};int main(int argc, char* argv[]){ A a; printf("%d /n",sizeof(a)); return 0;}//结果是4
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); }protected:private:};int main(int argc, char* argv[]){ A a; printf("%d /n",sizeof(a)); return 0;}//结果是 8
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); } virtual void Test1() { printf("A /n"); }protected:private:};int main(int argc, char* argv[]){ A a; printf("%d /n",sizeof(a)); return 0;}//结果还是8
发现:定义了虚函数 对象大小会多出4个字节,多个虚函数也只有多一个4字节
虚表的位置
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); }};class B:public A{public: virtual void Test() { printf("B /n"); }};void Fun(A* a){ a->Test();}int main(int argc, char* argv[]){ A a; B b; Fun(&a); Fun(&b); return 0;}
通过vc6的监视器发现a对象的具体结构
发现加了虚函数之后对象前面对了一个值 指向 0x00422fac
那么这里的值指向的就是虚表的位置
继续追踪
调出内存窗口查找此内存
里面的值是 00401028 (小端存储)
vc6中 ctrl +g 跳转到对应反汇编位置
这里指向的正好是A的test 方法
(此时多了个TEST1 是因为上次编译后的结果没有清理缓存 )
26: void Fun(A* a)27: {00401050 push ebp00401051 mov ebp,esp00401053 sub esp,40h00401056 push ebx00401057 push esi00401058 push edi00401059 lea edi,[ebp-40h]0040105C mov ecx,10h00401061 mov eax,0CCCCCCCCh00401066 rep stos dword ptr [edi]28: a->Test();//取参数也就是a对象的指针 到eax00401068 mov eax,dword ptr [ebp+8] //读取eax也就是a对象的首地址 也就是虚表的位置 0040106B mov edx,dword ptr [eax] 0040106D mov esi,esp//传递this指针,到ecx0040106F mov ecx,dword ptr [ebp+8]//调用虚表中记录的函数位置 这里是第一个就直接是edx00401072 call dword ptr [edx]00401074 cmp esi,esp00401076 call __chkesp (00401240)29: }//虚表00401023 jmp A::A (004010d0)00401028 jmp A::Test (00401090)0040102D jmp A::Test1 (00401110)00401032 jmp B::B (00401170)00401037 jmp Fun (00401050)0040103C jmp B::Test (004011c0)00401041 jmp A::A (00401200)
虚表的结构
据观察,虚表中存储的都是函数地址,每个地址占用4个字节,有几个虚函数,则就有几个地址
虚表的内容
子类没有重写时的值
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); }};class B:public A{public:};void Fun(A* a){ a->Test();}int main(int argc, char* argv[]){ B b; Fun(&b); return 0;}//虚表00401014 jmp A::Test (00401140)
子类重写时的值
#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{public: int x; virtual void Test() { printf("A /n"); }};class B:public A{public: virtual void Test() { printf("B /n"); }};void Fun(A* a){ a->Test();}int main(int argc, char* argv[]){ B b; Fun(&b); return 0;}//虚表@ILT+15(?Test@B@@UAEXXZ):00401014 jmp B
Test (004011e0)
析构函数问题
class A{private: int* a;public: A() { a = new int[10]; } ~A() { delete a; } int* get_a() { return a; }}class B:public A{private: int* b;public: B() { b = new int[5]; } ~B() { delete b; } int* get_arr(int flag) { if(flag == 1 ) { return b; } else { return get_a(); } }}int main(){ A* p = new B; delete p; return 0;}
上述代码执行时,如果直接调用 指针类型是父类,那么只会执行父类的析构函数释放掉 int* a
而b类中的int* b
却不会被释放掉
理论上最好的方式是 逐步往上调用所有的析构函数,这样才可以释放所有使用的内存
// _20180212.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include <stdio.h>#include <windows.h>class A{private: int* a;public: A() { a = new int[10]; } virtual ~A() { delete a; printf("析构 A /n"); } int* get_a() { return a; }};class B:public A{private: int* b;public: B() { b = new int[5]; } ~B() { delete b; printf("析构 B /n"); } int* get_arr(int flag) { if(flag == 1 ) { return b; } else { return get_a(); } }};int main(int argc,char* argv[]){ A* p = new B; delete p; return 0;}
这里 将父类的析构函数定义为虚函数,那么delete
的时候 就会调用子类重写父类虚析构函数的析构函数(虽然名字不相同,但是会自动重写_编译器约定)
并且此时析构函数现实从下往上逐步执行
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论