C++对象布局
对齐和大小端
结构体对齐
结构体的对齐要求是所有单个字段中的最大要求。数组的对齐要求与数组中每个元素的要求相同。如果复合类型具有多个层级,那么这些规则适用于所有层级。
在所有的字段中,字段d的对齐要求最大,为8字节。因此,这个结构体aggr_type需要按8字节对齐。它同时也需要一些填充,从而确保每个字段满足对齐要求。
字段c一共有3字节的填充,字段s有6字节的填充。这些填充使得紧接的字段i和字段d相应地对齐在需要的4字节和8字节上。
当编译器为栈分配变量时,会确保每个变量,无论是原始类型还是复合类型,都满足其对齐要求。
堆中分配的数据对象也需要满足相同的要求。然而内存管理器只知道请求的内存块大小,并不了解背后的数据对象的数据类型,也不知道它的对齐要求。为了正常工作,内存管理器会确保返回的内存块满足目标架构的最大可能对齐要求,尽管这意味着一些空间浪费,因为实际的数据对象的对齐可能并不需要这么严格。举个例子:大多数CPU架构最大的对齐要求是16字节,如果应用程序申请12字节的内存,那么内存管理器返回的内存块的地址需对齐在16字节上,也就是说必须能被16整除。如果内存管理器的可分配内存的起始地址不在16字节边界上,那么它需要浪费几字节,从下一个16的整数倍的地址处开始分配。
大小端
小端序(Little Endian)中,一个多字节数据的最低位字节(最不重要的字节)放在最低地址处,而最高位字节(最重要的字节)放在最高地址处。
大端序(Big Endian)中,一个多字节数据的最高位字节(最重要的字节)存储在最低的内存地址处,而最低位字节(最不重要的字节)存储在最高的内存地址处。
x86处理器采用的是小端字节序。
unsigned long var = 0x0123456789abcdef;
在小端架构中(x86_64),调试器显示的内存布局如下:
最低字节0xef被放在了低地址0x7fbff4a8,同时最高字节0x01被放在了高地址0x7fbfff4af。
在调试器里面,一般的输出都是从低地址到高地址。
C++对象布局
C++被广泛应用于大型程序中。调试C++程序存在一些独特的挑战,其中一个复杂之处在于C++数据对象的内存布局。除了在源代码中明确声明的数据成员(例如C结构)之外,编译器还需要在C++数据对象中插入一些隐藏的数据成员和辅助函数,以实现C++语义,例如继承和多态性。
g++,它具有一个很好的命令行选项“-fdump-lang-class”。该选项会从编译器的视角输出对象的详细布局,有助于我们理解对象的内存内容。
对于每个数据结构,g++首先输出其总大小和对齐要求。
_GUID结构体需要对齐4字节,因为它所有数据成员中的最大对齐(Data1)是4。
使用虚函数、继承等特性的C++数据结构具有更复杂的内存布局。除了类本身的布局外,编译器为每个类生成一个虚函数表vtable,我们可以把它看作一个隐性的全局变量。
IUnknown类的虚函数表vtable有5个条目:
由于文件是以64位模式编译的,因此每个条目长度为8字节,即一个函数指针的大小。表的第1个条目是到派生类对象的偏移量(如果vtable是为基类生成的)。该值加到嵌套基类的地址上,即基对象的this指针,结果是派生类对象的地址。对于IUnknown类,没有基类,因此值为0。稍后我们将看到此条目不为0的示例。
第2个条目指向该类的typeinfo对象。这是编译器生成的对象,在运行中抛出异常时用于栈展开。第3、第4和第5个条目分别是指向类的虚函数QueryInterface、AddRef和Release的函数指针。由于这些函数是纯虚函数,因此被设置为g++运行时函数__cxa_pure_virtual,这只是一个未解析虚函数的通用陷阱。
IUnknown类几乎为空,因为它只有一个隐藏的数据成员vptr,是指向IUnknown的vtable的指针。注意该指针指向第三个条目,即第一个虚函数,而不是vtable的开头。这是由编译器选择的实现方式。
参考: 高效C/C++调试