glibc源码分析-malloc-free
字数 2936 2025-08-22 12:22:37
glibc内存管理机制深入解析:malloc与free的实现原理
1. 核心数据结构分析
1.1 malloc_state结构体
malloc_state是ptmalloc分配器的核心管理结构,用于管理内存分配区(arena):
struct malloc_state {
mutex_t mutex; // 访问序列化锁
int flags; // 标志位
mfastbinptr fastbinsY[NFASTBINS]; // 快速bin数组
mchunkptr top; // 顶部空闲chunk
mchunkptr last_remainder; // 最近分割剩余的chunk
mchunkptr bins[NBINS * 2 - 2]; // 常规bin数组
unsigned int binmap[BINMAPSIZE]; // bin位图
struct malloc_state *next; // 链接的下一个arena
struct malloc_state *next_free; // 空闲arena链表
INTERNAL_SIZE_T attached_threads; // 附加线程数
INTERNAL_SIZE_T system_mem; // 系统分配的内存总量
INTERNAL_SIZE_T max_system_mem; // 系统分配的内存最大值
};
1.2 malloc_chunk结构体
malloc_chunk是内存块的基本管理单元:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; // 前一个chunk的大小(如果空闲)
INTERNAL_SIZE_T mchunk_size; // 当前chunk大小(包括元数据)
struct malloc_chunk *fd; // 双向链表前向指针(仅空闲时使用)
struct malloc_chunk *bk; // 双向链表后向指针(仅空闲时使用)
struct malloc_chunk *fd_nextsize; // 大块专用:指向下一个不同大小的块
struct malloc_chunk *bk_nextsize; // 大块专用:指向前一个不同大小的块
};
2. 内存分配流程(malloc)
2.1 初始化过程
首次调用__libc_malloc时的初始化流程:
- 检查
__malloc_hook,初始时指向malloc_hook_ini malloc_hook_ini函数将__malloc_hook置为NULL- 调用
ptmalloc_init进行初始化 - 设置
__malloc_initialized > 0表示初始化完成
2.2 分配流程
- 请求大小转换:
checked_request2size(bytes, nb)将用户请求大小转换为实际分配的chunk大小 - 获取分配区:
arena_get(ar_ptr, bytes)获取管理空闲空间的分配区 - 分配路径选择:
- fastbin分配:当
nb <= get_max_fast()时- 计算fastbin索引:
idx = fastbin_index(nb) - 获取对应fastbin链表头:
fb = &fastbin(av, idx)
- 计算fastbin索引:
- smallbin分配:当
in_smallbin_range(nb)为真时- 计算smallbin索引:
idx = smallbin_index(nb) - 获取对应smallbin:
bin = bin_at(av, idx)
- 计算smallbin索引:
- largebin分配:其他情况
- 先合并fastbin:
malloc_consolidate(av)
- 先合并fastbin:
- fastbin分配:当
- top chunk分割:当其他bin无法满足时
- 检查top chunk大小:
size = chunksize(av->top) - 如果足够则分割:
remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head(remainder, remainder_size | PREV_INUSE);
- 检查top chunk大小:
- 系统内存申请:当所有方法失败时调用
sysmalloc
3. 内存释放流程(free)
3.1 基本释放流程
- hook检查:检查
__free_hook是否为NULL - 指针转换:
p = mem2chunk(mem)将用户指针转换为chunk指针 - mmap检查:
chunk_is_mmapped(p)判断是否由mmap分配 - 获取分配区:
ar_ptr = arena_for_chunk(p) - 实际释放:调用
_int_free(ar_ptr, p, 0)
3.2 _int_free详细流程
-
安全检查:
- 地址有效性检查:
(uintptr_t)p > (uintptr_t)-size - 对齐检查:
aligned_OK(size) - 大小检查:
size >= MINSIZE
- 地址有效性检查:
-
fastbin释放:
- 检查大小:
(unsigned long)(size) <= (unsigned long)(get_max_fast()) - 设置fastbin标志:
set_fastchunks(av) - 获取fastbin索引:
idx = fastbin_index(size) - 检查double free:
if (__builtin_expect(old == p, 0)) - 插入fastbin链表:
p->fd = old2 = old
- 检查大小:
-
合并与unsorted bin:
- 检查相邻chunk是否空闲
- 合并操作:
unlink将相邻空闲chunk从链表中移除 - 插入unsorted bin:
bck = unsorted_chunks(av)
4. 关键机制与安全防护
4.1 unlink操作
unlink宏用于从双向链表中安全移除一个chunk:
#define unlink(AV, P, BK, FD) {
// 大小一致性检查
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");
else {
FD->bk = BK;
BK->fd = FD;
// 处理largebin的nextsize链表
if (!in_smallbin_range(chunksize_nomask(P)) &&
__builtin_expect(P->fd_nextsize != NULL, 0)) {
// nextsize链表完整性检查
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) ||
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr("corrupted double-linked list (not small)");
// 更新nextsize链表
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;
}
}
}
}
4.2 安全防护机制
-
Double Free检测:
- 检查fastbin链表头是否等于当前释放的chunk
- 绕过方法:在两次释放之间释放一个同大小的chunk
-
大小检查:
- fastbin取出时检查:
fastbin_index(chunksize(victim)) != idx - 利用
find_fake_fast工具伪造size
- fastbin取出时检查:
-
链表完整性检查:
- 检查
FD->bk == P && BK->fd == P - 检查nextsize链表的完整性
- 检查
5. 利用技术分析
5.1 信息泄露技术
-
unsorted bin泄露libc地址:
- 原理:释放到unsorted bin的chunk会记录main_arena地址
- 实现:释放chunk后读取其fd/bk指针
-
smallbin残留指针:
- 合并两个smallbin时可能残留main_arena指针
- 申请时不覆盖这些指针可泄露信息
5.2 任意地址写技术
-
unsorted bin攻击:
unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);- 控制bk可将
unsorted_chunks(av)写入任意地址-2*SIZE_SZ处 - 写入值为一个很大的数值
- 控制bk可将
-
largebin攻击:
- 利用largebin的nextsize链表操作
- 通过精心构造实现任意地址写
5.3 House of系列攻击
-
House of Spirit:
- 伪造fastbin chunk并释放
- 下次分配可获取伪造chunk
-
House of Einherjar:
- 利用off-by-one等漏洞修改chunk的size
- 实现chunk overlap
-
House of Force:
- 修改top chunk的size
- 控制后续分配位置
6. 关键宏定义解析
6.1 大小相关宏
#define chunksize(p) ((p)->size & ~(SIZE_BITS)) // 获取chunk实际大小
#define in_smallbin_range(sz) ((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE)
#define smallbin_index(sz) ((sz) >> SMALLBIN_WIDTH_SHIFT)
#define largebin_index_32(sz) \
((((unsigned long)(sz)) >> 6) <= 38)? 56 + (((unsigned long)(sz)) >> 6): \
((((unsigned long)(sz)) >> 9) <= 20)? 91 + (((unsigned long)(sz)) >> 9): \
((((unsigned long)(sz)) >> 12) <= 10)? 110 + (((unsigned long)(sz)) >> 12): \
((((unsigned long)(sz)) >> 15) <= 4)? 119 + (((unsigned long)(sz)) >> 15): \
((((unsigned long)(sz)) >> 18) <= 2)? 124 + (((unsigned long)(sz)) >> 18): \
126
6.2 指针转换宏
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ)) // 用户指针转chunk指针
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ)) // chunk指针转用户指针
#define bin_at(m, i) \
(mbinptr)(((char*)&((m)->bins[((i) - 1) * 2])) - offsetof(struct malloc_chunk, fd))
6.3 标志位宏
#define PREV_INUSE 0x1 // 前一个chunk在使用中
#define IS_MMAPPED 0x2 // 当前chunk由mmap分配
#define NON_MAIN_ARENA 0x4 // 当前chunk不属于主分配区
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
7. 调试与分析方法
-
gdb调试技巧:
p main_arena查看主分配区状态heap命令查看堆布局b _int_free在释放函数设断点
-
关键函数断点:
__libc_malloc_int_malloc__libc_free_int_freesysmalloc
-
内存布局分析:
- fastbin: FILO(先进后出)单链表
- smallbin: FIFO(先进先出)双向链表
- largebin: 大小排序的双向链表+nextsize索引
- unsortedbin: FIFO双向链表
8. 性能优化策略
-
fastbin设计:
- 小内存快速分配
- 单链表操作,无合并操作
-
binmap设计:
- 快速定位非空bin
- 减少遍历开销
-
多arena设计:
- 减少线程竞争
next和next_free管理arena链表
-
last_remainder优化:
- 记录最近分割的剩余chunk
- 提高局部性分配效率
9. 安全编程建议
-
分配注意事项:
- 检查malloc返回值是否为NULL
- 避免整数溢出导致分配异常大小
-
释放注意事项:
- 避免double free
- 释放后及时置空指针
- 避免use-after-free
-
敏感操作防护:
- 关键数据结构设置完整性校验
- 重要操作前进行安全检查
- 使用安全版本的分配函数
-
内存管理最佳实践:
- 统一分配释放接口
- 实现内存池管理大块内存
- 定期检查内存泄漏