堆基础-glibc_malloc_chunk,bin,threading,arena,system_call

Table of Contents

glibc_malloc_chunk

Overview

在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。当程序申请的 chunk 被 free 后,会被加入到相应的空闲管理列表中。无论chunk的大小、状态如何,他们都是使用同一数据结构——malloc_chunk,只不过是表现形式有所不同。

malloc_chunk结构如下:

/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* 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;
};

根据不同的chunk类型,malloc_chunk会有部分内容选择性“表示”。

堆段中存在的 chunk 类型如下:

  • Allocated chunk;
  • Free chunk;
  • Top chunk;
  • Last Remainder chunk.

allocated chunk

allocated chunk,也就是分配给用户的 chunk,其图示如下:

images

图中左方三个箭头依次表示:

  • chunk:该Allocated chunk的起始地址;
  • mem:该Allocated chunk中用户可用区域的起始地址
  • next_chunk:下一个 chunck(无论类型)的起始地址。

图中结构体内部各字段的含义依次为:

  • prev_size:若上一个 chunk 可用,则此字段赋值为上一个 chunk 的大小;否则,此字段被用来存储上一个 chunk 的用户数据;
  • size:此字段赋值本 chunk 的大小,其最后三位包含标志信息:
    • PREV_INUSE § – 置「1」表示上个 chunk 被分配;
    • IS_MMAPPED (M) – 置「1」表示这个 chunk 是通过 mmap 申请的(较大的内存);
    • NON_MAIN_ARENA (N) – 置「1」表示这个 chunk 属于一个 thread arena。

malloc_chunk 中的其余结构成员,如 fd、 bk,没有使用的必要而拿来存储用户数据; 用户请求的大小被转换为内部实际大小,因为需要额外空间存储 malloc_chunk,此外还需要考虑对齐。

free chunk

free chunk就是用户free后释放的chunk。 free_chunk.png

图中结构体内部各字段的含义依次为:

  • prev_size: 两个相邻 free chunk 会被合并成一个,因此该字段总是保存前一个 allocated chunk 的用户数据;
  • size: 该字段保存本 free chunk 的大小;
  • fd: Forward pointer —— 本字段指向同一 bin 中的下个 free chunk(free chunk 链表的前驱指针);
  • bk: Backward pointer —— 本字段指向同一 bin 中的上个 free chunk(free chunk 链表的后继指针)。

glibc_malloc_bin

Bins

Overview

用户释放掉的chunk不会立即归还系统,ptmalloc会同一管理heap和mmap映射区域中的chunk。当用户再一次请求分配内存的时候,ptmalloc分配器会试图在空闲的chunk中按照规则匹配一块内存给用户,从而避免频繁系统调用,降低内存分配的开销。
具体实现中,ptmalloc 采用分箱式方法对空闲的 chunk 进行管理。首先,它会根据空闲的 chunk 的大小以及使用状态将 chunk 初步分为 4 类:fast binssmall binslarge binsunsorted bin。每类中仍然有更细的划分,相似大小的 chunk 会用双向链表链接起来。也就是说,在每类 bin 的内部仍然会有多个互不相关的链表来保存不同大小的 chunk。

对于 small bins,large bins,unsorted bin 来说,ptmalloc 将它们维护在同一个数组中。这些 bin 对应的数据结构在 malloc_state 中,如下:

#define NBINS 128
/* Normal bins packed as described above */
mchunkptr bins[ NBINS * 2 - 2 ];

bins 主要用于索引不同 bin 的 fd 和 bk。

为了简化在双链接列表中的使用,每个 bin 的 header 都设置为 malloc_chunk 类型。这样可以避免 header 类型及其特殊处理。但是,为了节省空间和提高局部性,只分配 bin 的 fd/bk 指针,然后使用 repositioning tricks 将这些指针视为一个malloc_chunk*的字段。
以 32 位系统为例,bins 前 4 项的含义如下:

bin 下标 含义
0 bin1 的 fd / bin2 的 prev_size
1 bin1 的 bk / bin2 的 size
2 bin2 的 fd / bin3 的 prev_size
3 bin2 的 bk / bin3 的 size

bin2 的 prev_size、size 和 bin1 的 fd、bk 是重合的。由于我们只会使用 fd 和 bk 来索引链表,所以该重合部分的数据其实记录的是 bin1 的 fd、bk。 也就是说,虽然后一个 bin 和前一个 bin 共用部分数据,但是其实记录的仍然是前一个 bin 的链表数据。通过这样的复用,可以节省空间。

