android直接方法和虚方法,从 Arm 汇编看 Android C++虚函数实现原理

1、前言

C++ 通过虚函数来实现多态, 从而在运行时动态决定要调用的函数。 那么虚函数的调用过程具体是怎样的呢? 本文将基于 Arm 汇编, 剖析 C++虚函数的调用过程。 本文涉及到的代码采用 ndk-r10d 进行编译。

2、初窥 vtable

其实虚函数的调用是通过 vtable 来实现的。编译时, 编译器会为每一个申明有虚函数的类生成一个 vtable, 也就是说 vtable 和类一一对应。 vtable 中记录了所有虚函数的地址。 在对象初始化时, 对象会保存相应 vtable 的地址, 虚函数就可以根据其在 vtable 中的偏移来进行调用。 下面来看一个实际的例子:

class BaseA{private:int baseA_field;public:BaseA(int baseA_field);~BaseA();virtual void baseA_virtual_1(){printf(“\t[-] BaseA::baseA_virtual_1()\n”);}virtual void baseA_virtual_2(){printf(“\t[-] BaseA::baseA_virtual_2()\n”);}virtual void baseA_virtual_3(){printf(“\t[-] BaseA::baseA_virtual_3()\n”);}};BaseA::BaseA(int baseA_field){printf(“[+] BaseA constructor called\n”);this->baseA_field = baseA_field;}BaseA::~BaseA(){printf(“[+] BaseA destructor called\n\n”);}int main(int argc, char *argv[]){BaseA *baseA = new BaseA(1);baseA->baseA_virtual_2();delete baseA;return 0;}

上 述 代 码 中 , 类 BaseA 有 一 个 字 段 baseA_field 和 3 个 虚 函 数baseA_virtual_1()、 baseA_virtual_2()、 baseA_virtual_3()。 在 main()中初始化了一个 BaseA 的对象,并调用了其虚函数 baseA_virtual_2()。

用 IDA 分析 mian(), F5 查看其伪代码, 如下图所示:

89224241_1

第 5 行申请了 8byte 的空间, 因为 BaseA 有一个整型字段, 再加上 vtable 的地址所占的空间,共 8byte。

第 6 行调用 sub_5E4(), 即 BaseA 的构造函数, 对对象进行初始化。 注意调用类的实例方法时, 第一个参数始终是对象的地址。 BaseA 的构造函数伪代码如下:

89224241_2

由上可以知道, vtable 的地址会首先赋给 baseA 对象的前 4 个字节, 然后才执行构造函数的代码。 其中 vtable 的结构如下:

89224241_3

执行完构造函数后, BaseA 对象的内存如下所示:

89224241_4

接着看 main()伪代码的第 7 行, *(*v0 + 4)就是虚函数 baseA_virtual_2 的首地址, 因而(*(*v0 + 4))(v0);实际就是调用 baseA_virtual_2()。

3、无重写虚函数、 单继承

有一个类 SubClass 继承 BaseA, SubClass 有一个字段 subClass_field 和三个虚函数 subClass_virtual_1、 subClass_ virtual_2、 subClass_ virtual_3, SubClass 中没有重写 BaseA 中的虚函数( ps: 不重写虚函数没有多大意义,这里仅仅为了理解虚函数的实现机制), 此时代码如下:

class SubClass: public BaseA{private:int subClass_field;public:SubClass(int subClass_field, int baseA_field);~SubClass();virtual void subClass_virtula_1(){printf(“\t[-] SubClass::subClass_virtula_1()\n”);}virtual void subClass_virtula_2(){printf(“\t[-] SubClass::subClass_virtula_2()\n”);}virtual void subClass_virtula_3(){printf(“\t[-] SubClass::subClass_virtula_3()\n”);}};SubClass::SubClass(int subClass_field, int baseA_field) :BaseA(baseA_field){printf(“[+] SubClass constructor called\n”);this->subClass_field = subClass_field;}SubClass::~SubClass(){printf(“[+] SubClass destructor called\n\n”);}int main(int argc, char *argv[]){SubClass *subClass = new SubClass(2, 1);subClass->subClass_virtula_3();delete subClass;return 0;}

用 IDA 分析 main(), F5 查看其伪代码, 如下图所示:

89224241_5

第 5 行为 SubClass 对象申请了 0xC byte 的内存空间, 即 vtable 地址、baseA_field 和 subClass_field, 各 4byte。

第 6 行调用 SubClass 的构造函数,对SubClass 对象进行初始化, sub_760()的伪代码如下图所示:

89224241_6

