1、内存管理层次
平时业务代码都是用过glibc提供的malloc()函数申请内存,不用关心底层的内存管理实现,如果掌握了底层的内存管理原理,可以帮助我们设计出更优秀的业务代码
glibc主要负责给业务代码提供内存,相当于是1个内存零售商,它从操作系统那里批发大量的内存(以pagesize为单位,通常是4K大小),然后化整为零出售给业务代码,业务代码很多时候只需要少量的内存,例如malloc(100), 如果直接向操作系统申请4K大小的内存,存在明显的内存浪费,并且向操作系统申请内存需要系统调用,对性能也是一大损耗。 glibc内存管理的数据结构属于每个进程,即每个进程都有自己的零售商,进程A的零售商囤积的货物不能给进程B使用。因此我们要避免零售商囤积太多的货物(进程内部的glibc缓存太多的内存),大家都无法使用。
2、进程地址空间
测试程序
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> void* threadFunc(void* arg) { char *p = NULL; p = (char *)malloc(100); memset(p, 0xaa, 100); getchar(); } int main() { pthread_t t1; void* s; int ret; char* addr; addr = (char*) malloc(1000); ret = pthread_create(&t1, NULL, threadFunc, NULL); if(ret){ return -1; } ret = pthread_join(t1, &s); if(ret){ return -1; } return 0; }
在安装pwndbg的环境中,运行gdb调试命令,vmmap则可以输出进程的地址空间
进程地址空间
从上图我们可以看出,进程的地址空间主要分成如下几个部分
代码段
数据段
堆空间: 主要包括mmap(主线程malloc申请大于128KB, 线程malloc,线程栈空间, 主动调用mmap申 请) 和 sbrk(图中heap所示部分) 两个部分
共享库加载的空间: 库文件加载的位置
栈空间: 向下减小,几乎无限大
3、glibc内存管理数据结构
-
malloc最小单元chunk
用户调用malloc()函数的时候,申请的内存包含两个部分,头部信息和返回给用户使用的内存空间。我们称之为1个chunk。头部信息主要是管理内存的开销,例如标识内存的大小,内存的性质等。当free()释放内存的时候,glibc会把释放的chunk挂到空闲链表中。
-
chunk缓存在glibc中

