0x01 前言
glibc malloc学习记录
参考文章:https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/
教程是15年的文章,以下内容仅为学习该文章所得,不保证全部内容依然有效
0x02 memory allocator 简介
每一种memory allocator目标都是fast,scalable and memory efficient
memory allocator | 描述 |
---|---|
dlmalloc | General purpose allocator |
ptmalloc2 | glibc的内存分配器 |
jemalloc | FreeBSD和火狐的内存分配器 |
tcmalloc | google的内存分配器 |
libumem | Solaris的内存分配器 |
0x03 glibc malloc剖析
1.system call
本文提到的malloc均使用brk
和mmap
作为系统调用
brk:malloc小于128k的内存,往高地址增长
mmap:malloc大于128k的内存,在堆和栈之间找一块空闲的的虚拟内存(所以地址不连续)
sbrk:main thread使用
2.Threading
对于dlmalloc
,当两个线程同一时间调用malloc时,只有一个线程可以进入critical section
(临界区),因为freelist
数据结构会在可用线程之间共享。因此对于多线程程序来说,内存分配需要额外花费时间,造成性能下降
对于ptmalloc2
,当两个线程同一时间调用malloc时,内存会被马上分配,所以每个线程是维持着一个分散的heap segment
,因此维护这些堆的freelist
数据结构也是分散的
为每个线程维持separate heap
和freelist data structures
的行为被称为per thread arena
举例
/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void* threadFunc(void* arg) {
printf("Before malloc in thread 1\n");
getchar();
char* addr = (char*) malloc(1000);
printf("After malloc and before free in thread 1\n");
getchar();
free(addr);
printf("After free in thread 1\n");
getchar();
}
int main() {
pthread_t t1;
void* s;
int ret;
char* addr;
printf("Welcome to per thread arena example::%d\n",getpid());
printf("Before malloc in main thread\n");
// 在分配之前getchar
getchar();
// malloc内存空间
addr = (char*) malloc(1000);
printf("After malloc and before free in main thread\n");
// 在分配之后getchar
getchar();
// free内存空间
free(addr);
printf("After free in main thread\n");
// free之后getchar
getchar();
ret = pthread_create(&t1, NULL, threadFunc, NULL);
if(ret)
{
printf("Thread creation error\n");
return -1;
}
ret = pthread_join(t1, &s);
if(ret)
{
printf("Thread join error\n");
return -1;
}
return 0;
}
输出结果分析
# output
porlock@ubuntu:~/code/glibc$ ./a.out
Welcome to per thread arena example::17750
Before malloc in main thread
...等待输入
[注:因为环境问题以及潜在的分配机制更新问题,复现没有成功,因此使用原作者的实验结果进行学习]
这时我们另起一个bash进行查看进程内存映射。可以看到,此时还没有heap segment
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
b7e05000-b7e07000 rw-p 00000000 00:00 0
...省略部分输出
After malloc in main thread
之后,进程内存映射如下。heap segment
在(0804b000-0806c000)
,显然是调用brk
创建的heap
.
这一段连续的heap meory被称为arena
,因为这个arena
是由main创建的 thread,因此被称为main arena
。之后的分配请求都会延续这个arena进行,直到该arena
被free
掉。当内存空间不足时,通过程序中断来增大空间,(增大top chunk的大小)
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e05000-b7e07000 rw-p 00000000 00:00 0
3.Arena
在上面的例子中,我们发现main thread
包含main arena
,而thread 1
则包含自己独立的一个arena
。但是实际上thread
和arena
之间没有办法一一对应,显然一个application可以拥有很多thread(than number of cores)。事实上,arena的数量与系统核心数量相关
For 32 bit systems:
Number of arena = 2 * number of cores.
For 64 bit systems:
Number of arena = 8 * number of cores.
Mulitple Arena分析
举例:现在有一个多线程的应用(4个threads,一个main thread和3个user thread)运行在1核32位系统上。因此最多只能有2个arena。所以对于这个例子,glibc
必须实现arena的共享机制,它是怎么运作的呢?
- 当 main thread第一次调用malloc时,生成main arena,without arena竞争
- 当 thread 1 和 thread 2第一次调用malloc时,生成arena 1和arena 2,此时也没有竞争,这时候thread和arena出现一一对应的情况
- 当 thread3 第一次调用malloc时,arena数量已经达到限制量了,因此尝试reuse现有的arena(Main arena,arena 1,arena 2)
- 遍历arena并尝试lock arena来获取使用权。如果lock成功,将返回该arena给用户thread。如果找不到可用的arena,则再尝试请求上一个请求的arena,如果还是不成功则阻塞该thread等待这个arena可用。
4.heap_info
堆头部结构,一个arena
可以有多个堆,每个堆都有自己的头部。在一开始,每个arena
只包含一个堆,当堆的空间用完或不够时,arena
的一个新的堆将通过mmap
生成
Note:
- main arena没有多个堆,因此它没有
heap_info
结构。当Main arena耗尽空间后,sbrk’d heap segment将被拓展,直到其碰到memory mappig segment - 与thread arena不同,main arena的arena header并不是sbrk’d heap segment的一部分。它是一个
global variable
,因此存在于libc.so的data segment中
typedef struct _heap_info
{
mstate ar_ptr; /* 这个堆对应的arena */
struct _heap_info *prev; /* 前一个堆 */
size_t size; /* 当前的size(用bytes表示) */
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;
5.malloc_state
Arena
头部结构,一个Arena
可以有多个堆,但这些堆只有一个Arena header
存在。Arena header
包含一些信息:bins
,top chunk
,last remainder chunk
等等
struct malloc_state
{
/* Serialize access. */
mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags; //记录分配区的一些标志,比如bit0记录分配区是否有fast bin chunk等等
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS]; //存放每个fast chunk头部的指针
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top; //指向分配区的top chunk
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder; //指向last remainder chunk
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2]; //用于存储的unstored bin,small bins和large bins的chunk链表
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. */
struct malloc_state *next_free;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
结合malloc_state
结构及其代码仔细研究这两幅图,有收获。上图是main arena和thread arena的 single heap状态,下图是thread arena 的multiple-heap的状态
6.malloc_chunk
Chunk
头部结构,基于用户请求(malloc等),一个堆被划分成很多个chunks。每个chunk都有自己的chunk header
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* 如果前一个chunk是free状态,该值记录了前一个chunk的Size */
INTERNAL_SIZE_T size; /* 包括头部的Size in bytes */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* fd指向下一个(非物理相邻)空闲的chunk,bk指向上一个(非物理相邻)空闲的chunk */
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
7.chunk
chunk有以下几种类型
- Allocated chunk
- Free chunk
- Top chunk
- Last Remainder chunk
7.1 Allocated chunk
Allocated chunk
的结构如下
prev_size: 如果前一个chunk是free状态,这个field包含了前一个chunk的size。如果前一个chunk是allocated状态,那么这个field包含前一个chunk的user data(被前一个chunk所使用)
size: 这个field指出这个allocated chunk的size,最后三个bits是标志位
- PREV_INUSE§: 当前一个chunk是allocated chunk时,被设为1
- IS_MMAPPED(M): 当这个chunk是mmap’d时,被设为1
- NON_MAIN_ARENA(N): 当这个chunk属于一个thread arena时,被设为1
Note:
- malloc_chunk结构体的一些field(比如fd,bk)没有被allocated chunk所使用。因为这些field被用户数据填充了
- 用户请求的空间大小将被转换成
malloc_usable_size
。因为存储malloc_chunk和结构对齐需要一些额外的空间。转换的方式是不设置最后3位的可用大小,因此将其用于存储标记信息。
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if allocated | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |N|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7.2 Free Chunk
Free Chunk
的结构如下
prev_size: 没有两个free chunks会相邻在一起。当两个相邻的chunks都是free状态的话,它们会被combined成一个单独的free chunk。因此free chunk的previous chunk
总是allocated的,因此prev_size
指示上一个chunks的user data
大小
size: free chunk的size,最后三个bits是标志位,显然P常为1
fd: 前向指针,指向下一个chunk(in the same bin),而不是指向物理内存空间的下一个chunk
bk:后向指针,指向上一个chunk(in the same bin)
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |N|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
8.Bins
Bins是freelist datastructures
,用来管理free chunks。根据chunk的size,分成四种Bin。
- Fast bin
- Unsorted bin
- Small bin
- Large bin
下面的数据结构用于管理bins:
fastbinsY:管理fast bin,一共10个
bins:管理unsorted,small and large bins.一共有126个bins划分成三组:
Bin 1 : Unsorted bin
Bin 2 to Bin 63 : Small bin
Bin 64 to Bin 126 : Large bin
8.1 Fast Bin
16 ~ 80 bytes 的 chunks 被称为 fast chunk.因此管理这些chunks的bins被称为fast bins.在所有的bins中,fast bins的内存分配和回收速度最快。
- Number of bins : 10个
每一个fast bin 包含一个free chunks单项链表。使用单项链表是因为fast bins chunks并不会从list的中间被移除,添加和删除操作全部会在list的最前面完成,也就是(LIFO) - Chunk size : 8 bytes apart
举个例子,第一个Fast bin(index 0)管理大小为16 bytes的chunks。第二个Fast bin(index 1)管理大小为24 bytes(16+8)的chunks…
因此,每一个fast bin内的chunks的size是一致的 - 在malloc initialization阶段,最大的fast bin size被设置为64 bytes(而不是80)。因此defult fast bin chunks of size 是 16 to 64
- 两个free chunks可以相邻,不会被合并成单独的free chunks。虽然因此碎片化会严重些,但是速度快
- malloc(fast chunk)
- 在最初因为fast bin max size和fast bin indices是空的,因此尽管用户请求fast chunk,这里将不使用fast bin code,而是使用small bin code来为其服务
- 稍后,当这些结构不为空时,fast bin index将会恢复其相应的binlist
- 恢复的binlist中的第一个chunk,将会被removed and returned to the user
- free(fast chunk)
- 计算Fast bin index来恢复到对应的binlist
- 这个free chunk会被添加到上一步的binlist的最后位置
如下图,所谓的“最前面”就是这个最靠近fsatbinsY的chunk
8.2 Unsorted Bin
当small或large chunk被free时,他们不是添加到各自的bin中,而是添加到unsorted bin中。这个机制给予了"glibc malloc"第二次机会来reuse最近被free掉的chunks。
因此,内存分配和释放能够加快一点,因为原来查找可用bin的时间减少了。
- Number of bins : 1个
unsorted bin 为 free chunks维持一个双向链表 - Chunk size : 没有大小限制
8.3 Small Bin
小于 512 bytes 的 chunks被称为small chunks.管理small chunks的bins被称为small chunks.Small bins的内存分配和回收速度快于large bins但比fast bins慢
- Number of bins : 62个
每一个small bins 为 free chunks维持一个双向链表。addition happens at the front end,然后deletion happens at the rear end,即使用FIFO策略 - Chunk size : 8 bytes apart
即第一个Small bin(Bin 2)管理16 bytes的chunks binlist,第二个small bin(Bin 3)管理24 bytes的chunks binlist…
因为每个small bin管理的chunk的size都一样,因此small bin内不需要排序 - Coalescing : 两个free chunk相邻会被合并。
- malloc(small chunk) :
- 最初,所有的small bins均为空。因此尽管用户请求一个small chunk,这时不使用small bin code 而是使用unsorted bin code来提供服务
- 同样,在第一次调用malloc时,malloc_state中的小bin和大bin数据结构(bin)会被初始化。即bin会指向自己,表示它们是空的。
- 稍后,当small bin不为空时,它对应的binlist中最后一个块将删除并返回给用户
- free(small chunk) :
- 当准备free一个chunk时,先检查这个chunk的perivious和next chunk是否处于free状态,如果是则合并他们。即从它们各自的链表中取消这些块的链接,然后将这个新的合并chunk添加到unsorted bin linked list的beginning
8.4 Large Bin
大于 512 bytes 的 chunk被称为large chunk。管理large chunk 的bins被称为large bins。显然large bins的内存分配和回收速度是最慢的
- Number of bins : 63个
- 每一个large bins 为 free chunks维持一个双向链表。chunks的添加删除可以在任何位置发生(front or middle or rear)
- 63个bins管理不同大小的chunks
- 32个bins管理的chunks size符合64 bytes apart规律。即First large bin(Bin 65)管理大小from 512 to 568 bytes的chunks,Seconde large bin(Bin 66)管理大小from 576 to 632 bytes的chunks…
- 16个bins管理的chunks size符合512 bytes apart规律
- 8个bins管理的chunks size符合4096 bytes apart规律
- 4个bins管理的chunks size符合32768 bytes apart规律
- 2个bins管理的chunks size符合262144 bytes apart规律
- 1个bins管理其他剩余大小的chunks
- 与small size不同的是,large bin中的chunk并不是同样大小的。因此采用降序存储的方式,largest chunk会存在最前面,smallest chunk存在最后面。
- Coalescing : 两个free chunk相邻会被合并。
- malloc(large chunk) :
- 最初,所有的large bins均为空。因此尽管用户请求一个large chunk,这时largest bin code不会服务而是由next largest bin code尝试为其服务
- 同样,在第一次调用malloc时,malloc_state中的small bin和large bin数据结构(bin)会被初始化。即bin会指向自己,表示它们是空的。
- 稍后,当large bin不为空时,如果largest chunk(当前binlist最大块的大小)比用户请求的大时,binlist会从后往前寻找一个适合用户请求大小的chunk来提供服务。一旦找到这样的chunk,这个chunk会被分成两个chunks
- User chunk(用户请求大小的chunks,返回给用户)
- Remainder chunk(剩余大小的chunk,添加到unsorted bin中)
- 如果largest chunk size(in its binlist)还比用户请求的大小小,那么尝试下一个非空largest bin。然后next larest bin code会扫描binmaps来寻找下一个非空largest bin。如果找到相应bin,对应binlist的一个合适的chunk将被取出,split然后返回给用户.如果找不到,最后尝试使用top chunk
- free(large chunk) :
类似于free(small chunk),当准备free一个chunk时,先检查这个chunk的perivious和next chunk是否处于free状态,如果是则合并他们。即从它们各自的链表中取消这些块的链接,然后将这个新的合并chunk添加到unsorted bin linked list的beginning
Note:上面提到的fast bin code,unsorted bin code,small bin code,largest bin code,next largest bin code指的是检索适合的chunk的代码块,其区别在于判断条件,如下
# fast bin code
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
# unsorted bin code
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
# small bin code
if (in_smallbin_range (nb))
# largest bin code
if (!in_smallbin_range (nb))
# next largest bin code
++idx;
该部分在之后更加熟悉需要进一步阅读glibc源代码的时候会进行深入的学习
8.5 Top chunk
一个arena最顶端的chunk被称为top chunk。它不属于任意一个bin。当没有free blocks时,Top chunk为用户请求服务。如果top chunk size比用户请求的要大,那么top chunk将被split成两部分
- User chunk
- Remainder chunk
Remainder chunk将变成新的top chunk。如果top chunk的size比用户请求的要小。那么top chunk将通过sbrk(main arena)或者mmap(thread arena)系统调用来进行扩展
8.6 Last Remainder chunk
Remainder来源于最近split的小块请求。Last Remainder chunk有助于实现引用的局部性。即小块的连续malloc请求最终分配结果可能很相近。
问题来了,arena中那么多可用chunk,哪一块才是last remainder chunk?
当用户请求small chunk时,并不是直接由small bin和unsorted bin来服务。binmaps会扫描寻找下一个非空largest bin。如同上面所说一样,当寻找到这样的bin后,会将相应的chunk进行split操作,User chunk返回给用户,剩下一个remainder chunk添加到unsorted bin上。然后这一个remainder chunk也就成为了新的last remainder chunk
那么引用的局部性又是怎么实现的的?
当用户随后请求一个小块,如果last remainder chunk是unsorted bin中的唯一一个chunk,则它又会被split成两部分user chunk和remainder chunk。而这个remainder chunk又会变成新的last remainder chunk。因此,后续的内存分配结果是相邻的。
0x04 小结
对glibc malloc的理解还不够深入,所以称为小结。
1.Threading and Arena
不是一一映射的关系,只有一个main arena in main thread
2.Arena,heap,Bins and chunk
再贴图
Arena
管理heap,一个arena可以有多个堆(main arena只有一个堆)。malloc_state
是Arena
的头部结构,包含bins
,top chunk
,last remainder chunk
等等信息heap
的头部结构是heap_info
,每个堆都有自己的头部。一个堆被划分成很多个chunks
,chunk
的头部结构是malloc_chunk
(allocated chunk和free chunk的本质结构,示意图上只是为了凸显而把malloc_chunk凸显出来)Bins
是用来管理freelist
的数据结构,管理free chunks。根据所管理chunk的大小分成四类进行不同的管理