C++内存布局

来源:互联网 发布:radium mac 编辑:程序博客网 时间:2024/06/10 17:38

环境:Windows SP3,VC++6.0

        现在很多程序都是用C++写的,要写一个安全又高效的C++程序或者逆向一个用C++编译的程序首先就要知道C++对象在内存是怎么布局的。要声明的一点是,这里的程序没有使用RTTI,所以不太复杂。

        在这里首先要说的一点是,C++程序会大量得使用EXC寄存器,因为ECX是用来传递this指针的。

class A
{
public:
  A(){ x = 0xAA;}
  ~A(){}
public:
  void func(){ x+= 0xF; }
  int getX(){ return x; }
  virtual void vfunc1(){ printf("A::vfunc1/n"); }
  virtual void vfunc2(){ printf("A::vfunc2/n"); }
private:
  int x;
};

class B : public A
{
public:
  B(){ x = 0xBB; }
  ~B(){}
  virtual void vfunc1(){ printf("B::vfunc1/n"); }

  virtual void fff(){}

private:
  int x;
};

class C
{
public:
  C(){ x = 0xCC; }
  ~C(){}
  virtual void vfunc1(){ printf("C::vfunc1/n"); }

private:
  int x;
};

class D : public C, public A
{
public:
  D(){ x = 0xDD; }
  ~D(){}

  virtual void aaa(){}
private:
  int x;
};

class E
{
  int x;
};

 

1.new函数

       其实new和malloc一样,在内存分配时都调用的是_nh_malloc_dbg,所以在内存分配上效果是一样的。你可能会说,new在分配内存时会执行类的构造函数,而malloc不会,其实VC++6.0在这主面不是通过new实现的,而是在编译时就确定好了汇编代码,当new执行完后,若成功则直接执行该类的构造函数。

 

2.delete函数

       我们都知道,delete在释放对象前会调用对象的析构函数,然后释放空间,看了上面new的叙述,你可能会以为delete就是编译器确定汇编代码,先析构对象,再释放对象。其实不是这样的,在delete时,这些操作是封装在一起的,而不是像new一样是分开的(但不知道为什么不把new做得像delete一样,看着也好看,无语......)。比如对于类A的对象a,在delete a时,编译会转换下这个样子:call@ILT+10(A::`scalar deletingdestructor'),在这个函数里,先调用了A的析构函数,然后又调用的delete函数,在delete函数里,调用了free函数。

        下面看一段用new和delete的代码:

195:      A *a = new A;
00401B4D   push        8                                                         ;sizeof(A) = 8
00401B4F   call        operator new (00409310)                  ;调用new
00401B54   add         esp,4                                                   ;恢复栈
00401B57   mov         dword ptr [ebp-18h],eax                   ;把new的返回值存到一个局部变量里
00401B5A   mov         dword ptr [ebp-4],0                           ;设置SEH的trylevel
00401B61   cmp         dword ptr [ebp-18h],0                       ;检测new是否成功
00401B65   je          main+54h (00401b74)                         ;new不成功的话就不执行构造函数了
00401B67   mov         ecx,dword ptr [ebp-18h]                    ;把new得到的首地址放到eax里,实际上就是当前对象的this指针
00401B6A   call        @ILT+50(A::A) (00401037)               ;调用A类的构造函数,this指针会通过ecx传进去
00401B6F   mov         dword ptr [ebp-24h],eax                   ;把构造函数的返回值存到一个局部变量里,其实就是this指针
00401B72   jmp         main+5Bh (00401b7b)                      ;new成功会走这一句
00401B74   mov         dword ptr [ebp-24h],0                        ;new不成功会走这一句
00401B7B   mov         eax,dword ptr [ebp-24h]                   ;用eax保存构造函数的返回值
00401B7E   mov         dword ptr [ebp-14h],eax                   ;把eax放到个局部变量里
00401B81   mov         dword ptr [ebp-4],0FFFFFFFFh     ;设置SEH的trylevel
00401B88   mov         ecx,dword ptr [ebp-14h]                   ;从刚才保存的那个局部变量里取出this指针
00401B8B   mov         dword ptr [ebp-10h],ecx                  ;把this指针的值赋给a
196:
197:      delete a;
00401B8E   mov         edx,dword ptr [ebp-10h]                  ;取出a的值
00401B91   mov         dword ptr [ebp-20h],edx
00401B94   mov         eax,dword ptr [ebp-20h]
00401B97   mov         dword ptr [ebp-1Ch],eax
00401B9A   cmp         dword ptr [ebp-1Ch],0                    ;检验a是否为空
00401B9E   je          main+8Fh (00401baf)                        ;如果a为空,就不执行delete操作
00401BA0   push        1                                                        ;只是一个标志
00401BA2   mov         ecx,dword ptr [ebp-1Ch]                ;this指针
00401BA5   call        @ILT+10(A::`scalar deleting destructor') (0040100f)   ;这个函数会先调用析构函数,再调用delete
00401BAA   mov         dword ptr [ebp-28h],eax               ;保存delete的返回值
00401BAD   jmp         main+96h (00401bb6)
00401BAF   mov         dword ptr [ebp-28h],0
198:  }