在 SubClass的构造函数中,首先会执行父类 BaseA 的构造函数,然后将 vtable 的地址赋给 SubClass 对象的前 4 个字节, 这样就可以覆盖掉执行 BaseA 构造函数时赋给 SubClass 对象 BaseA 类 vtable 的地址。 其中 SubClass 类 vtable 的结构如下图所示:

89224241_7

执行完构造函数后, SubClass 对象的内存如下所示:

89224241_8

从上可见, SubClass 的 vtable 前面存的是父类 BaseA 虚函数的地址,后面存的是 SubClass 中申明的虚函数的地址。回头看 main()的伪代码,第 7 行, *(*v0+20)得到 vtable 第五项的值, 即subClass_virtual_3, 因而(*(*v0+20))(v0)就是在调用 subClass_virtual_3()。

4、有重写虚函数、 单继承

现在将 SubClass 的 subClass_ virtual_2()去掉, 重写 baseA_virtual_2(), 代码如下:

class SubClass: public BaseA{… …virtual void baseA_virtual_2(){printf(“\t[-] SubClass::baseA_virtual_2()\n”);}… …}… …int main(int argc, char *argv[]){BaseA *subClass = new SubClass(2, 1);subClass->baseA_virtual_2();delete subClass;return 0;}

用 IDA 分析 main(), F5 查看其伪代码, 如下图所示:

89224241_9

第 6 行调用 SubClass 的构造函数进行对象初始化, sub_758()的伪代码如下图所示:

89224241_10

第 9 行将 vtable 的值赋给 SubClass 对象的前 4 个字节, vtable 的结构如下图所示:

89224241_11

执行完 SubClass 的构造函数后, SubClass 对象的内存如下所示:

89224241_12

从上可以知道, 无重写时 vtable[1] = BaseA::baseA_virtual_2;重写后 vtable[1]= SubClass::baseA_virtual_2。因而, 在 vtable 中, 子类重写的虚函数会覆盖相应的父类中的虚函数地址。

回到 main()伪代码,第 7 行中, *(*v0 + 4)获取 vtable 中第一项的值, 由于v0 是 SubClass 对象的指针,因而*(*v0 + 4)就是SubClass::baseA_virtual_2, 即(*(*v0 + 4))(v0) 调用的是子类 SubClass 中的 baseA_virtual_2()。

分析到这里,相信大家对多态的实现机制应该有了一定的认识。

5、无重写虚函数、 多继承

C++ 支持多继承, 下面分析多继承情况下虚函数的调用机制。 首先分析多继承时,子类没有继承父类虚函数的情况。 考虑有如下继承关系的类:

89224241_13

其中 main()的代码如下:

int main(int argc, char *argv[]){SubClass *subClass = new SubClass(4, 3, 2, 1);subClass->BaseA::virtual_1();subClass->baseB_virtual_3();subClass->subClass_virtual_1();delete subClass;return 0;}

用 IDA 分析 main(), F5 查看其伪代码, 如下图所示:

89224241_14

第 5 行为 SubClass对象申请了 0x1C byte的空间,其中 4个字段占 0x10 byte,那么多出的 0xC byte 存的是什么呢? (其实是 3 个 vtable 的指针)

第 6 行调用 SubClass 的构造函数对 SubClass 对象进行初始化, sub_A1C()的伪代码如下图所示:

89224241_15

从上可知, 由于 SubClass 类有三个父类, 因而 SubClass 对象中有 3 个字段分别存着 3 个指向虚函数列表的地址, subClass 类对应的 vtable 如下图所示:

89224241_16

执行完 SubClass 类的构造函数后, SubClass 对象的内存结构如下图所示:

89224241_17

从上可知, SubClass 对象的内存结构是由 n * (vtable 指针 + 父类字段) + 子类字段(n 是父类的数目)构成的, 父类是按照申明的顺序排列, SubClass 中的虚函数地址存储在第一个父类 vtable 的后面。

回到 main()的伪代码:

第 7 行调用 sub_5F8(),即调用 subClass -> BaseA:: virtual_1(), 虽然是调用虚函数, 由于采用了作用域,编译器在编译阶段就明确知道要调用的函数,因而这里并没有通过 vtable 来进行调用。

第 8 行中, v0[2]是 SubClass 对象中 BaseB 类的 vtable 指针, *(v0[2]+8)得到虚函数 BaseB::baseB_virtual_3 的地址,因而(*(v0[2] + 8))(v0 + 2);对应源码中的subClass->baseB_virtual_3();。

注意这里第 0 个参数是 v0+2, 而不是 v0, 说明调用父类的虚函数时,第 0 个参数是 SubClass 对象中该父类所占内存空间的基址(其实调用父类其他函数也是如此)。

通过对第 8 行的分析,第 9 行(*(*v0 + 12))(v0);就很好理解了,即调用subClass->subClass_virtual_1();

