Linux堆内存管理深刻分析(下)

 Linux堆内存管理深刻分析前端

(下半部)

做者@走位,阿里聚安全git

0 前言回顾

在上一篇文章中(连接见文章底部),详细介绍了堆内存管理中涉及到的基本概念以及相互关系,同时也着重介绍了堆中chunk分配和释放策略中使用到的隐式链表技术。经过前面的介绍,咱们知道使用隐式链表来管理内存chunk总会涉及到内存的遍历,效率极低。对此glibc malloc引入了显示链表技术来提升堆内存分配和释放的效率。github

所谓的显示链表就是咱们在数据结构中经常使用的链表,而链表本质上就是将一些属性相同的“结点”串联起来,方便管理。在glibc malloc中这些链表统称为bin,链表中的“结点”就是各个chunk,结点的共同属性就是:1)均为free chunk;2)同一个链表中各个chunk的大小相等(有一个特例,详情见后文)。算法

 

1 bin介绍

如前文所述,bin是一种记录free chunk的链表数据结构。系统针对不一样大小的free chunk,将bin分为了4类:1) Fast bin; 2) Unsorted bin; 3) Small bin; 4) Large bin。c#

 

在glibc中用于记录bin的数据结构有两种,分别以下所示:数组

fastbinsY: 这是一个数组,用于记录全部的fast bins;安全

bins: 这也是一个数组,用于记录除fast bins以外的全部bins。事实上,一共有126个bins,分别是:数据结构

bin 1 为unsorted bin;函数

bin 2 到63为small bin;ui

bin 64到126为large bin。

其中具体数据结构定义以下:

struct malloc_state

{

  ……

  /* Fastbins */

  mfastbinptr fastbinsY[NFASTBINS];

  ……

  /* Normal bins packed as described above */

  mchunkptr bins[NBINS * 2 - 2];  // #define NBINS    128

  ……

};

这里mfastbinptr的定义:typedef struct malloc_chunk *mfastbinptr;

mchunkptr的定义:typedef struct malloc_chunk* mchunkptr;

 

画图更直观:

 

图1-1 bins分类

 

那么处于bins中个各个free chunk是如何连接在一块儿的呢?回顾malloc_chunk的数据结构:

struct malloc_chunk {

  /* #define INTERNAL_SIZE_T size_t */

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */

  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* 这两个指针只在free chunk中存在*/

  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;

};

其中的fd和bk指针就是指向当前chunk所属的链表中forward或者backward chunk。

 

2 Fast bin

 

既然有fast bin,那就确定有fast chunk——chunk size为1680字节的chunk就叫作fast chunk。为了便于后文描述,这里对chunk大小作以下约定:

1)      只要说到chunk size,那么就表示该malloc_chunk的实际总体大小;

2)      而说到chunk unused size,就表示该malloc_chunk中刨除诸如prev_size, size, fd和bk这类辅助成员以后的实际可用的大小。所以,对free chunk而言,其实际可用大小老是比实际总体大小少16字节。

 

在内存分配和释放过程当中,fast bin是全部bin中操做速度最快的。下面详细介绍fast bin的一些特性:

1) fast bin的个数——10个

2)每一个fast bin都是一个单链表(只使用fd指针)。为何使用单链表呢?由于在fast bin中不管是添加仍是移除fast chunk,都是对“链表尾”进行操做,而不会对某个中间的fast chunk进行操做。更具体点就是LIFO(后入先出)算法:添加操做(free内存)就是将新的fast chunk加入链表尾,删除操做(malloc内存)就是将链表尾部的fast chunk删除。须要注意的是,为了实现LIFO算法,fastbinsY数组中每一个fastbin元素均指向了该链表的rear end(尾结点),而尾结点经过其fd指针指向前一个结点,依次类推,如图2-1所示。

3) chunk size:10个fast bin中所包含的fast chunk size是按照步进8字节排列的,即第一个fast bin中全部fast chunk size均为16字节,第二个fast bin中为24字节,依次类推。在进行malloc初始化的时候,最大的fast chunk size被设置为80字节(chunk unused size为64字节),所以默认状况下大小为16到80字节的chunk被分类到fast chunk。详情如图2-1所示。

4) 不会对free chunk进行合并操做。鉴于设计fast bin的初衷就是进行快速的小内存分配和释放,所以系统将属于fast bin的chunk的P(未使用标志位)老是设置为1,这样即便当fast bin中有某个chunk同一个free chunk相邻的时候,系统也不会进行自动合并操做,而是保留二者。虽然这样作可能会形成额外的碎片化问题,但瑕不掩瑜。

5) malloc(fast chunk)操做:即用户经过malloc请求的大小属于fast chunk的大小范围(注意:用户请求size加上16字节就是实际内存chunk size)。在初始化的时候fast bin支持的最大内存大小以及全部fast bin链表都是空的,因此当最开始使用malloc申请内存的时候,即便申请的内存大小属于fast chunk的内存大小(即16到80字节),它也不会交由fast bin来处理,而是向下传递交由small bin来处理,若是small bin也为空的话就交给unsorted bin处理:

