1.研究方法以及ARM调用规范
最近在对C++编写的SO库进行逆向,如果掌握了对象的布局,那么逆向也能轻松些,所以萌发了研究对象布局的想法。
本文采用的研究方法是:编写C++代码,用gcc编译。通过IDA查看编译后的代码,分析ARM汇编代码,总结出内存布局。由于能力有限,本文研究的情形还是比较简单的。如果想深入研究的话,要读一读《深入C++对象模型》了。
在进行具体分析之前,先大致总结下ARM中的调用规范:
1、函数的参数分别放到R0~R3中,如果这四个寄存器无法容纳所有的参数,则剩余的参数放到堆栈中。参数放置时还要注意另外两个问题:
①参数对齐问题。如果第一个参数为int型(32位,ARM中按4字节对齐),第二个参数为long long型(64位,ARM中按8字节对齐),则参数放置结果为:第一参数放到R0,第二个参数放到R2~R3中,R1被空下了。
②精度提升问题。在可变参数中,char类型被提升为int,float类型被提升为double,在ARM中double要按8字节对齐。
2、函数的返回值放到R0中(32位的返回值),或者R0~R1中(64的返回值)。有一个例外是软浮点库中的__aeabi_ldivmod函数,该函数对两个长整形(longlong)进行除法以及求余操作,两数相除的商放到R0~R1中,而两数相除的余数即模放到R2~R3中。类似的还有__aeabi_idivmod函数,该函数对两个整数进行操作。但这与ARM的调用规范无关,只要软浮点库的调用者与实现者约定好就可以了。
3、子函数通过BL或BLX指令调用,BL或BLX会将函数的返回地址放置到LR寄存器中,函数运行结束时通过LR返回。
2.成员函数与静态成员函数
2.1 C++代码如下:
class ClassLayout { public: //构造函数 ClassLayout() { printf("Constructor!\n"); mIValue = 0; mFValue = 1.0; } //非虚成员函数 int PrintMember() { printf("%d -%f\n", mIValue, mFValue); return 0; } //静态成员函数 static voidSetAndPrintStaticValue(int value) { //静态成员函数中只能引用静态成员变量 mIStaticValue = value; printf("%d\n", mIStaticValue); } public: //内置类型成员变量 int mIValue; float mFValue; //静态成员变量 static intmIStaticValue; }; //静态成员变量初始化 int ClassLayout::mIStaticValue = 0; void TestInvoke() { ClassLayout *layout = newClassLayout(); layout->PrintMember(); //通过类名调用静态成员变量 ClassLayout::SetAndPrintStaticValue(0); delete layout; }
对C++代码的说明如下:
1、静态成员变量
在类中,静态成员可以实现多个对象之间的数据共享,因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。同样,静态成员变量不能在类的构造函数中初始化,要在类的实现体外初始化。
2、静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。可以直接通过类名调用,而不依赖类对象。在静态成员函数的实现中不能直接引用类中声明的非静态成员,可以引用类中声明的静态成员
2.2 编译后的代码如下(省略掉成员函数的汇编代码):
; 构造函数 ; ClassLayout::ClassLayout(void) _ZN11ClassLayoutC2Ev var_C = -0xC var_4 = -4 STR LR, [SP,#var_4]! SUB SP, SP, #0xC ;R0即构造函数的入参,指向本对象的起始地址,这里将R0暂存到栈中 STR R0, [SP,#0x10+var_C] ;printf("Constructor!\n") LDR R3, =(aConstructor - 0x1040) ADD R3, PC, R3 ; "Constructor!" MOV R0, R3 ; s BL puts ;mIValue = 0 LDR R3, [SP,#0x10+var_C] MOV R2, #0 STR R2, [R3] ;mFValue = 1.0 LDR R3, [SP,#0x10+var_C] LDR R2, =0x3F800000 STR R2, [R3,#4] LDR R3, [SP,#0x10+var_C] MOV R0, R3 ;构造函数的返回值就是构造好的对象的基址 ADD SP, SP, #0xC LDMFD SP!, {PC} ; End of function ClassLayout::ClassLayout(void) ; =============== S U B R O U T I N E======================================= ;成员函数 ;ClassLayout::PrintMember(void) _ZN11ClassLayout11PrintMemberEv ;这里省略 ; =============== S U B R O U T I N E======================================= ;成员函数 ; ClassLayout::SetAndPrintStaticValue(int) _ZN11ClassLayout22SetAndPrintStaticValueEi ;这里省略 ; =============== S U B R O U T I N E ======================================= ; 全局函数 ; TestInvoke(void) _Z10TestInvokev var_C = -0xC STMFD SP!, {R4,LR} SUB SP, SP, #8 ;ClassLayout*layout = new ClassLayout() MOV R0, #8 BL _Znwj ; 调用new先为对象申请内存空间,再在该内存上调用构造函数 MOV R4, R0 MOV R0, R4 BL _ZN11ClassLayoutC2Ev ;ClassLayout::ClassLayout(void) STR R4, [SP,#0x10+var_C] ;layout->PrintMember() LDR R0, [SP,#0x10+var_C] BL _ZN11ClassLayout11PrintMemberEv ;ClassLayout::PrintMember(void) ;ClassLayout::SetAndPrintStaticValue(0) MOV R0, #0 BL _ZN11ClassLayout22SetAndPrintStaticValueEi ; ;delete layout LDR R0, [SP,#0x10+var_C] ; BL _ZdlPv ; operator delete(void *) ADD SP, SP, #8 LDMFD SP!, {R4,PC} ; End of function TestInvoke(void)
对汇编代码的说明如下
1、符号修饰机制
众所周知,C++拥有封装、继承、多态三大特性,此外还支持命名空间与函数重载。最简单的重载例子如下,两个同名函数,参数列表不同即构成重载。
void func(int)
void func (float)
编译器如何区分这两个函数呢?这就引入了符号修饰机制。
不同的编译器,符号修饰机制不同,GCC的符号修饰机制如下:
①所有的符号都以“_Z”开头
②对于嵌套的名字(在命名空间或者在类里面),后面紧跟“N”,然后是各个命名空间或类的名字,每个名字前有一个数字,表示名字的长度。嵌套的名字以“E”结尾。如果是一个函数,则参数列表紧跟在“E”之后。
根据上面的原则,类的3个成员函数编译后的名称如下:
函数签名 | 修饰后的名称 |
ClassLayout:: ClassLayout() | _ZN11ClassLayoutC2Ev (C表示构造函数) |
int ClassLayout::PrintMember() | _ZN11ClassLayout11PrintMemberEv |
void ClassLayout:: SetAndPrintStaticValue(int) | ZN11ClassLayout22SetAndPrintStaticValueEi |
正因为符号修饰机制,在C++代码中引用C语言编写的库时,会遇到名字解析的问题。这时需要用到extern“C”关键字。详情不再赘述
2、类的大小
分析TestInvoke(void)的汇编代码,在new ClassLayout的对象时,编译器只给我们预留了8个字节的空间,这是为成员变量mIValue和mFValue预留的。在构造函数中,我们也能看到,在对象的内存布局中,成员变量的存储顺序与声明顺序一致。内存布局如下:
对象的内存布局中,只有两个成员变量,静态成员变量以及成员方法都不在对象的内存布局中。
3、构造函数
new一个对象时,底层的操作是先分配内存,再在内存上执行构造函数。本例中的构造函数虽然C++代码中没有参数,但汇编代码中却有一个入参,该入参就是通过new分配的内存块的起始地址。虽然构造函数没有指明返回值,但汇编代码中构造函数是有返回值的,返回值就是对象的基地址。
4、成员函数中的this指针
分析TestInvoke(void)的汇编代码,在调用成员函数ClassLayout::PrintMember(void)之前,将对象的地址放到R0寄存器中,此即this指针。可见成员函数中都隐含有this指针,指向对象自己。
5、静态成员函数中没有this指针
分析TestInvoke(void)的汇编代码,在调用静态成员函数ClassLayout::SetAndPrintStaticValue(void)时,R0存放的是该函数的第一参数(整形数0),并没有传入this指针。
3.单一继承与虚函数表
3.1 C++代码如下:
class GrandFather { public: GrandFather():mIGrandFather(10){} virtual void f() { printf("GrandFather : f\n"); } virtual void g() { printf("GrandFather :g\n"); } virtual void h() { printf("GrandFather :h\n"); } int mIGrandFather; }; class Father : public GrandFather { public: Father():mIFather(100){} //改写GrandFather的f方法 virtual void f() { printf("Father : f\n"); } //新增虚方法 virtual void j() { printf("Father : j\n"); } virtual void k() { printf("Father : k\n"); } int mIFather; }; class Child : public Father { public: Child():mIChild(1000){} //改写Father的f方法 virtual void f() { printf("Child : f\n"); } //改写Father的j方法 virtual void j() { printf("Child : j\n"); } //新增虚方法 virtual void m() { printf("Child : m\n"); } intmIChild; }; void TestInvoke() { Child *child = new Child(); //遍历虚方法 child->f(); child->g(); child->h(); child->j(); child->k(); child->m(); //打印成员变量 printf("GrandFather:mIGrandFather-%d\n",child->mIGrandFather); printf("Father:mIFather-%d\n",child->mIFather); printf("Child:mIChild-%d\n",child->mIChild); delete child; }
C++正是通过虚拟函数与继承实现多态的(多个子类继承自同一个父类,并且各个子类改写继承自父类的虚拟成员函数,这样当用父类指针指向子类对象,通过父类指针调用父类中声明的虚拟成员函数,不同的子类,表现出不同的状态,此即多态)。继承关系如下:
在GrandFather类中定义了三个虚方法;在Father类中,改写了(override)继承自GrandFather的虚方f,并新增了两个虚方法k和j。在Child类中,改写了继承自Father的虚方法f和k,并新增了一个虚拟方法m。这三个类分别有一个数据成员,分别初始化为(构造函数中的成员初始化列表)10、100和1000。
在VC++中,运行结果如下:
3.2 ARM的汇编代码如下:
先来看构造函数
;GrandFather构造函数 ;GrandFather::GrandFather(void) _ZN11GrandFatherC2Ev var_4 = -4 SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#8+var_4] LDR R3, =(_GLOBAL_OFFSET_TABLE_ - 0x105E) ADD R3, PC LDR R2, [SP,#8+var_4] LDR R1, =(_ZTV11GrandFather_ptr - 0x8FA0) LDR R3, [R3,R1] ;R3中存放的是GrandFather的虚函数表 ADDS R3, #8 STR R3, [R2] ;GrandFather的虚函数表偏移8字节放入对象内存布局的起始位置 LDR R3, [SP,#8+var_4] MOVS R2, #0xA STR R2, [R3,#4] ;[起始位置+0x4]存入mIGrandFather LDR R3, [SP,#8+var_4] MOVS R0, R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 BX LR ;Father构造函数 ;Father::Father(void) _ZN6FatherC2Ev var_C = -0xC PUSH {R4,LR} SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#0x10+var_C] LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x10D0) ADD R4, PC LDR R3, [SP,#0x10+var_C] MOVS R0, R3 ;在对象的地址上调用父类GrandFather的构造函数GrandFather::GrandFather(void) BL _ZN11GrandFatherC2Ev LDR R3, [SP,#0x10+var_C] LDR R2, =(_ZTV6Father_ptr - 0x8FA0) LDR R2, [R4,R2] ;获取Father的虚函数表 ADDS R2, #8 STR R2, [R3] ;Father的虚函数表偏移8个字节放入对象内存布局的起始地址处 LDR R3, [SP,#0x10+var_C] MOVS R2, #0x64 STR R2, [R3,#8] ;[起始地址+0x8]存放mIFather LDR R3, [SP,#0x10+var_C] MOVS R0, R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 POP {R4,PC} ;Child构造函数 ;Child::Child(void) _ZN5ChildC2Ev var_C = -0xC PUSH {R4,LR} SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#0x10+var_C] LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x114C) ADD R4, PC LDR R3, [SP,#0x10+var_C] MOVS R0, R3 BL _ZN6FatherC2Ev ; 在当前对象的内存布局上调用父类构造函数Father::Father(void) LDR R3, [SP,#0x10+var_C] LDR R2, =(_ZTV5Child_ptr - 0x8FA0) LDR R2, [R4,R2] ;获取Child的虚函数表 ADDS R2, #8 STR R2, [R3] ;Child虚函数表偏移8个字节放到对象的起始地址处 LDR R3, [SP,#0x10+var_C] MOVS R2, 0x3E8 STR R2, [R3,#0xC] ;[起始地址 + 0xC]存放mIChild LDR R3, [SP,#0x10+var_C] MOVS R0, R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 POP {R4,PC}
三个类的虚函数表如下:
;Child类的虚表 .data.rel.ro:00008E48 _ZTV5Child DCD 0, 0, _ZN5Child1fEv+1, _ZN11GrandFather1gEv+1, _ZN11GrandFather1hEv+1 _ZN5Child1jEv+1, _ZN6Father1kEv+1, _ZN5Child1mEv+1 ;Father类的虚表 .data.rel.ro:00008E68 _ZTV6Father DCD 0, 0, _ZN6Father1fEv+1, _ZN11GrandFather1gEv+1, _ZN11GrandFather1hEv+1 _ZN6Father1jEv+1, _ZN6Father1kEv+1 ;GrandFather类的虚表 .data.rel.ro:00008E88 _ZTV11GrandFather DCD 0, 0, _ZN11GrandFather1fEv+1, _ZN11GrandFather1gEv+1 _ZN11GrandFather1hEv+1
三个类的虚函数表说明如下:
1、虚表可以理解为函数地址(32位)的一维数组,前两个元素为0,用来分割两个相邻的虚表。在构造函数中,在为对象设置虚表时,要跳过前两个元素。
2、虚表中存放的是函数的地址,函数名即函数地址,这里在函数地址上+1,是因为thumb指令约定地址的最低位为1。
3、虚表中函数的顺序按声明的顺序排列。
4、在继承关系中,被override的虚函数,在虚函数表中会被更新,比如在Father的虚表中,f函数就被更新为Father类中的f方法。
5、类的虚函数表可以这样理解:首先从父类继承该表;然后被子类override过的虚函数,对应虚表中的地址会被更新;再有子类新加的虚函数会追加到虚表结尾。
构造函数说明如下:
1、子类负责父类的创建,在子类的构造函数中会先调用父类的构造函数“构造父类子对象”。
2、如果类定义了虚方法,则有虚方法表VTable,VTable的地址存入对象内存布局的首地址。成员变量依据继承与声明的顺序,放到后面。
3、对象的虚表被设置了三次,分别在GrandFather、Father以及Child的构造函数中设置,最终以Child的虚表为准。
通过上面的分析,我们不难得到Child对象的内存布局:
对象本身的sizeof为16个字节,包含一个指针(指向虚函数表)和三个数据成员
下面来看看虚函数的调用:
; TestInvoke(void) _Z10TestInvokev var_C = -0xC PUSH {R4,LR} SUB SP, SP, #8 ;Child *child = new Child() MOVS R0, #0x10 ;Child的sizeof为16个字节 BLX _Znwj ;operator new(uint) MOVS R4, R0 MOVS R0, R4 BL _ZN5ChildC2Ev ;Child::Child(void) STR R4, [SP,#0x10+var_C] ;child->f() LDR R3, [SP,#0x10+var_C] LDR R3, [R3] ;取虚表地址 LDR R3, [R3] ;取虚表的第一个元素,即虚函数f的地址 LDR R2, [SP,#0x10+var_C] MOVS R0, R2 ;this指针作为f的入参 BLX R3 ;调用f方法 ;child->g() LDR R3, [SP,#0x10+var_C] LDR R3, [R3] ;取虚表的地址 ADDS R3, #4 ;虚表偏移4个字节, LDR R3, [R3] ;取虚表的第二个元素,即虚函数g的地址 LDR R2, [SP,#0x10+var_C] MOVS R0, R2 ;准备thid指针作为g的入参 BLX R3 ;调用g方法 ;后面代码省略
说明:
调用虚方法时,先根据对象的内存布局找到虚函数表,再找到虚表中对应的函数地址,直接调用该函数(地址)即可。
4.多重继承与类型转换
4.1 C++代码如下:
class Base1 { public: Base1():mIBase1(10){} virtual void f() { printf("Base1 : f\n"); } virtual void g() { printf("Base1 :g\n"); } int mIBase1; }; class Base2 { public: Base2():mIBase2(100){} virtual void h() { printf("Base2 : h\n"); } virtual void j() { printf("Base2 :j\n"); } int mIBase2; }; class Derived : public Base1, public Base2 { public: Derived():mIDerived(100){} //改写虚方法 virtual void f() { printf("Derived : f\n"); } virtual void h() { printf("Derived : h\n"); } //新增虚方法 virtual void k() { printf("Derived :k\n"); } int mIDerived; }; void TestInvoke() { Derived *pd = new Derived(); //遍历虚方法 printf("===Callvirtual functions via Derived===\n"); pd->f(); pd->g(); pd->h(); pd->j(); pd->k(); //以父类Base1调用相关的虚函数 Base1 *pb1= (Base1*)pd; printf("\n===Callvirtual functions via Base1===\n"); pb1->f(); pb1->g(); //以父类Base2调用相关的虚函数 Base2 *pb2= (Base2*)pd; printf("\n===Callvirtual functions via Base2===\n"); pb2->h(); pb2->j(); deletepd; }
对C++代码的说明:
这次定义了两个基类,Base1和Base2,每个基类各定义了两个虚方法。派生类继承自Base1和Base2。继承关系如下:
运行结果如下:
我们可以以基类的指针指向子类对象,不同的基类,可以调用的虚方法只限于本类可见的方法(本类定义的方法以及本类继承得到的方法),底层是如何实现的呢?
4.2 ARM的汇编代码
先看构造函数:
;Base1构造函数 ;Base1::Base1(void) _ZN5Base1C2Ev var_4 = -4 SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#8+var_4] LDR R3, =(_GLOBAL_OFFSET_TABLE_ - 0xFCE) ADD R3, PC LDR R2, [SP,#8+var_4] LDR R1, =(_ZTV5Base1_ptr - 0x8FA0) LDR R3, [R3,R1] ;Base1的虚函数表 ADDS R3, #8 STR R3, [R2] ;Base1的虚函数表偏移8字节存放到对象的起始地址处 LDR R3, [SP,#8+var_4] MOVS R2, #0xA STR R2, [R3,#4] ;[起始地址+0x4]存入mIBase1,初始值为10 LDR R3, [SP,#8+var_4] MOVS R0,R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 BX LR ;Base2构造函数 ;Base2::Base2(void) _ZN5Base2C2Ev var_4 = -4 SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#8+var_4] LDR R3, =(_GLOBAL_OFFSET_TABLE_ - 0x1026) ADD R3, PC LDR R2, [SP,#8+var_4] LDR R1, =(_ZTV5Base2_ptr - 0x8FA0) LDR R3, [R3,R1] ;获取Base2的虚函数表 ADDS R3, #8 STR R3, [R2] ;Base2的虚函数表存入对象的基地址 LDR R3, [SP,#8+var_4] MOVS R2, #0x64 STR R2, [R3,#4] ;[起始地址+0x4]存入mIBase2,初始值为100 LDR R3, [SP,#8+var_4] MOVS R0, R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 BX LR ;Derived构造函数 ;Derived::Derived(void) _ZN7DerivedC2Ev var_C = -0xC PUSH {R4,LR} SUB SP, SP, #8 ;R0是构造函数入参,指向对象的基址,这里将R0暂存到栈中 STR R0, [SP,#0x10+var_C] LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x1080) ADD R4, PC LDR R3, [SP,#0x10+var_C] MOVS R0, R3 ;以对象的基址调用Base1的构造函数 BL _ZN5Base1C2Ev ;Base1::Base1(void) LDR R3, [SP,#0x10+var_C] ADDS R3, #8 MOVS R0, R3 ;以对象的基址偏移8个字节,再调用Base2的构造函数 BL _ZN5Base2C2Ev ;Base2::Base2(void) LDR R3, [SP,#0x10+var_C] LDR R2, =(_ZTV7Derived_ptr - 0x8FA0) LDR R2, [R4,R2] ;Derived的虚函数表 ADDS R2, #8 ;虚函数表偏移8个字节 ;Derived虚表偏移8个字节然后存入对象的基址(会覆盖Base1构造函数存入的Base1虚函数表) STR R2, [R3] LDR R3, [SP,#0x10+var_C] LDR R2, =(_ZTV7Derived_ptr - 0x8FA0) LDR R2, [R4,R2] ;Derived的虚函数表 ADDS R2, #0x20 ;虚函数表偏移0x20个字节, ;虚表偏移0x20个字节然后存入[对象基址+8](会覆盖掉Base2构造函数存入的 Base2虚函数表) STR R2, [R3,#8] LDR R3, [SP,#0x10+var_C] MOVS R2, #0x64 STR R2, [R3,#0x10] ;[对象基址+0x10]存入mIDerived,初始值为100 LDR R3, [SP,#0x10+var_C] MOVS R0, R3 ;构造函数的返回值就是对象的基址 ADD SP, SP, #8 POP {R4,PC} ;虚函数表如下: ;Derived的虚表 .data.rel.ro:00008E58 _ZTV7Derived DCD 0, 0, _ZN7Derived1fEv+1, _ZN5Base11gEv+1, _ZN7Derived1hEv+1 _ZN7Derived1kEv+1, 0xFFFFFFF8, 0, _ZThn8_N7Derived1hEv ;`non-virtual thunk to'Derived::h(void) _ZN5Base21jEv+1 ;Base2的虚表 .data.rel.ro:00008E80 _ZTV5Base2 DCD0, 0, _ZN5Base21hEv+1, _ZN5Base21jEv+1 ;Base1的虚表 .data.rel.ro:00008E90 _ZTV5Base1 DCD 0, 0, _ZN5Base11fEv+1, _ZN5Base11gEv+1
虚函数表说明如下:
Derived的虚表被分割成两部分(表中的0xFFFFFFF8、0这两个标志进行分割),前一部分中的虚函数包括:
① Derived继承自Base1的虚函数f;
② Derived改写的Base1中的虚函数g;
③ Derived改写的Base2中的函数h(注意这里)
④ Derived新增的虚函数k。
后一部分包括:
① Derived改写的Base2中的函数h的“非虚版本”(参考_ZThn8_N7Derived1hEv后面的注释,注意与上面的③是两个函数哦)。为什么要有这个“非虚”版本的函数呢?这与多重继承情况下,将子类指针转换成父类指针的实现有关。类型转换的问题后面还会讲到。我们先来看看这个“非虚”函数的实现。
;'non-virtual thunk to'Derived::h(void) _ZThn8_N7Derived1hEv LDR R12, =(_ZN7Derived1hEv+1 - 0x10E0) ADD R12, PC, R12 ;R0是this指针即对象的基址,这里是Derived中“Base2的子对象”的基址, ;将这个地址减8个字节,即减去Base1的size,这样,R0就指向了Derived的基址了。 SUB R0, R0, #8 BX R12 ;最终还是调用子类中对应的实现Derived::h(void)
是不是有点开始理解这个非虚函数了?因为这个函数改写自Base2,那么我们可以通过地址强转将Dervied指针强转成Base2的指针,然后通过Base2类型的指针调用该方法。在地址转换时,会将Derived对象的地址加Base1的size,这样就定位到了Base2子对象的地址。那么通过Base2的指针调用Derived中的函数时,当然要将this指针定位到Derived对象的基址啦,所以要在this指针上减去Base1的size。
② Derived继承自Base2中函数j。
我们不难得到Derived的内存布局:
Derived的sizeof为20,其中有两个虚函数表的指针。一个指针指向继承自Base1的虚函数、Dervied改写的虚函数和Derived新增的虚函数;一个指针指向继承自Base2的虚函数以及Derive改写的Base2中虚函数的“非虚”版本函数。
下面通过TestInvoke函数的代码,分析下类型转换过程。
; TestInvoke(void) _Z10TestInvokev var_14 = -0x14 var_10 = -0x10 var_C = -0xC PUSH {R4,LR} SUB SP, SP, #0x10 ;Derived*pd = new Derived() MOVS R0, #0x14 BLX _Znwj ;申请内存,可见Derived类的sizeof为20 MOVS R4, R0 MOVS R0, R4 BL _ZN7DerivedC2Ev ;调用Derived的构造函数Derived::Derived(void) STR R4, [SP,#0x18+var_14] ;printf("===Call virtual functions via Derived===\n") LDR R3, =(aCallVirtualFun - 0x1132) ADD R3, PC ; "===Call virtual functions viaDerived=="... MOVS R0, R3 ; s BLX puts ;pd->f() LDR R3, [SP,#0x18+var_14] ;对象的地址 LDR R3, [R3] ;虚函数表基址 LDR R3, [R3] ;虚表中的第一个函数,即f LDR R2, [SP,#0x18+var_14] MOVS R0, R2 BLX R3 ;调用f ;pd->g() LDR R3, [SP,#0x18+var_14] LDR R3, [R3] ADDS R3, #4 ;虚函数表偏移4字节,虚表中的第二个函数g LDR R3, [R3] LDR R2, [SP,#0x18+var_14] MOVS R0, R2 BLX R3 ;pd->h() LDR R3, [SP,#0x18+var_14] LDR R3, [R3] ADDS R3, #8 ;虚函数表偏移8字节,虚表中的第三个函数h LDR R3, [R3] LDR R2, [SP,#0x18+var_14] MOVS R0, R2 BLX R3 ;pd->j() LDR R3, [SP,#0x18+var_14] ;对象的基址偏移8字节,跳过Base1的size。此处存储的是第二个虚表的指针 LDR R3, [R3,#8] ADDS R3, #4 ;第二个虚表偏移4字节,此处存的是继承自Base2的j函数 LDR R3, [R3] LDR R2, [SP,#0x18+var_14] ADDS R2,#8 MOVS R0, R2 BLX R3 ;pd->k() LDR R3, [SP,#0x18+var_14] LDR R3, [R3] ADDS R3, #0xC LDR R3, [R3] LDR R2, [SP,#0x18+var_14] MOVS R0, R2 BLX R3 ;Base1 *pb1 = (Base1*)pd LDR R3, [SP,#0x18+var_14] ;局部变量pb1存放在堆栈中,其值就是对象的地址,可见这次地址转换,地址并没有改变。 STR R3, [SP,#0x18+var_10] ;printf("\n===Call virtual functions via Base1===\n") LDR R3, =(aCallVirtualF_0 - 0x1186) ADD R3, PC ; "\n===Call virtual functionsvia Base1==="... MOVS R0, R3 ; s BLX puts ;pb1->f() 以pb1为this指针进行虚函数调用 LDR R3, [SP,#0x18+var_10] LDR R3, [R3] LDR R3, [R3] LDR R2, [SP,#0x18+var_10] MOVS R0, R2 BLX R3 ;pb1->f() LDR R3, [SP,#0x18+var_10] LDR R3, [R3] ADDS R3, #4 LDR R3, [R3] LDR R2, [SP,#0x18+var_10] MOVS R0, R2 BLX R3 ;Base2 *pb2 = (Base2*)pd ;这次转换,编译器显得小心翼翼。先判断指针pd是否为NULL,为何要加这个判断, ;因为这次转换要将地址后移,跳过Base1的成分,所以要保证pd是正确的。 LDR R3, [SP,#0x18+var_14] CMP R3, #0 BEQ loc_11B0 LDR R3, [SP,#0x18+var_14] ;对象的基址偏移8字节,跳过Base1的size,即跳过Derived中"父类Base1的子对象" ADDS R3, #8 B loc_11B2 ; --------------------------------------------------------------------------- loc_11B0 MOVS R3, #0 ;如果pd为NULL,则pb2也为NULL loc_11B2 ;局部变量pb2存放在堆栈中,它代表这Derived中"Base2成分"的起始地址 STR R3, [SP,#0x18+var_C] ;printf("\n===Call virtual functions via Base2===\n") LDR R3, =(aCallVirtualF_1 - 0x11BA) ADD R3, PC ; "\n===Call virtual functionsvia Base2==="... MOVS R0, R3 ; s BLX puts ;pb2->h() LDR R3, [SP,#0x18+var_C] LDR R3, [R3] LDR R3,[R3] LDR R2, [SP,#0x18+var_C] MOVS R0, R2 BLX R3 ;调用h的"的非虚版本" ;pb2->j() LDR R3, [SP,#0x18+var_C] LDR R3, [R3] ADDS R3, #4 LDR R3, [R3] LDR R2, [SP,#0x18+var_C] MOVS R0, R2 BLX R3 LDR R3, [SP,#0x18+var_14] MOVS R0, R3 ; void * BLX _ZdlPv ; operator delete(void *) ADD SP, SP, #0x10 POP {R4,PC}
分析:在执行“Base2*pb2 = (Base2*)pd”时,将pd 加上Base1的size,这样就定位到了Derived中Base2的子对象,经过这个转换后,pb2的值与pd的值已经不相等了。地址强转过程以及通过pb2调用h的过程如下图: