本文对glibc堆管理器的各项主要内存操作,以及glibc 2.26后引入的tcache机制进行源码级分析,可作为查找使用。
glibc memory operations
- 第一次malloc,会初始分配一个
0x290
的chunk,top chunk split返回给user后,剩余部分继续作为top chunk - 通常heap的第一个chunk,
prev_inuse
都为1,防止非法内存访问
unlink
- 将双向链表中的一个chunk取出,也就是链表中的删除元素
- 维护
fd, bk
链表,若为large bin,则同时维护nextsize
链表 - 可能的泄漏点:不会修改两个链表的指针,可能会造成chunk地址泄漏
-
libc地址(通过
main_arena
)- Chunk 为head chunk,bk泄漏bins地址
- chunk 为tail chunk,fd泄漏bins地址
-
泄漏堆地址(中间的chunk都可)
-
static void
unlink_chunk (mstate av, mchunkptr p)
{
// check chunk size in 2 positions
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
// check double-linked list
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
// unlink
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{ // 维护nextsize双向链表,仅有large bin使用
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)
{ // 下一个块不在nextsize链表中,即size与p相同
if (p->fd_nextsize == p) // nextsize链表仅有一个元素
fd->fd_nextsize = fd->bk_nextsize = fd; // 重新构建,将下一个块作为nextsize链表
else
{ // nextsize链表不只一个元素,将fd插入nextsize链表(代替当前size)
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;
}
}
}
malloc
handler entry: __libc_malloc
, finally invokes _int_malloc
__libc_malloc
- 检查memory hook,若有,则直接执行
- 初始化时,将
memory_hook
设为malloc_hook_ini
,执行ptmalloc_init
- 初始化时,将
- 若配置tcache,使用tache
- 若程序为单线程,则直接调用
_int_malloc(main_arena, size)
进行内存分配,并直接返回 arena_get
获取可用arena,inpage ref- 调用
_int_malloc
进行实际malloc调用
_int_mallloc
-
调整request size到
nb
(+SIZE_SZ
,只需要一个,因为数据可以存储在下一个chunk的topSIZE_SZ
bytes)- 如果没有可用的arena,fallback到
sysmalloc
,使用mmap来分配chunk
- 如果没有可用的arena,fallback到
-
如果size满足fast bin,则直接到fast bin进行分配,有结果就直接返回
-
若在small bin 范围内,则尝试small bin,有结果就直接返回
-
否则调用large bin,只计算index,不直接搜索结果
-
在unsorted bin中遍历,从后(bk)往前 (av)
- 对于每一个unsorted bin中的chunk,检查其next块的属性(size)等是否与其匹配,检查fd和bk双向链表是否损坏
- 如果请求的是一个small bin,并且当前unsorted bin中只有一个last remainder chunk,就直接使用它。这将有助于提升在频繁malloc 小块时的效率。这是唯一没有使用best fit策略的地方,并且只有在没有合适的small chunk能够匹配当前请求时才会使用。
- 将chunk移出 unsorted bin,即使没有被直接使用,也会被放回对应的bin中
- 如果大小匹配,则直接使用该块,不放回
- 否则将该块放回bin
- 若为small bin,直接找到位置
- 若为large bin,则还需要保持其有序,还需要另行处理
nextsize
双链表
-
遍历结束,所有unsorted bin被清空,若没有刚好匹配上的chunk,则进行下一步
-
若request为large bin
- 若在当前bin 中找到第一个合适大小的chunk,将其移出队列(若有多个符合,取第二个,否则会破坏nextsize链表)
- 剩余部分不足MIN_SIZE,则使用整块
- 否则进行split,剩余部分组成一个remainder,加入unsorted bin头部,并设置chunk 内容
- 否则基于bitmap寻找下一个非空bin
- 注意bitmap延迟响应,偏否的蒙特卡洛,只有为0时才一定为空
- 所以检验得到false alarm后,需要更新bit
-
否则最后一步,使用 top chunk
use_top
- 若top chunk不足,则会进行fast bin碎片整理
consolidate
- 若没有fast bin,则调用sysmalloc进行堆扩容
- 以上两种例外情况结束后,都将返回上面重新检查bin等
- 若top chunk不足,则会进行fast bin碎片整理
-
通用
- 当一个chunk被选中使用,会执行以下操作
- 对应位置位(inuse,main_arena)
- 操作tcache,优先fill tcache,若已满则返回给用户
check_malloced_chunk
为debug预留接口,默认不开启chunk2mem
将chunk地址转换为user data首地址(加上header偏移0x20
)
- 当一个chunk被选中使用,会执行以下操作
ptmalloc_init
- 第一次运行malloc时,作为memory hook内容(
malloc_hook_ini
)被调用,所有线程和起来只用一次 - 所有线程的
main_arena
相同,都指向主线程的那个 - 初始化
main_arena
:malloc_init_state
,主要初始化bins链表头,设置top为unsorted bin(假top)
arena_get
获取一个可用arena,并上锁。如果没有找到可用的arena,则新建一个arena_get2
/* arena_get() acquires an arena and locks the corresponding mutex.
First, try the one last locked successfully by this thread. (This
is the common case and handled with a macro for speed.) Then, loop
once over the circularly linked list of arenas. If no arena is
readily available, create a new one. In this latter case, `size'
is just a hint as to how much memory will be required immediately
in the new arena. */
#define arena_get(ptr, size) do { \
ptr = thread_arena; \
arena_lock (ptr, size); \
} while (0)
#define arena_lock(ptr, size) do { \
if (ptr) \
__libc_lock_lock (ptr->mutex); \
else \
ptr = arena_get2 ((size), NULL); \
} while (0)
Arena_get2
:
- 若当前存在空闲arena in
free_list
,则直接返回 - 若当前无空闲,并且当前arena数量没有超过限制,则新建一个arena -
_int_new_arena
- 否则复用一个现有的arena
_int_new_arena
new_heap
调用mmap
来分配arena空间,top_pad
由multi thread配置,默认0x20000
,用于指定top chunk的padding- 前部size包含heap info以及arena 结构体(
mstate
),经过align up后对其到页(后面需要进行mmap操作),共0x21000
- mmap内存区间,映射size 为
HEAP_MAX_SIZE
,为DEFAULT_MMAP_THRESHOLD_MAX*2 = 64MB
,初始不赋予权限- 需要对齐到
HEAP_MAX_SIZE
,所以一开始mmap 2倍空间,并使用中间经过对齐的一段(类似align up),如果一开始mmap的2倍空间已经对齐,在下次申请新arena时,会使用原来剩余的部分,减少heap对内存空间的碎片化
- 需要对齐到
- 对申请部分(包括
align_top
)进行mprotect
,赋予读写权限 - 初始化
heap_info
其他信息
- 前部size包含heap info以及arena 结构体(
- 将arena放置在
heap_info
之后,并设置参数 - 将top chunk 放置在arena之后,并设置参数
Offset | Name | Type | Size |
---|---|---|---|
0x0 | Heap info | Heap_info | 0x20 |
0x20 | arena | Malloc_state | 0x898 |
0x8c0 (aligned) | Top chunk | malloc_chunk |
sysmalloc
- 若request size过大(
>= mp_.mmap_threshold
(0x20000
)),并且当前mmap块数量少于上限,则直接使用mmap为request分配,而不是扩展top- mmap分配的空间对齐到page size
- 由于不能使用下一个chunk的
prev_size
位置,所以当前chunk存在size_sz
的overhead - 同样将mmap内容作为chunk,需要初始化prev,以及size
- 正常拓展top
- 首先记录初始top信息,并检查top情况
- 若为main_arena
- 为top留出足够空间(
top_pad
) - 调用
MORECORE
来获取更多内存,系统中使用_sbrk
- (若存在)调用hook
- 若brk失败,则尝试mmap
- 使用mmap后,sbrk region为非连续段
- 若sbrk为拓展已有的top,简单拓展top即可
- 否则对堆做出调整
- 若为contiguous,且已有旧堆(比较奇怪),则需要增加旧堆的大小(增加到
old_sz + size
,即拓展新需求的空间),且记录新的brk位置到snd_brk
- 若非contiguous,需要建立fencepost
- 建立新的top chunk,并将原top chunk free掉
- 若为contiguous,且已有旧堆(比较奇怪),则需要增加旧堆的大小(增加到
- 为top留出足够空间(
- 若非 main_arena
- 尝试使用mmap来拓展已有堆
grow_heap
,heap最大不超过HEAP_MAX_SIZE
- 若失败,建立新堆
new_heap
- 将旧有的top chunk free掉,设置fence post
- 将heap的arena等属性设置好,将新开辟的空间初始化为top chunk
- 尝试使用mmap来拓展已有堆
- 完成拓展,将top chunk split,并返回给user
其他封装函数
calloc
返回n个元素和指定大小的内存,全部初始化为0
free
entry: __libc_free
libc_free
- 检查hook (通常没有)
- 将pointer转换为chunk ptr
- 若为mmap chunk (检查
mmaped bit
-chunk_is_mmaped
)- 调整mmap threshold,后续大小更小的(不超过最大阈值)的request不会触发mmap
__munmap
解除块映射
- 调用
_int_free
_int_free
- 检查chunk各项参数
- 处理tcache
- 检查fast bin
- 将chunk 插入bin头部,若非单线程,则需要使用原子操作
- 检查非mmapped chunk
- 尝试合并前面的chunk(如果
!prev_inuse
)- 若可以合并,则
unlink
前面的chunk
- 若可以合并,则
- 尝试合并后面的chunk
- 若后面为top chunk,则直接修改当前chunk参数,将其合并,直接结束
- 否则,若后面chunk正在使用,则将其
prev_inuse
清空,否则同样unlink
- 否则,若后面chunk正在使用,则将其
- 若后面为top chunk,则直接修改当前chunk参数,将其合并,直接结束
- 将可能合并后的chunk重新设置头,并将其插入unsorted bin头部(仅需要维护fd/bk链表即可,若为large chunk,还需要清空
next_size
位置)
- 尝试合并前面的chunk(如果
- 如果目前free的size(合并后)大于
FASTBIN_CONSOLIDATION_THRESHOLD
,则进行consolidate- 首先调用
malloc_consolidate
进行整理 - 若为main arena,则使用
systrim
缩小arena体积,缩小量为top pad - 否则使用
heap_trim
缩小mmap堆
- 首先调用
malloc_consolidate
- 对所有fast bin中的bin进行遍历
- 对每一个bin中的chunk进行遍历
- 合并前后chunk,与
_int_free
中第4步(检查非mmapped chunk)完全相同
- 合并前后chunk,与
- 对每一个bin中的chunk进行遍历
systrim
- 只在当前top chunk末尾与
current_brk
相同时使用,避免外部sbrk对外部数据的影响 - 调用
MORECORE(-extra)
将多余部分裁剪 - update top chunk and heap
heap_trim
- 如果当前heap仅剩top chunk,说明整个heap都可以被删除
- 清除上一个heap的fencepost,检查后将上一个heap的大小拓宽fencepost大小
- 删除当前heap
- consolidate previous chunk
- 更新 top
- 尝试收缩当前heap
shrink_heap
将多余部分重新映射为不可访问区域,用于节约内存
Other Operations
realloc
重新分配该内存块,提供两个参数oldmem和size,分别制定原内存块指针地址和需要修改的大小
- 若oldmem为空,直接转malloc,若size为0并且oldmem不为空,直接转free
- 进行相关安全检查,如wrap around,size等
- 若为mmaped chunk
- 尝试mremap (linux syscall),成功即返回
mremap_chunk
- 否则malloc,并进行
memcpy
,复制内存内容
- 尝试mremap (linux syscall),成功即返回
- 否则
- 若为单线程,则直接调用
_int_realloc
- 若为多线程,则尝试在该arena上进行
_int_realloc
,失败则在其他arena上尝试malloc
- 若为单线程,则直接调用
_int_realloc
- check size
- if new_size < old_size, 直接选择该块待用
- 若下一个chunk是top chunk,则缩减top chunk 直接使用,直接return
- 若下一个chunk 空闲且空间足够,直接使用下一个chunk,先unlink
- 否则,allocate, copy, free
- 若当前选择的chunk(该块,或与下一个合并块)较大,则需要split
tcache
tcache是glibc 2.26后引入技术,用于提升堆小块管理性能,但是缺乏检查,存在较多漏洞
structure
tcache引入两个新结构体tcache_entry
, tcache_perthread_struct
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS]; // count for entries
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
tcache_entry
用于指向下一个chunk的user data部分- 将
tcache_entry
放在chunk的user data部分,并且会复用该部分(tcache_entry
结构体)
工作原理
- 第一次 malloc 时,会先 malloc 一块内存用来存放
tcache_perthread_struct
-MAYBE_INIT_TCACHE
- free 内存,且 size 小于 small bin maxsize 时
- tcache 之前会放到 fastbin 或者 unsorted bin 中
- 在tcache出现后:
- 先放到对应的 tcache 中,直到 tcache 被填满(mp_.tcache_count, 默认是 7 个)
- tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
- tcache 中的 chunk 不会合并(不取消 inuse bit,fastbin也不会)
- malloc 内存,且 size 在 tcache 范围内
- 先从 tcache 取 chunk,直到 tcache 为空
- tcache 为空后,从 bin 中找
- tcache 为空时,如果
fastbin/smallbin/unsorted bin
中有 size 符合的 chunk,会先把fastbin/smallbin/unsorted bin
中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来
Source Code
MAYBE_INIT_TCACHE
tcache_init()
初始化tcache
- 获取一个可用arena,并malloc
tcache_perthread_struct
- 设置tcache变量,并memset清空区域
free
in _int_free
- 在user data部分建立一个entry
- 检查tcache double free
tcache_put
将chunk 放入tcache
tcache_put
- 将chunk插入tcache单链表
- 设置entry的key为
tcache
,以便检查double free
malloc
- 若对应位置(
tcidx
)count不为0,则可以使用tcache (可在bins命令中查看) - 使用FIFO机制,同样从头部取一个块返还给user
- 由于entry直接为user data头部指针,所以直接返还给用户空间