《C++反汇编与逆向分析技术揭秘》之十一——虚函数

  • 虚函数的机制

当类中定义有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张地址表中,这张表被称为虚函数地址表。编译器还会在类中添加一个虚表指针。

举例:

CVirtual类的构造函数中没有进行任何操作,但是我们来看构造函数内部,还是有一个赋初值的操作:

这个地址指向的是一个数组:

这些数组中的内容就是虚函数的指针:

值得注意的是,如果没有虚指针的存在,那么CVirtual大小就是4字节。有了这个指针存在就是8字节。

本例子中,使用了一个空的构造函数,但是编译器自己擅自插入了代码,实现了对虚表的初始化。如果我们没有提供任何构造函数的话,那么编译器就会提供一个默认的构造函数对虚表进行初始化。

当函数被调用时,会间接访问虚表,得到对应的虚函数的地址,并调用执行。这种通过虚表间接寻址访问的情况只有在使用对象的指针或引用来调用虚函数的时候才会出现。当直接使用对象调用自身的虚函数时,没有必要查表访问。因为已经明确调用的是自身的成员函数了,根本没有构成多态性

举例:

直接通过对象调用虚函数的时候,就是直接用对象的地址作为隐含参数传递给这个虚函数:

这个虚函数此时和普通的成员函数没有区别。之所以要隐含传递对象的地址,是为了能够准确适用对象中所包含的数据成员。

但是如果构成了多态,调用方式就不同了:

因为你实际上不知道pcv指针指向的具体类型是什么,所以要到虚表中找到所指向的真正的对象的那个虚函数。

虚表指针的初始化,是判断一个函数是构造函数的充分条件。

析构函数对虚表如何操作?在考虑这个问题之前,我们先要知道,为什么析构函数要使用虚函数:

如果父类和子类的虚函数分别如下所示:

我们在执行delete指针之后,会是如下流程:

即只调用了父类的虚函数。而如果把析构函数设置为虚的:

则是如下调用流程:

delete删除指针的时候调用的是子类的虚函数,而子类的虚函数内部又调用了父类的虚函数。而调用父类的虚函数之前,ecx指针中仍保留的是子类对象的首地址:

子类的析构函数调用自身的虚成员函数:

随后调用父类的析构函数:

父类的析构函数中没有间接寻址,直接调用了Show1和Show2:

但是无论是子类还是父类的虚析构函数中都会有这么一步操作:把当前类的虚表的首地址赋值到虚表指针当中去。这是为了防止在析构函数中调用虚函数时取到非自身的虚表。为什么要这么做?举例说明:

先调用A的构造函数:

A类填充虚表:

调用虚函数:

调用完A的构造函数,继续往下执行B的构造函数中的其余部分,为了能够正常调用B的func2,这里必须要还原虚表

析构函数中同理。

  • 虚函数的识别
时间: 2024-07-28 23:10:17

《C++反汇编与逆向分析技术揭秘》之十一——虚函数的相关文章

《C++反汇编与逆向分析技术揭秘》--认识启动函数,找到用户入口

