1. 程式人生 > >Glibc:淺談 malloc() 函式具體實現

Glibc:淺談 malloc() 函式具體實現

簡介

使用者主動向系統申請堆空間,可以通過 malloc() 或者 realloc() 等函式來操作,其中最主要的函式就是 malloc() 函式。malloc() 函式的定義位於 glibc 的 malloc.c 檔案中。實際上在 glibc 內部 malloc() 函式只是__libc_malloc() 函式的別名,而 __libc_malloc() 函式的工作又主要由 _int_malloc() 完成。因此,分析malloc() 函式,即是分析 __libc_malloc() 以及 _int_malloc() 這兩個函式。

本文以 glibc 2.24 版本中的 malloc() 函式為講述物件,從原始碼的角度簡要地分析 malloc() 函式的具體實現,注意因為完整的 malloc 分配流程過於複雜,所以這裡並不打算對 malloc 進行一次完全的剖析,而只是分析 malloc 在處理小空間分配申請時的一個基本的流程。注意,要讀懂此文需要讀者對 Linux 平臺的堆分配有基本瞭解,明白 bin

chunk 等基本要素的含義。

原始碼

首先,malloc__libc_malloc 的別名:

strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)

__libc_malloc()

void *
__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;

  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if
(__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0)); arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1
, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) (void) mutex_unlock (&ar_ptr->mutex); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; } libc_hidden_def (__libc_malloc)

_int_malloc()

/*
   ------------------------------ malloc ------------------------------
 */

