• 汇编视角分析C++虚函数实现原理


    1.概述

            虚函数是c++语言非常重要的机制,日常的c++编程工作中经常使用虚函数,通过汇编视角来探究虚函数的实现原理,有助于深刻理解虚函数的内部机制。尤其要说明的是:c++语法规范并没有规定虚函数的具体实现方案,不同的编译器实现方式可以不同,本文基于arm32平台,g++编译器来分析虚函数表的实现机制,基本主流编译器的原理基本是相似的,所以理解g++实现方案再去分析其他编译器的实现也是类似的。

    2.示例代码

    1. class State {
    2. public:
    3. State(int id) {
    4. this->mId = id;
    5. }
    6. State(const State &state) {
    7. printf("State copy constructor\n");
    8. }
    9. virtual void dump() {
    10. printf("State mId:%d\n", mId);
    11. }
    12. public:
    13. int mId;
    14. };
    15. class AudioState : public State {
    16. public:
    17. AudioState(int id) : State(id) {
    18. }
    19. void dump() {
    20. printf("AudioState mId:%d\n", mId);
    21. }
    22. };
    23. int main(){
    24. printf("%d\n" ,sizeof(AudioState));
    25. State *ps = new AudioState(1);
    26. ps->dump();
    27. return 0;
    28. }

    代码编译和反汇编:

    1. arm-linux-androideabi-g++ -pie -fPIE -o vtable vtable.cpp
    2. arm-linux-androideabi-objdump vtable > vtable.txt

    3. 虚函数调用实现基本原理

    1. 如果是通过类指针调用相应的虚函数,通过vtable实现调用。

    2. 如果子类重新实现了虚函数,vtable中对应项填写子类的函数(如上图),否则,填写父类德函数。

    如下代码:

    1. AudioState *ps = new AudioState(1);
    2. ps->dump();

    调用dump函数的时候,首先从AudioState对象的vptr指针拿到虚函数表(vtable),然后获取到虚函数表中dump函数对应的表项,所以最终调用到AudioState::dump函数。下面通过g++ arm汇编分析上述调用过程,帮助我们更深刻的理解虚函数实现原理。

    4.虚函数调用实现

    1. AudioState虚函数表在哪里构建?

    答:AudioState构造函数里面

    下面分析AudioState的汇编代码:

    1. 000014b0 <_ZN10AudioStateC1Ei>:
    2. 14b0: e92d4810 push {r4, fp, lr}
    3. 14b4: e28db008 add fp, sp, #8
    4. 14b8: e24dd00c sub sp, sp, #12
    5. //fp-16栈处存储对象指针,即对象首地址
    6. 14bc: e50b0010 str r0, [fp, #-16]
    7. //fp-20栈处存储构造函数的参数,即id参数
    8. 14c0: e50b1014 str r1, [fp, #-20] ; 0xffffffec
    9. //以下两行将r4 = 10abc(1500地址处的值) + 14d0 = 11F8c
    10. 14c4: e59f4034 ldr r4, [pc, #52] ; 1500 <_ZN10AudioStateC1Ei+0x50>
    11. 14c8: e08f4004 add r4, pc, r4
    12. 14cc: e51b3010 ldr r3, [fp, #-16]
    13. 14d0: e1a00003 mov r0, r3
    14. 14d4: e51b1014 ldr r1, [fp, #-20] ; 0xffffffec
    15. 14d8: ebffffd0 bl 1420 <_ZN5StateC1Ei>
    16. //r3存储AudioState对象的首地址
    17. 14dc: e51b3010 ldr r3, [fp, #-16]
    18. //r2从如下地址取值:ffffffa4(1504处的值, -92的补码) + r4= 11F8c - (5c) = 11F30
    19. //r2 = [11F30] = 11c68, 11c68就是AudioState虚函数表
    20. 14e0: e59f201c ldr r2, [pc, #28] ; 1504 <_ZN10AudioStateC1Ei+0x54>
    21. 14e4: e7942002 ldr r2, [r4, r2]
    22. //r2 取AudioState虚函数表的第三项(前面存储了typeinfo相关项), r2 = 11c70
    23. 14e8: e2822008 add r2, r2, #8
    24. //将r2 11c70这个地址存储对象的首地址,即vptr = 11c70
    25. 14ec: e5832000 str r2, [r3]
    26. 14f0: e51b3010 ldr r3, [fp, #-16]
    27. 14f4: e1a00003 mov r0, r3
    28. 14f8: e24bd008 sub sp, fp, #8
    29. 14fc: e8bd8810 pop {r4, fp, pc}
    30. 1500: 00010abc ; instruction: 0x00010abc
    31. 1504: ffffffa4 ; instruction: 0xffffffa4
    32. 00011c68 <_ZTV10AudioState>:
    33. 11c68: 00000000 andeq r0, r0, r0
    34. 11c6c: 00011c84 andeq r1, r1, r4, lsl #25
    35. 11c70: 00001508 andeq r1, r0, r8, lsl #10 //1508即AudioState::dump
    36. 11c74: 00000000 andeq r0, r0, r0
    37. 00001508 <_ZN10AudioState4dumpEv>:
    38. 1508: e92d4800 push {fp, lr}
    39. 150c: e28db004 add fp, sp, #4
    40. 1510: e24dd008 sub sp, sp, #8
    41. 1514: e50b0008 str r0, [fp, #-8]
    42. 1518: e51b3008 ldr r3, [fp, #-8]
    43. 151c: e5933004 ldr r3, [r3, #4]
    44. 1520: e59f2014 ldr r2, [pc, #20] ; 153c <_ZN10AudioState4dumpEv+0x34>
    45. 1524: e08f2002 add r2, pc, r2
    46. 1528: e1a00002 mov r0, r2
    47. 152c: e1a01003 mov r1, r3
    48. 1530: ebffff14 bl 1188 <printf@plt>
    49. 1534: e24bd004 sub sp, fp, #4
    50. 1538: e8bd8800 pop {fp, pc}
    51. 153c: 0000d434 andeq sp, r0, r4, lsr r4

    根据上面汇编代码14ec行,最终将r2(11c70)存入r3中指向地址,其中r2时虚函数表的地址,对应的表项内容:1508,对应的是AudioState::dump函数地址,说明g++编译器会把vptr放在对象的开端处。

    2. 虚函数调用流程?

    答:通过vptr找到对应的虚函数

    main函数的汇编代码:

    1. 000013d4
      :
    2. 13d4: e92d4810 push {r4, fp, lr}
    3. 13d8: e28db008 add fp, sp, #8
    4. 13dc: e24dd00c sub sp, sp, #12
    5. 13e0: e3a00008 mov r0, #8
    6. 13e4: fa000094 blx 163c <_Znwj>
    7. 13e8: e1a04000 mov r4, r0
    8. 13ec: e1a00004 mov r0, r4
    9. 13f0: e3a01001 mov r1, #1
    10. 13f4: eb00002d bl 14b0 <_ZN10AudioStateC1Ei>
    11. //将AudioState对象指针ps存储fp-16处
    12. 13f8: e50b4010 str r4, [fp, #-16]
    13. //ps指针读入r3
    14. 13fc: e51b3010 ldr r3, [fp, #-16]
    15. //vptr读入r3
    16. 1400: e5933000 ldr r3, [r3]
    17. //AudioState::dump虚函数地址读入r3,即1508
    18. 1404: e5933000 ldr r3, [r3]
    19. 1408: e51b0010 ldr r0, [fp, #-16]
    20. //跳转到dump函数执行
    21. 140c: e12fff33 blx r3
    22. 1410: e3a03000 mov r3, #0
    23. 1414: e1a00003 mov r0, r3
    24. 1418: e24bd008 sub sp, fp, #8
    25. 141c: e8bd8810 pop {r4, fp, pc}

    注意:如果直接使用类对象调用虚函数,不会通过虚函数,汇编代码中可以看到回直接bl跳转相应对象的函数。

  • 相关阅读:
    JAVA计算机毕业设计中医保健网站Mybatis+系统+数据库+调试部署
    有序表2:跳表
    服务器被矿工入侵记录
    通过Vagrant安装虚拟机常见Bug
    如何在DBNet中加入新的主干网络
    Revit内建模型的基础教学分享
    数据分析:RT-qPCR分析及R语言绘图
    SpringBoot一站式功能提供框架(二)Mybatis Plus分页、Websocket 消息推送、提取word--柚子真好吃
    nacos实战项目中的配置
    linux服务器集群分发scp与rsync
  • 原文地址:https://blog.csdn.net/GetNextWindow/article/details/126207373