C++反汇编学习笔记4——结构体和类1

来源:互联网 发布:做军工定制软件如何 编辑:程序博客网 时间:2024/05/16 10:42

两年前写的,欢迎大家吐槽!

转载请注明出处。

在C++中类和结构体都具有构造函数、析构函数和成员函数,两者的唯一区别就在于结构体的默认访问控制为public,而类则是private。在C++中对于访问控制都是在编译期进行检查,所以在执行时程序不会对访问控制做任何检查和限制,因此在反汇编中两者没有区别,两者原理相同只是类型名称不同。

1.对象的内存布局

了解C++就必然要了解类,所以类和对象之间的关系这里就不再多说,就举几个简单的小例子:

1.1CNumber类

class CNumber

{

public:

    CNumber()

    {

        m_nOne = 1;

        m_nTwo = 2;

    }

    intGetNumberOne()

    {

        returnm_nOne;

    }

    intGetNumberTwo()

    {

        returnm_nTwo;

    }

private:

    int m_nOne;

    int m_nTwo;

};

void main(int argc,char* argv[])

{

      170: CNumber Number;

0042845E  lea        ecx,[Number] 

00428461  call       CNumber::CNumber (427117h) ;调用构造函数对其进行初始化

 }

CNumber这个类有两个整形成员变量m_nOne和m_nTwo。现在来看一下构造函数的内容。

    13:      m_nOne= 1;

004284D3  mov        eax,dword ptr [this] 

004284D6  mov        dword ptr [eax],1 

    14:      m_nTwo= 2;

004284DC  mov        eax,dword ptr [this] 

004284DF  mov         dword ptr [eax+4],2 

其中使用了this指针,this指针指向对象的首地址,相当于&Number。再看内存:

0x0012FF60  0100 00 00 02 00 00 00

可以看到this指针后面的8个字节已经变成了1和2,这正是两个成员变量所在的内存。所以Number对象在内存中占据8字节。

是不是所有的类的大小都是所有的成员变量的大小之和呢?这显然是不对的,有以下几种情况:

A.空类:空类中没有任何数据成员,按常理它的大小应该是0才对,但是事实并非如此。没有数据成员但是可能有成员函数,所以空类必须可以实例化,如果大小为0则无法实例化,因此会非配给它1字节用于实例化,但是这1字节并没有被使用。将上面的CNumber类中的数据成员和函数内的代码全部注释掉成为一个空类,但保留函数名。此时可以看到&Number的地址:,说明它确实存在于内存,但是可以看到里面并没有内容。

B.内存对齐

类或结构体中的数据成员会按照他们的出现顺序依次申请内存空间。由于内存对齐的原因,数据成员并不会像数组那样连续的排列,由于数据类型可能不同,所以大小也有可能不同。还是来看例子:

struct  tagTEST

{

    shortsShort;       //2字节

    int nInt;           // 4字节

};

    tagTEST test;

    test.nInt = 5;

    test.sShort = 2;

    printf("%d\n", sizeof( test ) );

上面是C++源代码,再来看一下内存:

0x0012FF60  0200 cc cc 05 00 00 00

可以看到在0x0002后面有两个字节是没有使用的,这就是为了内存对齐,这里int是4字节,默认是8字节,所以对齐值为4字节。本来如果不对齐则结构体只要2+4=6字节,但是对齐之后要8字节。

还有一种现象就是结构体中的数据定义顺序不同,结构体的大小也可能会不同。

当结构体如此定义时:

struct  tagTEST

{

    char cChar;     //1字节

    int nInt;           // 4字节

    shortsShort;       //2字节

};

初始化部分:

    tagTEST test;

    test.cChar = 'a';

    test.nInt = 5;

    test.sShort = 2;

    printf("%d\n", sizeof( test ) );

此时查看内存:0x0012FF58  61 cc cc cc 05 00 00 00 02 00 cc cc,输出结构体大小为12。但是若是将结构体定义改成如下:

struct  tagTEST

{

    char cChar;     //1字节

    shortsShort;       //2字节

    int nInt;            // 4字节

};

则内存变成了0x0012FF60  61 cc 02 00 05 00 00 00,显示的大小也变成了8。

当然,也可以自己调整内存的对齐值,如下面的例子:

#pragma pack(1)

struct  tagTEST

{

    char cChar;     //1字节

    shortsShort;       //2字节

    int nInt;            // 4字节

};

内存变成了0x0012FF60  61 02 00 05 00 00 00,显示大小变成了7。当然修改对齐值也并非一定生效,当它大于结构体内的最大的数据类型的长度时就会无效。

当结构体的成员中有数组时,是根据数组元素的类型计算对齐值,而不是根据数组的整体大小计算。下面有个小例子:

struct cArray

{

    char cChar;

    char Array[4 ];

    shortsShort;

};

cArray carray;

    carray.cChar = 'a';

    for( int i = 0; i < 4; i++ )

        carray.Array[ i ] = 'b';

    carray.sShort = 3;

结构体cArray中存在一个大小为4的数组,将其赋值之后查看内存:

