针对 glibc 中 realloc() 函数源码在 2.2x ~ 2.3x 版本的深度解析
字数 2662 2025-08-29 22:41:10
glibc中realloc()函数源码深度解析(2.2x ~ 2.3x版本)
前置知识:常见定义
Glibc 2.23
- GCC优化:
__builtin_expect(expr, val)- 表示expr的结果预期是val,符合预期为真,反之为假 - 堆块转换:
chunk2mem(p):堆块指针转数据起始位置mem2chunk(oldmem):数据起始位置转堆块指针
- 堆块操作:
chunksize(oldp):获取堆块大小chunk_is_mmapped(oldp):检测堆块是否由mmap()创建
- 大小计算:
request2size(req):根据请求大小计算实际分配大小(对齐+头部元数据)checked_request2size(req):带检查的版本
- 错误处理:
assert(expr):条件不成立时触发__malloc_assert()malloc_printerr()
Glibc 2.27
- 报错方式更加规范
- 新增
__builtin_unreachable():告诉编译器该位置不可能被执行 assert()宏定义改为__assert_fail()malloc_printerr()函数定义简化
Glibc 2.31
- 结果优化改变,封装入不同宏定义
checked_request2size(req)定义改变
Glibc 2.35
- 新增内存标记功能:
__libc_mtag_tag_region():内存区域标签标记__libc_mtag_new_tag():生成新内存标签mtag_enabled:是否启用内存标记的标志memsize(p):返回数据区大小tag_region():封装函数tag_new_usable():封装旧函数+新特性
realloc()函数核心逻辑分析(以2.23版本为例)
初始处理
-
检查
__realloc_hook:- 非空则跳转到写入的地址
- 通常第一次存放堆初始化地址
-
边界情况处理:
- 指针有效但size=0:调用
__libc_free - 指针无效但size≠0:调用
__libc_malloc
- 指针有效但size=0:调用
-
获取堆块信息:
- 判断是否为mmap分配:
- 是:将ar_ptr设为NULL
- 否:设为堆块所在的arena
- 判断是否为mmap分配:
安全检查
-
空间环绕检查:
(uintptr_t)oldp > (uintptr_t)-oldsize- 防止整数溢出漏洞
- 确保oldp不是伪造指针(避免地址+size导致溢出)
-
对齐检查:
misaligned_chunk(oldp):堆块大小必须对齐
-
大小计算:
- 使用
checked_request2size(req)获取实际需要的大小
- 使用
mmap块处理
- 检查系统是否支持
mremap:- 不支持时:
- 若
oldsize - SIZE_SZ >= nb:直接返回旧堆块 - 否则:调用
__libc_malloc申请新块,复制内容后munmap_chunk释放旧块
- 若
- 不支持时:
arena锁定
- 调用
mutex_lock()锁定arena防止多线程竞争 - 调用
_int_realloc()进行分配 - 完成后
mutex_unlock()
分配结果检查
assert(!newp || chunk_is_mmapped(mem2chunk(newp)) ||
ar_ptr == arena_for_chunk(mem2chunk(newp)))
newp == NULL:允许(分配失败)- 通过mmap处理的堆块:允许
- 属于当前arena的堆块:允许
分配失败处理
- 如果
_int_realloc()失败:- 直接申请新块
- 复制内容
- 通过
_int_free()释放旧块
_int_realloc()内部逻辑
初步检查
-
堆块大小检查:
- 不能小于
2*SIZE_SZ - 不能大于当前arena申请的内存
- 违规报错:"realloc(): invalid old size"
- 不能小于
-
下一堆块检查:
- 不能过小(<
2*SIZE_SZ) - 不能过大
- 违规报错:"realloc(): invalid next size"
- 不能过小(<
缩小堆块流程
- 如果旧堆块足够大:
- 初步将新堆块指针指向旧堆块
- 记录新堆块大小与旧堆块相同
- 进入缩小步骤
扩大堆块流程
-
下一堆块是Top Chunk:
- 条件:
oldsize + nextsize >= nb + MINSIZE - 操作:
set_head_size()设置新大小- 非main_arena时标记
NON_MAIN_ARENA - 更新top指针为
oldp + nb - 设置新top chunk头部信息(大小=
newsize - nb,加PREV_INUSE标志)
- 条件:
-
下一堆块空闲(非fastbin):
- 条件:
PREV_INUSE为空 - 操作:调用
unlink合并堆块 - 返回旧堆块数据地址指针
- 条件:
-
下一堆块在使用中或属于fastbin:
- 条件:
PREV_INUSE为1 - 操作:
- 申请新块(
_int_malloc) - 复制内容
- 释放旧块(
_int_free) - 若申请堆块相邻则跳过复制
- 申请新块(
- 条件:
堆块缩小处理
- 计算缩小后剩余大小:
remainder_size = oldsize - nb - 检查:
remainder_size < MINSIZE:放弃分割- 否则:
- 分割为独立堆块
- 设置元数据(大小、remainder标记、arena标志)
- 调用
_int_free()释放
各版本主要变化
Glibc 2.27
- 报错位置直接调用报错函数输出
- 更加及时和规范
Glibc 2.31
- 堆块复制直接使用
memcpy()
Glibc 2.35
- 移除
realloc_hook,改为内部初始化 - 复制内容优化,增加安全性
Glibc 2.39
- 增加检查:验证需要更改的堆块大小是否被修改过
realloc()整体工作逻辑总结
-
初始处理:
- 执行
realloc_hook(如果有) - 处理边界情况(ptr有效size=0或ptr无效size≠0)
- 执行
-
系统调用路径:
- 指针有效且size较大需要走系统调用:
- 缩小:不做改变,返回原堆块
- 扩大:系统调用生成新块并复制
- 出错返回NULL
- 指针有效且size较大需要走系统调用:
-
常规路径:
- arena上锁防止竞争
- 调用
_int_realloc()- 大小检查
- mmap检查
- 邻块检查
- 解锁arena
- 堆块合法性检查
-
扩大处理:
- 下一堆块是Top Chunk且足够大:直接扩大
- 下一堆块空闲且大小合适:
unlink合并 - 下一堆块在使用中:申请+复制+释放
-
缩小处理:
- 剩余空间≥MINSIZE:分割并释放
- 否则不缩小
-
失败处理:
_int_realloc()失败时尝试申请+复制+释放- 彻底失败则报错