原标题:使用gdb+python查看C/C++进程内存分配情况

内存使用和管理在C/C++程序中是一个无法绕开的问题, 在gdb支持python 以后, 我们就可以使用gdb这个新的特性来帮助我们查看在glibc ptmalloc算法中管理的内存的情况。为了方便, 下面我们主要针对x64环境。

在可以查看内存分配情况以前, 我们当然需要知道ptmalloc算法大致是一个什么样子的。 你只需要以ptmalloc analysis为关键字google一下就可以看到很多的相关文章,例如 Glibc 内存管理 或者 Understanding glibc malloc

在这里我们大致介绍一下 ptmalloc 是如何管理内存的。 ptmalloc 的基本思想是将从内核分配出的大块连续内存(例如 64M )拿出来,然后按照一定的大小切分成若干个小的内存块。

这些内存块用链表链接起来, 当请求一块内存的时候, 就从链表中查找出一个最小可以满足需求的块交给应用程序, 如果没有合适的内存块,那么可能需要从更大的内存块中切分出来合适大小的内存块,链接到已有的链上,然后交给应用程序。

当应用程序 free 掉不再需要的内存块的时候, ptmalloc 就会把这个内存块标记为 free 并且在适当的时候查看到该内存块的周围的内存块也处于 free 状态的时候,那么 ptmalloc 会尝试将这些内存块合并成一个更大的内存块。

当然了 ptmalloc 里面还做了很多优化。例如, 小内存块的使用是很频繁的, 所以在 ptmalloc 里面针对小内存块设立了一个 fastbin ,专门使用单链表来记录这些小内存块, 以方便在下次应用程序需要分配小内存块的时候可以更加迅速的得到响应。

为了可以了解 ptmalloc 的内存管理情况, 3 个概念我们需要了解一下。

第一个是 malloc_chunk, 如下是其定义

struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize;};

malloc_chunk 位于每一个小块内存的最前端, 当该内存块处于被分配状态的时候,从 fd 开始的内存区域都属于用户数据区域。也就是说此时 fd, bk, fd_nextsize, bk_nextsize 都是无效的。

同时 mchunk_size 的低 3 位有特殊用途, 最低位(称之为 P )用于指示前一个内存块是否处于空闲状态, 次低位(称之为 M )表明当前的内存块是否来源于 mmap ,倒数第三个低位 ( 称之为 A) 表示当前的内存块属于主分配区还是非主分配区。

mchunk_size& ~0x7) 就是当前内存块的真实大小。 而当该内存块处于空闲状态,那么 fd bk 就将用来形成链表, 根据 chunk 的大小不同,可能会形成单向链表, 双向链表, 或者跳表。

第二个需要了解的是 malloc_state

struct malloc_state{ ...

/* Fastbins */ mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2 ];

/* Linked list */ struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free;

/* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;};typedef struct malloc_state *mstate;

上面的数据结构里面去掉了一些这里不需要讲解的 field 。一个 malloc_state 代表一个用于内存分配的 heap 多个 heap 可以通过 next 链接成一个链表。 malloc_state fastBinsY 的每一项都指向一个相同大小内存块的单链表。而 bins 中则是使用两项分别作为双链表的 head tail ,来形成一个双向链表。

第三个需要链接的是 heap_info. 这个概念是因为多线程而引进的。

typedef struct _heap_info{ mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READ|PROT_WRITE. */ ...