<C++反汇编与逆向分析>和<程序员的自我修养>都是以VC6的代码作为例子讲解的.这里是在vs2017下,CRT代码有些区别,但整体流程上都是初始化环境,设置参数,最后转到用户main函数. class COne { public: COne() { printf("COne \r\n"); } ~COne() { printf("~COne \r\n"); } }; COne g_One; int main() { printf("

《C++反汇编与逆向分析技术揭秘》之十——析构函数

局部对象 当对象所在作用域结束之后,销毁栈空间,此时析构函数被调用. 举例: 函数返回时自动调用析构函数: 堆对象 调用析构代理函数来处理析构函数: 为什么使用析构代理函数来调用析构函数?考虑到如果delete的对象是一个对象数组,可以使用析构代理函数依次析构所有的对象.举例: 调用了构造代理函数: vector deleting destructor函数中先对标志位进行判断: 如果没有跳转,表明delete的是一个数组,则会调用调用析构代理函数对对象进行逐一的析构.如果进行了跳转,则只进行一次

《C++反汇编与逆向分析技术揭秘》--数据类型

  浮点数类型 IEEE标准从逻辑上采用一个三元组{S, E, M}来表示一个数N,它规定基数为2,符号位S用0和1分别表示正和负,尾数M用原码表示,阶码E用移码表示.根据浮点数的规格化方法,尾数域的最高有效位总是1,由此,该标准约定这一位不予存储,而是认为隐藏在小数点的左边,因此,尾数域所表示的值是1.M(实际存储的是M),这样可使尾数的表示范围比实际存储多一位.为了表示指数的正负,阶码E通常采用移码方式来表示,将数据的指数e 加上一个固定的偏移量后作为该数的阶码,这样做既可避免出现正负指数,

《C++反编译与逆向分析技术揭秘》之学习笔记01--浮点数转IEEE编码

※浮点数转IEEE编码 1.float类型的IEEE编码(31,30~23,22~0=>符号位,指数位,尾数位)eg1:12.25 经过IEEE编码后的各位情况:符号位:0指数位:3+127, 10000010尾数位:100010000000000000000004字节二进制:0x41440000vc6.0截图: eg2:-0.125 经过IEEE编码后的各位情况:符号位:1指数位:-3+127, 01111100尾数位:000000000000000000000004字节二进制:0xBE000

《C++反编译与逆向分析技术揭秘》之学习笔记03--函数的调用方式

※函数的调用方式 EBP:扩展基址指针寄存器(extended base pointer) 其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部. ESP:(Extended stack pointer)是指针寄存器的一种,用于指向栈的栈顶. _cdecl:C/C++默认的调用方式,调用方平衡栈,不定参数的函数可以试用. 调用方:1.参数压栈.esp-=42.调用函数.3.实现栈平衡.esp+=4 此处的printf也是同样道理0x004010CB.0x004010CC两处压入参数,共8个字节

《C++反编译与逆向分析技术揭秘》之学习笔记02--结构体和类之内存分布

※结构体和类之内存分布 1.空类的大小空类:其实空类至少会占用1个字节的长度. 2.字节对齐在为结构体和类中的数据成员分配内存时,结构体中的当前数据成员类型长度为M,指定对齐值为N,那么实际对齐值位q=min(M,N),其成员的地址安排在q的倍数上. vc6.0缺省对齐8个字节sShort占用2个字节,所以安排的地址0x0012FF70为2的倍数.nInt占用4个字节,所以安排的地址0x0012FF74为4的倍数.因为结构体中最大的字段长度为4,所以对齐值调整为4个字节.因为test对象为8个字

c++反汇编与逆向分析 小结

第一章  熟悉工作环境和相关工具1.1 熟悉OllyDBG  操作技巧1.2 反汇编静态分析工具 IDA(最专业的逆向工具)    快捷键    功能     Enter     跟进函数实现     Esc       返回跟进处    A         解释光标处的地址为一个字符串的首地址     B         十六进制数与二进制数转换    C         解释光标处的地址为一条指令     D         解释光标处的地址为数据,没按一次将会转换这个地址的数据长度   

C++反汇编第二讲,反汇编中识别虚表指针,以及指向的虚函数地址

讲解之前,了解下什么是虚函数,什么是虚表指针,了解下语法,(也算复习了) 开发知识为了不码字了,找了一篇介绍比较好的,这里我扣过来了,当然也可以看原博客链接: http://blog.csdn.net/hackbuteer1/article/details/7558868 一丶虚函数讲解(复习开发,熟悉内存模型) 1.复习开发知识 首先:强调一个概念 定义一个函数为虚函数,不代表函数为不被实现的函数. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数. 定义一个函数为纯虚函数,才代表函数

逆向分析技术

1 . 函数参数传递 调用约定 _cdecl _stdcall  _fastcall 传参顺序 左->右 右->左 寄存器和堆栈传参 平衡堆栈者 调用者 子程序 子程序 int _cdecl Add(char a, long b, int c, int d) { return (a + b + c + d); } push 4 push 3 push 2 push 1 call Add (1161A60h) add esp,10h //调用者平衡堆栈 int _stdcall Add(char