static void *
_int_malloc (mstate av, size_t bytes)
{
  INTERNAL_SIZE_T nb;               /* normalized request size */
  unsigned int idx;                 /* associated bin index */
  mbinptr bin;                      /* associated bin */

  mchunkptr victim;                 /* inspected/selected chunk */
  INTERNAL_SIZE_T size;             /* its size */
  int victim_index;                 /* its bin index */

  mchunkptr remainder;              /* remainder from a split */
  unsigned long remainder_size;     /* its size */

  unsigned int block;               /* bit map traverser */
  unsigned int bit;                 /* bit map traverser */
  unsigned int map;                 /* current word of binmap */

  mchunkptr fwd;                    /* misc temp for linking */
  mchunkptr bck;                    /* misc temp for linking */

  const char *errstr = NULL;

  /*
     Convert request size to internal form by adding SIZE_SZ bytes
     overhead plus possibly more to obtain necessary alignment and/or
     to obtain a size of at least MINSIZE, the smallest allocatable
     size. Also, checked_request2size traps (returning 0) request sizes
     that are so large that they wrap around zero when padded and
     aligned.
   */

  checked_request2size (bytes, nb);

  /* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
     mmap.  */
  if (__glibc_unlikely (av == NULL))
    {
      void *p = sysmalloc (nb, av);
      if (p != NULL)
    alloc_perturb (p, bytes);
      return p;
    }

  /*
     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.
   */

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      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), av);
              return NULL;
            }
          check_remalloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

  /*
     If a small request, check regular bin.  Since these "smallbins"
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */

  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          if (victim == 0) /* initialization check */
            malloc_consolidate (av);
          else
            {
              bck = victim->bk;
    if (__glibc_unlikely (bck->fd != victim))
                {
                  errstr = "malloc(): smallbin double linked list corrupted";
                  goto errout;
                }
              set_inuse_bit_at_offset (victim, nb);
              bin->bk = bck;
              bck->fd = bin;

              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }
    }

  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */

  else
    {
      idx = largebin_index (nb);
      if (have_fastchunks (av))
        malloc_consolidate (av);
    }

  /*
     Process recently freed or remaindered chunks, taking one only if
     it is exact fit, or, if this a small request, the chunk is remainder from
     the most recent non-exact fit.  Place other traversed chunks in
     bins.  Note that this step is the only place in any routine where
     chunks are placed in bins.

     The outer loop here is needed because we might not realize until
     near the end of malloc that we should have consolidated, so must
     do so and retry. This happens at most once, and only when we would
     otherwise need to expand memory to service a "small" request.
   */

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);

          /*
             If a small request, try to use last remainder if it is the
             only chunk in unsorted bin.  This helps promote locality for
             runs of consecutive small requests. This is the only
             exception to best-fit, and applies only when there is
             no exact fit for a small chunk.
           */

          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
            {
              /* split and reattach remainder */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
              av->last_remainder = remainder;
              remainder->bk = remainder->fd = unsorted_chunks (av);
              if (!in_smallbin_range (remainder_size))
                {
                  remainder->fd_nextsize = NULL;
                  remainder->bk_nextsize = NULL;
                }

              set_head (victim, nb | PREV_INUSE |
                        (av != &main_arena ? NON_MAIN_ARENA : 0));
              set_head (remainder, remainder_size | PREV_INUSE);
              set_foot (remainder, remainder_size);

              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

          /* Take now instead of binning if exact fit */

          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

          /* place chunk in bin */

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
                  if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
                    {
                      fwd = bck;
                      bck = bck->bk;

                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                  else
                    {
                      assert ((fwd->size & NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size < fwd->size)
                        {
                          fwd = fwd->fd_nextsize;
                          assert ((fwd->size & NON_MAIN_ARENA) == 0);
                        }

                      if ((unsigned long) size == (unsigned long) fwd->size)
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

#define MAX_ITERS       10000
          if (++iters >= MAX_ITERS)
            break;
        }

      /*
         If a large request, scan through the chunks of current bin in
         sorted order to find smallest that fits.  Use the skip list for this.
       */

      if (!in_smallbin_range (nb))
        {
          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin &&
              (unsigned long) (victim->size) >= (unsigned long) (nb))
            {
              victim = victim->bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb)))
                victim = victim->bk_nextsize;

              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
              if (victim != last (bin) && victim->size == victim->fd->size)
                victim = victim->fd;

              remainder_size = size - nb;
              unlink (av, victim, bck, fwd);

              /* Exhaust */
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
                    {
                      errstr = "malloc(): corrupted unsorted chunks";
                      goto errout;
                    }
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder->fd_nextsize = NULL;
                      remainder->bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }

      /*
         Search for a chunk by scanning bins, starting with next largest
         bin. This search is strictly by best-fit; i.e., the smallest
         (with ties going to approximately the least recently used) chunk
         that fits is selected.

         The bitmap avoids needing to check that most blocks are nonempty.
         The particular case of skipping all bins during warm-up phases
         when no chunks have been returned yet is faster than it might look.
       */

      ++idx;
      bin = bin_at (av, idx);
      block = idx2block (idx);
      map = av->binmap[block];
      bit = idx2bit (idx);

      for (;; )
        {
          /* Skip rest of block if there are no more set bits in this block.  */
          if (bit > map || bit == 0)
            {
              do
                {
                  if (++block >= BINMAPSIZE) /* out of bins */
                    goto use_top;
                }
              while ((map = av->binmap[block]) == 0);

              bin = bin_at (av, (block << BINMAPSHIFT));
              bit = 1;
            }

          /* Advance to bin with set bit. There must be one. */
          while ((bit & map) == 0)
            {
              bin = next_bin (bin);
              bit <<= 1;
              assert (bit != 0);
            }

          /* Inspect the bin. It is likely to be non-empty */
          victim = last (bin);

          /*  If a false alarm (empty bin), clear the bit. */
          if (victim == bin)
            {
              av->binmap[block] = map &= ~bit; /* Write through */
              bin = next_bin (bin);
              bit <<= 1;
            }

          else
            {
              size = chunksize (victim);

              /*  We know the first chunk in this bin is big enough to use. */
              assert ((unsigned long) (size) >= (unsigned long) (nb));

              remainder_size = size - nb;

              /* unlink */
              unlink (av, victim, bck, fwd);

              /* Exhaust */
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }

              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);

                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
                    {
                      errstr = "malloc(): corrupted unsorted chunks 2";
                      goto errout;
                    }
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;

                  /* advertise as last remainder */
                  if (in_smallbin_range (nb))
                    av->last_remainder = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder->fd_nextsize = NULL;
                      remainder->bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }

    use_top:
      /*
         If large enough, split off the chunk bordering the end of memory
         (held in av->top). Note that this is in accord with the best-fit
         search rule.  In effect, av->top is treated as larger (and thus
         less well fitting) than any other available chunk since it can
         be extended to be as large as necessary (up to system
         limitations).

         We require that av->top always exists (i.e., has size >=
         MINSIZE) after initialization, so if it would otherwise be
         exhausted by current request, it is replenished. (The main
         reason for ensuring it exists is that we may need MINSIZE space
         to put in fenceposts in sysmalloc.)
       */

      victim = av->top;
      size = chunksize (victim);

      if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
        {
          remainder_size = size - nb;
          remainder = chunk_at_offset (victim, nb);
          av->top = remainder;
          set_head (victim, nb | PREV_INUSE |
                    (av != &main_arena ? NON_MAIN_ARENA : 0));
          set_head (remainder, remainder_size | PREV_INUSE);

          check_malloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }

      /* When we are using atomic ops to free fast chunks we can get
         here for all block sizes.  */
      else if (have_fastchunks (av))
        {
          malloc_consolidate (av);
          /* restore original bin index */
          if (in_smallbin_range (nb))
            idx = smallbin_index (nb);
          else
            idx = largebin_index (nb);
        }

      /*
         Otherwise, relay to handle system-dependent cases
       */
      else
        {
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p;
        }
    }
}