数组中的 bin 依次如下

  1. 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的 chunk 比较杂。
  2. 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
  3. small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。

fastbin的使用标记总是被置1的,所以不会被处理。

Fast Bin

大小为 16 ~ 80字节的chunk被称为fast chunk。在所有的bins中,fast bins路径享有最快的内存分配及释放速度。

  • 数量:10
  • 每个 fast bin 都维护着一条 free chunk 的单链表,采用单链表是因为链表中所有 chunk 的大小相等,增删 chunk 发生在链表顶端即可;—— LIFO(Last in first out)
  • chunk 大小:8 字节递增
  • fast bins 由一系列所维护 chunk 大小以 8 字节递增的 bins 组成。也即,fast bin[0] 维护大小为 16 字节的 chunk、fast bin[1] 维护大小为 24 字节的 chunk。依此类推……
  • 指定 fast bin 中所有 chunk 大小相同;
  • 在 malloc 初始化过程中,最大的 fast bin 的大小被设置为 64 而非 80 字节。因为默认情况下只有大小 16 ~ 64 的 chunk 被归为 fast chunk 。
  • 无需合并 —— 两个相邻 chunk 不会被合并。虽然这可能会加剧内存碎片化,但也大大加速了内存释放的速度!
  • malloc(fast chunk)
  • 初始情况下 fast chunck 最大尺寸以及 fast bin 相应数据结构均未初始化,因此即使用户请求内存大小落在 fast chunk 相应区间,服务用户请求的也将是 small bin 路径而非 fast bin 路径;
  • 初始化后,将在计算 fast bin 索引后检索相应 bin;
  • 相应 bin 中被检索的第一个 chunk 将被摘除并返回给用户。
  • free(fast chunk)
    • 计算 fast bin 索引以索引相应 bin;
    • free 掉的 chunk 将被添加到上述 bin 的顶端。 fast_chunk.png

Unsorted Bin

当small chunk和large chunk被free掉的时候,它们并不是被添加到各自的bin中,而是被添加在unsorted bin中,这使得分配器可以重新使用最近被free掉的chunk,从而消除寻找合适的bin的时间开销,提升内存分配和释放的效率。

何时,unsorted bin的chunks会移动到small/large chunk中? —> 在内存分配的时候,在前后检索fast/small bins未果之后,在特定条件下,会将unsorted bin中的 chunks转移到合适的bin中去(small/large)。

数量-大小

  • unsorted bin包括一个用于保存free chunk的双向链表。
  • chunk的大小无限制,任何大小的chunk均可以添加到这里。

Small Bin

大小小于512字节的chunk被成为small chunk,保存small chunks的bin被称为small bin.

数量-大小

  • 数量:62
    • 每个small bin都维护着一条free chunk的双向循环链表。采用双向链表的原因是,small bins中的chunk可能会从链表中部摘除。这里新增项放在链表的头部位置,而从链表的尾部位置移除项。
  • chunk大小:8字节递增。
    • Small bins 由一系列所维护 chunk 大小以 8 字节递增的 bins 组成。举例而言,small bin[0] (Bin 2)维护着大小为 16 字节的 chunks、small bin[1](Bin 3)维护着大小为 24 字节的 chunks ,依此类推……指定 small bin 中所有 chunk 大小均相同,因此无需排序。

合并

相邻的free chunk将被合并,这减缓了内存碎片化,但是减慢了 free 的速度

malloc(small chunk)

  • 初始情况下,small bins都是NULL,因此尽管用户请求small chunk,提供服务的将是unsorted bin 路径而不是small bin路径;
  • 第一次调用malloc时,维护在 malloc_state中的small bins和large bins将被初始化,它们都会指向自身以表示其为空;
  • 此后当small bin非空,相应的bin会摘除其中最后一个chunk并返回给用户;

free(small chunk)

free chunk 的时候,检查其前后的chunk是否空闲,若是则合并,也即把它们从所属的链表中摘除并合并成一个新的chunk,新chunk会添加在unsorted bin的前端。

Large Bin

