决定学习堆溢出很久了但却因为内心对源码的恐惧一直不愿意解出这块内容,于是在实习面试的时候被问到linux的内存管理一脸懵逼啥都不会。。。。痛定思痛利用一周末时间将堆的实现硬着头皮看了一遍,现在记录如下。
参考内容:
CTFwiki 深入理解堆的实现
Glibc内存管理Ptmalloc2源代码分析
内核数据结构 mm_struct (在include/linux/mm_types.h中定义)中的成员变量 start_code 和 end_code 是进程代码段的起始和终止地址,start_data 和 end_data 是进程数据段的起始和终止地址,start_stack 是进程堆栈 段起始地址,start_brk 是进程动态内存分配起始地址(堆的起始地址),还有一个 brk(堆 的当前最后地址),就是动态内存分配当前的终止地址。。brk()是一个非常简单的系统调用, 只是简单地改变 mm_struct 结构的成员变量 brk 的值,从而达到改变堆大小的目的。
这两个函数的定义:
#include
int brk(void *addr); //参数为堆的新边界
void *sbrk(intptr_t increment); //参数 increment 为 0 时,sbrk()返回的是进程的当前 brk 值, increment 为正数时扩展 brk 值,当 increment 为负值时收缩 brk 值
brk()系统调用源码分析:待补充
mmap()函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的 大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap 执行相反的操 作,删除特定地址区域的对象映射。
函数定义如下:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
源码分析:待补充
参数说明:
addr:映射区的开始地址
length:映射区长度
prot:期望的内存保护标志,不能与文件的打开模式冲突。
flags:指定映射对象的类型,映射选项和映射页是否可以共享。
fd:有效的文件描述词。如果 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。
主分配区,每个进程可以自主分配的内存区域。每个进程在向系统申请内存时,系统都会给它一段较大的内存,这段内存成为arena,以后再有申请内存的请求时,会直接从arena中获取内存,而不用向系统申请,从而提高效率。
非主分配区。为了能够支持多线程,增加了non_main_arena支持,non_main_arena和main_arena作用相同,提高线程申请内存的效率。
主分配 区可以访问进程的 heap 区域和 mmap 映射区域,也就是说主分配区可以使用 sbrk 和 mmap 向操作系统申请虚拟内存。而非主分配区只能访问进程的 mmap 映射区域,主分配区每 次使用 mmap()向操作系统“批发”HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统默 认为 64MB)大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售” 出去,毕竟系统调用是相对低效的,直接从用户空间分配内存快多了
数据结构定义:
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T 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;
};
Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理(边界标记法,顾名思义)
malloc_chunk各个域的含义:
因此可以获得chunk在两种状态下的结构:
chunk空闲时,M状态不存在,只有AP状态。
用户内存空间区域存储了4个指针:fd、bk、fd_nextsize、bk_nextsize,这四个指针的作用在前面有提到过
chunk相关宏定义 参见CTFwiki,里面有很详细的介绍,这些宏定义在之后的源代码分析中都用的到
对于空闲的 chunk,ptmalloc 采用分箱式内存管理方式,根据空闲 chunk 的大小和处于 的状态将其放在四个不同的 bin 中,这四个空闲 chunk 的容器包括 fast bins,unsorted bin, small bins 和 large bins。Fast bins 是小内存块的高速缓存,当一些大小小于 64 字节的 chunk 被回收时,首先会放入 fast bins 中,在分配小内存时,首先会查看 fast bins 中是否有合适的 内存块,如果存在,则直接返回 fast bins 中的内存块,以加快分配速度。Usorted bin 只有一 个,回收的 chunk 块必须先放到 unsorted bin 中,分配内存时会查看 unsorted bin 中是否有 合适的 chunk,如果找到满足条件的 chunk,则直接返回给用户,否则将 unsorted bin 的所 有 chunk 放入 small bins 或是 large bins 中。Small bins 用于存放固定大小的 chunk,共 64 个 bin,最小的 chunk 大小为 16 字节或 32 字节,每个 bin 的大小相差 8 字节或是 16 字节,当 分配小内存块时,采用精确匹配的方式从 small bins 中查找合适的 chunk。Large bins 用于存 储大于等于 512B 或 1024B 的空闲 chunk,这些 chunk 使用双向链表的形式按大小顺序排序, 分配内存时按最近匹配方式从 large bins 中分配 chunk。
ptmalloc 维护了 62 个双向环形链表(每个链表都具有链表头节点,加头节点的最大作 用就是便于对链表内节点的统一处理,即简化编程),每一个链表内的各空闲 chunk 的大小 一致,因此当应用程序需要分配某个字节大小的内存空间时直接在对应的链表内取就可以了, 这样既可以很好的满足应用程序的内存空间申请请求而又不会出现太多的内存碎片。我们可 以用如下图来表示在 SIZE_SZ 为 4B 的平台上 ptmalloc 对 512B 字节以下的空闲 chunk 组织方 式(所谓的分箱机制)。
Large bins 一共包括 63 个 bin, 每个 bin 中的 chunk 大小不是一个固定公差的等差数列,而是分成 6 组 bin,每组 bin 是一个 固定公差的等差数列,每组的 bin 数量依次为 32、16、8、4、2、1,公差依次为 64B、512B、 4096B、32768B、262144B 等。
例如:第一个large_bin的起始地址为512B
unsorted bin 处于我们之前所说的bin数组下标1处。故而 unsorted bin只有一个链表。Unsorted bin 可以看作是 small bins 和 large bins 的 cache.,以双 向链表管理空闲 chunk,空闲 chunk 不排序,所有的 chunk 在回收时都要先放到 unsorted bin中,分配时,如果在 unsorted bin 中没有合适的 chunk,就会把 unsorted bin 中的所有 chunk 分别加入到所属的 bin 中,然后再在 bin 中分配合适的 chunk
fast bins 主要是用于提高小内存的分配效率,默认情况下,对于 SIZE_SZ 为 4B 的平台, 小于 64B 的 chunk 分配请求,对于 SIZE_SZ 为 8B 的平台,小于 128B 的 chunk 分配请求,首 先会查找 fast bins 中是否有所需大小的 chunk 存在(精确匹配),如果存在,就直接返回
有了这些数据结构的知识,可以去研究内存分配与回收的具体实现了。
Void_t* public_mALLOc(size_t bytes)
{
mstate ar_ptr;
Void_t *victim;
__malloc_ptr_t (*hook) (size_t, __const __malloc_ptr_t)= force_reg (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));
首先检查是否存在内存分配的 hook 函数,如果存在,调用 hook 函数,并返回,hook 函数主要用于进程在创建新线程过程中分配内存,或者支持用户提供的内存分配函数.
arena_lookup(ar_ptr);
arena_lock(ar_ptr, bytes); //获取分配区指针并上锁
if(!ar_ptr)
return 0;
victim = _int_malloc(ar_ptr, bytes); //调用_int_malloc函数分配相应大小内存
获取分配区指针,如果获取分配区失败,返回退出,否则,调用_int_malloc()函数分配 内存
static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size */
unsigned int idx; /* associated bin index */
mbinptr bin; /* associated bin */
mchunkptr victim; /* inspected/selected chunk */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */
mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */
unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */
mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */
const char *errstr = NULL;
/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
checked_request2size(bytes, nb);
定义了一堆内部变量,并通过checked_request2size()将所需分配的字节大小转换成chunk大小(添加控制头并对齐)
/*
If the size qualifies as a fastbin, first check corresponding bin.
This code is safe to execute even if av is not yet initialized, so we
can try it without checking, which saves some time on this fast path.
*/
if ((unsigned long)(nb) <= (unsigned long)(get_max_fast ())) {
idx = fastbin_index(nb);
mfastbinptr* fb = &fastbin (av, idx);
#ifdef ATOMIC_FASTBINS
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))!= victim);
#else
victim = *fb;
#endif
if (victim != 0) {
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim));
}
#ifndef ATOMIC_FASTBINS
*fb = victim->fd;
#endif
check_remalloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
if (__builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}
}
这里的部分代码开启是 ATOMIC_FASTBINS 优化,用于防止多线程时产生的错误。我们只想搞清楚代码逻辑,因此可以无视ATOMIC_FASTBINS优化,这样的化只需看下面代码即可
if ((unsigned long)(nb) <= (unsigned long)(get_max_fast ())) {
idx = fastbin_index(nb); //获得对应的fastbin的下标
mfastbinptr* fb = &fastbin (av, idx); // 得到对应的fastbin的头指针
victim = *fb;
if (victim != 0) {
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr,chunk2mem (victim));
}
*fb = victim->fd; //将头指针的下一个 chunk 作为空闲 chunk 链表的头部。
check_remalloced_chunk(av, victim, nb);
void *p = chunk2mem(victim); //取出第一个chunk
if (__builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}
首先根据所需chunk大小获得所需 fast bin 的空 闲 chunk 链表的头指针,然后将头指针的下一个 chunk 作为空闲 chunk 链表的头部。取出第一个chunk,并调用chunk2mem() 函数返回用户所需的内存块。
如果需要分配的大小属于small bin,则会执行以下代码:
/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
if (in_smallbin_range(nb)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena) set_non_main_arena(victim);
// 细致的检查,非调试状态没有作用
check_malloced_chunk(av, victim, nb);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else {
idx = largebin_index(nb);
if (have_fastchunks(av))
malloc_consolidate(av);
}
如果fast bin和small bin都不符合要求,那就一定是large bin。在分配large bin的时候,并不会直接去分配,而是先检查是否存在fast bin,如果存在则调用malloc_consolidate()函数合并fast bin中的chunk,并将这些空闲chunk加入到unsorted bin中。
遍历unsorted bin
// 如果 unsorted bin 不为空
// First In First Out
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
// victim 为 unsorted bin 的最后一个 chunk
// bck 为 unsorted bin 的倒数第二个 chunk
bck = victim->bk;
// 判断得到的 chunk 是否满足要求,不能过小,也不能过大
// 一般 system_mem 的大小为132K
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
// 得到victim对应的chunk大小。
size = chunksize(victim);
先写到这儿了,剩下的考完试再写(未完待续。。。)