__libc_malloc() 分析

引數

首先看 __libc_malloc() 的引數:

void * __libc_malloc (size_t bytes)

這裡的 bytes 即是使用者申請分配的空間的大小,並且注意這是使用者提出申請的原始大小,比如說 malloc(12),那麼這裡 bytes 就是 12,而非經過 request2size 巨集計算得到的對應 chunk 的大小。

__malloc_hook 全域性鉤子

ptmalloc 定義了一個全域性鉤子 __malloc_hook,這個鉤子會被賦值為 malloc_hook_ini 函式:

void *weak_variable (*__malloc_hook)
  (size_t __size, const void *) = malloc_hook_ini;

如果我們需要自定義堆分配函式,那麼就可以把 __malloc_hook 重新設定成我們自定義的函式,在 __libc_malloc 的最開始處會判斷是否呼叫 __malloc_hook。也就是說 ptmalloc 給我們提供了一個機會去使用自己定義的堆分配函式來完成對堆空間的申請,申請完成後直接返回,如下:

  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

如果我們沒有自定義堆分配函式,而是選擇預設的 ptmalloc 來幫我們完成申請,那麼在使用者在第一次呼叫 malloc 函式時會首先轉入 malloc_hook_ini 函式裡面,這個函式的定義在 hook.c 檔案,如下:

static void *
malloc_hook_ini (size_t sz, const void *caller)
{
  __malloc_hook = NULL;
  ptmalloc_init ();
  return __libc_malloc (sz);
}

可見在 malloc_hook_ini 會把 __malloc_hook 設定為空,然後呼叫 ptmalloc_init 函式,這個函式的工作是完成對 ptmalloc 的初始化,最後又重複呼叫 __libc_malloc 函式。

因此可知,在我們第一次呼叫 malloc 申請堆空間的時候,首先會進入 malloc_hook_ini 函式裡面進行對 ptmalloc 的初始化工作,然後再次進入 __libc_malloc 的時候,此時鉤子 __malloc_hook 已經被置空了,從而繼續執行剩餘的程式碼,即轉入 _int_malloc 函式。

換個說法,第一次呼叫 malloc 函式時函式呼叫路徑如下:

malloc -> __libc_malloc -> __malloc_hook(即malloc_hook_ini) -> ptmalloc_init -> __libc_malloc -> _int_malloc

以後使用者再呼叫 malloc 函式的時候,路徑將是這樣的:

malloc -> __libc_malloc -> _int_malloc

ptmalloc_init

這裡簡單說一下 ptmalloc_init 函式,ptmalloc_init 的定義在 arena.c 檔案裡面,它裡面有這樣的一些操作:

static void
ptmalloc_init (void)
{
  if (__malloc_initialized >= 0)
    return;

  __malloc_initialized = 0;

  // 初始化 ptmalloc 操作
  ...
  ...

  __malloc_initialized = 1;
}