char pad[- 6 * SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;

一个应用程序一定有一个主分配区, 主分配区在 ptmalloc 中对应一个静态变量 static struct malloc_state main_arena 但是对于多线程的程序, 一般情况下新的线程会有新的 mstate 与之对应, 这个时候的 mstate heap_info 的一部分, 多个 heap_info 通过 heap_info::prev 形成一个单链表。

为了能够使用 gdb+python 查看内存分配的情况, 我们首先需要配置一下我们的环境。 centos6 为例。

1. 首先需要安装libc的调试符号。

· 修改/etc/yum.repos.d/CentOS-Debuginfo.repo文件中的enabled为1

· 使用命令 yum install yum-utils安装debuginfo-install

· 使用命令 debuginfo-install glibc安装glibc的调试符号

2. 安装gdb(需要能够支持python 的)

· yum install gdb

为了可以在gdb中查看内存的情况, 我们需要对刚才讲到的几个数据结构进行解析。 在gdb的python 中我们可以使用gdb.lookup_type来查找某个具体的数据结构symbol, 例如

#point to malloc_chunktype_mchunkptr = gdb.lookup_type( "mchunkptr" )# long is used for most address calculationtype_long = gdb.lookup_type( "long" )#point to heap_infotype_heapinfo = gdb.lookup_type( "struct _heap_info" ).pointer()#point to malloc_statetype_mstate = gdb.lookup_type( "struct malloc_state" ).pointer()还可以使用gdb.parse_and_eval来获取某个变量的值, 例如:main_arena = gdb.parse_and_eval( "main_arena" )

有了 gdb+python 这个工具再加上前面了解的几个基本概念, 我们就可以制作一个东西帮我们来了解这个 ptmalloc 的内存管理情况了。

首先, 我们需要知道当前的 process 有多少个 mstate, 通过 main_arena 我们就可以获得该信息。 我们使用 main_arena = gdb.parse_and_eval("main_arena") 拿到 main_arena 的值以后, 可以通过 malloc_state::next 来找到下一个 mstate, 一直到当 next 指向 main_arena 自己的时候,所有的 mstate 就被找到了。

现在, 我们有了所有的 mstate 我们就可以通过 mstate 找到其上所有的小内存块, 以及处于空闲状态的小内存块。 为了找到所有的小内存块, 我们需要为每一个 mstate 代表的大内存块确定一下边界,也就是这个大内存块的起始地址。由于 mstate 分为主分配区和非主分配区, 所以在解析 mstate 所代表的大内存块的起始地址的时候也需要分别对待。

对于主分配区, 由于其主要使用 sbrk 来分配内存, 所所以找到 sbrk_base main_arena top 就可以确定其对应的内存块其实地址。 而对于非主分配区, 每一个 mstate 实际上包含在一个 heap_info 里面, 所以会稍微复杂一点,因为这个时候 mstate 指向的地址是 heap_info 的一部分, 通过 mstate_address & (~HEAP_MASK) 可以获得 heap_info 指向的地址。然后我们可以通过 heap_info 中的 size 之类的 field ,找到其对应的内存的起始地址。

当找到大内存块的起始地址以后,接下来我们就需要在其中找到所有的内存块和处于空闲状态的内存块。 回忆刚才的内容, malloc_chunk 通过 mchunk_size( 实际上是 mchunk_size&(~0x07)) 就可以找到所有的内存块了。 也就是从大内存块的起点开始, 加上当前 chunk 的大小得到的位置即为新的小内存块的起始地址,如此重复一直遍历到当前大内存块的结束。 这些所有的内存块(占用或者空闲状态的小内存块)都被查找出来了。

接下来我们需要查找出那些处于空闲状态的内存块。 这个时候 malloc_state fastbinsY bins 所分别代表的 fast chunk normal chunk 链表就可以帮我们的忙了。我们首先遍历 fastbins fastbins 的每一项都是一个单链表, malloc_chunk::fd 指向下一个相同大小的 chunk 。当 fd 指向空的时候表示链表结束。分析 normal bins 也非常方便, normal bins 每两项用来作为一个双向链表的 head tail 指针, 所以我们可以从 tail 开始一直遍历到 head 指针结束。在 normal bins 中针对较大内存块会采用跳表提高查找速度,不过这个对于我们解析空闲状态的 chunk 没有帮助,所以就可以忽略掉。

很明显找到了所有的内存块以及处于空闲的状态的内存块, 做一个集合差我们就可以知道处于分配状态的内存块有哪些了。而且我们有内存块的大小,也可以按照内存的大小做一个分类。如果该内存块是分配给一个 struct 或者 class 那么我们还可以通过查找 symbol 来查看这个内存块上的结构化数据。 这里 (https://gist.github.com/ZhangHongQuan- Dianrong/c906e2f81844e336a883597dc56c69f4)

提供了一个可以用于解析 ptmalloc 内存分配的 python 脚本, 里面实现了简单的按照 chunk 大小查找已经分配的内存块等基本功能。

有了这些小内存块的分布情况, 我们在遇到有些极端情况,例如, 部署在客户现场的某个程序发生了内存异常增长的情况而又不能直接调试的情况。 那么我们就可以获取该 process core 。然后使用上面的方法对该 core 文件进行分析,找到大量被分配的内存的共性,然后进行分析。很可能这样可以很快帮你找到问题的所在。 你还可以使用这个方法构思出更多可以帮助你的小工具。

Glibc 内存管理

(https://paper.seebug.org/papers/Archive/refs/heap/glibc%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86ptmalloc%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90.pdf)

Understanding glibc malloc

(https://sploitfun.wordpress.com/tag/ptmalloc/)

返回搜狐,查看更多

责任编辑:

声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。