glibc源码分析-_2.23
优先分析版本:2.23,2.27,2.31,2.35
glibc2.23
也是终于开始分析源码了

吓哭了

malloc.c
这么长的注释是在写故事吗

malloc_chunk

prev_size(offset 0x0)
如果前一个chunk 被free了,记录bk chunk的size
也就是说,如果前一个chunk正在使用(allocated),它的user data 可以覆盖下一个chunk的prev_size,存在 space overlap
size(offset 0x4/0x8)
记录的是当前chunk的大小(including ‘size’)
由于8/16字节对齐,size的低3位恒为0,这3位就被glibc作为储存状态标志位

fd & bk(0x10 & 0x18)

fd_nextsize & bk_nextsize (0x20 & 0x28)
给large bin用的(free)
底下还有画的图

这是一个正在使用的chunk的布局

free掉的
malloc_state

mutex_t mutex
互斥锁
int flags
标志位
主要记录当前arena是否有fastbin,用于判断是否需要执行malloc_consolidate
mfastbinptr fastbinsY[NFASTBINS]
fastbins数组,放单向链表头
mchunkptr bins[NBINS * 2 - 2]
普通bins(unsorted,small,large)的数组
由于每个bin都是由fd和bk组成的双向链表头,成对出现,所以要用*2-2,unsorted bin 的fd , bk指针就在这个数组内
64 位系统下,NBINS 通常定义为 128

| 索引位置 | 对应指针 | 逻辑身份 |
|---|---|---|
<font style="color:rgb(68, 71, 70);">bins[0]</font> |
<font style="color:rgb(68, 71, 70);">fd</font> |
Unsorted Bin 的头指针(Forward) |
<font style="color:rgb(68, 71, 70);">bins[1]</font> |
<font style="color:rgb(68, 71, 70);">bk</font> |
Unsorted Bin 的尾指针(Backward) |
<font style="color:rgb(68, 71, 70);">bins[2]</font> |
<font style="color:rgb(68, 71, 70);">fd</font> |
Small Bin 2 的头指针 |
<font style="color:rgb(68, 71, 70);">bins[3]</font> |
<font style="color:rgb(68, 71, 70);">bk</font> |
Small Bin 2 的尾指针 |
| … | … | … |
mchunkptr top
指向top chunk的指针,在古老的houst of force有用
mchunkptr last_remainder
指向最近一次从 Unsorted Bin 拆分后剩下的那个“碎块”
unsigned int binmap [BINMAPSIZE]
malloc 不会逐个查bins是否为空,先看位图,如果某一位为0则对应bin为空
struct malloc_state *next
指向下一个arena
system_mem/max_system_mem
当前 arena 从系统申请的内存总量和历史峰值
static struct malloc_state main_arena

这是干什么的


main_arena是唯一静态定义的arena其他线程的arena是动态申请的


.attached_threads = 1
用于记录当前有多少个线程连接到这个arena
_int_malloc


真是好大一坨
checked_request2size (bytes, nb)
将请求的bytes转换为nb(normalized request size 16字节对齐)

av(arena) == NULL时,heap未初始化或其他原因,
直接调用syamalloc 申请 memory(brk or mmap)
下面怎么if else窜起来了

to fastbin


检查nb是否在fastbin范围内后用fastbin_index()将nb转化成idx,再用fastbin定位到该arena中的链表头指针

mchunkptr pp = *fb;
先尝试从单链表头取出一个chunk -> victim
victim == NULL则break
catomic_compare_and_exchange_val_acq()用于处理多线程
检查当前的fd是否等于victim,防止被其他线程抢先

用victim里的size重新算一遍idx,算出的idx必须等于进入if分支的idx
那么改fd的时候就要找附近的一个看起来是合法size的字节


to smallbin

可以看出smallbin用的是双向链表

判断大小,算idx,拿bin头指针
3410 if ((victim = last (bin)) != bin)
last(bin)就是bin->bk,如果bin->bk == bin,则链表为空,跳过

如果victim == 0,调用malloc_consolidate(),将初始化分配区
并合并fastbins中的所有的空闲块进入unsorted bin

检查victim->bk->fd = victim
3422 set_inuse_bit_at_offset (victim, nb);


脱链

标志位与指针转化
to largebin

malloc size 在largebin的区间时