進入 ptmalloc_init,首先判斷 __malloc_initialized 的值,__malloc_initialized 是一個全域性變數,它標記著 ptmalloc 的初始化狀態,如下:

  • >0 –> 初始化完成
  • =0 –> 正在初始化
  • <0 –> 尚未初始化

在 ptmalloc_init 中完成對 ptmalloc 的初始化工作後,置 __malloc_initialized 為 1,表示 ptmalloc 已經被初始化,之後再次進入 ptmalloc_init 時就會直接退出,不會重複初始化。

轉入 _int_malloc()

經過全域性鉤子 __malloc_hook 的折騰,我們就準備進入 _int_malloc 了:

void *
__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;

  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

  arena_get (ar_ptr, bytes);

  victim = _int_malloc (ar_ptr, bytes);

  ...

  if (ar_ptr != NULL)
    (void) mutex_unlock (&ar_ptr->mutex);

  ...

  return victim;
}

由以上程式碼可知,首先呼叫 arena_get 巨集獲取到分配區 ar_ptr,分配區管理著一大片空閒的空間,我們申請的堆空間就位於這片空間裡面。arena_get 不僅負責分配區的獲取,還負責分配區的加鎖操作,因為 ptmalloc 是支援多執行緒的,但是一個分配區在同一時間只能被一個執行緒操作,所以需要加鎖。如果當前分配區鏈上都沒有空閒的分配區,那麼 arena_get 還負責建立一個新的分配區並返回,bytes 引數就是在建立新的分配區時用於作為新的分配區的空間大小的參考的。

在呼叫完 _int_malloc 之後還有一小段程式碼用於檢查分配是否成功,如果不成功則會繼續尋找可用的分配區進行分配,這裡就不細說了。函式的最後呼叫 mutex_unlock 對分配區解鎖並返回申請到的空間 victim。

_int_malloc 分析

0x0 計算 chunk size

上面說到,__libc_malloc 的引數 bytes 是使用者提交的最原始的空間大小,但是 ptmalloc 分配時是以 chunk 為單位分配的,由原始的空間大小計算得到 chunk 的空間大小由 checked_request2size 巨集完成:

  /*
     Convert request size to internal form by adding SIZE_SZ bytes
     overhead plus possibly more to obtain necessary alignment and/or
     to obtain a size of at least MINSIZE, the smallest allocatable
     size. Also, checked_request2size traps (returning 0) request sizes
     that are so large that they wrap around zero when padded and
     aligned.
   */

  checked_request2size (bytes, nb);

nb 即為計算得到的 chunk 的大小,它在我們後面的分析裡會一直出現,很重要。

0x1 是否命中 fastbins

從 fastbin 中分配 chunk 相當簡單:

  /*
     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.
   */

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      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), av);
              return NULL;
            }
          check_remalloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

通過 get_max_fast 巨集判斷如果 nb 屬於 fastbins,首先獲取到 fastbins 上相應的 fastbin 連結串列,然後獲取連結串列上第一個 chunk。如果該 chunk 不為空,那麼更改連結串列頭指向第二個 chunk,並通過 chunk2mem 巨集返回 chunk 中資料區的地址即可。注意這裡無需設定當前空閒 chunk 的使用標誌位,因為 fastbins 中的空閒 chunk 為了避免被合併,它的使用標誌位依舊是 1,即依然在“使用中”。

如果該 chunk 為空,說明當前 fastbin 中沒有剛好匹配 nb 大小的空閒 chunk。注意在 fastbins 中分配 chunk 時是精確匹配而非最佳匹配。

0x2 是否命中 smallbins

如果在 fastbins 裡面沒有找到滿足要求的空閒 chunk 或者 nb 不屬於 fastbins,並且 nb 屬於 smallbins,那麼執行以下程式碼:

/*
     If a small request, check regular bin.  Since these "smallbins"
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */

  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          if (victim == 0) /* initialization check */
            malloc_consolidate (av);
          else
            {
              bck = victim->bk;
    if (__glibc_unlikely (bck->fd != victim))
                {
                  errstr = "malloc(): smallbin double linked list corrupted";
                  goto errout;
                }
              set_inuse_bit_at_offset (victim, nb);
              bin->bk = bck;
              bck->fd = bin;

              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }
    }
    else{
      ...
    }

