glibc_malloc_threading
多线程支持
linux早期使用dlmalloc作为默认分配器,在dlmalloc中只有一个线程能访问临界区(critical section),因为所有线程共享freelist的数据结构。在ptmalloc2中当两个线程同时调用malloc的时候,内存均会得以分配,因为每个线程都维护着单独的堆段,因此维护这些堆的freelist数据结构也是分开的。这种为每个线程维护单独的堆和空闲列表数据结构的行为称为每个线程领域(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();
addr = (char*) malloc(1000);
printf("After malloc and before free in main thread\n");
getchar();
free(addr);
printf("After free in main thread\n");
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;
}
输出分析
(没有产生预期的效果,按照步骤先记录实验的过程,后面能够发现问题再进行修复),目前猜测是由于内核版本不同,会默认申请一个main arena,所以在主线程申请的时候,会直接在main arena中进行处理,所以没有体现。
1, 主线程malloc前
Welcome to per thread arena example::10710
Before malloc in main thread
cat /proc/10710/maps
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
2, 主线程malloc之后
主线程的堆是堆内存移动program break产生的(移动brk),即使只申请了1000字节的大小但是实际产生了132kb的堆。这块连续的堆区域被称为arena。因为这个arena是主线程建立的,所以称为main arena。接下来的申请会在arena中的剩余部分进行申请。分配完成或者不够的时候,会继续通过移动brk位置扩容,扩容后top chunk的大小也会随之调整,以将新增加的区域加进去。同时,arena也可以在top chunk过大时缩小。
top chunk 是一个 arena 位于最顶层的 chunk。
After malloc and before free in main thread
cat /proc/10710/maps
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
3, 主线程free之后
当分配的内存区域 free 掉时,其并不会立即归还给操作系统,而仅仅是移交给了作为库函数的分配器。这块 free掉的内存添加在了main arenas bin中(在 glibc malloc 中,空闲列表数据结构被称为bin)。随后当用户请求内存时,分配器就不再向内核申请新堆了,而是先试着各个「bin」中查找空闲内存。只有当 bin 中不存在空闲内存时,分配器才会继续向内核申请内存。
After free in main thread
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
4, 在thread1 malloc前
thread1 的堆尚不存在,但其栈已产生(进入对应的函数了)
Before malloc in thread 1
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae130e4000-7fae130e5000 ---p 00000000 00:00 0
7fae130e5000-7fae138e5000 rw-p 00000000 00:00 0
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
5, 在thread1 malloc之后
thread1 的堆段建立在了内存映射段中,这也表明了堆内存是使用 mmap
系统调用产生的,而非同主线程一样使用 sbrk
系统调用。类似地,尽管用户只请求了 1000B,但是映射到程地址空间的堆内存足有 1MB。这 1MB 中,只有 132KB 被设置了读写权限,并成为该线程的堆内存。这段连续内存(132KB)被称为thread arena。
注意:当用户请求超过 128KB(比如
malloc(132*1024)
) 大小并且此时 arena 中没有足够的空间来满足用户的请求时,内存将通过mmap
系统调用(不再是sbrk
)分配,而不论请求是发自 main arena 还是 thread arena。
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.soAfter malloc and before free in thread 1
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae0c000000-7fae0c021000 rw-p 00000000 00:00 0
7fae0c021000-7fae10000000 ---p 00000000 00:00 0
7fae130e4000-7fae130e5000 ---p 00000000 00:00 0
7fae130e5000-7fae138e5000 rw-p 00000000 00:00 0
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
6, 在thread1 free之后
free
不会把内存归还给操作系统,而是移交给分配器,然后添加在了thread arenas bin中
After free in thread 1
00400000-00401000 r-xp 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
00600000-00601000 rw-p 00000000 08:01 789522 /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/threading
020c1000-020e2000 rw-p 00000000 00:00 0 [heap]
7fae0c000000-7fae0c021000 rw-p 00000000 00:00 0
7fae0c021000-7fae10000000 ---p 00000000 00:00 0
7fae130e4000-7fae130e5000 ---p 00000000 00:00 0
7fae130e5000-7fae138e5000 rw-p 00000000 00:00 0
7fae138e5000-7fae13aa5000 r-xp 00000000 08:01 2629237 /lib/x86_64-linux-gnu/libc-2.23.so
总结
虽然实际实验的结果并没有那么理想,但是我们可以总结其中我们需要理解的点。
1.ptmalloc中可以支持多线程同时申请堆块,并且每个线程可以独立管理。
2.主线程中产生的areana是brk产生的,线程中是mmap产生的。
3.第一次brk、mmap的堆空间的申请都会产生一块很大的空间(主:132kb的堆;线程:1MB,132KB 被设置了读写权限)
4.申请的堆空间被free后并不是直接返还,而是给分配器,后续按照bin进行处理管理。
glibc_malloc_arena
Arena
arena的数量
上面可以见的,主线程中包含main areana,而线程中可以包含其自己管理的thread arena。但是线程拥有的arena数量受限制系统核数(数量过多,开销过高,效率降低)
For 32 bit systems:
Number of arena = 2 * number of cores.
For 64 bit systems:
Number of arena = 8 * number of cores.
Multiple Arena
(arena共享、复用?)
例如,现有有一个场景有一个运行在单核计算机上的32位操作系统上的多线程应用,开启了四个线程(一个主线程+3个线程)。这里的线程数4>(2*1),所以分配器中可能有arena会被线程共享。
那么如何进行共享的呢?
1.当主线程第一次调用malloc,已经建立的main areana会被没有任何竞争的使用。
2.当thread1和thread2第一次调用malloc的时候,新的 arena 将被创建,且将被没有任何竞争地使用。此时线程和 arena 之间存在一一映射关系。
3.当thread3第一次调用 malloc 时,arena 的数量限制被计算出来,结果显示已超出,因此尝试复用已经存在的 arena(也即 Main arena 或 Arena 1 或 Arena 2);
4.复用:
- 一旦遍历到可用arena,就开始自旋申请该arena的锁;
- 如果上锁成功(比如说main arena上锁成功),就将该arena返回用户;
- 如果没找到可用arena,thread 3的malloc将被阻塞,直到有可用的arena为止。
5.当thread 3调用 malloc时(第二次了),分配器会尝试使用上一次使用的 arena(也即,main arena),从而尽量提高缓存命中率。当 main arena 可用时就用,否则 thread 3 就一直阻塞,直至 main arena 空闲。因此现在 main arena 实际上是被 main thread 和 thread 3 所共享。
Multiple Heaps
在glibc malloc中主要有3种数据结构:
- heap_info ——Heap Header—— 一个thread arena可以维护多个堆。每个堆都有自己的堆 Header(注:也即头部元数据)。一般情况下,每个thread arena都只维护一个堆,什么时候Thread Arena会维护多个堆呢?当这个堆的空间耗尽时,新的堆(而非连续内存区域)就会被
mmap
到这个 aerna里; - malloc_state ——Arena header—— 一个thread arena可以维护多个堆,这些堆另外共享同一个 arena header。Arena header描述的信息包括:bins、top chunk、last remainder chunk等;
- malloc_chunk ——Chunk header—— 根据用户请求,每个堆被分为若干chunk。每个chunk都有自己的 chunk header。
- Main arena无需维护多个堆,因此也无需heap_info。当空间耗尽时,与thread arena不同,main arena可以通过
sbrk
拓展堆段,直至堆段碰到内存映射段;- 与thread arena不同,main arena的arena header不是保存在通过
sbrk
申请的堆段里,而是作为一个全局变量,可以在libc.so的数据段中找到。
参考链接
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/
https://blog.csdn.net/maokelong95/article/details/51989081