6、有重写虚函数、 多继承

接下来分析多继承时,有重写虚函数的情况。 考虑有如下继承关系的类:

89224241_18

在 main() 中将 SubClass 对象指针转为不同的父类指针进行虚函数调用,main()的代码如下:

int main(int argc, char *argv[]){SubClass *subClass = new SubClass(4,3,2,1);((BaseA *)subClass)->virtual_1();((BaseB *)subClass)->virtual_2();((BaseC *)subClass)->virtual_1();((BaseC *)subClass)->baseC_virtual_2();delete subClass;return 0;}

用 IDA 分析 main(), F5 查看其伪代码, 如下图所示:

89224241_19

第 6 行调用 SubClass 的构造函数对 SubClass 对象进行初始化, 其构造函数 sub_758() 的伪代码如下图所示:

89224241_20

从上可知, 编译器对 SubClass 类的构造函数进行了优化, 将对父类构造函数的调用优化成了 inline 的形式。 第 16-18 行是对 vtable 指针进行初始化, vtable的结构如下图所示:

89224241_21

执行完 SubClass 类的构造函数后, SubClass 对象的内存结构如下图所示:

89224241_22

下面对 SubClass 对象内存布局进行详细分析(可以与多继承无重写虚函数时的内存结构进行对比):

3 个父类都定义了虚函数 virtual_1,并且 SubClass 重写了 virtual_1, 因而 3个父类对应 vtable 中 virtual_1 的值都改为了 SubClass::virtual_1。这样, 当SubClass 对象指针转为任意父类的指针调用 virtual_1 时,调用的都是SubClass::virtual_1;

SubClass 重写了 BaseA 和 BaseB 中的虚函数 virtual_2, 因而 BaseA 和 BaseB 对应 vtable 中 virtual_2 的值都改为了 SubClass::virtual_2。

SubClass 还重写了 BaseC 中的虚函数 baseC_virtual_2,因而 BaseC 对应 vtable 中 baseC_virtual_2 的值改为了SubClass::baseC_virtual_2。这样当 SubClass 对象指针转为 BaseC 类型的指针调用 baseC_virtual_2 时, 调用的就是 SubClass::baseC_vritual_2。 注意, 由于重写的虚函数 baseC_virtual_2 不是第一个父类 BaseA 的虚函数, 所以在 SubClass 类的虚函数列表(位于 BaseA 虚函数列表的后面)中, 还需要按照申明顺序添加SubClass::baseC_virtual_2, 这样, SubClass 对象的指针就可以调用SubClass::baseC_virtual_2 了。

接着分析 main()的伪代码:

第 7 行, **v0 就是 SubClass::virtual_1; (**v0)(v0);对应源码中的((BaseA*)subClass)->virtual_1();。

第 8 行, *(*(v0 + 8) + 4)得到 BaseB 对应 vtable 中的 SubClass::virtual_2 ,(*(*(v0 + 8) + 4))(v0 + 8);就是((BaseB *)subClass)->virtual_2();。

第 9 行, **(v0 + 16)得到 BaseC 对应 vtable 中的 SubClass::virtual_1, (**(v0+ 16))(v0 + 16);就是((BaseC *)subClass)->virtual_1();。

第 10 行 , *(*(v0 + 16) + 4) 得 到 BaseC 对 应 vtable 中 的 SubClass::baseC_virtual_2 , (*(*(v0 + 16) + 4))(v0 + 16); 就 是 ((BaseC*)subClass)->baseC_virtual_2();

至此, 分析完了 C++ 虚函数实现的基本原理。 对于多重继承的情况,和前面的类似。比如,还有一个类 SubSubClass 继承 SubClass,并重写了虚函数 virtual_1,那 么 将 SubClass 的 vtable 中 所 有 的 SubClass::virtual_1 替 换 为 SubSubClass::virtual_1,得到的结果就是 SubSubClass 的 vtable。

7、总结

本文从 Arm 汇编的角度分析了 Android 中 C++ 虚函数的实现机制。编译时,编译器会为每一个申明有虚函数的类生成一个 vtable, 当对象初始化时,会将 vtable 的地址赋给对象,这样虚函数就可以根据其在 vtable 中的偏移来进行调用。

其中,一个对象所占的内存空间不仅与类的字段有关系,还与类的继承关系有关,对于单继承,一个对象会有一个 vtable 的指针; 如果继承关系中存在多继承, 那么该对象会为每一个多继承的父类保存一个 vtable 的指针。

8、参考

http://blog.csdn.net/haoel/article/details/1948051/

89224241_23

89224241_25

点击阅读原文,一起玩耍

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值