类的实例在内存中的布局?
我们在高级语言上写了一个类,它代表了我们对一事物的描述;如果我们不了解它的实例在内存在如何布局,这也不要紧,这与使面向对象的一大特色之一封装不蒙而合,对上层屏蔽了细节,你不了解它也可以,它能好好工作;但是随着对上层的了解,我想揭开它的面孔 ..
扫盲之一:内存对齐 ,如果不知道他说的内容可以去网上搜一搜;
看了网上的东东,给了我们一些概念上的东西,那我们在 VC 上动动手(我使用的是 VS2005, 中文)
1 . VC 在哪里设置对齐
2. 使用 #pragma pack 来设置
以下来自 MSDN
Specifies packing alignment for structure, union, and class members.
#pragma pack( [show ]| [ push |pop ][, identifier ], n )
例子:查看一个简单的类在内存中的情况
1. 我们选择的对齐方式是默认,(在我的机器上为 4 字节对齐方式)
如下一个类他会是什么一个情况呢?
class Test
{
public :
int GetBuy () const { return m_iBuy ; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
先给出答案,再分析,它表现的将会如下所示
1>class Test size(12):
1> +---
1> 0 | m_iBuy
1> 4 | m_cType
1> | <alignment member> (size=3)
1> 8 | m_iSell
1> +---
看见没,中间的 char 后面被填充了 3 个字节,这其实就是根据对齐方式,依次布局在内存中,不够得被填充了,
注:上面那段其实是 VC 生成的,要输出这个额外的信息,需要使用命令:
/d1 reportAllClassLayout
用法之一:
加入之后你在编译时, output 窗口变可以产生。
如何 Test 被修改成如下方式时呢?
class Test
{
public :
int GetBuy () const { return m_iBuy ; }
private :
int m_iBuy ;
char m_cType ;
char m_cType1 ;
int m_iSell ;
};
更进一步下面呢?
#include <string>
class Test
{
public :
int GetBuy () const { return m_iBuy ; }
private :
int m_iBuy ;
char m_cType ;
short m_cType1 ;
std ::string str ;
char m_cType2 ;
int m_iSell ;
};
不要认为这些不重要,也许在面试中遇到了,你正确的回答,那一定能给你加分,当然正确的理解,在某些时候对你也很有帮助, 1. 合理的布局能帮你省掉空间,尤其在嵌入式中,内存很紧张的情况,当然这是一个优化的策略,一般的 C++ 程序中,合理的表达描述或许更加重要一些, 2. 理解这些在逆向中也很重要
1>class Test size(48):
1> +---
1> 0 | m_iBuy
1> 4 | m_cType
1> | <alignment member> (size=1)
1> 6 | m_sType1
1> 8 | ?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@ str
1>40 | m_cType2
1> | <alignment member> (size=3)
1>44 | m_iSell
看到没,如果我们队std::string 没有好好的理解的话,那又需要看看文档,了解它的实现方式,呵呵撤远了
也许我们需要更加的深入一些,如果有了虚函数呢
class Test
{
public :
int GetBuy () const { return m_iBuy ; }
virtual bool GetInfo () const { return 0; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
1>class Test size(16):
1> +---
1> 0 | {vfptr}
1> 4 | m_iBuy
1> 8 | m_cType
1> | <alignment member> (size=3)
1>12 | m_iSell
1> +---
多出一个虚表 vftable ,里面有 vfptr (virture function pointer)
既然知道了怎么多我们看看他在内存中的情况
将 Test 修改如下:
class Test
{
public :
Test () : m_iBuy (99), m_cType (100),m_iSell (101)
{
}
int GetBuy () const { return m_iBuy ; }
virtual bool GetInfo () const { return 0; }
virtual bool GetType () const { return 0; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
我们在构造时初始化,在 C++ 中,变量并没被初始化,由上图可以看出,因此我们必须将这些值进行初始化,养成好的习惯,否则是指针之类的会很麻烦了,也许你的程序很多时候是正常运行的,有时确崩溃了。
99 = 0x63, 100 = 0x64 , 101 = 0x65
打开 VC 内存,如下图
看到没,我们通过地址找到了 Test 实例在内存中的位置,看到了 63 , 64 , 65 。前面有两个数字,不用想,肯定是用来描述虚表的, /(^o^)/~ , 但是为啥是 63 00 00 00 呢,占四字节我们可以理解,咋跑到头上去了, 99 不是 0x 00 00 00 63 么?
字节 的存储顺序:原理时间情况和 CPU 有关
Intel 体系的芯片使用的编码方式属于 Little-Endian, 而另外一些使用 Big-Endian 类
Big-Endian 高位字节存入低地址,低位字节存入高地址,依次排列
Little-Endian 低位字节存入低地址,高位字节存入高地址,反序排列
呵呵知道地址了,我小试一下改改值看看。代码如下:
int _tmain (int argc , _TCHAR * argv [])
{
Test kTest ;
int iBuy = kTest .GetBuy ();
int * pHack = NULL ;
pHack = reinterpret_cast <int *>(0x0012FF58);
*pHack = 0x00006789;
return 0;
}
呵呵,在 return 0 时下断点,值果然被修改,内存中果然变成了 89 67 00 00 ,并被显示成红色
继承的相应的内存布局呢
class Test
{
public :
Test () : m_iBuy (99), m_cType (100),m_iSell (101)
{
}
int GetBuy () const { return m_iBuy ; }
virtual bool GetInfo () const { return 0; }
virtual bool GetType () const { return 0; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
class TestBoy : public Test
{
public :
TestBoy (): m_iAge (15)
{
}
protected :
int m_iAge ;
};
顾使用前面的方法,看一下
1>class Test size(16):
1> +---
1> 0 | {vfptr}
1> 4 | m_iBuy
1> 8 | m_cType
1> | <alignment member> (size=3)
1>12 | m_iSell
1> +---
1>Test::$vftable@:
1> | &Test_meta
1> | 0
1> 0 | &Test::GetInfo
1> 1 | &Test::GetType
1>Test::GetInfo this adjustor: 0
1>Test::GetType this adjustor: 0
1>class TestBoy size(20):
1> +---
1> | +--- (base class Test)
1> 0 | | {vfptr}
1> 4 | | m_iBuy
1> 8 | | m_cType
1> | | <alignment member> (size=3)
1>12 | | m_iSell
1> | +---
1>16 | m_iAge
1> +---
1>TestBoy::$vftable@:
1> | &TestBoy_meta
1> | 0
1> 0 | &Test::GetInfo
1> 1 | &Test::GetType
由此可以看出先加父类放入前边,后面再加自己的数据
如果自己也有虚函数呢?那又是怎么处理的?
lass Test
{
public :
Test () : m_iBuy (99), m_cType (100),m_iSell (101)
{
}
int GetBuy () const { return m_iBuy ; }
virtual bool GetInfo () const { return 0; }
virtual bool GetType () const { return 0; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
class TestBoy : public Test
{
public :
TestBoy (): m_iAge (15)
{
}
virtual bool GetInfo () const { return 1; }
virtual int GetAge () const { return m_iAge ; }
protected :
int m_iAge ;
short m_sInfo ;
};
我们输出其结果看一看,它究竟会采用什么样的策略来处理
1>class Test size(16):
1> +---
1> 0 | {vfptr}
1> 4 | m_iBuy
1> 8 | m_cType
1> | <alignment member> (size=3)
1>12 | m_iSell
1> +---
1>Test::$vftable@:
1> | &Test_meta
1> | 0
1> 0 | &Test::GetInfo
1> 1 | &Test::GetType
1>Test::GetInfo this adjustor: 0
1>Test::GetType this adjustor: 0
1>class TestBoy size(24):
1> +---
1> | +--- (base class Test)
1> 0 | | {vfptr}
1> 4 | | m_iBuy
1> 8 | | m_cType
1> | | <alignment member> (size=3)
1>12 | | m_iSell
1> | +---
1>16 | m_iAge
1>20 | m_sInfo
1> | <alignment member> (size=2)
1> +---
1>TestBoy::$vftable@:
1> | &TestBoy_meta
1> | 0
1> 0 | &TestBoy::GetInfo
1> 1 | &Test::GetType
1> 2 | &TestBoy::GetAge
1>TestBoy::GetInfo this adjustor: 0
1> TestBoy::GetAge this adjustor: 0
由此可以虚表还是使用父类的,但是虚表的内容变化了
这样我就明白了继承关系,是如何处理的
但是。。很少用的。。一些语言没有的。。多重继承呢?
多重继承又是很容易出错的,那我们也用同样的方法来分析一下
class TestGirl
{
public :
TestGirl () : m_iBuy (99), m_cType (100),m_iSell (101)
{
}
int GetBuy () const { return m_iBuy ; }
virtual bool GetInfo () const { return 0; }
virtual bool GetType () const { return 0; }
private :
int m_iBuy ;
char m_cType ;
int m_iSell ;
};
class TestBoy
{
public :
TestBoy (): m_iAge (15)
{
}
virtual bool GetInfo () const { return 1; }
virtual int GetAge () const { return m_iAge ; }
protected :
int m_iAge ;
short m_sInfo ;
};
class Test : public TestBoy , public TestGirl
{
public :
virtual int GetMoney () { return 100; }
private :
char m_cMoney ;
};
1>class TestGirl size(16):
1> +---
1> 0 | {vfptr}
1> 4 | m_iBuy
1> 8 | m_cType
1> | <alignment member> (size=3)
1>12 | m_iSell
1> +---
1>TestGirl::$vftable@:
1> | &TestGirl_meta
1> | 0
1> 0 | &TestGirl::GetInfo
1> 1 | &TestGirl::GetType
1>TestGirl::GetInfo this adjustor: 0
1>TestGirl::GetType this adjustor: 0
1>class TestBoy size(12):
1> +---
1> 0 | {vfptr}
1> 4 | m_iAge
1> 8 | m_sInfo
1> | <alignment member> (size=2)
1> +---
1>TestBoy::$vftable@:
1> | &TestBoy_meta
1> | 0
1> 0 | &TestBoy::GetInfo
1> 1 | &TestBoy::GetAge
1>TestBoy::GetInfo this adjustor: 0
1>TestBoy::GetAge this adjustor: 0
1>class Test size(32):
1> +---
1> | +--- (base class TestBoy)
1> 0 | | {vfptr}
1> 4 | | m_iAge
1> 8 | | m_sInfo
1> | | <alignment member> (size=2)
1> | +---
1> | +--- (base class TestGirl)
1>12 | | {vfptr}
1>16 | | m_iBuy
1>20 | | m_cType
1> | | <alignment member> (size=3)
1>24 | | m_iSell
1> | +---
1>28 | m_cMoney
1> | <alignment member> (size=3)
1> +---
1>Test::$vftable@TestBoy@:
1> | &Test_meta
1> | 0
1> 0 | &TestBoy::GetInfo
1> 1 | &TestBoy::GetAge
1> 2 | &Test::GetMoney
1>Test::$vftable@TestGirl@:
1> | -12
1> 0 | &TestGirl::GetInfo
1> 1 | &TestGirl::GetType
由此可见,基类的还是被顺序的放在前面,单虚表的内容有所替换,从 Test_meta 中可以看到。
反过来我们可以理解继承的调度关系;
在实际应用中,也发现过这样的问题: 1. 对齐方式不一样,导致函数后面栈平衡不了, 2. 数据清零,拿到指针然后一个 memset ,全部为零,结果破坏虚表; 这种问题在编译的时候发现不了,都是运行时错误