学习Android上的内存分配jemalloc

之前写过一篇文章介绍过libc malloc,主要是PC上的malloc机制。这篇文章要介绍的是jemalloc,Android早在5.0就切换到了jemalloc。所以想要学习Android上堆相关的漏洞利用就要对jemalloc有所了解。

jemalloc基础

jemalloc内部结构如下图所示:

1.region
region是调用malloc返回给用户的实际内存,在内存中连续分布,不包含元数据。根据大小不同,划分为三种类型:

  • small 最大0x3800字节,相同大小的small region在同一个run里;
  • large 最大0x3E000字节(Android6),规整到页大小的整数倍;
  • huge 大于0x3E000字节(Android6),规整到chunk大小(4MB)的整数倍。

2.run
run是存放连续的大小相同的region的容器,大小为页大小的整数倍,内部存放small/large类型的region,也不包含元数据。
3.chunk
chunk是存放run的容器,大小为4MB(可调)或其倍数,且为4MB对齐,操作系统返回的内存被划分到chunk中管理。chunk中存储着关于自身以及它管理的run的元数据。
chunk中的元数据结构,mapbit[0]与mapmisc[0]指向chunk中的第一个run:

chunk元数据中mapmisc中的bitmap结构管理着run中的region的分配使用:

Android6 -> Android7的变化
1.chunk大小:

Arch 32-bit 64-bit
Android6 0x40000 0x40000
Android7 0x80000 0x200000

2.元数据的变化
增加了mapbias与mapbits flags

堆溢出

1.small region溢出

2.run溢出

3.chunk溢出

jemalloc内存管理

arena内存分配器
用来缓解线程间分配memory时的竞争问题,每个进程中有多个arena(arena数量由jemalloc配置决定,在Android上硬编码为两个)。每一个arena彼此独立,管理各自的chunk。每个线程在第一次malloc时,建立起与各自的arena的联系,一个线程只指向一个arena。
在malloc申请内存中,arena与线程缓存的关系:

申请的内存在jemalloc内部实际是通过arena分配的,且在每一个线程中都有一个缓存(tcache)。
每个arena都有一个bin数组,每一个bin中存放着对应size的run(有size相同的small region),用红黑树按地址排序存储未满的run。runcur指向目前正在使用的run。 通过arena分配内存流程:

  • 找到对应的bin
  • 从bin中选择一个run(runcur)
  • 从run中分配出一个region

通过arena释放内存流程,找到存放region的run,然后释放这个region。
线程缓存(tcache)
每一个线程维护着一个对small/large内存分配的缓存tcache,tcache有一个tbin数组,每一个tbin存放着对应size的region缓存栈。分配内存时,没有直接去通过arena要region,而是先去查找对应的tbin存栈avail: 线程申请内存时,会从缓存栈顶pop出一个最近被free到缓存栈上的内存地址,作为新malloc的返回地址。直至缓存栈为空,再向arena申请对应size的region,arena向缓存栈中填充,将内存地址压入缓存栈。 线程缓存在释放内存时的作用:将释放的内存地址压入缓存栈。 同样,缓存栈满了之后,也会将对应的region还给arena,还的数量是缓存栈的一半。

总结

这里只讲了一些比较基础的东西,还有很多细节没有讲到,比如分配和释放时的函数流程,对应的结构等,因为我看的文章讲的都有一些不同,这是因为jemalloc版本更新很快。所以真正想要深入理解jemalloc只能看jemalloc的源码喽。
后面有空的话会找一些jemalloc相关的练习,结合参考文献中提到的shadow工具来具体调试学习。
reference
https://github.com/jemalloc/jemalloc
Exploit Android jemalloc
基于jemalloc的Android漏洞利用技巧—-CENSUS
jemalloc内存分配器详解