c++内存布局

在分析代码执行类漏洞时,需要搞定程序的控制流。我们知道,在C语言中,一般是通过覆盖函数返回地址,来劫持控制流实现跳转的。在C++中,除此之外,多个一个虚函数表的东西,通过覆盖对象的虚函数表也是可以劫持控制流的。作为基础积累,本篇简要概括下C++的内存布局,主要是自己学习总结,部分内容来自网上。
注:所有实验在Android平台实现。

一、非继承情形

1. 仅有非static成员和方法

定义一个Person类

class Person  
{  
    public:  
        Person():mId(0), mAge(20){
            for(int i=0;i<10;i++){
                grade[i]='A'+i;
            }
        }
        void print1()
        {  
            cout << "id: " << mId  
                 << ", age: " << mAge << endl;  
        }


      void print2()
        {
            cout << "i am print2  "<< endl;
        }

    public :
        int mId;  
        int mAge;
        char grade[10];

};

测试代码:

typedef void (Person::*mPrint)();
int main(){
    Person  p;
    int* ptr= (int*) &p;

    cout << "address of Person obj:"<cout << "size of Person obj:"<< sizeof(p)<cout<< "address of mTd:"<< &p.mId<<",value = "<< p.mId<cout<< "address of mAge:"<< &p.mAge<<",value = "<< p.mAge<cout<< "address of grade:"<< &p.grade<<",value = "<< p.grade[0]<printf("address of print1: %p\n",f1);
    //cout<< "address of function print1:"<< f1 <
    (p.*f1)();

    mPrint f2= &Person::print2;
    //cout<< "address of function print2:"<< f2 <
    printf("address of print2: %p\n",f2);
    (p.*f2)();
} 

运行结果:
c++内存布局_第1张图片
据此可以看出,非静态类成员是保存在对象结构中的,而非静态成员函数在成员结构之外。此时,对象的内存模型为:
c++内存布局_第2张图片

2. 包含static函数和成员

static成员变量和函数不是和对象绑定的,所以不占对象的内存空间。
采用类似的测试:
c++内存布局_第3张图片

c++内存布局_第4张图片

3. 含有虚函数

有虚函数时,对象首先存储虚函数表,即对象的首地址就是虚函数表地址。
Person类:

class Person  
{  
    public:  
        Person():mId(0), mAge(20){
            for(int i=0;i<10;i++){
                grade[i]='A'+i;
            }
        }
        void print1()
        {  
            cout << "id: " << mId  
                 << ", age: " << mAge << endl;  
        }


      void print2()
        {
            cout << "i am print2  "<< endl;
        }

       static void getName(){
            printf("my name is venscor!\n");
       }

      virtual void job1()
      {
          cout << "Person 1 !!!!" << endl;
      }

      virtual void job2()
    {
        cout << "Person 2 ..." << endl;
    }

   virtual void job3()
    {
        cout << "Person 3 &&&&" << endl;
    }

  virtual ~Person()
  {
      cout << "~~~~~Person" << endl;
  }

    public :
        char grade[10];
        int mId;  
        int mAge;

};

测试代码:

typedef void (Person::*mPrint)();
int main(){
    Person  p;
    int* ptr= (int*) &p;

    cout << "address of Person obj:"<"size of Person obj:"<< sizeof(p)<"address of mTd:"<< &p.mId<<",value = "<< p.mId<"address of mAge:"<< &p.mAge<<",value = "<< p.mAge<"address of grade:"<< &p.grade<<",value = "<< p.grade[0]<printf("address of print1: %p\n",f1);
    //cout<< "address of function print1:"<< f1 <*f1)();

    mPrint f2= &Person::print2;
    //cout<< "address of function print2:"<< f2 <printf("address of print2: %p\n",f2);
    (p.*f2)();

    printf("----------test static---------\n");


    typedef void (*GetName)();
    GetName ff= &Person::getName;
        //cout<< "address of function print2:"<< f2 <printf("address of print2: %p\n",ff);
        ff();

    printf("------------------------------------------------\n");

    int ** vtabptr=(int **)*(int**)&p;
    typedef void (*FuncPtr)();
    for (int i = 0; i < 3 && *vtabptr != NULL; ++i)
    {
        FuncPtr func = (FuncPtr)*vtabptr;
        printf("%p\n",func);
        func();
        ++vtabptr;
    }
     //虚函数表里面其他内容
     while (*vtabptr)
        {
            cout << "*vtbl == " << *vtabptr << endl;
            ++vtabptr;
        }

} 

运行结果:
c++内存布局_第5张图片
增加了虚函数后,对象所占空间大小增加了4,并且成员变量地址也从对象地址+4开始,可以看出,对象地址的前四个字节是用来存放一个指针,此指针指向虚函数表。
从函数调用顺序也可以看出,虚函数表中存放的函数地址的位置也是和前面类中定义的顺序是一致的。

