姓名:房小慧
学号:17101223361
专业:软件工程
转载自:https://m.sohu.com/a/206411984_575744/?pvid=000115_3w_a
【嵌牛导读】:内存使用和管理在C/C++程序中是一个无法绕开的问题,在gdb支持python 以后,我们就可以使用gdb这个新的特性来帮助我们查看在glibc ptmalloc算法中管理的内存的情况。
【嵌牛鼻子】:Glibc 内存管理
【嵌牛提问】:怎么用python管理内存?
【嵌牛正文】:
内存使用和管理在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. */...
charpad[-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")#longis used formost 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/)