Glibc realloc function procedure analysis
glibc2.32 source code: malloc.c - realloc part
Wrapper:
1 | void * |
基本流程:
- 如果realloc的size为零,等于调用free
- 如果传入的地址为空,完全等价于malloc
- 否则,进入真正的_int_realloc
_int_realloc function:
1 | /* |
其中在代码开始有一个check inuse chunk,主要是检查当前请求realloc的堆块,宏定义展开就是这个函数,拿出来看一下:
1 | static void |
check流程:
- 先调用check chunk,这个先不详细看了
- check此堆块是不是在使用,如果是空闲状态会报错
- 取next_chunk,如果前一个堆块是空闲的进入if,check前一个堆块后面是不是本堆块,当然了如果前一个堆块正在使用,就没必要check。
- 然后如果下一个堆块是top chunk的话,check top chunk的preinuse位和size位
- 如果下一个堆块不是top chunk且是空闲状态,check free chunk,和检查前一个空闲堆块一样调用do check free chunk
过了check之后要检查下一个chunk的size合法性即大于两倍的SIZE_SZ且小于av->system_mem,然后开始真正的realloc:
情况一:如果旧的size大于请求的新size,直接切,即先赋值newp = oldp; newsize = oldsize;然后remainder_size = newsize - nb;如果remainder能够成为一个新chunk(即大于chunk的MINSIZE),就把它free掉,返回新的chunk内容地址(也就是原地址),不过chunk大小变小了。
情况二:如果请求的size大于原来的size,流程稍微复杂一点:
- 如果下一个堆块是top chunk的话并且old size+top chunk size大于请求的chunk大小,也就是说剩余的topchunk大小可以补足原来的chunk以满足请求,切top chunk即可,并返回原地址。
- 如果下一个堆块不是top chunk且下一个堆块是空闲的,并且加上当前堆块的大小能够满足请求的大小,调用unlink_chunk(av, next);放在后面分析
- 如果不是以上两种情况,也就是说当前堆块的连续后面没有可用的空闲内存,此时就会调用malloc,让allocator调用malloc(request_size - MALLOC_ALIGN_MASK),但是malloc有可能返回的是旧堆块的连续下一块内存(不是已经过了第二个if么,为什么还会malloc到地址连续的空闲堆块呢?这不矛盾吗wtf),这种情况下还是会把这俩合并,剩余的切割,然后返回给用户。如果新堆块不是连续的,则copy原来的数据过去,返回新的堆块内存,并把原来的堆块释放掉。
最后来看看unlink_chunk函数:
1 | /* Take a chunk off a bin list. */ |
传入参数的是next堆块指针,首先会检查size和presize是否匹配;然后检查双向链表完整性(也就是说,只针对于bin中的空闲chunk?realloc不会考虑tcache?),check完就执行unlink,而且如果是largebin的话会复杂一点。
Poc:
1 | int main() |
before realloc:
1 | pwndbg> par |
after realloc:
1 | pwndbg> par |
总结一下:
一般情况realloc不会合并前面的空闲堆块,一般向下寻找可用的堆块,如果找不到就去malloc新的然后copy、free。
另外,realloc有两个会调用free的地方,一是realloc(addr,0)的时候,等价于free;二是申请比原来小的size,会把剩下的那部分free掉,无论是合并新的堆块还是申请的size,只要new size大于request size都要切割(如果能够满足切割成最小堆块或更大的话),会通过free释放。