smallbins 依然是精確匹配。首先獲取到所在 smallbin,然後通過 (victim = last (bin)) != bin 判斷該 smallbin 是否為空。如果為空則說明獲取失敗,沒有合適的空閒 chunk,直接跳過剩餘程式碼去執行後面的步驟。否則判斷獲取到的連結串列尾的 chunk,即程式碼中的 victim 是否為空,如果為空,說明當前分配區還沒有初始化,則 smallbin 還沒有被初始化,此時會轉至 malloc_consolidate 函式進行初始化後跳過剩餘程式碼。否則獲取連結串列尾部的空閒 chunk 並返回。注意這裡還要通過 set_inuse_bit_at_offset (victim, nb); 設定當前返回 chunk 的使用標誌位,以及非主分配區標誌位。

值得注意的是,如果在 smallbins 已經初始化但沒有找到合適的空閒 chunk,那麼是不會呼叫 malloc_consolidate 來清空 fastbins 的。

0x3 非 smallbins 則轉入 malloc_consolidate 整理 fastbins

如果 nb 不屬於 smallbins,說明申請的空間為 largebins,這時候不會立即檢查 largebins,而是會呼叫 malloc_consolidate 函式將 fastbins 裡面的空閒 chunk 合併整理到 unsortedbin 中,如下:

  if (in_smallbin_range (nb))
    {
    ...
    }
  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */
  else
    {
      idx = largebin_index (nb);
      if (have_fastchunks (av))
        malloc_consolidate (av);
    }

當然,在呼叫 malloc_consolidate 之前先通過 have_fastchunks 檢查當前 fastbins 是否為空,如果本就已經空了,自然沒有整理的必要。標記 fastbins 是否為空的是分配區管理的一個數據成員 flags,在 free 函式中當被釋放空間插入到 fastbins 中時這個資料成員被設定,在 malloc_consolidate 函式中整理 fastbins 時這個資料成員被清除。

0x4 整理 unsortedbin 到 smallbins 以及 largebins

如果經過以上步驟都沒有找到合適的空閒 chunk,此時將開始檢查並整理 unsortedbin,注意這個過程是一邊檢查一邊整理的,即如果有合適的就返回退出,如果沒有就把 unsortedbin 中的每一個空閒 chunk 整理到 smallbins 或者 largebins 中。

首先進來是一個非常大的 for 迴圈,我把這個迴圈整理如下:

    for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          ...
          ...
        }

      if (!in_smallbin_range (nb))
        {
          ...
          ...
        }

      for (;; )
        {
          ...
          ...
        }

    use_top:
      ...
      ...
    }

可見這個迴圈主要由四部分組成,我們先關注第一部分,也即是整理 unsortedbin,如下:

        while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);
      ...
        }

首先獲取 unsortedbin 的第一個 chunk,即 victim,並且獲取到它後面一個 chunk 以及它的大小,這裡還進行了一些檢查。

0x40 是否切割 last_remainder