0x0012FF5C  61 62 62 62 62 cc 03 00

可以看到这里是以2字节来作为内存对齐值得,所以只在数组后面填充了1字节。若是采用编译器默认的8字节则需要在数组后面填入3字节然后再sShort变量后再填入6字节方可,显然编译器为了节约内存资源并不会这样做。

当结构体嵌套时编译器又是如何分配内存资源的呢?下面还是来看例子:

struct tagTEST

{

    char cChar;

    shortsShort;  

    int nInt;       

};

struct Nest

{

    char cChar;

    tagTEST test;

};

结构体定义:

Nest nest;

    nest.cChar = 'a';

    nest.test.cChar = 'b';

    nest.test.nInt = 5;

    nest.test.sShort = 2;

定义之后再查看内存:0x0012FF58 61 cc cc cc 62 cc02 0005 00 00 00,输出nest对象的大小是12。这说明了嵌套结构体的对齐值并非是按照被嵌套的结构体的大小来作为对齐值,而是是以被嵌套的对齐值来作为依据。这里tagTEST结构体占8字节,若是按8字节对齐,则nest对象大小为16字节,但是这里是12字节。tagTEST的对齐值为4字节,cChar为1字节,所以Nest的对齐值自然就是4字节而并非8字节。

至于结构体中有静态数据成员以及对象为全局对象时的情况在后面会依次介绍。

2.this指针

this指针就是保存当前对象首地址的指针。首先,从结构体的寻址方式:

tagTEST test;

    tagTEST *pTest = &test;

    test.cChar = 'a';

    test.nInt = 5;

    test.sShort = 2;

可以看到 和,由此可见结构体和类中的数据成员的地址是结构体或类的首地址加上数据成员在其中的偏移量。

下面来看一个this指针的事例:

CTest类:

class CTest

 {

 public:

     voidSetNumber(intnNumber)

    {

        m_nInt= nNumber;

    }

 public:

    int m_nInt;

};

;对main函数进行分析

   217:    CTest Test;

   218:      Test.SetNumber(5);

0042B12E  push       5 

0042B130  lea        ecx,[Test]  ;取出对象的首地址存入ecx中

0042B134  call       CTest::SetNumber (427117h) 

   219: printf("CTest: %d\r\n", Test.m_nInt);

0042B139  mov        eax,dword ptr [Test]  ;取出对象首地址处的4字节

0042B13C push        eax 

0042B13D  push       offset string "CTest : %d\r\n" (472F78h) 

0042B142  call       @ILT+3895(_printf) (426F3Ch) 

0042B147  add        esp,8 

;对Test类进行分析

    52: class CTest

    53: {

    54: public:

    55:       void SetNumber(intnNumber)

    56:      {

00428300  push       ebp 

00428301  mov        ebp,esp 

00428303  sub        esp,0CCh 

00428309  push       ebx 

0042830A  push        esi 

0042830B  push       edi 

0042830C  push        ecx ;由于后面要用ecx做循环计数器,所以先保存它的值

0042830D  lea        edi,[ebp-0CCh] 

00428313  mov        ecx,33h 

00428318  mov        eax,0CCCCCCCCh 

0042831D  rep stos   dword ptr es:[edi] 

0042831F  pop         ecx ;将类的首地址重新存入ecx中

00428320  mov        dword ptr [ebp-8],ecx  ;ebp-8存储着this指针的值,将类的首地址存入这里

    57:          m_nInt = nNumber;

00428323  mov        eax,dword ptr [this]  ;将类的首地址存入eax,相当于mov  eax, [ebp-8]

00428326  mov        ecx,dword ptr [nNumber]  ;相当于mov  ecx,[ebp+8],ebp+8里存着参数值

00428329  mov        dword ptr [eax],ecx  ;对nInt进行赋值

    58:      };后续代码略

以上代码便是对象调用成员函数,这里通过寄存器ecx传递对象的首地址的方法便是this指针的由来,传递给ecx之后,编译器会将首地址复制到栈的指定位置作为this指针的值。所有的成员函数都有一个隐藏的参数,就是自身类型的对象的指针this,这样的默认调用约定就是thiscall。同样,在成员函数中访问数据成员也是需要通过this指针的。

当成员函数的调用方式为__stdcall时,this指针将不会使用寄存器ecx传递,而是使用栈传递,下面将SetNumber函数改为使用__stdcall调用方式再来看this指针的传递方式:

   218:      Test.SetNumber(5);

0042B12E  push       5 

0042B130  lea        eax,[Test]  ;相当于lea  eax,[ebp-8],利用栈进行传递

0042B133  push       eax 

0042B134  call       CTest::SetNumber (427121h) 

    57:          m_nInt = nNumber;

0042831E  mov    eax, [ebp+8] ;取出this指针存入eax

.text:00428321  mov    ecx, [ebp+0Ch] ;取出nNumber的值

.text:00428324  mov    [eax], ecx

这里使用__stdcall方式调用函数使得this指针的识别变得困难,__cdecl调用方式类似,只是在栈平衡时有所不同。

0 0
原创粉丝点击