二、继承情形

此部分内容基本来自 C++对象模型之详述C++对象的内存布局,只做一个简单记录。

1. 单一继承

类代码:

class Base
{
    public:
        Base()
        {
            mBase1 = 101;
            mBase2 = 102;
        }
        virtual void func1()
        {
            cout << "Base::func1()" << endl;
        }
        virtual void func2()
        {
            cout << "Base::func2()" << endl;
        }
    private:
        int mBase1;
        int mBase2;
};

class Derived : public Base
{
    public:
        Derived():
            Base()
        {
            mDerived1 = 1001;
            mDerived2 = 1002;
        }
        virtual void func2()
        {
            cout << "Derived::func2()" << endl;
        }
        virtual void func3()
        {
            cout << "Derived::func3()" << endl;
        }
    private:
        int mDerived1;
        int mDerived2;
};

遍历虚函数表:

void visitVtbl(int **vtbl, int count)
{
    cout << vtbl << endl;
    cout << "\t[-1]: " << (long)vtbl[-1] << endl;

    typedef void (*FuncPtr)();
    for (int i = 0; vtbl[i] && i < count; ++i)
    {
        cout << "\t[" << i << "]: " << vtbl[i] << " -> ";
        FuncPtr func = (FuncPtr)vtbl[i];
        func();
    }
}

测试代码:

int main()
{
    Derived d;
    char *p = (char*)&d;
    visitVtbl((int**)*(int**)p, 3);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;

    return 0;
}

运行结果:
c++内存布局_第6张图片
据此,可以看出单继承时的对象的内存结构:
c++内存布局_第7张图片

  • 对象内存结构中,首先是虚拟函数表地址(指针);
  • 非static成员,按照申明顺序,先放置基类的成员,再放置继承类的成员;
  • 对于virtual函数,在虚函数表中的位置,同样是先放置基类的虚函数,再放置子类的虚函数地址;
  • 子类新添加的虚函数,放在基类虚函数后面;
  • 如果子类重写了基类的虚函数,则对应覆盖原来父类的虚函数。

2. 多继承

类代码:

class Base1
{
    public:
        Base1()
        {
            mBase1 = 101;
        }
        virtual void funcA()
        {
            cout << "Base1::funcA()" << endl;
        }
        virtual void funcB()
        {
            cout << "Base1::funcB()" << endl;
        }
    private:
        int mBase1;
};

class Base2
{
    public:
        Base2()
        {
            mBase2 = 102;
        }
        virtual void funcA()
        {
            cout << "Base2::funcA()" << endl;
        }
        virtual void funcC()
        {
            cout << "Base2::funcC()" << endl;
        }
    private:
        int mBase2;
};

class Derived : public Base1, public Base2
{
    public:
        Derived():
            Base1(),
            Base2()
        {
            mDerived = 1001;
        }
        virtual void funcD()
        {
            cout << "Derived::funcD()" << endl;
        }
        virtual void funcA()
        {
            cout << "Derived::funcA()" << endl;
        }
    private:
        int mDerived;
};

测试代码:

int main()
{
    Derived d;
    char *p = (char*)&d;
    visitVtbl((int**)*(int**)p, 3);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    visitVtbl((int**)*(int**)p, 3);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;

    return 0;
}

运行结果:
c++内存布局_第8张图片
可以看出,多个继承时,对象结构先存放主继承(第一个)的虚函数表,接着存放主继承类的成员,然后存储次继承类的虚函数表和成员,最后才是子类的成员。

c++内存布局_第9张图片

  • 一个子类继承了n个类时,会存在n张虚函数表,其中一个为主虚函数表。表中首先存放父类的虚函数地址,然后存放子类的虚函数地址。
  • 每个父类对象按照顺序在子类对象结构中排序,并且,最后存放子类的成员。
  • 若子类重写父类的虚函数,则父类中所有签名相同的虚函数都会被覆盖。即在对应的虚函数表中,会变成子类的虚函数地址。

3. 重复继承

类代码:

class Base
{
    public:
        Base()
        {
            mBase = 11;
        }
        virtual void funcA()
        {
            cout << "Base::funcA()" << endl;
        }
        virtual void funcX()
        {
            cout << "Base::funcX()" << endl;
        }
    protected:
        int mBase;
};
class Base1 : public Base
{
    public:
        Base1():
            Base()
        {
            mBase1 = 101;
        }
        virtual void funcA()
        {
            cout << "Base1::funcA()" << endl;
        }
        virtual void funcB()
        {
            cout << "Base1::funcB()" << endl;
        }
    private:
        int mBase1;
};
class Base2 : public Base
{
    public:
        Base2():
            Base()
        {
            mBase2 = 102;
        }
        virtual void funcA()
        {
            cout << "Base2::funcA()" << endl;
        }
        virtual void funcC()
        {
            cout << "Base2::funcC()" << endl;
        }
    private:
        int mBase2;
};
class Derived : public Base1, public Base2
{
    public:
        Derived():
            Base1(),
            Base2()
        {
            mDerived = 1001;
        }
        virtual void funcD()
        {
            cout << "Derived::funcD()" << endl;
        }
        virtual void funcA()
        {
            cout << "Derived::funcA()" << endl;
        }
    private:
        int mDerived;
};

测试代码:

int main()
{
    Derived d;
    char *p = (char*)&d;
    visitVtbl((int**)*(int**)p, 4);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    visitVtbl((int**)*(int**)p, 3);
    p += sizeof(int**);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    p += sizeof(int);

    cout << *(int*)p << endl;
    return 0;
}

运行结果:
c++内存布局_第10张图片
据此,可以看出重复继承时对象的内存结构:
c++内存布局_第11张图片

  • 重复继承后,位于继承层次顶端的父类Base分别被子类Base1和Base2继承,并被类Derived继承。所以在D中有类的对象中,存在Base1的子对象,同时也存在Base2的子对象,这两个子对象都拥有Base子对象,所以Base子对象(成员mBase)在Derived中存在两份。
  • 二义性的原因。由于在子类的对象中,存在两份父类的成员,当在Derived类中使用如下语句:mBase = 1;就会产生歧义。因为在该对象中有两处的变量的名字都叫mBase,所以编译器不能判断究竟该使用哪一个成员变量。所以在访问Base中的成员时,需要加上域作用符来明确说明是哪一个子类的成员,如:Base1::mBase = 1;

4. 单一虚拟继承

这里测试代码时,发现运行时会出错,就是子类在覆盖了虚继承的父类方法时,在父类的虚函数表中取地址然后调用时会出错。代码这里就不在提出来了,直接用别人的结论好了。


class Base  { ...... }; 
class Base1 : virtual public Base  { ...... };

c++内存布局_第12张图片

  • 成员的顺序问题。在普通的单一继承中:基类成员->子类成员。在单一虚继承中:普通基类成员->派生类成员->虚基类成员。
  • vptr的个数问题。在普通的单一继承:派生类只有一个虚函数表,所以其对象只有一个vptr。单一虚继承中:派生类的虚函数表有n个(n为虚基类的个数)额外的虚数函数表,即总有n+1个虚函数表。
    3)派生自虚基类的派生类的虚函数表中,并不含有虚基类中的virtual函数,但是派生类重写的virtual函数会在所有虚函数表中得到更新。如本例中,第一个虚函数表中,并不含有Base::funcX的函数地址。

5. 钻石型虚拟继承

类结构:

class Base  { ...... };   
class Base1 : virtual public Base  { ...... };  
class Base2 : virtual public Base  { ...... };  
class Derived : public Base1, public Base2 { ...... };  

c++内存布局_第13张图片
使用虚继承后,在派生类的对象中只存在一份的Base子对象,从而避免了二义性。由于是多重继承,且有一个虚基类(Base),所以Derived类拥有三个虚函数表,其对象存在三个vptr。如上图所示,第一个虚函数表是由于多重继承而与第一基类(Base1)共享的主要实例,第二个虚函数表是与其他基类(Base2)有关的次要实例,第三个是虚基类的虚函数表。

类Derived的成员与Base1中的成员排列顺序相同,首先是以声明顺序排列其普通基类的成员,接着是派生类的成员,最后是虚基类的成员。

派生自虚基类的派生类的虚函数表中,也不含有虚基类中的virtual函数,派生类重写的virtual函数会在所有虚函数表中得到更新。

在类Derived的对象中,Base(虚基类)子对象部分为共享区域,而其他部分为不变区域。

写在最后

我们知道,在对象的内存结构中,只保存有对象的非static成员变量和虚函数表。那么,程序在运行时,是如何通过对象找到对应的函数呢?其实,这些不在对象内存结构中的数据,在编译时就已经确定了,那些对象的非static函数,其实在编译的时候,已经确定了对应的内存位置,调用时直接换成对应的call 0xaddr就ok了,和普通c的函数已经没有什么区别。不过,对于对象的非static函数,和对象存在一定的绑定关系,会将函数的第一个参数变成this指针,用以明确是那个对象的方法。例如,Base::funcA()方法编译后就是funcA(this)的形式。这一点,和在做so逆向的时候是一致的。

对于static方法,已经inline函数,在编译后,在发生函数调用时,会直接把对应的汇编代码copy到调用处。

你可能感兴趣的:(Android安全)