接下來是一個判斷,這個判斷用到了 last_remainder 這個 chunk,它是分配區的一個特殊成員,和 top chunk 一樣是一個特殊的 chunk:

           /*
             If a small request, try to use last remainder if it is the
             only chunk in unsorted bin.  This helps promote locality for
             runs of consecutive small requests. This is the only
             exception to best-fit, and applies only when there is
             no exact fit for a small chunk.
           */

          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
            {
              /* split and reattach remainder */
              remainder_size = size - nb;
              remainder = chunk_at_offset (victim, nb);
              unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
              av->last_remainder = remainder;
              remainder->bk = remainder->fd = unsorted_chunks (av);
              if (!in_smallbin_range (remainder_size))
                {
                  remainder->fd_nextsize = NULL;
                  remainder->bk_nextsize = NULL;
                }

              set_head (victim, nb | PREV_INUSE |
                        (av != &main_arena ? NON_MAIN_ARENA : 0));
              set_head (remainder, remainder_size | PREV_INUSE);
              set_foot (remainder, remainder_size);

              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

具體的判斷條件如下:

  • 申請空間 nb 在 smallbins 範圍內
  • unsortedbin 僅有唯一一個空閒 chunk
  • 唯一的一個空閒 chunk 是 last_remainder
  • 唯一一個空閒 chunk 的大小可以進行切割

如果這四個條件同時滿足,那麼就將該唯一一個空閒 chunk 切割之後返回,剩餘的空間作為新的 last_remainder 並插入到 unsortedbin 表頭。

這裡注意到,如果切割之後新的 last_remainder 不屬於 smallbins,那麼要把 fd_nextsize 以及 bk_nextsize 這兩個成員給置空。

0x41 精確匹配則返回

unsortedbin 本來就是作為 smallbins 和 largebins 的緩衝區的存在,它裡面存放著許多 free 時插入的空閒空間或者整理 fastbins 之後插入的空閒空間,很有可能這些空間裡就有一個剛好能夠精確匹配我們需要申請的空間大小。如果找到這樣的空閒 chunk,自然是直接返回即可:

          /* Take now instead of binning if exact fit */

          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }

0x42 插入到 smallbins

如果當前空閒 chunk 不能精確匹配並且屬於 smallbins,那麼就將其插入到 smallbins 合適的 smallbin 中,注意是插入到連結串列頭部。

          if (in_smallbin_range (size))
            {
              victim_index = smallbin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
            }
          else
            {
              ...
            }
          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

0x43 插入到 largebins

插入到 largebins 要麻煩很多,因為 largebins 每一個連結串列上的 chunk 大小並不是相等的,而是處於一個範圍內即可,而且每一個 largebin 並不是一組雙向迴圈連結串列,而是兩組,即 fd 和 bk 構成一個雙向迴圈連結串列,而 fd_nextsize 和 bk_nextsize 組合構成另一個雙向迴圈連結串列。因此在插入時不僅尋找合適的插入位置更加複雜,修復連結串列也多了許多工作。

0x430 插入到連結串列頭

首先獲取相應 largebin 的連結串列頭 bck 以及第一個空閒 chunk 為 fwd,然後有一個判斷:

          if (in_smallbin_range (size))
            {
              ...
            }
          else
            {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  ...
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
          }
          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

當 fwd == bck 時,說明此時 largebin 為空,直接插入到連結串列頭即可。

0x431 插入到連結串列尾

通過 bck->bk 即可找到當前 largebin 的最後一個 chunk,因為在 largebin 中 chunk 按大小從大到小排序,因此最後一個 cunk 也即是最小的一個 chunk。如果待插入 chunk 小於該最小的空閒 chunk,則插入到連結串列尾:

              if (fwd != bck)
                {
                  /* if smaller than smallest, bypass loop below */
                  assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
                  if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
                    {
                      fwd = bck;
                      bck = bck->bk;

                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }                 
                }