大小-数量

  • 数量:62
    • 每个large bin都维护着一条free chunk的双向循环链表。采用双向链表的原因是,large bins中的chunk可能会从链表中的任意位置插入及删除。
  • 大小:large bin中所有chunk大小不一定相同,各chunk大小递减保存。最大的chunk保存顶端,而最小的chunk保存在尾端;
    • 这 63 个 bins
      • 32 个 bins 所维护的 chunk 大小以 64B 递增,也即 large chunk[0](Bin 65) 维护着大小为 512B ~ 568B 的 chunk 、large chunk[1](Bin 66) 维护着大小为 576B ~ 632B 的 chunk,依此类推……
      • 16 个 bins 所维护的 chunk 大小以 512 字节递增;
      • 8 个 bins 所维护的 chunk 大小以 4096 字节递增;
      • 4 个 bins 所维护的 chunk 大小以 32768 字节递增;
      • 2 个 bins 所维护的 chunk 大小以 262144 字节递增;
      • 1 个 bin 维护所有剩余 chunk 大小;

合并

两个相邻的空闲 chunk 会被合并

malloc(large chunk)

  • 初始情况下,large bin都会是NULL,因此尽管用户请求large chunk,提供服务的将是next largetst bin路径而不是large bin路径。
  • 第一次调用malloc时,维护在malloc_state中的small bin和large bin将被初始化,它们都会指向自身以表示其为空;
  • 此后当large bin非空,如果相应bin中的最大chunk大小大于用户请求大小,分配器就从该bin顶端遍历到尾端,以找到一个大小最接近用户请求的chunk。一旦找到,相应chunk就会被切分成两块:
    • User chunk(用户请求大小)—— 返回给用户;
    • Remainder chunk (剩余大小)—— 添加到unsorted bin。
  • 如果相应bin中的最大 chunk 大小小于用户请求大小,分配器就会扫描binmaps,从而查找最小非空 bin。如果找到了这样的bin,就从中选择合适的chunk并切割给用户;反之就使用top chunk响应用户请求。

free(large chunk)

类似于 small chunk。

Top Chunk

一个arena中最顶部的chunk被称为top chunk。它不属于任何bin。当所有bin中都没有合适空闲内存时,就会使用top chunk来响应用户请求。

当top chunk的大小比用户请求的大小大的时候,top chunk会分割为两个部分:

  • User chunk,返回给用户;
    • Remainder chunk,剩余部分,将成为新的top chunk。

当top chunk的大小比用户请求的大小小的时候,top chunk就通过 sbrk(main arena)或 mmap( thread arena)系统调用扩容

top chunk的prev_inuse比特位始终为1,否则其前面的chunk就会被合并到top chunk中。初始情况下,我们可以将 unsorted chunk 作为 top chunk

Last Remainder Chunk

「last remainder chunk」即最后一次 small request 中因分割而得到的剩余部分,它有利于改进引用局部性,也即后续对 small chunk 的 malloc 请求可能最终被分配得彼此靠近。

arena 中的若干 chunks,哪个有资格成为 last remainder chunk ? 当用户请求 small chunk 而无法从 small bin 和 unsorted bin 得到服务时,分配器就会通过扫描 binmaps 找到最小非空 bin。正如前文所提及的,如果这样的 bin 找到了,其中最合适的 chunk 就会分割为两部分:

  • 返回给用户的 User chunk
  • 添加到 unsorted bin 中的 Remainder chunk 这一 Remainder chunk 就将成为 last remainder chunk。

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;
}

没有产生预期效果(疑似因内核版本不同)

主线程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

主线程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

主线程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

在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

在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

在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的数据段中找到。

glibc_malloc_system_call

Syscalls used by malloc

brk

brk通过增加程序中断位置(program break location / brk)从内核中获取内存(非零初始化),最初,堆段的起始(start_brk)和结束(brk)指向相同的位置。

当ASLR关闭时,start_brk和brk将指向data/bss段的end(end_data)
当ASLR打开时,start_brk和brk将指向data/bss段的end(end_data)加上随机的brk的偏移。

/* sbrk and brk example */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
        void *curr_brk, *tmp_brk = NULL;

        printf("Welcome to sbrk example:%d\n", getpid());

        /* sbrk(0) gives current program break location */
        tmp_brk = curr_brk = sbrk(0);
        printf("Program Break Location1:%p\n", curr_brk);
        getchar();

        /* brk(addr) increments/decrements program break location */
        brk(curr_brk+4096);

        curr_brk = sbrk(0);
        printf("Program break Location2:%p\n", curr_brk);
        getchar();

        brk(tmp_brk);

        curr_brk = sbrk(0);
        printf("Program Break Location3:%p\n", curr_brk);
        getchar();

        return 0;
}

输出:

 ./brk 
Welcome to sbrk example:6699
Program Break Location1:0x21cd000   -> cat map()

Program break Location2:0x21ce000

Program Break Location3:0x21cd000
cat /proc/6699/maps
00400000-00401000 r-xp 00000000 08:01 789617                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/brk
00600000-00601000 rw-p 00000000 08:01 789617                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/brk
021ac000-021ce000 rw-p 00000000 00:00 0                                  [heap]
7f0b29077000-7f0b29237000 r-xp 00000000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so
7f0b29237000-7f0b29437000 ---p 001c0000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so
7f0b29437000-7f0b2943b000 r--p 001c0000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so
7f0b2943b000-7f0b2943d000 rw-p 001c4000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so
7f0b2943d000-7f0b29441000 rw-p 00000000 00:00 0 
7f0b29441000-7f0b29467000 r-xp 00000000 08:01 2629229                    /lib/x86_64-linux-gnu/ld-2.23.so
7f0b29642000-7f0b29645000 rw-p 00000000 00:00 0 
7f0b29666000-7f0b29667000 r--p 00025000 08:01 2629229                    /lib/x86_64-linux-gnu/ld-2.23.so
7f0b29667000-7f0b29668000 rw-p 00026000 08:01 2629229                    /lib/x86_64-linux-gnu/ld-2.23.so
7f0b29668000-7f0b29669000 rw-p 00000000 00:00 0 
7ffcb750d000-7ffcb752e000 rw-p 00000000 00:00 0                          [stack]
7ffcb7598000-7ffcb759b000 r--p 00000000 00:00 0                          [vvar]
7ffcb759b000-7ffcb759d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

mmap

malloc通过mmap进行私有匿名的段映射。私有匿名映射的目的是分配新内存(零填充),而新内存将由调用进程使用。

分析实例

/* Private anonymous mapping example using mmap syscall */
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

void static inline errExit(const char* msg)
{
        printf("%s failed. Exiting the process\n", msg);
        exit(-1);
}

int main()
{
        int ret = -1;
        printf("Welcome to private anonymous mapping example::PID:%d\n", getpid());
        printf("Before mmap\n");
        getchar();
        char* addr = NULL;
        addr = mmap(NULL, (size_t)132*1024, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (addr == MAP_FAILED)
                errExit("mmap");
        printf("After mmap\n");
        getchar();

        /* Unmap mapped region. */
        ret = munmap(addr, (size_t)132*1024);
        if(ret == -1)
                errExit("munmap");
        printf("After munmap\n");
        getchar();
        return 0;
}
Before mmap
    00400000-00401000 r-xp 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00600000-00601000 rw-p 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00f74000-00f95000 rw-p 00000000 00:00 0                                  [heap]
    7f46271b0000-7f4627370000 r-xp 00000000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so


After mmap
    00400000-00401000 r-xp 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00600000-00601000 rw-p 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00f74000-00f95000 rw-p 00000000 00:00 0                                  [heap]
    7f46271b0000-7f4627370000 r-xp 00000000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so

After munmap
    00400000-00401000 r-xp 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00600000-00601000 rw-p 00000000 08:01 789619                             /home/giantbranch/Desktop/CTF/PWN/glibc_malloc/mmap
    00f74000-00f95000 rw-p 00000000 00:00 0                                  [heap]
    7f46271b0000-7f4627370000 r-xp 00000000 08:01 2629237                    /lib/x86_64-linux-gnu/libc-2.23.so

理论上map之后heap段会增加一段我们设置增加的段大小0x21000的,但是实际编译出来没有产生这个效果,不清楚为什么。同样unmap以后增加的映射段会重新减掉恢复成原先映射之前的大小。

总结

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;malloc小于128k的内存使用brk分配内存。其具体操作示例见下图1,先申请一个30k的堆A,之后再申请B,malloc申请的时候都说edata段的移动既可以完成分配(实际上对应物理页需要等到进程读取内存时,发生缺页中断才会进行分配)。A需要释放的话,需要B提前释放(会产生内存碎片)。

  1. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存进行分配。任意块需要释放可以随时释放。

References