/* Maximum size of memory handled in fastbins.  */

static INTERNAL_SIZE_T global_max_fast;

 

/* offset 2 to use otherwise unindexable first 2 bins */

/*这里SIZE_SZ就是sizeof(size_t),在32位系统为4,64位为8,fastbin_index就是根据要malloc的size来快速计算该size应该属于哪个fast bin,即该fast bin的索引。由于fast bin中chunk是从16字节开始的,全部这里以8字节为单位(32位系统为例)有减2*8 = 16的操做!*/

#define fastbin_index(sz) \

  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

 

 

/* The maximum fastbin request size we support */

#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)

 

#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

 

 

那么fast bin 是在哪?怎么进行初始化的呢?当咱们第一次调用malloc(fast bin)的时候,系统执行_int_malloc函数,该函数首先会发现当前fast bin为空,就转交给small bin处理,进而又发现small bin 也为空,就调用malloc_consolidate函数对malloc_state结构体进行初始化,malloc_consolidate函数主要完成如下几个功能:

a. 首先判断当前malloc_state结构体中的fast bin是否为空,若是为空就说明整个malloc_state都没有完成初始化,须要对malloc_state进行初始化。

b. malloc_state的初始化操做由函数malloc_init_state(av)完成,该函数先初始化除fast bin以外的全部的bins(构建双链表,详情见后文small bins介绍),再初始化fast bins。

而后当再次执行malloc(fast chunk)函数的时候,此时fast bin相关数据不为空了,就开始使用fast bin(见下面代码中的※1部分):

static void *

_int_malloc (mstate av, size_t bytes)

{

  ……

  /*

     If the size qualifies as a fastbin, first check corresponding bin.

     This code is safe to execute even if av is not yet initialized, so we

     can try it without checking, which saves some time on this fast path.

   */

   //第一次执行malloc(fast chunk)时这里判断为false,由于此时get_max_fast ()为0

   if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))

    {

  ※1  idx = fastbin_index (nb);

      mfastbinptr *fb = &fastbin (av, idx);

      mchunkptr pp = *fb;

      do

        {

          victim = pp;

          if (victim == NULL)

            break;

        }

   ※2 while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))!= victim);

      if (victim != 0)

        {

          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))

            {

              errstr = "malloc(): memory corruption (fast)";

            errout:

              malloc_printerr (check_action, errstr, chunk2mem (victim));

              return NULL;

            }

          check_remalloced_chunk (av, victim, nb);

          void *p = chunk2mem (victim);

          alloc_perturb (p, bytes);

          return p;

        }

    }

获得第一个来自于fast bin的chunk以后,系统就将该chunk从对应的fast bin中移除,并将其地址返回给用户,见上面代码※2处。

6) free(fast chunk)操做:这个操做很简单,主要分为两步:先经过chunksize函数根据传入的地址指针获取该指针对应的chunk的大小;而后根据这个chunk大小获取该chunk所属的fast bin,而后再将此chunk添加到该fast bin的链尾便可。整个操做都是在_int_free函数中完成。

在main arena中Fast bins(即数组fastbinsY)的总体操做示意图以下图所示:

图2-1 fast bin示意图

 

3 Unsorted bin

当释放较小或较大的chunk的时候,若是系统没有将它们添加到对应的bins中(为何,在什么状况下会发生这种事情呢?详情见后文),系统就将这些chunk添加到unsorted bin中。为何要这么作呢?这主要是为了让“glibc malloc机制”可以有第二次机会从新利用最近释放的chunk(第一次机会就是fast bin机制)。利用unsorted bin,能够加快内存的分配和释放操做,由于整个操做都再也不须要花费额外的时间去查找合适的bin了。

Unsorted bin的特性以下:

1) unsorted bin的个数: 1个。unsorted bin是一个由free chunks组成的循环双链表。

2) Chunk size: 在unsorted bin中,对chunk的大小并无限制,任何大小的chunk均可以归属到unsorted bin中。这就是前言说的特例了,不过特例并不是仅仅这一个,后文会介绍。

 

4 Small bin

小于512字节的chunk称之为small chunk,small bin就是用于管理small chunk的。就内存的分配和释放速度而言,small bin比larger bin快,但比fast bin慢。

Small bin的特性以下:

1) small bin个数:62个。每一个small bin也是一个由对应free chunk组成的循环双链表。同时Small bin采用FIFO(先入先出)算法:内存释放操做就将新释放的chunk添加到链表的front end(前端),分配操做就从链表的rear end(尾端)中获取chunk。

2) chunk size: 同一个small bin中全部chunk大小是一?样的,且第一个small bin中chunk大小为16字节,后续每一个small bin中chunk的大小依次增长8字节,即最后一个small bin的chunk为16 + 62 * 8 = 512字节。

3) 合并操做:相邻的free chunk须要进行合并操做,即合并成一个大的free chunk。具体操做见下文free(small chunk)介绍。

