searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

闲谈glibc内存管理

2023-10-24 01:23:35
80
0

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合并,则会导致无法归还内存给操作系统,导致内存泄露;

0条评论
0 / 1000
湛****涛
2文章数
0粉丝数
湛****涛
2 文章 | 0 粉丝
湛****涛
2文章数
0粉丝数
湛****涛
2 文章 | 0 粉丝
原创

闲谈glibc内存管理

2023-10-24 01:23:35
80
0

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合并,则会导致无法归还内存给操作系统,导致内存泄露;

文章来自个人专栏
湛松涛
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
1
0