to unsorted bin
到了unsorted bin画风就诡异起来了

可以发现,unsorted bin这一块都镶入了for( ; ; )死循环

不太好切片分析,我copy了
1 | |
这就是unsorted bin的分拣规则吗
1.从 Unsorted Bin 摘下 chunk 并检查

首先,我们知道在glibc中bin是malloc_chunk结构体数组(in main_arena)
unsorted_chunks(av)为宏定义,指向main_arena中idx为1的chunk
这个chunk不在user数据区,它是当双向循环链表的“头”用的
这里的if有对chunk ->size的大小检查,
victim->size <= 2 * SIZE_SZ 是检查size最小值
victim->size > av->system_mem 检查最大值
size = chunksize (victim);用于提取victim->size,去掉低3位
2.尝试利用 last_remainder 快速切割 (针对 Small Request)

Last Remainder 机制
如果刚刚切开一个大块分给用户,剩下的那部分(Remainder)很有可能紧接着就被再次申请
可以看到if后面有好长一段判断逻辑
1 | |

算余量,定新块,改链表
原来的去掉,新切的塞回来

收尾工作
check_malloced_chunk (av, victim, nb);
检查对齐
void *p = chunk2mem (victim);
p = (char *)victim + 2 * SIZE_SZ
alloc_perturb是什么

3.移出 Unsorted Bin 与精确匹配


将victim从双向循环链表中移除
没有safe unlinking(bck->fd == victim)

检查chunk大小,改PREV_INUSE,return p
4.分拣归仓,small bin or large bin

当 Unsorted Bin 中的 chunk 既没有被“精确匹配”拎走,也没有被“最后剩余块”逻辑切开时,分配器就会把它正式移出 Unsorted Bin,并根据它的 size 投递到Small Bin 或 Large Bin

进smallbin
Small Bin 内部不排序,所有堆块大小相同,直接插入链表头部即可

进large bin就麻烦了

largebin 对比smallbin,它是有序的,按size从大到小
| 特性 | Smallbin | Largebin |
|---|---|---|
| 尺寸 | 固定(0x20, 0x30…) | 范围(Range) |
| 排序 | FIFO | 按 Size 降序 + FIFO |
| 指针数量 | 2 (<font style="color:rgb(68, 71, 70);">fd</font>, <font style="color:rgb(68, 71, 70);">bk</font>) |
4 (<font style="color:rgb(68, 71, 70);">fd</font>, <font style="color:rgb(68, 71, 70);">bk</font>, <font style="color:rgb(68, 71, 70);">fd_ns</font>, <font style="color:rgb(68, 71, 70);">bk_ns</font>) |
| 分配策略 | 精确匹配 | 最佳匹配 (Best-fit) |
| 攻击难点 | 主要是 <font style="color:rgb(68, 71, 70);">unlink</font>检查 |
需要操作 <font style="color:rgb(68, 71, 70);">nextsize</font>指针,逻辑较深 |
为了加速查找,它引入了第二套指针:fd_nextsize 和 bk_nextsize(跳表结构)

情况1:当前堆块比 Bin 中最小的还要小
直接插入到链表尾部,并维护 nextsize 链表(循环结构)

情况2:寻找插入位置

情况3:大小已存在,插入到该大小对应组的第二个节点(优化性能,不更新 nextsize)

情况4:找到位置,插入新大小节点

情况5:Bin 原本为空,自己作为 nextsize 的起点
最终的链表链接
不论是 Small 还是 Large,最后都会执行这一组标准的双向链表插入操作

mark_bin()的作用


对于 Small Bin:新堆块总是插入到 Bin 头部,所以 bck 通常是 Bin 头部,fwd 是原本的第一块
对于 Large Bin:fwd 和 bck 是根据大小排序逻辑计算出来的插入点
5. 扫描 Large Bin、Binmap 与 Top Chunk

如果 Unsorted Bin 遍历了一圈(默认最多 10000 次)还没返回,就只能去更大的 Bin 里找兜底方案了说是

防止极端情况下 unsorted 链表特别长导致这一轮 malloc 花费太久
也保护自己:如果链表结构被破坏(比如自环或环很大),可以避免在这里无限循环
5.1 Large Bin 的 Best-Fit 深度扫描
对“大请求”:从当前 largebin 中按 size 有序链找最佳 fit
接下来是这段(只有在“大请求”即 !in_smallbin_range(nb) 时走)只处理 large request