0x432 插入到連結串列中間

                  if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
                    {
                      ...
                    }
                  else
                    {
                      assert ((fwd->size & NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size < fwd->size)
                        {
                          fwd = fwd->fd_nextsize;
                          assert ((fwd->size & NON_MAIN_ARENA) == 0);
                        }

                      if ((unsigned long) size == (unsigned long) fwd->size)
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }

插入到連結串列中間時,首先通過一個 while 迴圈找到合適的插入位置,注意到這裡用的是 fwd = fwd->fd_nextsize,這裡就體現了第二組雙向迴圈連結串列的作用,即可以用於加快尋找合適的插入位置的速度。

找到位置之後判斷,當前待插入 chunk 的大小是否和當前位置所在一組相同尺寸的 chunks 的大小相同,如果相同,則插入到該組 chunks 的第二位。注意這裡是插入到第二位,因為如果插入成為第一個,就要修改第二組雙向迴圈連結串列,無疑耗費更多時間。

否則說明當前待插入 chunk 的大小和 largebin 中任何一組相同尺寸的 chunks 的大小都不相同,只能自己插入併成為一組新的相同尺寸的 chunks 的第一個 chunk。

0x44 中斷整理 unsortedbin

中斷整理 unsortedbin 有兩種情況,一種是已經遍歷整理完 unsortedbin 中所有空閒 chunk,在 while 判斷處即可自然終止。另一種情況如下所示:

    while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {

          ...
          ...

#define MAX_ITERS       10000
          if (++iters >= MAX_ITERS)
            break;
        }

可見 while 迴圈維護著一個計數器,當整理 unsortedbin 中的空閒 chunks 總數達到 10000 個的時候,就會 break 中斷迴圈,避免因為整理 unsortedbin 而耗費太多時間。

0x5 從 smallbins 或 largebins 分配

來到這一步,unsortedbin 已經空了,我們繼續從 smallbins 或者 largebins 分配。但對於 smallbins 以及 largebins 有一點不同的是,此時 smallbins 已經確定無法精確匹配了,只能使用最佳匹配,因為剛開始時經過了一輪對 smallbins 的掃描,即 nb 所對應的那一個 smallbin 已經確定不可能存在合適的空閒 chunk 了,而 largebin 則還存在精確匹配的可能。

0x50 從所在 largebin 精確分配或最佳匹配

首先判斷 nb 是否屬於 largebins,如果屬於則檢查 nb 對應的那一個 largebin 上是否有滿足要求的空閒 chunk。注意在這個 largebin 上,也僅僅在這個 largebin 上還存在著精確匹配的可能。

進來先通過 (victim = first (bin)) != bin 判斷當前 largebin 是否非空,並且最大的一個空閒 chunk 是否比 nb 要大:

       /*
         If a large request, scan through the chunks of current bin in
         sorted order to find smallest that fits.  Use the skip list for this.
       */

      if (!in_smallbin_range (nb))
        {
          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin &&
              (unsigned long) (victim->size) >= (unsigned long) (nb))
            {
              ...
            }
        }

如果都滿足,則通過一個 while 迴圈找到滿足分配要求的那一組相同尺寸的空閒 chunks:

              victim = victim->bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb)))
                victim = victim->bk_nextsize;

如果這一組 chunks 的數量超過兩個且大小剛好精確匹配,則返回第二個空閒 chunk,不選擇第一個返回是因為返回第一個的話需要重新調整第二組雙向迴圈連結串列,否則就直接返回找到的第一個 chunk。這裡使用了 unlink 巨集將選擇的 chunk 脫鏈:

              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
              if (victim != last (bin) && victim->size == victim->fd->size)
                victim = victim->fd;

                remainder_size = size - nb;
              unlink (av, victim, bck, fwd);

這還不行,因為找到的 chunk 如果不是精確匹配分配要求,就會進行切割併產生一個剩餘的空間,如果這個空間不足以成為一個新的 chunk,即比最小的 chunk 的大小要小,那麼就直接把整個 chunk 返回給使用者,否則切割之後把剩餘的空間作為一個新 chunk 並插入到 unsortedbin 中:

              /* Exhaust */
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
                  if (__glibc_unlikely (fwd->bk != bck))
                    {
                      errstr = "malloc(): corrupted unsorted chunks";
                      goto errout;
                    }
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder->fd_nextsize = NULL;
                      remainder->bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }

0x51 從最接近的 smallbin 或 largebin 最佳匹配

如果 nb 屬於 smallbins,或者不屬於 largebins 但經過以上步驟仍然沒有找到合適的 chunk,此時就只能從 smallbin 或者 largebin 裡面進行最佳匹配了。這裡的最佳匹配的意思是說,從 nb 所對應的那一個 smallbin 或者 largebin 開始往上搜索離它最近並且非空的一個 smallbin 或者 largebin,從中取出最小的一個 空閒 chunk 返回,這個空閒 chunk 必定是不會精確匹配並且會產生剩餘空間的,但它已經是能找到的離 nb 最接近的一個空閒 chunk 了。

這裡 ptmalloc 設計了一個很巧妙的技巧,用來加快尋找的速度。在分配區裡有一個數據成員,是一個位圖陣列,BINMAPSIZE = 4:

  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];

由註釋也可以知道,這是 bins 的點陣圖,標記每一個 bin 是否為空。我們知道,bins 陣列長度