终于鼓起勇气写这么一篇博客,一直想把malloc.c的源码看一下,算是一篇总结吧。
说明:本篇文章的所有源码均来自glibc-2.29,主要是arena.c和malloc.c这两个源文件。
操作系统内存分配的相关函数
对于堆内存的分配,操作系统提供了brk系统调用,glibc中提供了sbrk()函数,start_brk指向进程堆的起始地址,brk是堆的当前最后地址,malloc通过内核的brk系统调用来增加brk的值,从而向操作系统申请内存。初始时start_brk和brk指向同一地址:
1 | 1. 未开启ASLR时,start_brk和brk指向bss段的末尾 |
mmap函数将一个文件或其他对象映射进内存,文件被映射到多个页上。munmap删除特定地址区域的对象映射。
数据结构
malloc_state
为了更有效的处理多线程的应用程序,glibc的malloc允许多块内存区域在同一时间处于active的状态,不同的线程可以互不干扰的访问不用的内存区域,这些内存区域就叫做“arenas”,“main_arena”是程序初始分配的内存区域,每个进程只有一个main_arena(主分配区),可以有多个non_main_arena(非主分配区)。每个arena结构体都有一个互斥锁,来控制线程对于arena的访问。管理arena的结构是malloc_state,每个分配区都是malloc_state的一个实例。主分配区通过sbrk()进行堆的扩展,非主分配区通过mmap函数来建立。
1 | //malloc.c |
ptmalloc使用结构体malloc_par来进行参数管理,只有一个malloc_par实例。
1 | //malloc.c |
这里说一下成员arena_test和arena_max,当每个进程的分配区数量小于等于arena_test时,不会重用已有的分配区;当系统分配区数量达到arena_max,就不会再创建新的分配区,只会重用已有的分配区。32位系统下arena_test默认值为2,arena_max为2 系统核数;64位系统下arena_test默认值为8,arena_max为8 系统核数。
malloc_chunk
ptmalloc使用chunk对内存进行管理,malloc_chunk被用来管理这些chunk的数据结构。
1 | //malloc.c |
ptmalloc使用边界标记法对chunk进行管理,充分考虑到空间复用,已分配的chunk的结构如下,chunk指向一个chunk的起始地址,mem是使用malloc分配后返回给用户的指针。
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
空闲的chunk被存储在循环的双向链表中,结构如下:
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
chunk的低三个bit用来标识chunk的属性:
1 | P(PREV_INUSE):标识前一个chunk是否处于使用状态,若P=1,则上一个chunk处于使用状态;P=0则上一个chunk处于freed状态。 |
几个数值
INTERNAL_SIZE_T和SIZE_SZ相同,在32位下是4个字节,64位下是8个字节的无符号整数。
1 |
|
MALLOC_ALIGNMENT在32位下是8个字节,64位下是16个字节的无符号整数。
1 |
|
相关宏
chunk指向一个chunk的起始地址,mem是使用malloc分配后返回给用户的指针,SIZE_SZ类型是size_t,在64位下是64位无符号整数,8个字节;在32位下是32位无符号整数,4个字节。所以64位下malloc headers是0x19个字节,在32位下是0x8个字节。
1 | //malloc.c |
最小的chunk大小,offsetof是计算fd_nextsize在结构体malloc_chunk的偏移,在64位下最小chunk是0x20个字节,在32位下最小chunk是0x10个字节。
1 | //malloc.c |
最小申请的chunk大小,与MIN_CHUNK_SIZE大小相等。
1 | //malloc.c |
检查chunk是否对齐:
1 | //malloc.c |
检查申请的chunk是否太大,因为边界检查的数值类型是无符号整数,防止溢出又回到0.
1 | //malloc.c |
以下将其用户实际请求的大小转化为实际chunk的大小。因为每一个chunk都要
1 | //malloc.c |
标记chunk属性
上文提到每一个chunk的低三个比特用来标识chunk的三个属性,从低到高分别为是否分配(P)、是否由mmap分配(M)、是否由main_arena分配(A)。下面是与标志位相关的宏。
1 | //malloc.c |
获取chunk的大小可以过滤掉3个标志位,或者通过malloc_chunk结构体的成员mchunk->size来获得。
1 |
|
获取下一个相邻和上一个相邻的chunk,这两个函数比较重要,很多漏洞就是利用这个计算方式来逃避分配或释放时的一些检查。
1 | /* Ptr to next physical malloc_chunk. */ |
其他的函数用到的时候再说。
bins
对于空闲的chunk,ptmalloc采用分箱式的内存管理方式,根据空闲chunk的大小和处于的状态将其放在三类不同的bin中,这三类空闲chunk的容器分别是smallbin,largebin和unsorted bin。三类bins存储在一个数组中,这个数组对应结构体malloc_state中的成员bins:
1 |
|
较小的空闲chunk被释放后并不会立即放到smallbins中,为了提高分配的速率,ptmalloc会先将较小的空闲chunk先放到fastbins中,fastbins是小内存块的高速缓存,在分配小内存时,首先会查看fastbins中是否由合适的chunk,如果存在,则直接返回fastbins中符合要求的内存块,提高了分配效率。bins中一些通用的宏如下:
1 | typedef struct malloc_chunk *mbinptr; |
fastbin
它是小内存块的高速缓存,采用单链表结构对bin进行组织,每个链表中的bin采用FIFO策略。当用户所需的chunk小于fastbin的最大大小时,会先在fastbin中寻找是否有符合要求的chunk,如果有的话,直接分配该chunk。fastbin由malloc_state中的成员来维护:
1 | typedef struct malloc_chunk *mfastbinptr; |
fastbin的默认最大大小规定如下,在32位下是64字节,在64位下是128字节。在默认最大大小情况下,32位平台中的fastbin链表一共有7个,大小分别是0x10,0x18,0x20,0x28,0x30,0x38,0x40。64位平台中的fastbin链表有7个,大小分别是0x20,0x30,0x40,0x50,0x60,0x70,0x80。
1 | /* M_MXFAST is a standard SVID/XPG tuning option, usually listed in malloc.h */ |
ptmalloc在默认情况下会调用set_max_fast(s)函数将global_max_fast设置为DEFAULT_MXFAST:
1 | /* Maximum size of memory handled in fastbins. */ |
fastbin规定了最大的fastbin大小MAX_FAST_SIZE,在32位下最大大小是80 4 / 4 = 80字节,;在64位下最大大小是80 8 / 4 = 160字节。当MAX_FAST_SIZE为0时,系统不再支持fastbin。可以使用get_max_fast函数获取当前规定的最大的fastbin大小。
1 | /* The maximum fastbin request size we support */ |
fastbin链表中的inuse位始终置1,在释放时不会反生改变,因此它在释放后不会与其他相邻的空闲chunk合并。但是当释放的chunk与该chunk相邻的空闲chunk合并大小大于FASTBIN_CONSOLIDATION_THRESHOLD时,就认为当前系统中存在的内存碎片较多,需要把fastbin中的所有chunk进行合并,以减少内存碎片对系统的影响,合并由malloc_consolidate函数执行。
1 |
fastbin与索引相关的宏如下:
1 | typedef struct malloc_chunk *mfastbinptr; |
fastbin其他宏如下,malloc_state->Flags中的bit1如果为0,表示MORECORE返回的是连续的虚拟地址空间;bit1为1表示MORECORE返回的是非连续地址空间。对于主分配区,默认返回连续地址空间;对于线程中的非主分配区,使用mmap分配大块虚拟内存,然后进行切分满足相应分配需求,默认情况下mmap映射区域不保证虚拟地址空间是连续的。
1 | /* |
smallbin
在bins数组中,索引2-63的bin称为smallbin,同一个smallbin双向链表中的chunk的大小相同,两个相邻索引的smallbin链表中的chunk的大小相差2*SIZE_SZ个字节。在32位平台中相差8个字节,最小的chunk大小为16字节,最大的chunk是504个字节,一共有62个双向链表。在64位平台中,相邻双向链表中相差16个字节,最小的chunk大小为32字节,最大的chunk大小为1008字节。smallbins中每个双向链表采用FIFO的规则,同一链表中先被释放的chunk会先被分配出去。相关宏如下:
1 |
|
largebin
在32位平台中,largebins指的大于等于512字节的空闲chunk;在64位平台中,指的是大于等于1024字节的空闲chunk。largebins一共包含63个bin,每个bin中chunk大小不相同,这些bins一共被分为6组,每组bin是一个有固定公差的等差数列,不同平台每组公差如下所示:
1 | 组数 1 2 3 4 5 6 |
32位平台计算largebin在bins中的索引的宏如下,比如第一个largebin的起始大小是512字节,512 >> 8 = 6,6 + 56 = 64,这是第一个largebin的下标。
1 |
|
64位的宏如下,第一个largebin的起始大小是1024字节,1024 >> 6 = 16, 16 + 48 = 64,这是第一个largebin的下标。
1 |
|
这个宏就是根据sz大小来计算其在bins中的索引,从而定位到smallbins或largebins对应大小的双向链表。
1 |
|
unsorted bin
unsorted bin是smallbins和largebins的缓存,只有一个,以双向链表的形式管理chunk,所有符合进入unsorted bins中的chunk按照FIFO的规则管理。当释放的chunk不符合进入fastbin的大小要求时,且与top chunk不相邻,则会进入到unsorted bin中;当unsorted bin中的一个较大的chunk被分割后,且剩余部分大于MINSIZE时,剩余的chunk会保留在unsorted bin链表中。相关宏如下,可以看到,unsorted bin在bins中的索引为1.
1 | /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */ |
top chunk
在第一次分配时,堆会被分割成两部分,一部分分配给用户,剩下的就是top chunk,当用户请求的chunk每一个bin都不能满足其分配要求时,如果top chunk的大小小于用户请求大小,就要从top chunk中进行chunk的分割,并分配给用户,并将剩余的部分作为新的top chunk。当top chunk不满足用户分配要求时,在main_arena中通过sbrk进行heap的扩展,在non_main_arena中使用mmap进行heap的扩展。另外,top chunk的prev_iuse位始终置1,否则它前面的chunk就会被合并到top chunk中。
last remainder
当用户请求的chunk和系统找到的chunk可能存在大小不一致的情况,比如在unsorted bin中会将空闲chunk进行分割,一部分返回给用户,剩下的就是last remainder,但top chunk不是last remainder。
参考
https://sourceware.org/glibc/wiki/MallocInternals
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure-zh/
《glibc内存管理ptmalloc源代码分析@华庭》