先检查这个 Large Bin 是否为空,或者里面最大的 chunk(链表头 first(bin))是否都比你要申请的 nb 小。如果是,直接跳过这个 bin

在 Large Bin 中,如果有多个大小完全相同的 chunk,它们会被挂在同一个位置,互相之间只通过 fd 和 bk 链接,而 fd_nextsize 和 bk_nextsize只保留在第一个(首个插入的)该大小的 chunk 上
拿走第二个 chunk,就不需要去nextsize指针

unlink
摘下 chunk 后,如果它比你申请的 nb 还要大,就需要进行切割

剩的太少,不切

够大就切
3635:算remainder指针
3638-3648:放回unsorted bin

清指针

重新打标签

return
5.2 Binmap 位图扫描




当前请求的 size 对应的 Bin 没货就++idx找下一个更大的
为了极速查找,glibc 把所有的 Bin 分成了几个大的 Block(每个 block 是一个整数,用 bit 代表对应的 bin 是否有货)
3693: 如果一个 block 是 0,说明这连续的几十个 Bin 全是空的,直接跳过整个 block
3691: 如果所有的 block 都扫完了还是没货,就会直接去找 Top Chunk



检查后unlink,剩下的太小就不切

大块就切
每次发生大块切割时,glibc 会检查你当前申请的尺寸 nb 是不是属于 Small Bin 的范围(通常是小于 0x400 字节)。如果是,它就会把切剩下的这块肉(remainder)的地址,记录在当前 arena 的 last_remainder 变量中

进large bin就清指针
5.3 use_top
前面的都不行就找top chunk(wilderness)



house of force
这里对 top chunk size 没有检查
没什么好说的,跳到3811


相当于消除内存碎片

最后没招了就向os申请
_int_free
_int_free不多,200多行


各个bin要用到的指针

check pointer and size

chunksize(p)读取p->mchunk_size并屏蔽(A|M|P)
3861-3862:防size过大,检查p指针16字节对齐
3873:size至少是0x20bytes
3868:打印错误
3879:确保当前chunk在使用中
_int_free处理的几类chunk
1.fastbin chunk

2.nom chunk

3.mapped chunk

fastbin 路径
3880-3956


触发条件:size <= get_max_fast()

如果开启了这个宏,并且当前释放的 chunk 物理上紧挨着 top chunk,那么它不能进入 fastbin,而是会进入后面的逻辑直接和 top chunk 合并

通过 chunk_at_offset(p, size) 找到当前 chunk 在内存中物理相邻的下一个 chunk。检查这个 next chunk 的 size 字段

如果当前没锁(have_lock==0),它会临时加锁重查一次,避免并发导致误报(malloc.c:3901-3919)
3879-3919:
这段检查 next chunk 头是否离谱:
next->size <= 2*SIZE_SZ 或 chunksize(next) >= av->system_mem
命中就报:free(): invalid next size (fast)

填充用户区数据

set_fastchunks(av):标记 arena 里存在 fastbin 块
idx = fastbin_index(size) 找 bin 下标
fb = &fastbin(av, idx) 拿到该链表头

注释写的还挺详细的
3935-3938:检查 bin 的顶部是不是我们准备添加的 chunk。仅检查 old == p,即“你要插入的 chunk 已经在链表头”能拦截 A -> free(A) -> free(A),但fastbin dup(A,B,A)不行
3944-3948:防多线程


普通 chunk(非 mmapped,且不进 fastbin)
3956-4098

要动双向链表,加个lock
3967:算nextchunk

一大坨检查
不能释放 top chunk,next chunk 不能越界,PREV_INUSE 检查,next chunk size 合法性
3999:填充字节进user data
向后合并 (Backward Consolidation) (4001-4007)



向前合并 (Forward Consolidation) (4009-4059)

如果nextchunk也free了,两个合并,放入unsorted bin
如果nextchuck是top chunk,直接跟top chunk合并
大块释放的后续处理 (4060-4098)

4076:合并fastbin chunk
4078-4092:如果合并后top chunk过大,则用sbrk或非main arena的用trim返还mem给os

lock检查
mmap chunk释放(4099-4107)

直接用munmap_chunk()