针对 glibc 中 realloc() 函数源码在 2.2x ~ 2.3x 版本的深度解析
字数 2662 2025-08-29 22:41:10

glibc中realloc()函数源码深度解析(2.2x ~ 2.3x版本)

前置知识:常见定义

Glibc 2.23

  1. GCC优化__builtin_expect(expr, val) - 表示expr的结果预期是val,符合预期为真,反之为假
  2. 堆块转换
    • chunk2mem(p):堆块指针转数据起始位置
    • mem2chunk(oldmem):数据起始位置转堆块指针
  3. 堆块操作
    • chunksize(oldp):获取堆块大小
    • chunk_is_mmapped(oldp):检测堆块是否由mmap()创建
  4. 大小计算
    • request2size(req):根据请求大小计算实际分配大小(对齐+头部元数据)
    • checked_request2size(req):带检查的版本
  5. 错误处理
    • 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版本为例)

初始处理

  1. 检查__realloc_hook

    • 非空则跳转到写入的地址
    • 通常第一次存放堆初始化地址
  2. 边界情况处理:

    • 指针有效但size=0:调用__libc_free
    • 指针无效但size≠0:调用__libc_malloc
  3. 获取堆块信息:

    • 判断是否为mmap分配:
      • 是:将ar_ptr设为NULL
      • 否:设为堆块所在的arena

安全检查

  1. 空间环绕检查

    (uintptr_t)oldp > (uintptr_t)-oldsize
    
    • 防止整数溢出漏洞
    • 确保oldp不是伪造指针(避免地址+size导致溢出)
  2. 对齐检查

    • misaligned_chunk(oldp):堆块大小必须对齐
  3. 大小计算

    • 使用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()内部逻辑

初步检查

  1. 堆块大小检查:

    • 不能小于2*SIZE_SZ
    • 不能大于当前arena申请的内存
    • 违规报错:"realloc(): invalid old size"
  2. 下一堆块检查:

    • 不能过小(<2*SIZE_SZ
    • 不能过大
    • 违规报错:"realloc(): invalid next size"

缩小堆块流程

  1. 如果旧堆块足够大:
    • 初步将新堆块指针指向旧堆块
    • 记录新堆块大小与旧堆块相同
    • 进入缩小步骤

扩大堆块流程

  1. 下一堆块是Top Chunk

    • 条件:oldsize + nextsize >= nb + MINSIZE
    • 操作:
      • set_head_size()设置新大小
      • 非main_arena时标记NON_MAIN_ARENA
      • 更新top指针为oldp + nb
      • 设置新top chunk头部信息(大小=newsize - nb,加PREV_INUSE标志)
  2. 下一堆块空闲(非fastbin)

    • 条件:PREV_INUSE为空
    • 操作:调用unlink合并堆块
    • 返回旧堆块数据地址指针
  3. 下一堆块在使用中或属于fastbin

    • 条件:PREV_INUSE为1
    • 操作:
      • 申请新块(_int_malloc
      • 复制内容
      • 释放旧块(_int_free
      • 若申请堆块相邻则跳过复制

堆块缩小处理

  1. 计算缩小后剩余大小:remainder_size = oldsize - nb
  2. 检查:
    • remainder_size < MINSIZE:放弃分割
    • 否则:
      • 分割为独立堆块
      • 设置元数据(大小、remainder标记、arena标志)
      • 调用_int_free()释放

各版本主要变化

Glibc 2.27

  • 报错位置直接调用报错函数输出
  • 更加及时和规范

Glibc 2.31

  • 堆块复制直接使用memcpy()

Glibc 2.35

  1. 移除realloc_hook,改为内部初始化
  2. 复制内容优化,增加安全性

Glibc 2.39

  • 增加检查:验证需要更改的堆块大小是否被修改过

realloc()整体工作逻辑总结

  1. 初始处理

    • 执行realloc_hook(如果有)
    • 处理边界情况(ptr有效size=0或ptr无效size≠0)
  2. 系统调用路径

    • 指针有效且size较大需要走系统调用:
      • 缩小:不做改变,返回原堆块
      • 扩大:系统调用生成新块并复制
      • 出错返回NULL
  3. 常规路径

    • arena上锁防止竞争
    • 调用_int_realloc()
      • 大小检查
      • mmap检查
      • 邻块检查
    • 解锁arena
    • 堆块合法性检查
  4. 扩大处理

    • 下一堆块是Top Chunk且足够大:直接扩大
    • 下一堆块空闲且大小合适:unlink合并
    • 下一堆块在使用中:申请+复制+释放
  5. 缩小处理

    • 剩余空间≥MINSIZE:分割并释放
    • 否则不缩小
  6. 失败处理

    • _int_realloc()失败时尝试申请+复制+释放
    • 彻底失败则报错
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 获取堆块信息: 判断是否为mmap分配: 是:将ar_ ptr设为NULL 否:设为堆块所在的arena 安全检查 空间环绕检查 : 防止整数溢出漏洞 确保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() 分配结果检查 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 常规路径 : arena上锁防止竞争 调用 _int_realloc() 大小检查 mmap检查 邻块检查 解锁arena 堆块合法性检查 扩大处理 : 下一堆块是Top Chunk且足够大:直接扩大 下一堆块空闲且大小合适: unlink 合并 下一堆块在使用中:申请+复制+释放 缩小处理 : 剩余空间≥MINSIZE:分割并释放 否则不缩小 失败处理 : _int_realloc() 失败时尝试申请+复制+释放 彻底失败则报错