3.构造函数和析构函数

        从上面的代码,我们就可以看出来,在调用构造函数前必须要把对象的this指针放到ECX里去,下面看一下类A的构造函数:

22:   A::A()
23:   {
004012F0   push        ebp
004012F1   mov         ebp,esp
004012F3   sub         esp,44h
004012F6   push        ebx
004012F7   push        esi
004012F8   push        edi
004012F9   push        ecx                                                   ;注意,这里会保护ECX,因为下面要用ECX当循环变量
004012FA   lea         edi,[ebp-44h]
004012FD   mov         ecx,11h
00401302   mov         eax,0CCCCCCCCh
00401307   rep stos    dword ptr [edi]
00401309   pop         ecx                                             ;循环完后,把ECX取出,ECX不是在A()里被初始化的,说明这个值需要外部调用函数传一个正确的值,也就是要初始化的对象的this指针
0040130A   mov         dword ptr [ebp-4],ecx             ;把this指针放到个局部变量里,先存起来
0040130D   mov         eax,dword ptr [ebp-4]
00401310   mov         dword ptr [eax],offset A::`vftable' (00432020)    ;初始化虚函数表,如果没有虚函数,则没这一句
24:       x = 0xAA;
00401316   mov         ecx,dword ptr [ebp-4]
00401319   mov         dword ptr [ecx+4],0AAh       ;对成员变量的赋值,是通过对this指针的偏移来实现的
25:   }
00401320   mov         eax,dword ptr [ebp-4]
00401323   pop         edi
00401324   pop         esi
00401325   pop         ebx
00401326   mov         esp,ebp
00401328   pop         ebp
00401329   ret

        析构函数的实现与构造函数是类似的。

 

4.成员函数

        成员函数的调用也需要先把该对象的this指针存入ECX里,和构造函数挺像的,所以如果只给你一个汇编出来的C++程序,很可能会出现误把成员函数当做构造函数。

196:      A a;
00401B9D   lea         ecx,[ebp-14h]
00401BA0   call        @ILT+45(A::A) (00401032)          ;调用构造函数

201:      a.func();
00401BD0   lea         ecx,[ebp-14h]
00401BD3   call        @ILT+10(A::func) (0040100f)       ;如果只是call 0040100f,是不是很容易当成构造函数?

 

5.虚函数和虚函数表

       C++为了实现继承和多态,所以使用了虚函数这个概念。当一个类没有虚函数时,它的对象里存的就只是该类的成员变量,有多少变量,这个类就占多大内存;而如果当一个类有虚函数时,它的this指针的0偏移处存的是该对象的虚函数表的地址,然后下面才存的是变量;如果当一个类继承自一个有虚函数的基类时,它也先有继承该基类的虚函数表的地址和变量然后才是自己的变量;如果该子类有一个自己的虚函数时,它会把该虚函数的地址跟在它继承来的虚函数表的后面;如果当一个类多继承自多个在虚函数的大基类时,它的内存布局会是(地址由低到高):继承列表中第一个类的虚函数表的地址和变量,继承列表中第二个类的虚函数表的地址和变量,......,自己的变量;如果该子类也有一个自己的虚函数时,它为把这个虚函数的地址跟到第一个虚函数表的后面。

        看了这些,现在看看我上面写的几个类的内存是如何布局的吧:

Object a(0x12FF6C): //a的地址
  0x12FF6C : 0x433020                                 //a的虚函数表的地址
  0x12FF70 : 0xAA                                         //a的成员变量x
Object a(0x12FF6C) _vftable:
  1  0x433020 : 0x40101E                      //vfunc1的地址
  2   0x433024 : 0x40100A                     //vfunc2的地址
Object b(0x12FF60):
  0x12FF60 : 0x433060
  0x12FF64 : 0xAA
  0x12FF68 : 0xBB
Object b(0x12FF60) _vftable:
  1  0x433060 : 0x401019                   //B覆盖的vfunc1
  2  0x433064 : 0x40100A                 //A原来的vfunc2
  3  0x433068 : 0x40105A                   //B添的fff
Object c(0x12FF58):
  0x12FF58 : 0x433094
  0x12FF5C : 0xCC
Object c(0x12FF58) _vftable:
  1 0x433094 : 0x401014                       //C只有一个虚函数vfunc1
Object d(0x12FF44):
  0x12FF44 : 0x4330D0                        //C的虚函数表的地址
  0x12FF48 : 0xCC                                 //C的变量
  0x12FF4C : 0x4330C0                      //A的虚函数表的地址
  0x12FF50 : 0xAA                                //A的变量
  0x12FF54 : 0xDD
Object d(0x12FF44) _vftable:
  1  0x4330D0 : 0x401014               //C原来的vfunc1
  2  0x4330D4 : 0x401069              //D添的aaa
Object d(0x12FF4C) _vftable:                     //A原来的虚函数表
  1  0x4330C0 : 0x40101E
  2  0x4330C4 : 0x40100A
Object e(0x12FF40):
  0x12FF40 : 0xCCCCCCCC                 //只有一个变量,没有虚函数

        这里还要说明的一个问题是,D继承自C和A,而C和A都有一个不同的虚函数vfunc1,所以D会有两个vfunc1,如果在写代码时写成了d.vfunc1();这会在编译时产生错误:

    error C2385: 'D::vfunc1' is ambiguous

不过这个是很好理解的。

 

        至此,我们已经大致了解了基本的C++反汇编后是一个什么样子了。

 

附:

输出上面的内存布局的程序(类定义在最前面,程序中使用的常量都是经验值,在VC++6.0上没问题,在其它编译器上就不敢保证了):

void printVftableAddr(const char *varName, void *a)
{
  int *x = (int *)a;
  int *y = (int *)(*x);
  if((int)y <= 0x0000ffff)
  {
    printf("Object %s(0x%X) doesn't have a _vftable/n", varName, a);
    return;
  }
  printf("Object %s(0x%X) _vftable:/n", varName, a);
  for(int i = 0; i < 10; i++)
  {
    if(*(y + i) == NULL || *(y + i) >= 0x500000)
    {
      break;
    }
    printf("/t%d/t0x%X : 0x%X/n", i + 1, y + i, *(y + i));
  }
}

void printObjectAddr(const char *varName, void *a, int size)
{
  int i = 0;
  printf("Object %s(0x%X):/n", varName, a);
  for(i = 0; i < size / 4; i++)
  {
  printf("/t0x%X : 0x%X/n", (int*)a + i, *((int*)a + i));
  }
  int *x = (int *)a;
  for(i = 0; i < size / 4; i++, x++)
  {
    if(*x <= 0x400000)
    {
      continue;
    }
    printVftableAddr(varName, x);
  }
}

int main(int argc, char* argv[])
{
  A a;
  B b;
  C c;
  D d;
  E e;
  printObjectAddr("a", &a, sizeof(a));
  printObjectAddr("b", &b, sizeof(b));
  printObjectAddr("c", &c, sizeof(c));
  printObjectAddr("d", &d, sizeof(d));
  printObjectAddr("e", &e, sizeof(e));

  return 0;
}