在前面的章节中,我们看到了两种流行的内存管理器实现。正如本章开头所说的,理解堆的元数据结构对调试内存问题有很大帮助。因为堆元数据告诉我们应用程序数据对象的基本状态,它可以为查找内存损坏的原因提供重要线索。
尽管许多内存管理器看起来很相似,但它们或多或少地使用了不同的数据结构记录正在使用和已经释放的内存块。不管程序中使用何种内存管理器,我们都应该尽量去学一学它的堆数据结构,从而尽可能地发挥我们的知识优势。通过解密内存块的比特和字节,可以揭露底层数据对象的信息并以多种方式帮助调试,笔者将在本章后面的示例中详细展示。
调试器通常不知道如何去解释堆元数据,然而我们可以通过检查内存内容来获取有用信息。因为手动调查巨大的内存区域的效率是很低的,所以这是我们使用调试器插件自动化工作的好时机。笔者在每天的工作中经常使用一些这样的调试器插件,并把它们集成到了Core Analyzer 里面。关于带有Core Analyzer功能的GDB的安装和入门,将在第10章讲解,本章直接使用其中一些通俗易懂的命令。
这些拓展功能的命令用于显示ptmalloc管理的内存块或者arena的信息。这些命令的实现利用了内存管理器的内部数据结构体,从而查询和检验堆地址,或者遍历整个堆来寻找潜在的内存损坏;或者打印出堆的统计情况。下面是这些命令用法的一些例子。
示例1:使用命令“heap /block”。该命令接收一个地址,然后输出这个地址所属内存块的状态。
在示例中,数据组的第12个元素存储一个指向大小为56字节的内存块,并且该内存块正在使用中。注意,圆括号里的chunk信息是ptmalloc的内部数据结构,它从用户内存块的前16字节开始,大小是64字节。用户空间开始于地址0x503440,大小是56字节。我们可以看到有8字节的内部数据结构开销。
示例2:显示ptmalloc管理的heap可调整参数和统计信息。
Heap总共有3个arena,17个mmap-ed内存块,共7MB。主arena开始地址是0x55555555e010,结束地址是0x55555557f000。
我们怎么从ptmalloc里获取这些信息呢?正如前面介绍的,每个内存块之前都有一个名为malloc_chunk的小数据结构,即块标签。如果用户输入一个由函数malloc返回的有效地址,则内存块的标签正好在这个地址的前面。块标签的size字段说明当前块的大小。为了知道当前块是在使用中还是空闲的,需要计算下一个块的地址。当前块的状态编码在下一个块的size字段中。可以看一下这里的代码实现 ,摘录如下:
如果输入地址并非由malloc分配的有效起始地址,情况就会变得复杂起来。一方面,我们可能正在处理一个可疑的内存损坏问题。例如,用户可能错误地引用了已被释放的内存块。在这之后,ptmalloc可能将该空闲块与邻近的空闲块合并,或将其重新分配。此时,原先的malloc_chunk数据结构就不再适用。如果此时试图读取那个地址前的标记,我们只会获取到随机值,这对调试并无帮助。
另一方面,应用程序有时也会合法地引用到某个有效内存块的中间位置。例如,为了实现具有继承特性的复杂C++类,编译器可能会将一个内存块划分成多个段,每一个段代表一个基类。因此,调试器可能会碰到一个指针,指向这些片段中的某一部分,这样的片段通常对应于一个接口基类。在这种情境下,该指针不一定指向由分配器返回的实际内存块的开始,也就是说,不指向派生类的实际起始地址。简单的脚本在这种情况下可能无法准确地找到对应的内存块。于是,我们需要采用更复杂的方法,即遍历整个内存区域,以确定包含特定地址的内存块。这个实现方法将在后续的章节中进行描述。