C++对象在内存中的布局(读汇编代码)

来源:互联网 发布:大数据技术与应用就业 编辑:程序博客网 时间:2024/06/09 03:19


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)_ZN11ClassLayoutC2Evvar_C           = -0xCvar_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)_Z10TestInvokevvar_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)_ZN11GrandFatherC2Evvar_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)_ZN5Base1C2Evvar_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         = -0x14var_10         = -0x10var_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也为NULLloc_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的过程如下图:


0 0
原创粉丝点击