4) malloc(small chunk)操做:相似于fast bins,最初全部的small bin都是空的,所以在对这些small bin完成初始化以前,即便用户请求的内存大小属于small chunk也不会交由small bin进行处理,而是交由unsorted bin处理,若是unsorted bin也不能处理的话,glibc malloc就依次遍历后续的全部bins,找出第一个知足要求的bin,若是全部的bin都不知足的话,就转而使用top chunk,若是top chunk大小不够,那么就扩充top chunk,这样就必定能知足需求了(还记得上一篇文章中在Top Chunk中留下的问题么?答案就在这里)。注意遍历后续bins以及以后的操做一样被large bin所使用,所以,将这部份内容放到large bin的malloc操做中加以介绍。

那么glibc malloc是如何初始化这些bins的呢?由于这些bin属于malloc_state结构体,因此在初始化malloc_state的时候就会对这些bin进行初始化,代码以下:

malloc_init_state (mstate av)

{

  int i;

  mbinptr bin;

 

  /* Establish circular links for normal bins */

  for (i = 1; i < NBINS; ++i)

    {

      bin = bin_at (av, i);

      bin->fd = bin->bk = bin;

}

……

}

注意在malloc源码中,将bins数组中的第一个成员索引值设置为了1,而不是咱们经常使用的0(在bin_at宏中,自动将i进行了减1处理…)。从上面代码能够看出在初始化的时候glibc malloc将全部bin的指针都指向了本身——这就表明这些bin都是空的。

事后,当再次调用malloc(small chunk)的时候,若是该chunk size对应的small bin不为空,就从该small bin链表中取得small chunk,不然就须要交给unsorted bin及以后的逻辑来处理了。

5) free(small chunk):当释放small chunk的时候,先检查该chunk相邻的chunk是否为free,若是是的话就进行合并操做:将这些chunks合并成新的chunk,而后将它们从small bin中移除,最后将新的chunk添加到unsorted bin中。

 

5 Large bin

大于512字节的chunk称之为large chunk,large bin就是用于管理这些large chunk的。

Large bin的特性以下:

1) large bin的数量:63个。Large bin相似于small bin,只是须要注意两点:一是同一个large bin中每一个chunk的大小能够不同,但必须处于某个给定的范围(特例2) ;二是large chunk能够添加、删除在large bin的任何一个位置。

在这63个large bins中,前32个large bin依次以64字节步长为间隔,即第一个large bin中chunk size为512~575字节,第二个large bin中chunk size为576 ~ 639字节。紧随其后的16个large bin依次以512字节步长为间隔;以后的8个bin以步长4096为间隔;再以后的4个bin以32768字节为间隔;以后的2个bin以262144字节为间隔;剩下的chunk就放在最后一个large bin中。

鉴于同一个large bin中每一个chunk的大小不必定相同,所以为了加快内存分配和释放的速度,就将同一个large bin中的全部chunk按照chunk size进行从大到小的排列:最大的chunk放在链表的front end,最小的chunk放在rear end。

2) 合并操做:相似于small bin。

3) malloc(large chunk)操做:

初始化完成以前的操做相似于small bin,这里主要讨论large bins初始化完成以后的操做。首先肯定用户请求的大小属于哪个large bin,而后判断该large bin中最大的chunk的size是否大于用户请求的size(只须要对比链表中front end的size便可)。若是大于,就从rear end开始遍历该large bin,找到第一个size相等或接近的chunk,分配给用户。若是该chunk大于用户请求的size的话,就将该chunk拆分为两个chunk:前者返回给用户,且size等同于用户请求的size;剩余的部分作为一个新的chunk添加到unsorted bin中。

若是该large bin中最大的chunk的size小于用户请求的size的话,那么就依次查看后续的large bin中是否有知足需求的chunk,不过须要注意的是鉴于bin的个数较多(不一样bin中的chunk极有可能在不一样的内存页中),若是按照上一段中介绍的方法进行遍历的话(即遍历每一个bin中的chunk),就可能会发生屡次内存页中断操做,进而严重影响检索速度,因此glibc malloc设计了Binmap结构体来帮助提升bin-by-bin检索的速度。Binmap记录了各个bin中是否为空,经过bitmap能够避免检索一些空的bin。若是经过binmap找到了下一个非空的large bin的话,就按照上一段中的方法分配chunk,不然就使用top chunk来分配合适的内存。

4) Free(large chunk):相似于small chunk。

了解上面知识以后,再结合下图5-1,就不难理解各种bins的处理逻辑了:

6 总结

至此glibc malloc中涉及到的全部显示链表技术已经介绍完毕。鉴于篇幅和精力有限,本文没能详细介绍完全部的技术细节,可是我相信带着这些知识点再去研究glibc malloc的话,定能起到事半功倍的效果。

另外,就我我的所了解到的基于堆溢出攻击而言,掌握以上知识,已经足够理解绝大部分堆溢出攻击技术了。所以,后面的文章将会结合这些知识详细介绍各个攻击技术的实现原理。

老规矩:若有错误,欢迎斧正!

 

上期文章回顾

Linux堆内存管理深刻分析(上)

 

做者:走位@阿里聚安全,更多安全技术文章,请点击阿里聚安全博客 

相关文章
相关标签/搜索