-
chunk主要分成两部分:
1、allocated: 被业务代码申请
2、free: 业务代码释放内存、可能被缓存在glibc中,主要挂接在各自分配区的bins链表上
-
分配区
当进程有多个线程的时候,如果只有1个分配区,会有锁的开销。因此glibc设计的时候,根据线程数量、CPU数量等参数,设计了多个分配区(struct malloc_state)。包含1个主分配区main_arean和多个非主分配区(如果有需要)。
主分配区: 分配区管理信息(struct malloc_state)存储在进程的数据段中,是一个全局变量。主要通过sbrk()向内核申请内存。
非主分配区:分配区管理信息(struct malloc_state)存储在子堆的heap_info之后(进程地址空间mmap申请出来的空间的sub-heap中)。主要通过mmap向内核申请内存。如果申请的内存使用完毕,则使用mmap再次申请一个sub-heap子堆,并且在heap_info的指针,prev指向前1个sub-heap。
4、主分配区空间的申请与释放
4.1 申请的空间大于128KB
如果malloc申请的空间大于128KB,则glibc认为申请的空间很大,可以直接从内核批发申请大内存,可以绕过零售商(glibc主要零售小内存)。因此大于128KB的时候,直接在mmap区域直接向内核申请。
在主线程中malloc申请512KB的空间
ptr = (char *)malloc(512*1024);
memset(ptr, 0xbb, 256*1024);
-
vmmap查看
0x7ffff751b000 0x7ffff759c000 rw-p 81000 0 [anon_7ffff751b]
-
查看内存值
pwndbg> x/8x 0x7ffff751b000 0x7ffff751b000: 0x00000000 0x00000000 0x00081002 0x00000000 0x7ffff751b010: 0xbbbbbbbb 0xbbbbbbbb 0xbbbbbbbb 0xbbbbbbbb pwndbg>
-
分析
0x81000 = 516KB = 512KB(返回给业务代码使用) + 4KB(chunk头使用16字节,因为向内核申请内存以页pagesize为单位,所以需要填充为4KB)
读取chunk头的信息,内存值为0x00081002, 其中0x81000表示内存大小 0x2表示该片内存空间是(0:主分配区; 1: mmap申请的 0: 前面的内存空闲)
当释放该内存空间的时候,检测到该内存是mmap申请的(010),直接归还给操作系统。因此申请内存大于128KB的时候,内存的申请和释放glibc没有参与
4.2、申请的空间小于128KB
4.2.1、 heap内存空间充足
主分配区分配内存的时候,分配步骤如下:
- 首先从main_arena的bins链表中找到空闲的内存;
- 如果空闲内存不够用的时候,则从top指针指向的地址分配内存;
- 如果top指针指向的内存仍然不够用,通过增长Heap堆空间,向操作系统申请内存
主分配区释放内存的步骤:
- 释放内存的时候,如果邻接chunk非空闲,则直接添加到bins链表中
- 如果邻接chunk空闲,则合并后再添加到bins链表中
- 如果和top chunk邻接,则直接和top chunk合并,top chunk增大到一定阈值,则sbrk()向下减少,归还内存给操作系统
缺陷:
如图中所示,如果chunk A一直不释放,即使chunkA 到chunkB中间的内存都已经释放,导致无法和top chunk合并,内存也没有办法归还操作系统
4.2.2、 heap空间不足
主分配区的Heap空间向上增长,如果增长过程中发现上面的地址被使用了,则无法继续增长,可以在mmap区域申请内存,并且把main_arena的top指针指向新申请的区域
- 缺陷:
由于新申请的mmap内存没有heap_info结构体,没有prev指针指向Heap空间。导致即使mmap的空间都释放了(top指针指向mmap的基地址,无法指向Heap空间),Heap空间一直无法释放,导致内存泄露
- 测试程序
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h>
#include <sys/mman.h> #define ARRAY_SIZE 1024
int main(int argc, char** argv)
{
char* ptr_arr[ARRAY_SIZE]; int i; char* mmap_var; char* addr;
printf("begin: sbrk:%p\n", sbrk(0));
/* 向操作系统批发132KB的堆空间 */
addr = (char *)malloc(100); memset(addr, 0x11, 100);
printf("after malloc(100): sbrk:%p\n", sbrk(0));
/* mmap占据堆顶后1M的地址空间 */
mmap_var = mmap((void*)sbrk(0) + 1024*1024, 127*1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("after mmap_var sbrk:%p mmap_var: %p\n", sbrk(0), mmap_var);
/* 分配内存,总大小超过1M,导致main_arena被拆分 */
for( i = 0; i < ARRAY_SIZE; i++) {
ptr_arr[i] = malloc(4096);
} printf("after malloc(4M) sbrk:%p\n", sbrk(0));
/* 释放所有内存,观察内存使用是否改变 */
for( i = 0; i < ARRAY_SIZE; i++) {
free(ptr_arr[i]);
} free(addr);
printf("after free all memory: sbrk:%p\n", sbrk(0)); munmap(mmap_var, 127*1024);
return 1;
}
- 调试、在释放所有内存的地方打断点
pwndbg> bins
tcachebins
0x70 [ 1]: 0x5555555596b0 ◂— 0x0
fastbins
empty
unsortedbin
all: 0x7ffff7bc3000 —▸ 0x7ffff7cc3000 —▸ 0x555555559710 —▸ 0x7ffff7fafbe0 (main_arena+96) ◂— 0x7ffff7bc3000 // 即使释放了所有内存,Heap空间的内存仍然没有释放,缓存在glibc中
smallbins
empty
largebins
empty
pwndbg> p main_arena.top //所有内存释放了,top还是指向了mmap的底部位置
$7 = (mchunkptr) 0x7ffff7ac3000
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x555555559000 0x555555661000 rw-p 108000 0 [heap] // 堆空间,因为mmap无法增长了
0x55555567a000 0x55555569a000 rw-p 20000 0 [anon_55555567a] //mmap的空间
0x7ffff7ac3000 0x7ffff7dc3000 rw-p 300000 0 [anon_7ffff7ac3] //堆空间不够,通过mmap申请的堆空间
pwndbg> arenas
arena type arena address heap address map start map end perm size offset file
------------ --------------- -------------- -------------- -------------- ------ ------ -------- ----------------
main_arena 0x7ffff7fafb80 0x555555559000 0x555555559000 0x555555661000 rw-p 108000 0 [heap]
↳ 0x7ffff7ac3000 0x7ffff7ac3000 0x7ffff7dc3000 rw-p 300000 0 [anon_7ffff7ac3]
pwndbg> p mmap_var
$8 = 0x55555567a000 ""
pwndbg>
- 代码中打印glibc中的信息
void print_info()
{
struct mallinfo mi = mallinfo();
printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\
\tmmap_total=%lu mmap_count=%lu\n", mi.arena, mi.fordblks, mi.uordblks, mi.hblkhd, mi.hblks);
}
5、非主分配区空间的申请与释放
5.1 申请的空间大于128KB
行为和主分配区一致
5.2、申请的空间小于128KB
主分配区通过Heap增长申请空间、但是非主分分配区通过mmap分配64M大小的sub-heap。64M大小的sub-heap通过heap_info的prev指针连接起来,构成一个类似于主分配区的大的Heap空间。non-main arena的top指针指向最后一个sub-heap,当sub-heap的空间都归还给操作系统后,top又指向sub-heap的prev指针指向的sub-heap.
6、链接空闲内存块chunk的链表bins
从图中我们可以看到,不同内存大小的块需要放入不同的bin中,由于释放内存的时候,需要前后合并,所以释放内存周期会变长。 为了加快小内存的释放速度,引入了fastbin(小于128字节), 当释放小于128字节的内存的时候,直接放入fastbin, 不需要内存合并。申请内存的时候,如果在small bins找不到合适的,会触发fastbin的合并,合并的会放入unsorted bin中
问题:
如果释放了64字节的chunk,该chunk紧邻top chunk, 如果该chunk直接放入了fastbin,没有和top chunk合并,则会导致无法归还内存给操作系统,导致内存泄露;