堆释放(Heap Release)是将已分配的堆内存返还给操作系统,以供其他部分使用。在C语言中,可以使用free()函数来释放动态分配的内存。堆释放的目的是为了释放无用的内存,避免内存泄漏和内存碎片问题,同时提高内存利用率。
一、__libc_free
同malloc()函数一样,free()函数实际上是_libc_free(),其定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void __libc_free(void *mem) { mstate ar_ptr; mchunkptr p; void (*hook)(void *, const void *) = atomic_forced_read(__free_hook); if (__builtin_expect(hook != NULL, 0)) { (*hook)(mem, RETURN_ADDRESS(0)); return; } if (mem == 0) return; p = mem2chunk(mem); if (chunk_is_mmapped(p)) { if (!mp_.no_dyn_threshold && chunksize_nomask(p) > mp_.mmap_threshold && chunksize_nomask(p) <= DEFAULT_MMAP_THRESHOLD_MAX && !DUMPED_MAIN_ARENA_CHUNK(p)) { mp_.mmap_threshold = chunksize(p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk(p); return; } ar_ptr = arena_for_chunk(p); _int_free(ar_ptr, p, 0); }
|
这里主要关注__free_hook和_int_free函数,free_hook函数在后续溢出get_shell的时候会遇到,_int_free则是后续执行free的主体。
二、_int_free
首先定义一系列所需的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13
| static void _int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; mfastbinptr *fb; mchunkptr nextchunk; INTERNAL_SIZE_T nextsize; int nextinuse; INTERNAL_SIZE_T prevsize; mchunkptr bck; mchunkptr fwd;
const char *errstr = NULL; int locked = 0; size = chunksize (p);
|
对chunk做—些检查。
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) || __builtin_expect (misaligned_chunk (p), 0)) { errstr = "free(): invalid pointer"; errout: if (!have_lock && locked) (void) mutex_unlock (&av->mutex); malloc_printerr (check_action, errstr, chunk2mem (p), av); return; } if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size))) { errstr = "free(): invalid size"; goto errout; } check_inuse_chunk(av, p);
|
然后,判断该chunk是否在fast bin范围内,如果是,就插入到fast bin中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| if ((unsigned long)(size) <= (unsigned long)(get_max_fast ()) #if TRIM_FASTBINS && (chunk_at_offset(p, size) != av->top) #endif ){ if (__builtin_expect ( chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0) || __builtin_expect ( chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0)) { if (have_lock || ({ assert (locked == 0); mutex_lock(&av->mutex); locked = 1; chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem; })) { errstr = "free(): invalid next size (fast)"; goto errout; } if (! have_lock) { (void)mutex_unlock(&av->mutex); locked = 0; } } free_perturb (chunk2mem(p), size - 2 * SIZE_SZ); set_fastchunks(av); unsigned int idx = fastbin_index(size); fb = &fastbin (av, idx); mchunkptr old = *fb, old2; unsigned int old_idx = ~0u; do { if (__builtin_expect (old == p, 0)) { errstr = "double free or corruption (fasttop)"; goto errout; } if (have_lock && old != NULL) old_idx = fastbin_index(chunksize(old)); p->fd = old2 = old; } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0)) { errstr = "invalid fastbin entry (free)"; goto errout; } }
|
如果该chunk并非mmap()生成的,就需要进行合并。先向后合并,再向前合并。在没有锁的情况下,先获得锁,然后确认该chunk可以合并。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| else if (!chunk_is_mmapped(p)) { if (! have_lock) { (void)mutex_lock(&av->mutex); locked = 1; } nextchunk = chunk_at_offset(p, size); if (__glibc_unlikely (p == av->top)) { errstr = "double free or corruption (top)"; goto errout; } if (__builtin_expect (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)), 0)) { errstr = "double free or corruption (out)"; goto errout; } if (__glibc_unlikely (!prev_inuse(nextchunk))) { errstr = "double free or corruption (!prev)"; goto errout; } nextsize = chunksize(nextchunk); if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0) || __builtin_expect (nextsize >= av->system_mem, 0)) { errstr = "free(): invalid next size (normal)"; goto errout; } free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
|
之后进行合并。向后合并、向前合并、unlink单独介绍。如果合并后的 chunk 的大小大于FASTBIN_CONSOLIDATION_THRESHOLD,那就向系统返还内存。一般合并到 top chunk 都会执行这部分代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) { if (have_fastchunks(av)) malloc_consolidate(av); if (av == &main_arena) { #ifndef MORECORE_CANNOT_TRIM if ((unsigned long)(chunksize(av->top)) >= (unsigned long)(mp_.trim_threshold)) systrim(mp_.top_pad, av); #endif } else { heap_info *heap = heap_for_ptr(top(av)); assert(heap->ar_ptr == av); heap_trim(heap, mp_.top_pad); } } if (! have_lock) { assert (locked); (void)mutex_unlock(&av->mutex); } } else { munmap_chunk (p); } }
|
整体流程:
①如果在 fastbin 范围则加入到 fast bin 头部并返回。否则转下一步。
②如果不是 mmap 申请的内存,就进行合并。先向后合并,再向前合并。
③如果合并后的 chunk 的大小大于FASTBIN_CONSOLIDATION_THRESHOLD 就向系统返还内存。否则调用munmap_chunk 释放 chunk。
三、unlink
unlink在合并和切割remainder时会用到。先介绍unlink,基础unlink代码如下:
1 2 3 4 5 6 7 8 9 10
| unlink_chunk (mstate av, mchunkptr p) { if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) malloc_printerr ("corrupted size vs. prev_size"); FD = p->fd; BK = p->bk; if (__builtin_expect (FD->bk != p || BK->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); FD->bk = BK; BK->fd = FD;
|
1、首先,检查当前内存块 P 的大小与下一个内存块的 prev_size 是否一致。如果不一致,表示堆数据结构出现了错误。
2、保存当前内存块 P 的前驱和后继节点的指针到 BK 和 FD 变量中。
3、检查前驱节点的 bk 指针和后继节点的 fd 指针是否正确指向当前内存块 P。如果不正确,表示双向链表的链接关系出现了错误。
4、如果前驱和后继节点的链接关系正确,就将前驱节点的 bk 指针指向后继节点,后继节点的 fd 指针指向前驱节点。即将当前内存块 P 从双向链表中移除。
如图所示:

剩余代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)"); if (FD->fd_nextsize == NULL) { if (p->fd_nextsize == p) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = p->fd_nextsize; FD->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = FD; p->bk_nextsize->fd_nextsize = FD; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } }
|
5、判断它是否在large bin的链表中。即当前内存块 P 不属于小型内存块范围(in_smallbin_range),并且具有 fd_nextsize 指针。在的话转到第6步,不在就结束程序。
6、检查 P 的fd_nextsize、bk_nextsize是否正确链接到前后节点。如果不正确,表示大型large bin的链接关系出现错误。
7、判断下一个chunk的fd_nextsize是否为空。即判断P是否是当前链表中的唯一一个在large bin中的空闲块。如果不是,转到第10步。
8、判断p的下一个大小不同的空闲块是否指向自身,即判断是否是large bin中唯一一个chunk。如果是,将下一个空闲块fd_nextsize、bk_nextsize都指向它本身,即重新初始化large bin。
9、否则,将下一个空闲块fd_nextsize、bk_nextsize指向p的fd_nextsize、bk_nextsize,将lagre bin上p的前后节点都指向下一个空闲块。(使p的下一块chunk替代p)
10、将前驱节点的 bk_nextsize 指针指向后继节点,后继节点的 fd_nextsize 指针指向前驱节点。即将当前内存块 P 从large bin中移除。
四、向后合并
合并低地址处相邻的chunk,会先更新 p 的 size 以及指向,然后调用 unlink() 宏将 chunk 从其链接的 bin 中脱链。代码如下:
1 2 3 4 5 6
| if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink(av, p, bck, fwd); }
|
①检查当前chunk的前一块chunk是否是free(检查p的previnuse位是否为0)(如果空闲就返回为真,进行向后合并)
②获取前一块chunk的size,并更新size
③通过减p->prev_size来获得指向前一块chunk的指针,chunk_at_offset将p+size这段内存强制看成一个chunk结构体。
④把p指向的块进行脱链,也就是前一块chunk进行unlink
如图所示:

在这里值得注意的是,这里的size是一个变量,P->size并没有发生改变;其次chunk_at_offset只是返回一个地址(指针),即只是P的指向改变了,其它内存布局都没有变。后续会在向前合并中,通过set_head来改变内存布局(一定会发生)。
五、向前合并
合并高地址处相邻的chunk。如果向前相邻 top_chunk****,则直接合并到 top_chunk****。
1 2 3 4 5 6 7 8 9
| if (nextchunk != av->top) { ... } else { size += nextsize; set_head(p, size | PREV_INUSE); av->top = p; check_chunk(av, p); }
|
该操作会将size加上top chunk的size,通过set_head将p、size和状态位设置到内存块头部(top chunk的PREV_INUSE始终为1),然后将top chunk指针指向p。

如果向前不相邻 top_chunk,则尝试向前合并后插入到 unsorted_bin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { unlink(av, nextchunk, bck, fwd); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0); bck = unsorted_chunks(av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("free(): corrupted unsorted chunks"); p->fd = fwd; p->bk = bck; if (!in_smallbin_range(size)){ p->fd_nextsize = NULL; p->bk_nextsize = NULL; } bck->fd = p; fwd->bk = p; set_head(p, size | PREV_INUSE); set_foot(p, size); check_free_chunk(av, p); }
|
检测nextchunk是否free,是通过inuse_bit_at_offset(nextchunk, nextsize)来获得nextchunk的相邻下一块chunk的的PREV_INUSE(P)位实现的。P为 0就表示已经被free,就进入unlink流程。如果nextchunk不是free,就修改nextchunk的PREV_INUSE(P)为0,表示当前chunk是free的。
执行前:

执行后:

如果可以向前合并就合并,不论是否合并,都会将其插入到unsorted_bin中,然后执行set_head和set_foot,来重设chunk。