刘收获

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

虚表和虚表指针2.0

  继续从汇编内存层次上对虚表和虚表指针进行分析

  (任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)

 

0x01 对象调用自身虚函数

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CVirtual
{
public:
    virtual int GetNumber()
    {
        return m_nNumber;
        return 0;
    }
    virtual void SetNumber(int nNumber)
    {
        m_nNumber = nNumber;
    }
private:
    int m_nNumber;
};
 
int main()
{
    //int a = sizeof(CVirtual);
    CVirtual TheVirtual;
 
    TheVirtual.SetNumber(20);
    printf("%d\n", TheVirtual.GetNumber());
    return 0;
}

  反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    21: int main()
    22: {
00EC3DA0 55                   push        ebp 
00EC3DA1 8B EC                mov         ebp,esp 
00EC3DA3 81 EC D0 00 00 00    sub         esp,0D0h 
00EC3DA9 53                   push        ebx 
00EC3DAA 56                   push        esi 
00EC3DAB 57                   push        edi 
00EC3DAC 8D BD 30 FF FF FF    lea         edi,[ebp-0D0h] 
00EC3DB2 B9 34 00 00 00       mov         ecx,34h 
00EC3DB7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh 
00EC3DBC F3 AB                rep stos    dword ptr es:[edi] 
    23:     //int a = sizeof(CVirtual);
    24:     CVirtual TheVirtual;
00EC3DBE 8D 4D F4             lea         ecx,[TheVirtual] 
00EC3DC1 E8 6E D5 FF FF       call        CVirtual::CVirtual (0EC1334h) 
    25:
    26:     TheVirtual.SetNumber(20);
00EC3DC6 6A 14                push        14h 
00EC3DC8 8D 4D F4             lea         ecx,[TheVirtual] 
00EC3DCB E8 0B D4 FF FF       call        CVirtual::SetNumber (0EC11DBh) 
    27:     printf("%d\n", TheVirtual.GetNumber());
00EC3DD0 8D 4D F4             lea         ecx,[TheVirtual] 
00EC3DD3 E8 BC D4 FF FF       call        CVirtual::GetNumber (0EC1294h) 
00EC3DD8 50                   push        eax 
00EC3DD9 68 3C 6B EC 00       push        offset string "%d\n" (0EC6B3Ch) 
00EC3DDE E8 97 D5 FF FF       call        _printf (0EC137Ah) 
00EC3DE3 83 C4 08             add         esp,8 
    28:     return 0;
00EC3DE6 33 C0                xor         eax,eax 
    29: }

  虚函数SetNumber()反汇编分析:、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    13:     virtual void SetNumber(int nNumber)
    14:     {
00EC1770 55                   push        ebp 
00EC1771 8B EC                mov         ebp,esp 
00EC1773 81 EC CC 00 00 00    sub         esp,0CCh 
00EC1779 53                   push        ebx 
00EC177A 56                   push        esi 
00EC177B 57                   push        edi 
00EC177C 51                   push        ecx 
00EC177D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh] 
00EC1783 B9 33 00 00 00       mov         ecx,33h 
00EC1788 B8 CC CC CC CC       mov         eax,0CCCCCCCCh 
00EC178D F3 AB                rep stos    dword ptr es:[edi] 
00EC178F 59                   pop         ecx 
00EC1790 89 4D F8             mov         dword ptr [this],ecx 
    15:         m_nNumber = nNumber;
00EC1793 8B 45 F8             mov         eax,dword ptr [this
00EC1796 8B 4D 08             mov         ecx,dword ptr [nNumber] 
00EC1799 89 48 04             mov         dword ptr [eax+4],ecx 
    16:     }
00EC179C 5F                   pop         edi 
    16:     }
00EC179D 5E                   pop         esi 
00EC179E 5B                   pop         ebx 
00EC179F 8B E5                mov         esp,ebp 
00EC17A1 5D                   pop         ebp 
00EC17A2 C2 04 00             ret         4 

  可以看到,虚函数与普通函数的实现流程并无差别,并没有看到虚表指针之类的操作。

  也就是说,直接通过对象调用自身的成员虚函数的时候,编译器使用了直接调用函数的方式,没有访问续表指针,来间接获取虚函数地址。

 

 

 

 

0x02 析构函数对虚表指针的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CVirtual
{
public:
    virtual int GetNumber()
    {
        return m_nNumber;
        return 0;
    }
    virtual void SetNumber(int nNumber)
    {
        m_nNumber = nNumber;
    }
    ~CVirtual()
    {
 
    }
private:
    int m_nNumber;
};
 
int main()
{
    //int a = sizeof(CVirtual);
    CVirtual TheVirtual;
 
    //TheVirtual.SetNumber(20);
    //printf("%d\n", TheVirtual.GetNumber());
    return 0;
}

  反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   17:  ~CVirtual()
    18:     {
00A51730 55                   push        ebp 
00A51731 8B EC                mov         ebp,esp 
00A51733 81 EC CC 00 00 00    sub         esp,0CCh 
00A51739 53                   push        ebx 
00A5173A 56                   push        esi 
00A5173B 57                   push        edi 
00A5173C 51                   push        ecx 
00A5173D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh] 
00A51743 B9 33 00 00 00       mov         ecx,33h 
00A51748 B8 CC CC CC CC       mov         eax,0CCCCCCCCh 
00A5174D F3 AB                rep stos    dword ptr es:[edi] 
00A5174F 59                   pop         ecx 
00A51750 89 4D F8             mov         dword ptr [this],ecx 
00A51753 8B 45 F8             mov         eax,dword ptr [this
00A51756 C7 00 34 6B A5 00    mov         dword ptr [eax],offset CVirtual::`vftable' (0A56B34h) 
    19:
    20:     }

  从析构函数中摘取关键的汇编指令:

1
2
3
00A5174F 59                   pop         ecx 
00A51750 89 4D F8             mov         dword ptr [this],ecx 
00A51753 8B 45 F8             mov         eax,dword ptr [this

  可以看出这和我写的上一篇博客:虚表与虚表指针中构造函数的操作是一模一样的,pop ecx还原this指针的值到ecx中,然后通过ecx赋值给this指针,再由eax得到this指针。

  最后一步:

1
00A51756 C7 00 34 6B A5 00    mov         dword ptr [eax],offset CVirtual::`vftable' (0A56B34h) 

  将当前类的续表首地址赋值到虚表指针中。

 

  通过分析构造函数和析构函数的流程可知:

  两者对虚标的操作过程几乎一致,都是将虚表指针设置为当前对象所属类的虚表首地址。看起来相同,其实差别很大。

  构造函数当中的虚表指针初始化,是将虚表指针初始化为正确的虚函数表基地址;而析构函数写入虚表指针,是将原对象的虚表指针重新赋值,其指针可能指向了另外一个虚表。

 

  下面有一份继承代码的反汇编来验证析构函数中对象虚表指针的重新赋值(验证结果为:父类指针指向子类的对象,并调用子类中的同名虚函数时,虚表指针会指向子类的虚函数表,再调用子类的函数,对象析构时,父类析构函数会将当前的虚表指针重新赋值为父类的虚表指针。)

  代码流程:

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream> 
using namespace std;
 
class A
{
public:
    void 刘大大()
    {
        printf("1\n");
    }
    virtual void 刘小()
    {
        printf("2\n");
    }
    ~A()
    {
 
    }
};
class B : public A
{
public:
    void 刘大大()
    {
        printf("3\n");
    }
    void 刘小()
    {
        printf("4\n");
    }
};
int main(void)
{
    B b;
 
    A *p = &b;
 
    p->刘小();
 
    return 0;
}

  准备调用刘小()函数前,对象中的虚表指针指向B类的虚表:

 

  结束后B的析构函数中callA的析构函数:

 

  A的析构函数重新赋值虚表指针后,虚表指针指向了A的虚表:

 

posted on   沉疴  阅读(287)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示