C++反汇编第三讲,反汇编中识别继承关系,父类,子类,成员对象
讲解目录: 1.各类在内存中的表现形式? ?备注: 主要复习开发知识,和反汇编没有关系,但是是理解反汇编的前提. ? ? 2.子类继承父类 2.1 子类中有虚函数,父类中有虚函数 : 都有的情况下 ? ?2.2 子类中没有虚函数,父类中有虚函数 : 子类没有,父类有的情况 2.1 2.2的情况都是一样的. ? ? 2.3? ?子类中有虚函数,父类中没有虚函数 : 子有父没有的的情况下 ? ?2.4 子类父类都没有虚函数的情况下 第二专题大总结. 熟悉反汇编可以直接看这个总结, 3.结合第二专题的成员对象有无虚表行为 3.1成员对象有虚表的情况 3.2成员对象没有虚表的情况 第三专题大总结 ? 4.重载运算符的识别 5.纯虚函数的反汇编 6.模版识别. 一丶各类在内存中的表现形式(复习开发知识) 讲解之前,我们首先要明白C/C++中的类的内存结构.继承之后的内存结构 普通类的内存结构: class MyTest { public: MyTest(); ~MyTest(); public: int m_int; }; MyTest::MyTest(){} MyTest::~MyTest(){} int main(int argc,char* argv[]) { MyTest test; //定义对象 return 0; } 对应内存结构图 高级代码: 这是普通的一个类的内存结构图,因为我们只有一个成员,大小是一个4字节的,所以初始化为CC 总结: 普通类根据成员进行申请内存. 带有虚关键字的类(可能有虚函数或者虚构造) PS:?类声明同上,但是析构前边加上了virtual 关键字,变为了虚析构 内存结构图: 可以看出,申请了八个字节,启动前4个字节是虚表指针,指向了虚表 后四个字节才是真正的为成员申请的内存. ? 总结: 带有虚函数(虚关键字)的时候,内存中会把前4个字节当做虚表指针,并且在构造的时候初始化. ? 子类继承父类,(都有虚函数的情况下)重要: 高级代码: class MyFather { public: MyFather(); virtual ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //继承 { public: MyChild(); virtual ~MyChild(); float m_flt; }; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc,char* argv[]) { MyChild test; //定义对象 return 0; } 内存结构图 总共申请了12个字节,前4个字节是虚表指针,后4个字节是父类的m_int成员,在后面才是子类的真正的成员. 说到这里我们就要说下复写虚表指针的操作. 首先我们知道:? 子类构造的时候,会先构造父类,也就是说,父类的内存会先申请,并且把虚表指针填写到前4个字节位置,? 而构造完毕父类之后,构造自己的时候,这时候虚表指针又写入子类的虚表指针了.产生了覆盖了. 流程图: ?看上面图可以知道,我们子类继承父类,并且填写了虚表指针为子类的,此时 则可以写成? 父类指针指向子类? ?例如:? Myfather *pFa = new MyChild;? pfa指向的位置就是父类区域的起始位置, 而且不会超过父类区域,所以是安全的,此时因为构造完毕,虚表指针是子类的,所以调用虚函数的时候,则是调用子类的虚函数了. 而且也说明了 为什么子类指针不能指向父类.这样会产生越界问题. 总结: 子类继承父类时候,有虚函数的时候,会先把头4字节申请出来填写为虚表指针,而且会产生复写(重复写入). 第一次,构造父类,填写为父类指针,第二次构造完父类则会填写为子类的虚表指针.
? 二丶子类继承父类反汇编中的结构 2.1 子类中有虚函数,父类中有虚函数 : 都有的情况下 高级代码: class MyFather { public: MyFather(); virtual ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather { public: MyChild(); virtual ~MyChild(); float m_flt; }; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc,char* argv[]) { MyChild test; //定义对象 return 0; } Debug下的反汇编 PS: 代码太多,只说明这个反汇编在哪个函数中 1.main函数中找到构造 2.构造中生成的反汇编 ? 可以看出,构造中又有一个Call,这个Call是构造父类的,构造完毕之后填写自己的虚表指针. 3.父类构造 父类构造填写虚表指针,也就是对象的前4个字节修改为父类的虚表指针.而后通过第二步,得出,当构造完父类之后,其前4个字节会被子类重新写入.也就产生了复写过程 ? 总结:? 1.子类构造的时候会先构造父类,父类构造中先填写虚表指针. 2.父类构造完成之后,子类会重新写入虚表指针. 3..子类继承父类,都有虚函数的情况下,会产生复写行为,对象首地址4个字节处填写虚表. ? ?2.2 子类中没有虚函数,父类中有虚函数?: 子类没有,父类有的情况 PS: 高级代码中,子类类声明去掉了虚函数 Debug下的反汇编代码: 1.main函数下构造的反汇编 2.构造内部反汇编 看到这一步我们明白了,首先构造父类,因为父类有虚函数,所以肯定会有虚表指针填写,而下方也填写了一次虚表指针.由此得出 父类有虚函数,子类没有虚函数则子类也会有虚表.也会产生复写行为. 总结: 父有,子没有,子类也会有虚表,而且也会产生虚表指针复写行为. 且只要父类有虚函数,不管子类有没有虚函数,子类都会产生虚表,且会复写虚表指针. ? 2.3 子类有虚函数,父类没有虚函数 高级代码:?子类中定义了虚函数,父类则把虚函数去掉了. Debug下的反汇编代码 1.main函数下构造 2.构造内部 看其内部得出,父类没有虚函数的情况下,其对象 +4位置,跳过前边的4个字节,来构造父类,构造完毕之后填写子类虚表指针. 3.父类构造内部 父类构造内部没有产生虚表指针填写行为 ? 总结: 子类有虚表,父类没有,则会跳过虚表指针的位置来构造父类,当构造完毕父类之后前4个字节填写子类的虚表指针. ? 2.4 子类,父类都没有虚函数的情况下 ?直接构造内存,没有虚表,也不会产生虚表指针复写,可以当做结构体还原. ? ? 第二专题大总结 1.父类有虚函数,子类不管有没有虚函数,都会有虚表 2.父类有虚函数构造的时候会填写虚表指针,且子类也会填写虚表指针,两者会产生虚表指针复写行为 3.子类中有虚函数,则会跳过虚表指针来构造父类,其子类会在构造完毕父类之后填写虚表指针,不会产生虚表指针复写行为. 三丶结合第二专题的成员对象有无虚表行为 3.1成员对象没有虚表的情况下 高级代码: class MyMemberObj //成员对象 { public: MyMemberObj(){} ~MyMemberObj(){} }; class MyFather //父类 { public: MyFather(); ~MyFather(); public: int m_int; }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //子类继承父类 { public: MyChild(); virtual ~MyChild(); MyMemberObj m_memberobj; //成员对象 float m_flt; }; MyChild::MyChild(){} MyChild::~MyChild(){} int main(int argc,char* argv[]) { MyChild test; //定义对象 return 0; } Debug下的反汇编 1.main函数下的构造 2.构造内部 ? 1.构造父类,因为父类没有虚函数,所以+4构造一下,且父类有一个成员,所以申请了4个字节空间 2.成员变量的构造+8的位置开始构造,父类构造完毕之后构造,且此时成员对象没有虚函数. 3.子类在自己的头4个字节位置处填写虚表指针. ? 3.成员对象构造内部 ? 成员对象内部不会产生写虚表的行为. ? 总结: 成员对象没有虚函数的情况下,会在合适偏移位置处进行构造,注意合适位置处的用语,如果你是子类的成员对象,肯定会先构造父类,父类成员很多,则你的偏移位置则不固定. ? 3.2成员对象有虚表的情况下. Debug下的汇编代码: 因为其类之加了一个虚关键字,析构变为了虚析构,产生了虚表的动作.所以其汇编代码1,2步没有改变,同上. 不同的是构造的时候,成员对象有了虚函数,构造的时候则会填写虚表. ? ? 总结: 1.有成员对象的时候其成员对象内部没有虚表产生,则会在合适位置构造成员对象. 2.有成员对象的时候,其成员对象内部有虚表产生,则在合适位置填写虚表指针,并且构造成员对象. ? ? 四丶反汇编中重载运算符的识别 在说重载运算符的时候,我们首先熟悉一下运算符重载的高级代码: ? 简单的运算符重载 函数类型 operator 运算符名称 (形参表列) 高深一点的可以参考博客,这里不再重复讲解.复习开发知识可以参考博客链接?http://c.biancheng.net/cpp/biancheng/view/215.html 高级代码: int operator+(MyChild& a,MyFather& b) { return (int)a.m_flt + b.m_int; } int main(int argc,char* argv[]) { MyChild a; //定义对象 MyFather b; cout << a + b << endl; return 0; } 在反汇编中,其实运算符重载就是调用函数.只不过换了一种函数的认知方式. ? 其实不难.当做函数还原就好. 说道这里,我们可以说下运算符重载的额外认知. 比如我们熟悉的 1.数学中的中缀式? ?a + b / c - d * e 这种表达式就是中缀表达式 2.波兰式 -+a/bc*de? 中缀转化为了波兰式,我们学习数据结构的树的时候就学习过这种方式,这个是编译原理中的.适用于计算机的识别. 怎么转换的 Sub(add(a,Div(b,c),Imul(d,e); 转为汇编代码,比如a + b /c 我们则写成? add(a,div(b,然后转为汇编表达式即可.最终的结果则是上面写的波兰式.只不过按照语义,变为符号化了. ? ? 五丶纯虚函数的反汇编 我们知道,纯虚函数是为了子类实现了,自己不能实现,但是反汇编代码中其实实现了,只不过里面调用了提示错误的API.就是为了你不小心调用的时候提示不能创建xxx对象的实例.等等一些列的错误. 高级代码: class MyFather //父类 { public: MyFather(); ~MyFather(); virtual void show() = 0; //纯虚函数 }; MyFather::MyFather(){} MyFather::~MyFather(){} class MyChild : public MyFather //子类继承父类 { public: MyChild(); virtual ~MyChild(); virtual void show(); }; MyChild::MyChild(){} MyChild::~MyChild(){} void MyChild::show() { cout << 1 << endl; } int main(int argc,char* argv[]) { MyChild a; //定义对象 a.show(); return 0; } Debug下反汇编 我们直接看纯虚函数内部了,在子类构造的时候父类会构造,父类构造自己的时候会填写虚表指针,我们直接找父类的虚表指针即可.然后定位虚表中的第二项. 第一项是父类的虚析构,第二项才是我们的.
纯虚函数在低版本就是19h,并且调用__amsg_exit,且如果弄了签名,则是__purecall 高版本不太一样,高版本不是简单的这样调用了(vs系列)它会保存当时的寄存器信息啊,什么的,然后写日志用的.反正结果是一样的. 高版本自己可以试试看一看有什么不同. ? ? 六丶模版识别. 模版和运算符重载一样,都是函数,编译为反汇编的代码都是函数调用.而且函数和函数的重载不同,它生成的反汇编代码有多处. 高级代码: template <typename T> T MySub(T a,T b) { return a - b; } int main(int argc,char* argv[]) { printf("%drn",MySub(1,2)); printf("%frn",MySub(3.0f,1.0f)); printf("%lfrn",MySub(8.3,4.3)); return 0; } 运行结果: ? Debug下反汇编. 虽然都是一样调用,但是其内部是不同的.每个函数都有自己的汇编代码. ? ? 转载于: 作者:IBinary出处:http://www.cnblogs.com/iBinary/ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |