飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 577|回复: 6

[C/C++] 【原创】从汇编角度,理解C++虚函数表

[复制链接]
  • TA的每日心情
    开心
    2024-6-9 16:20
  • 签到天数: 24 天

    [LV.4]偶尔看看III

    发表于 2024-8-29 11:14:40 | 显示全部楼层 |阅读模式
    当通过指针访问类的成员函数时:

    • 如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数
    • 如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。


    编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。
    如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable。

    我们首先从代码角度,来验证一下上面的说明:

    我们首先用VC6,写一段代码:

    1. #include <iOStream>
    2. using namespace std;

    3. //基类Base
    4. class Base{
    5. public:
    6.         int x;
    7.         void Test()
    8.         {
    9.                 cout << "A" << endl;
    10.         }
    11. };

    12. //派生类Derived
    13. class Derived: public Base{
    14. public:
    15.         void Test()
    16.         {
    17.                 cout <<"B" <<endl;
    18.         }
    19. };

    20. void Fun(Base *p)
    21. {
    22.         p->Test();        //多态
    23. }

    24. void main()
    25. {
    26.         Base a;
    27.         Derived b;

    28.         Fun(&b);

    29. }
    复制代码
    输出结果为:
    B
    如果将main函数中的Fun(&b);修改为Fun(&a);,那输出结果为:
    A
    因此验证了如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;也就是说,指针指向的对象属于哪个类就调用哪个类的函数

    当我们去掉Base类中virtual void Test()的virtual,修改为void Test(),那无论传入的是&b还是&a,那输出结果都是
    A
    因此验证了如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数


    现在我们分析下虚函数表:如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable。

    首先看一下,非虚函数和虚函数的调用在汇编角度有什么区别。
    还是去掉Base类中virtual void Test()的virtual,修改为void Test(),然后跟进汇编:
    1. 25:       p->Test();  //多态
    2. 00401188   mov         ecx,dword ptr [ebp+8]
    3. 0040118B   call        @ILT+75(Base::Test) (00401050)        //在程序编译后,即将地址固化为00401050,即为Base::Test。
    复制代码
    我们可以看到,在非虚函数的情况下,函数的调用是编译时绑定(编译时绑定是指在程序执行之前,由编译器和连接器确定方法调用的目标),也就是直接调用。

    如果还原为以上的示例代码,即为原来的虚函数模式,然后跟进汇编:
    1. 25:       p->Test();  //多态
    2. 004011A8   mov         eax,dword ptr [ebp+8]
    3. 004011AB   mov         edx,dword ptr [eax]
    4. 004011AD   mov         esi,esp
    5. 004011AF   mov         ecx,dword ptr [ebp+8]
    6. 004011B2   call        dword ptr [edx]        //程序编译后,地址为dword ptr [edx],[edx]是一个可变变量,即地址非固定。
    复制代码
    我们可以看到,在虚函数的情况下,函数的调用是运行时绑定(运行时绑定是指在程序运行时,根据对象的实际类型确定方法调用的目标。运行时绑定也叫作动态绑定或后期绑定),也就是简间接调用。

    这就是为什么虚函数能够调用派生类函数的在汇编层面的原因:虚函数是运行时绑定(也叫动态绑定或后期绑定)。

    那我们继续分析上面的反汇编代码:
    004011A8   mov         eax,dword ptr [ebp+8]        //使用[ebp+8]指定栈中存储的第1个参数,并将其读出到 eax 寄存器中。第一个参数为b对象的地址。
    004011AB   mov         edx,dword ptr [eax]             //将b对象的地址的第一个双字节的数值,读出到edx。这个数值就是虚函数表的的地址,
    004011B2   call           dword ptr [edx]                   //调用函数,函数地址即为虚函数表的第一个双字节值。

    b对象的内存模型中,开始的第一个dword的值为虚函数表的地址;因为就一个虚函数,所以虚函数表的第一个dword的值就是这次调用的函数地址。
    PYG19周年生日快乐!
  • TA的每日心情
    难过
    5 天前
  • 签到天数: 579 天

    [LV.9]以坛为家II

    发表于 2024-8-30 08:53:18 | 显示全部楼层
    都是知识啊,多谢分享
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    5 天前
  • 签到天数: 3 天

    [LV.2]偶尔看看I

    发表于 2024-8-30 15:33:35 | 显示全部楼层
    感谢楼主分享!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    昨天 08:45
  • 签到天数: 249 天

    [LV.8]以坛为家I

    发表于 2024-8-31 08:35:49 | 显示全部楼层
    这个举例好,理解透彻了
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    昨天 08:46
  • 签到天数: 880 天

    [LV.10]以坛为家III

    发表于 2024-9-1 09:37:06 | 显示全部楼层
    感谢楼主分享
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    昨天 08:33
  • 签到天数: 1060 天

    [LV.10]以坛为家III

    发表于 5 天前 | 显示全部楼层
    PYG有你更精彩!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表