10791 字
54 分钟
glibc-2.35 malloc.c源码审计笔记

此笔记之前我也有做过glibc malloc源码的部分审计

准备工作#

gdb调试环境搭建#

首先准备好源码,gdb,以及gdb对应的插件。这里不做过多赘述。

最重要的是gdb的源码调试功能。先vim ~/.gdbinit打开gdb的设置文件,输入dir 调试文件路径即可。

宏定义#

宏定义(macro definition)是 C/C++ 中的一种预处理指令,可以在编译之前替换源代码中的一些文本。简单来说就是用宏自定义了一些其它符号,这些符号在使用时全等于被替换的内容。

#define Date "2023_01_20"

也可以使用\作为续行符

#define  WORKING_DIE  “/home/lcc/linux/nfs/rootfs/lib/\
                  modules/4.1.15/all_flile/c_text/for_text/

通过使用 \,可以让代码看起来更加的整洁,提高了代码的可读性,但是在使用时,一定不能让 \右侧出现除了换行符以外的任何字符,空格也不可以,否则会导致错误出现。

审计笔记#

关键的宏定义#

struct malloc_chunk;
typedef struct malloc_chunk* mchunkptr;
  • Line1103 struct malloc_chunk 声明malloc_chunk结构体
  • Line1104 typedef 声明指向malloc_chunk的指针mchunkptr
static void* _int_malloc(mstate, size_t);
static void _int_free(mstate, mchunkptr, int);
static void* _int_realloc(mstate, mchunkptr, INTERNAL_SIZE_T,
INTERNAL_SIZE_T);
static void* _int_memalign(mstate, size_t, size_t);
  • Line1108 声明_int_malloc函数本体<—_libc_malloc<—malloc
  • Line1109 声明_int_free函数本体<—_libc_free<—free
  • Line1112 声明用于内存对齐的函数

malloc_chunk#

struct malloc_chunk {
	INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
	INTERNAL_SIZE_T mchunk_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;
};

typedef unsigned int size_t;&define INTERNAL_SIZE_T size_t

  • 定义malloc_chunk结构体 At Line 1153-1164
    • 成员变量mchunk_prev_size是一个无符号整型,是相邻的前一个chunk的大小
    • 成员变量mchunk_size是一个无符号整型,是当前chunk的大小
    • 成员变量fd是一个指向chunk的指针,只在当前chunk被free时使用,指向上一个被free的chunk且两chunk在同一个bin中
    • 成员变量bk是一个指向chunk的指针,只在当前chunk被free时使用,指向下一个被free的chunk且两chunk在同一个bin中
    • 成员变量fd_nextsizebk_nextsize是指向chunk的指针,只用于大的chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		| Size of previous chunk, if unallocated (P clear) |
		+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		| Size of chunk, in bytes                    |A|M|P|
		mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		| User data starts here...                         .
		. (malloc_usable_size() bytes)                     .
		.                                                  |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

一些简单的宏定义

  • Line1300 #define CHUNK_HDR_SZ (2 * SIZE_SZ) chunk header的大小,一般为0x10
  • Line1304 #define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ)) 将chunk的地址转换为用户的内存指针
  • Line1313 #define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize)) 最小的chunk大小,保证内存对齐。
  • Line1317-1318 定义MINSIZE,最小的chunk大小
  • Line1322 #define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0) 检查内存对齐
  • Line1331-1334 定义返回malloc_size的宏request2size((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

checked_request2size#

checked_request2size (size_t req, size_t *sz) __nonnull (1)  
{  
 if (__glibc_unlikely (req > PTRDIFF_MAX))  
   return false;  
  
 /* When using tagged memory, we cannot share the end of the user  
    block with the header for the next chunk, so ensure that we  
    allocate blocks that are rounded up to the granule size.  Take  
    care not to overflow from close to MAX_SIZE_T to a small  
    number.  Ideally, this would be part of request2size(), but that  
    must be a macro that produces a compile time constant if passed  
    a constant literal.  */  
 if (__glibc_unlikely (mtag_enabled))  
   {  
     /* Ensure this is not evaluated if !mtag_enabled, see gcc PR 99551.  */  
     asm ("");  
  
     req = (req + (__MTAG_GRANULE_SIZE - 1)) &  
           ~(size_t)(__MTAG_GRANULE_SIZE - 1);  
   }  
  
 *sz = request2size (req);  
 return true;  
}

这段代码定义了一个名为 checked_request2size 的静态内联函数,用于检查并调整用户请求的内存大小。这个函数主要用于确保请求的内存大小在处理前既不会造成溢出,也不会因为内存标记(tagging)的需求而导致地址不对齐的问题。

逐步分解#

  1. 函数声明与参数
  • 函数名为 checked_request2size,接受两个参数:size_t reqsize_t *sz,其中 req 是用户请求的内存大小,sz 是指向调整后的内存大小的指针。

  • __nonnull (1) 是一个 GCC 属性,用于告诉编译器 req 参数不能为 NULL,这在函数内部可以减少对 NULL 的检查,提高代码执行效率。

  1. 检查请求大小是否溢出
  • if (__glibc_unlikely (req > PTRDIFF_MAX)) 检查请求的内存大小是否超过了 PTRDIFF_MAX

  • 如果超过,说明请求的内存大小太大,可能会导致后续的计算溢出,因此函数返回 false,表示这个请求不能被处理。

  1. 处理内存标记的情况
  • if (__glibc_unlikely (mtag_enabled)) 检查内存标记是否被启用。

  • 如果内存标记被启用,需要确保分配的内存块大小是根据内存标记的粒度大小(__MTAG_GRANULE_SIZE)对齐的。这是因为内存标记会占用额外的空间,如果空间不按粒度对齐,会导致内存地址错误。

  • req = (req + (__MTAG_GRANULE_SIZE - 1)) & ~(size_t)(__MTAG_GRANULE_SIZE - 1); 这行代码的作用是将请求的内存大小向上调整到最近的 __MTAG_GRANULE_SIZE 的倍数。具体而言,它首先将请求的内存大小加上 __MTAG_GRANULE_SIZE - 1,然后通过按位与操作清除小于 __MTAG_GRANULE_SIZE 的位,从而得到对齐后的内存大小。

  1. 调整内存大小
  • *sz = request2size (req); 调用 request2size 函数将调整后的请求内存大小转换为实际可分配的内存块大小,并存储到 sz 指向的变量中。

  • request2size 是一个宏,用于将请求的内存大小加上头信息大小(SIZE_SZ)和内存对齐要求(MALLOC_ALIGN_MASK),并确保最终的内存大小至少为 MINSIZE,同时是 MALLOC_ALIGNMENT 的倍数。

  1. 返回结果
  • 如果请求的内存大小未超过 PTRDIFF_MAX,且成功调整内存大小,函数返回 true,表示可以处理这个请求。

主要功能总结#

  • 内存大小检查:确保用户请求的内存大小不会导致后续计算溢出。

  • 内存对齐处理:根据内存标记的需求调整内存块大小,确保其满足对齐要求。

  • 内存大小调整:将请求的内存大小转换为实际可分配的内存块大小。

总之,checked_request2size 函数的主要目的是确保用户请求的内存大小既安全又符合内存分配器的需求,在处理内存分配请求之前对其进行必要的检查和调整。

unlink_chunk (mstate av, mchunkptr p)
{
	if (chunksize (p) != prev_size (next_chunk (p))) //检查下一个chunk的prevsize和当前chunk的size是否相同
		malloc_printerr ("corrupted size vs. prev_size");
	
	mchunkptr fd = p->fd; //定义指向前一个chunk的指针
	mchunkptr bk = p->bk; //定义指向后一个chunk的指针

	if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) //检查该chunk是否在一个双向链表中,fd->bk是前一个chunk的“下一个chunk指针”,即当前chunk,bk->fd同理
		malloc_printerr ("corrupted double-linked list");
	
	fd->bk = bk; //将当前chunk的前一个chunk的“下一个chunk指针”指向当前chunk的后一个chunk
	bk->fd = fd; //将当前chunk的后一个chunk的“前一个chunk指针”指向当前chunk的前一个chunk
	//这里完成出链表的操作

	if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) //检查是否为大块内存
		{
		if (p->fd_nextsize->bk_nextsize != p
		|| p->bk_nextsize->fd_nextsize != p) //检查大块内存的链表完整性
			malloc_printerr ("corrupted double-linked list (not small)");

		if (fd->fd_nextsize == NULL) //大块内存链表的出链操作
	{
		if (p->fd_nextsize == p)
			fd->fd_nextsize = fd->bk_nextsize = fd;
		else
			{
				fd->fd_nextsize = p->fd_nextsize;
				fd->bk_nextsize = p->bk_nextsize;
				p->fd_nextsize->bk_nextsize = fd;
				p->bk_nextsize->fd_nextsize = fd;
			}
	}
		else
	{
		p->fd_nextsize->bk_nextsize = p->bk_nextsize;
		p->bk_nextsize->fd_nextsize = p->fd_nextsize;
	}
		}
}

代码分解与解释#

  1. 检查下一个chunk的prev_size字段是否与当前chunk的size字段一致

  c   if (chunksize (p) != prev_size (next_chunk (p)))       malloc_printerr ("corrupted size vs. prev_size");   

  - chunksize(p) 获取当前chunk p 的大小(不包括使用位)。
  - next_chunk(p) 计算并获取下一个相邻的chunk。
  - prev_size(next_chunk(p)) 获取下一个chunk的 prev_size 字段,该字段应该存储的是当前chunk的大小。
  - 如果这两个值不一致,说明内存块损坏或者数据结构被错误地修改了,此时调用 malloc_printerr 输出错误信息。

  1. 检查当前chunk是否在双向链表中

  c   if (__builtin_expect (fd->bk != p || bk->fd != p, 0))       malloc_printerr ("corrupted double-linked list");   

  - p->fd 是当前chunk的“下一个chunk指针”,p->bk 是“前一个chunk指针”。
  - fdbk 分别是 p->fdp->bk 的别名。
  - 通过检查 fd->bk 是否等于 p,以及 bk->fd 是否等于 p,验证当前chunk是否正确地存在于一个双向链表中。
  - 如果 fd->bk != pbk->fd != p,则表示内存块的双向链表连接被破坏,同样调用 malloc_printerr 输出错误信息。

  1. 从双向链表中移除当前chunk

  c   fd->bk = bk;   bk->fd = fd;   

  - 将当前chunk p 的前一个chunk fd 的“下一个chunk指针”指向 p 的后一个chunk bk
  - 将 p 的后一个chunk bk 的“前一个chunk指针”指向 p 的前一个chunk fd。u
  - 这样就完成了从双向链表中移除当前chunk p 的操作。  

  1. 检查并处理大块内存的链表

  ```c
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
  {
    if (p->fd_nextsize->bk_nextsize != p
        || p->bk_nextsize->fd_nextsize != p)
      malloc_printerr (“corrupted double-linked list (not small)”);

    if (fd->fd_nextsize == NULL)
    {
      if (p->fd_nextsize == p)
        fd->fd_nextsize = fd->bk_nextsize = fd;
      else
      {
        fd->fd_nextsize = p->fd_nextsize;
        fd->bk_nextsize = p->bk_nextsize;
        p->fd_nextsize->bk_nextsize = fd;
        p->bk_nextsize->fd_nextsize = fd;
      }
    }
    else
    {
      p->fd_nextsize->bk_nextsize = p->bk_nextsize;
      p->bk_nextsize->fd_nextsize = p->fd_nextsize;
    }
  }
  ```

  - in_smallbin_range(chunksize_nomask(p)) 检查当前chunk是否属于小块内存的范围(如果chunk的大小包含使用位也不影响判断)。从
  - 如果chunk不是小块内存,而是大块内存,并且 p->fd_nextsize 不为NULL(这表示当前chunk在大块内存的链表中),则进行额外的检查和操作。
  - p->fd_nextsize->bk_nextsize != pp->bk_nextsize->fd_nextsize != p 用于验证大块内存链表的完整性。)
  - 如果链表头 fdfd_nextsize 为NULL,表示当前chunk就是大块内存链表的头,需要特殊处理以重新建立链表头。
  - 如果链表头不为NULL,则进行常规的出链表操作,将 p 的前后节点重新连接起来,移除 p。处

总结#

这段代码的主要功能是从一个双向链表中移除一个自由内存块。在移除之前,代码会进行一系列的检查,确保chunk的数据结构没有被破坏,从而保证内存管理的安全性和准确性。具体来说:  

  • 验证当前chunk的 size 字段与下一个chunk的 prev_size 字段是否匹配。码
  • 检查当前chunk是否正确地存在于一个双向链表中。p
  • 如果chunk属于大块内存范围,还会额外检查大块内存链表的完整性,并进行相应的出链表操作。检
  • 如果检查发现任何问题,代码会通过 malloc_printerr 输出错误信息,这有助于调试和检测内存损坏。

⚠️ 在unlink后,unlink的chunk的fd和bk并未发生改变。

struct malloc_state#

struct malloc_state
{
	__libc_lock_define (, mutex);
	int flags;
	int have_fastchunks;
	mfastbinptr fastbinsY[NFASTBINS];
	mchunkptr top;
	mchunkptr last_remainder;
	mchunkptr bins[NBINS * 2 - 2];
	unsigned int binmap[BINMAPSIZE];
	struct malloc_state *next;
	struct malloc_state *next_free;
	INTERNAL_SIZE_T attached_threads;
	INTERNAL_SIZE_T system_mem;
	INTERNAL_SIZE_T max_system_mem;
};

逐步分解与详细解释#

结构体定义#

这个代码片段定义了一个名为 malloc_state 的结构体,用于管理内存分配器的状态。这个结构体在 glibc 的 malloc 实现中扮演着核心角色。

成员变量详解#

  1. 序列化访问
      c   __libc_lock_define (, mutex);   
      - mutex: 这是一个锁,用于确保在多线程环境中对 malloc_state 的访问是线程安全的。__libc_lock_define 是 glibc 提供的宏,用于定义一个线程锁,防止多个线程同时修改分配器的状态导致数据不一致。

  2. 标志位
      c   int flags;   
      - flags: 一个整数类型的标志位,用于存储各种标志信息,比如是否使用 mmap 分配内存、是否内存区域是连续的等等。这些标志会在内存分配的不同情况下被设置或清除,以影响内存分配的行为。

  3. 指示是否包含最近插入的空闲块
      c   int have_fastchunks;   
      - have_fastchunks: 这是一个布尔值(实际上使用整数来表示),用于指示是否有最近插入的空闲块在 fastbins 中。have_fastchunks 在每次插入一个空闲块到 fastbins 时会被设置,而在内存分配器合并 fastbins
    时会被清除。这个标志用于优化内存分配过程中的查找效率,避免不必要的合并操作。

  4. Fastbins
      c   mfastbinptr fastbinsY[NFASTBINS];   
      - fastbinsY: 这是一个包含 mfastbinptr 类型的数组,用于存储最近释放的较小内存块(即 fastbins)。Fastbins 中的内存块不会与其他空闲块合并,这使得它们可以更快地被分配出去。数组大小为 NFASTBINS
    每个 bin 包含相同大小的内存块,分配器会根据请求的内存大小,将快释放的内存块放入合适的 fastbin 中。

  5. Top-chunk 的基地址
      c   mchunkptr top;   
      - top: 这是一个指向 malloc_chunk 类型的指针,用于管理内存分配器中当前可用的最大内存块。top 是内存分配器中唯一不包含在任何 bin 中的内存块,它位于堆的顶部,用于处理那些不能通过现有的 bin 来处
    理的内存请求。当需要分配的内存大于现有 bin 中的最大块时,top 将被扩展或替换。

  6. 最近一次小请求分割后剩余的内存块
      c   mchunkptr last_remainder;   
      - last_remainder: 这是一个指向 malloc_chunk 类型的指针,用于存储最近一次小请求分割后剩余的内存块。当一个请求的内存大小小于某个 bin 中的最大内存块大小时,该内存块会被分割,满足请求的部分会被分
    配出去,剩余的部分则会被存放在 last_remainder 中,以便后续的内存分配请求可以使用。

  7. 普通 bins
      c   mchunkptr bins[NBINS * 2 - 2];   
      - bins: 这是一个包含 mchunkptr 类型的数组,用于存储普通 bin 中的空闲内存块。bins 中的内存块按照大小进行分组,每组内存块大小相同或相近。glibc 使用这些 bin 来管理堆内存的分配和释放,从而减少内
    存碎片和提高内存分配的效率。

  8. bins 的位图
      c   unsigned int binmap[BINMAPSIZE];   
      - binmap: 这是一个位图数组,用于记录 bins 中哪些 bin 为空。每个 bin 对应一个位,如果某个 bin 为空,则对应的位为 0;否则为 1。binmap 的使用可以加速内存分配过程中的查找操作,避免遍历空的 bin,
    从而提高分配器的性能。个

  9. 链表
      c   struct malloc_state *next;   
      - next: 这是一个指向 malloc_state 类型的指针,用于形成一个链表结构,将多个 malloc_state 实例连接起来。这个链表结构用于管理多个内存分配区域(arena),在多线程环境中允许多个线程同时使用不同的
    arena。n

  10. 空闲 arenas 的链表
      c   struct malloc_state *next_free;   
      - next_free: 这是一个指向 malloc_state 类型的指针,用于管理那些被标记为“空闲”的 arena。next_free 连接了所有空闲的 arena,当需要一个新的 arena 时,可以从这个链表中获取一个空闲的 arena 而不需
    要重新创建。f

  11. 附加到此 arena 的线程数
      c   INTERNAL_SIZE_T attached_threads;   
      - attached_threads: 这是一个整数类型的变量,用于记录附加到此 arena 的线程数。如果 arena 在空闲链表中,则该值为 0。否则,它表示当前有多少个线程正在使用这个 arena。这个计数器用于管理 arena 的生命
    周期,当最后一个线程离开 arena 时,arena 将被标记为空闲并放入 next_free 链表中。

  12. 从此 arena 中分配的内存总量
      c   INTERNAL_SIZE_T system_mem;   INTERNAL_SIZE_T max_system_mem;   
      - system_mem: 这是一个整数类型的变量,用于记录从系统中分配给此 arena 的内存总量。这个变量会随着每次从系统获取新的内存块而增加。当
      - max_system_mem: 这是一个整数类型的变量,用于记录此 arena 从系统中分配过的最大内存总量。这个变量会随着每次系统分配的内存总量超过当前的最大值时进行更新。这两个变量用于监控内存分配器对系统内存的
    使用情况,帮助分配器进行内存扩展和释放的决策。,

总结#

malloc_state 结构体是 glibc 中用于管理内存分配器状态的核心数据结构。它负责存储当前可用的最大内存块(top)、最近一次小请求分割后剩余的内存块(last_remainder)、普通 bin(bins)和 fastbin(fas tbinsY)等信息。同时,malloc_state 结构体还支持多线程环境下的序列化访问,确保多个线程可以安全地使用同一个 arena。一

主要功能#

  • 管理内存分配:通过 toplast_remainder 管理堆内存的分配和释放。,
  • 支持多线程:通过 mutex 实现线程安全。i
  • 减少内存碎片:通过使用 fastbins 和 bins 等机制减少内存碎片。配
  • 监控内存使用:通过 system_memmax_system_mem 监控内存使用情况。  
  • 支持多个 arena:通过 nextnext_free 实现多个 arena 的管理,提高内存分配效率。

sysmalloc_mmap#

用于分配大内存

static void *
sysmalloc_mmap (INTERNAL_SIZE_T nb, size_t pagesize, int extra_flags, mstate av)
{
	long int size;
/*
Round up size to nearest page. For mmapped chunks, the overhead is one
SIZE_SZ unit larger than for normal chunks, because there is no
following chunk whose prev_size field could be used.

See the front_misalign handling below, for glibc there is no need for further alignments unless we have have high alignment.
sysmalloc 处理需要从系统获取更多内存的 malloc 情况。
在进入时,假设 av->top 没有足够的空间来满足 nb 字节的请求,
因此需要扩展或替换 av->top。
*/

if (MALLOC_ALIGNMENT == CHUNK_HDR_SZ) // 判断MALLOC_ALIGNMENT是否等于CHUNK_HDR_SZ, 即是否启用了对齐
	size = ALIGN_UP (nb + SIZE_SZ, pagesize);
else
	size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);

//这里计算申请的size,如果启用了对齐,则需要对齐到pagesize的倍数,否则只需要对齐到MALLOC_ALIGNMENT的倍数

/* Don't try if size wraps around 0. */
if ((unsigned long) (size) <= (unsigned long) (nb))
	return MAP_FAILED; //如果申请的大小超过了计算的size,则返回MAP_FAILED

char *mm = (char *) MMAP (0, size,
				mtag_mmap_flags | PROT_READ | PROT_WRITE,
				extra_flags); //设置映射区域的权限为可读写
if (mm == MAP_FAILED)
	return mm;

#ifdef MAP_HUGETLB
if (!(extra_flags & MAP_HUGETLB))
	madvise_thp (mm, size); //调整内存分配策略
#endif

/*
The offset to the start of the mmapped region is stored in the prev_size
field of the chunk. This allows us to adjust returned start address to
meet alignment requirements here and in memalign(), and still be able to
compute proper address argument for later munmap in free() and realloc().
mmapped 区域起始处的偏移量存储在块的 prev_size
字段中。这使我们能够调整返回的起始地址,以满足此处和 memalign() 中的对齐要求,
并且仍然能够为 free() 和 realloc() 中的后续 munmap 计算正确的地址参数。
*/

INTERNAL_SIZE_T front_misalign; /* unusable bytes at front of new space */
if (MALLOC_ALIGNMENT == CHUNK_HDR_SZ)
{
/* For glibc, chunk2mem increases the address by CHUNK_HDR_SZ and
MALLOC_ALIGN_MASK is CHUNK_HDR_SZ-1. Each mmap'ed area is page
aligned and therefore definitely MALLOC_ALIGN_MASK-aligned.
对于 glibc,chunk2mem 将地址增加 CHUNK_HDR_SZ,而
MALLOC_ALIGN_MASK 为 CHUNK_HDR_SZ-1。每个 mmap 区域都是页面对齐的,
因此肯定是 MALLOC_ALIGN_MASK 对齐的。
*/
	assert (((INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK) == 0);
	front_misalign = 0;
}
else
	front_misalign = (INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK;
//检测新空间的前面部分是否有无法使用的字节
mchunkptr p; /* 指向alloc的chunk指针 */

if (front_misalign > 0) //有字节不可用的情况
{
	ptrdiff_t correction = MALLOC_ALIGNMENT - front_misalign;
	p = (mchunkptr) (mm + correction); //向后分配内存
	set_prev_size (p, correction);
	set_head (p, (size - correction) | IS_MMAPPED);
}
else
{
	p = (mchunkptr) mm;
	set_prev_size (p, 0);
	set_head (p, size | IS_MMAPPED);
}
/* update statistics */

int new = atomic_exchange_and_add (&mp_.n_mmaps, 1) + 1;
atomic_max (&mp_.max_n_mmaps, new);

unsigned long sum;
sum = atomic_exchange_and_add (&mp_.mmapped_mem, size) + size;
atomic_max (&mp_.max_mmapped_mem, sum);

check_chunk (av, p);

return chunk2mem (p);
}

逐步分解与详细解释#

函数声明#

static void *  
sysmalloc_mmap (INTERNAL_SIZE_T nb, size_t pagesize, int extra_flags, mstate av)  
  • INTERNAL_SIZE_T nb: 需要分配的内存大小(以字节为单位)。
  • size_t pagesize: 系统的页面大小。
  • int extra_flags: 传递给 mmap 系统调用的额外标志。
  • mstate av: 内存分配状态结构体,包含了当前分配器的状态信息。

计算申请的内存大小#

if (MALLOC_ALIGNMENT == CHUNK_HDR_SZ)  
   size = ALIGN_UP (nb + SIZE_SZ, pagesize);  
else  
   size = ALIGN_UP (nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);  
  • 如果 MALLOC_ALIGNMENT 等于 CHUNK_HDR_SZ,则直接将 nb + SIZE_SZ 对齐到页面大小。
  • 否则,将 nb + SIZE_SZ + MALLOC_ALIGN_MASK 对齐到页面大小。
  • SIZE_SZ 通常是一个常量,表示块头部的大小。
  • MALLOC_ALIGN_MASK 也通常是一个常量,用于内存对齐。

检查内存大小是否溢出#

if ((unsigned long) (size) <= (unsigned long) (nb))  
   return MAP_FAILED;  
  • 检查计算后的大小 size 是否小于等于原始请求大小 nb,如果是,则认为内存大小计算溢出,返回 MA P_FAILED

使用 mmap 分配内存#

char *mm = (char *) MMAP (0, size, mtag_mmap_flags | PROT_READ | PROT_WRITE, extra_flags);  
if (mm == MAP_FAILED)  
   return mm;  
  • 使用 mmap 函数分配大小为 size 的内存区域。
  • 设置内存映射的权限为可读写 (PROT_READ | PROT_WRITE)。
  • 如果 mmap 返回 MAP_FAILED,则表示内存分配失败,直接返回错误码。

调整透明大页策略#

#ifdef MAP_HUGETLB  
if (!(extra_flags & MAP_HUGETLB))    
   madvise_thp (mm, size);  
#endif  
  • 如果 extra_flags 中没有设置 MAP_HUGETLB 标志,且系统支持透明大页 (TRANSPARENT_HUGEPAGE),
    则调用 madvise_thp 函数,将分配的内存区域建议为使用透明大页。

计算对齐偏移量#

INTERNAL_SIZE_T front_misalign;  
if (MALLOC_ALIGNMENT == CHUNK_HDR_SZ)  
   {  
     assert (((INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK) == 0);  
     front_misalign = 0;  
   }  
else  
   front_misalign = (INTERNAL_SIZE_T) chunk2mem (mm) & MALLOC_ALIGN_MASK;  
  • 如果启用了对齐且 MALLOC_ALIGNMENT 等于 CHUNK_HDR_SZ,则不需要进一步的对齐处理,front_misal ign 设置为 0。
  • 否则,计算新分配内存区域起始地址相对于 MALLOC_ALIGNMENT 的对齐偏移量 front_misalign

调整分配的内存区域#

if (front_misalign > 0)    
   {  
     ptrdiff_t correction = MALLOC_ALIGNMENT - front_misalign;  
     p = (mchunkptr) (mm + correction);  
     set_prev_size (p, correction);  
     set_head (p, (size - correction) | IS_MMAPPED);  
   }  
else  
   {  
     p = (mchunkptr) mm;  
     set_prev_size (p, 0);  
     set_head (p, size | IS_MMAPPED);  
   }  
  • 如果存在对齐偏移量 (front_misalign > 0),则计算需要调整的大小 correction,使得调整后的内存
    区域起始地址满足对齐要求。调整后的起始地址存储在 p 中,并设置其 prev_size 字段为 correction
  • 如果没有对齐偏移量,则直接将 mm 转换为 mchunkptr 类型并存储在 p 中,设置其 prev_size
    段为 0。
  • 设置 p 的头部信息,包含实际分配的大小以及 IS_MMAPPED 标志,表示该内存区域是通过 mmap 分配
    的。

更新统计信息#

int new = atomic_exchange_and_add (&mp_.n_mmaps, 1) + 1;  
atomic_max (&mp_.max_n_mmaps, new);  
  
unsigned long sum;  
sum = atomic_exchange_and_add (&mp_.mmapped_mem, size) + size;  
atomic_max (&mp_.max_mmapped_mem, sum);  
  • 更新内存映射的统计信息,包括已映射的内存区域的数量 n_mmaps 和已映射的总内存大小 mmapped_mem
  • 使用 atomic_exchange_and_add 函数进行原子操作,确保多线程环境下对统计信息的正确更新。

检查分配的内存块#

check_chunk (av, p);  
  • 调用 check_chunk 函数,检查分配的内存块是否符合预期的内存分配器状态。
  • 该函数会进行一系列的断言检查,确保分配的内存块在地址、大小等方面没有问题。

返回分配的内存块#

return chunk2mem (p);  
  • 将内存块 p 转换为用户可访问的内存地址,返回给调用者。
  • chunk2mem 函数会将内存块指针转换为指向实际数据区域的指针,用户可以直接使用这个地址进行数据读
    写。c

总结#

主要功能#

sysmalloc_mmap 函数的主要功能是从系统中分配指定大小的内存区域,并确保分配的内存是页面对齐的。如果分配的内存区域存在对齐偏移量,则会进行调整,使得用户实际使用的内存地址满足对齐要求。

关键功能点域#

  1. 内存大小对齐:根据内存分配器的对齐要求,计算需要分配的内存大小,并确保其对齐到页面大小。
  2. 分配内存:使用 mmap 函数分配指定大小的内存区域。
  3. 调整透明大页策略:如果系统支持透明大页,建议将分配的内存区域使用透明大页。
  4. 处理对齐偏移量:如果分配的内存区域存在对齐偏移量,则计算调整大小,并设置内存块的头部信息。`
  5. 更新统计信息:更新已映射的内存区域的数量和总内存大小。
  6. 检查内存块:确保分配的内存块符合预期的内存分配器状态。
  7. 返回内存块:将用户实际使用的内存地址返回给调用者。

__libc_malloc#

void *  
__libc_malloc (size_t bytes)  //该版本禁用了hook函数,所以不会检查hook  
{  
 mstate ar_ptr;  //指向arena的指针  
 void *victim;  
  
 _Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,  
                 "PTRDIFF_MAX is not more than half of SIZE_MAX");  
  
 if (!__malloc_initialized)  //未初始化先初始化  
   ptmalloc_init ();  
#if USE_TCACHE  //启用tcache的相关部分  
 /* int_free also calls request2size, be careful to not pad twice.  */  
 size_t tbytes;  
 if (!checked_request2size (bytes, &tbytes))  
   {  
     __set_errno (ENOMEM);  
     return NULL;  
   }  
 size_t tc_idx = csize2tidx (tbytes);  
  
 MAYBE_INIT_TCACHE ();  
  
 DIAG_PUSH_NEEDS_COMMENT;  
 if (tc_idx < mp_.tcache_bins  
     && tcache  
     && tcache->counts[tc_idx] > 0)  
   {  
     victim = tcache_get (tc_idx);  
     return tag_new_usable (victim);  
   }  
 DIAG_POP_NEEDS_COMMENT;  
#endif  
  
 if (SINGLE_THREAD_P)  
   {  
     victim = tag_new_usable (_int_malloc (&main_arena, bytes));  
     assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||  
             &main_arena == arena_for_chunk (mem2chunk (victim)));  
     return victim;  
   }  
  
 arena_get (ar_ptr, bytes);  //寻找arena试图分配内存
  
 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)  //如果分配失败,会再去尝试其他的arena  
   {  
     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) //如果申请到了arena那么退出之前会解锁试  
   __libc_lock_unlock (ar_ptr->mutex);  
  
 victim = tag_new_usable (victim); //给申请到的内存打上标签    
  
 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||  
         ar_ptr == arena_for_chunk (mem2chunk (victim)));  
 return victim;  //判断目前的状态,要么没有申请到内存,要么是mmap了内存,要么申请到的内存分配在arena  中    
}

__int_malloc#

tcache的相关最后单独写

__int_malloc的原型#

static void * _int_malloc (mstate av, size_t bytes)
  • mstate av:malloc所分配内存所在的arena
  • size_t bytes:malloc所分配内存的大小

变量申请#

INTERNAL_SIZE_T nb; /* 标准化请求大小 */
unsigned int idx; /* associated bin index */
mbinptr bin; /* 关联的 bin 索引 */

mchunkptr victim; /* 检查/选定的块 */
INTERNAL_SIZE_T size; /* 选定块的大小 */
int victim_index; /* 选定块的bin的索引 */

mchunkptr remainder; /* 分割后的剩余部分 */
unsigned long remainder_size; /* 剩余部分大小 */

unsigned int block; /* 位图遍历器 */
unsigned int bit; /* 位图遍历器 */
unsigned int map; /* current word of binmap */

mchunkptr fwd; /* 链接用,前一个块 */
mchunkptr bck; /* 连接用,后一个块 */

前置检查#

  • 检查请求大小是否合理#

if (!checked_request2size (bytes, &nb))
{
	__set_errno (ENOMEM);
	return NULL;
}

checked_request2size(bytes, &nb):这是一个函数调用,用于将用户请求分配的内存大小bytes转换为内部使用的形式nb。这个转换包括增加一些必要的开销(例如,SIZE_SZ字节的开销用于存储块的大小信息)以及确保请求大小已经对齐到必要的边界(例如,为了满足内存对齐要求,请求的大小可能会被向上调整)。此外,这个函数还会检查请求的大小是否合理,即不会因为增加开销和对齐而导致内存请求大小溢出。如果请求的大小不合理,函数将返回false

  • 检查arena是否可用#

if (__glibc_unlikely (av == NULL))
	{
		void *p = sysmalloc (nb, av);
		if (p != NULL)
	alloc_perturb (p, bytes);
		return p;
	}

如果没有可用的arena,av == NULL,那么退回到sysmalloc去mmap一个chunk,进行打乱后返回这个块的指针。

alloc_perturb函数的目的是为了在分配的内存块中写入一些特定的值,以帮助在调试过程中检测到使用未初始化内存的情况。这个操作通常被称为“内存打乱”或“内存污染”,其目的是确保分配的内存块不会包含残留的旧数据,从而帮助开发者更早地发现潜在的错误。

Fastbin部分#

Fastbin介绍#

大多数程序经常会申请以及释放一些比较小的内存块。如果将一些较小的 chunk 释放之后发现存在与之相邻的空闲的 chunk 并将它们进行合并,那么当下一次再次申请相应大小的 chunk 时,就需要对 chunk 进行分割,这样就大大降低了堆的利用效率。**因为我们把大部分时间花在了合并、分割以及中间检查的过程中。**因此,ptmalloc 中专门设计了 fast bin,对应的变量就是 malloc state 中的 fastbinsY

为了更加高效地利用 fast bin,glibc 采用单向链表对其中的每个 bin 进行组织,并且每个 bin 采取 LIFO 策略,最近释放的 chunk 会更早地被分配,所以会更加适合于局部性。也就是说,当用户需要的 chunk 的大小小于 fastbin 的最大大小时, ptmalloc 会首先判断 fastbin 中相应的 bin 中是否有对应大小的空闲块,如果有的话,就会直接从这个 bin 中获取 chunk。如果没有的话,ptmalloc 才会做接下来的一系列操作。

需要特别注意的是,fastbin 范围的 chunk 的 inuse 始终被置为 1。因此它们不会和其它被释放的 chunk 合并。 但可以被malloc_consolidate合并

Fastbin大小

Fastbins[idx=0,hold_size=00-0x18,size=0x20] 
Fastbins[idx=1,hold_size=0x19-0x28,size=0x30] 
Fastbins[idx=2,hold_size=0x29-0x38,size=0x40] 
Fastbins[idx=3,hold_size=0x39-0x48,size=0x50] 
Fastbins[idx=4,hold_size=0x49-0x58,size=0x60] 
Fastbins[idx=5,hold_size=0x59-0x68,size=0x70] 
Fastbins[idx=6,hold_size=0x69-0x78,size=0x80]

如果我们申请的chunk大小位于Fastbin范围内,那么先检查Fastbin

  • 宏:从Fastbin中移除内存块#

#define REMOVE_FB(fb, victim, pp)    \
do   \
	{    \
		victim = pp;    \
		if (victim == NULL);    \
break;    \
		pp = REVEAL_PTR(victim -> fd);    \
		if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp)))    \
malloc_printerr("malloc(): unaligned fastbin chunk detected");     \
	}     \
while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim))     \
	!= victim);    \  

定义宏REMOVE_FB(fb, victim, pp)

  • fb:指向fastbin头的指针
  • victim:要移除的内存块
  • pp:用于遍历链表的临时变量

首先宏将当前指针pp赋给victim,表示我们要从fastbin中移除这个块。如果victim为空,说明没有可用的内存块,直接跳出。如果存在,则使用REVEAL_PTR(victim -> fb)去更新pp为victim的下一个节点。最后检查内存对齐,如果对不齐会报错。

  • Fastbin相关#

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
		{
	//得到对应fb的下标
		idx = fastbin_index(nb);
	//得到对应fb链表的头指针
		mfastbinptr *fb = &fastbin (av, idx);
	//遍历用指针
		mchunkptr pp;
	//选中的chunk
		victim = *fb
		//如果victim可用,进行移除操作
		if (victim != NULL)
	{
		//检查是否对齐
		if (__glibc_unlikely ( misaligned_chunk(victim)))
			malloc_printerr ("malloc(): unaligned fastbin chunk detected 2");
		//单线程时正确管理
		if (SINGLE_THREAD_P)
			*fb = REVEAL_PTR (victim -> fd);
		else
			REMOVE_FB(fb , pp, victim);
		//取出内存后,对内存进行二次检查
		if (__glibc_likely (victim != NULL))
			{
			size_t victim_idx = fastbin_index (chunksize (victim));
			//如果victim_idx与检查的idx不一致,说明内存损坏,直接报错
			if (__builtin_expect (victim_idx != idx, 0))
		malloc_printerr ("malloc(): memory corruption (fast)");
			check_remalloced_chunk (av, victim, nb);
			void *p = chunk2mem (victim);
			alloc_perturb (p, bytes);
			return p;
			}
	}
		}

如果命中了Fastbin的范围,那么进行检查有无可用的Fastbin,如果可用,那么初始化相关变量,先检查是否对齐,再进行对Fastbin的取出操作,取出后检查取出的Fastbin的idx是否前后一致,打乱后返回。

SmallBin部分#

  • Smallbin介绍#

Small Bin中chunk大小的计算如下

chunk_size = 2 * SIZE_SZ * index
下标SIZE_SZ = 8
20x20
30x30
40x40
x0x10 * x
630x3f0
small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致。比如对于 32 位系统来说,下标 2 对应的双向链表中存储的 chunk 大小为均为 16 字节。每个链表都有链表头结点,这样可以方便对于链表内部结点的管理。此外,small bins 中每个 bin 对应的链表采用 FIFO 的规则,所以同一个链表中先被释放的 chunk 会先被分配出去。
  • Smallbin相关#

if (in_small_bin_range (nb))
	{
		//获取 small bin的
		idx = smallbin_index (nb);
		//获取bin中chunk的指针
		bin = bin_at(av, idx);
		//考察链表中最后一个bin,如果相等,则该bin为空
		//如果不为空,进行以下操作
		if ((victim = last (bin)) != bin)
			{
			//获取倒数第二个chunk
			bck = victim -> bk;
	if (__glibc_unlikely (bck -> fd != victim))//检查链表是否完整,是否被伪造
		malloc_printerr ("malloc(): smallbin double linked list corrupted");
			//设置victim对应的inuse位,表示取出了该chunk
			set_inuse_bit_at_offset (victim, nb);
			//取出最后一个chunk
			bin -> bk = bck;
			bin -> fd = bin;
			//非mainarena标志设置
			if (av != &main_arena)
		set_non_main_arena (victim);
			check_malloced_chunk (av, victim, nb);
			void *p = chunk2mem (victim);
			alloc_perturb (p, bytes);
			return p;
			}
	}

首先判断请求的内存大小是否属于smallbin的范围,如果是,则找到对应的小bin,并从中取出最后一个可用的内存块作为分配的内存块。然后,代码会检查该内存块的完整性,并将其从链表中移除。最后,代码会设置分配的内存块的使用位,并验证内存块的正确性。

Not Fast Or Small?#

如果用户申请的大小Fastbin和Smallbin都不满足,程序就会考虑Largebin。但并没有直接去遍历Largebin,而是先利用malloc_consolidate处理Fastbin中的所有chunk,能够合并的合并放入unsortedbin,不能合并的直接放进去,然后在大循环中处理unsortedbin的部分。

为什么不直接从相应的 bin 中取出 large chunk 呢?这是 ptmalloc 的机制,它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片

else
	{
	idx = largebin_index(nb);
	if (atomic_load_relaxed (&av -> have_fastchunks))
		malloc_consolidate(av);
	}

遍历Unsorted Bin#

程序执行到这里说明用户申请的chunk大小Fastbin和Smallbin都不满足,在整理了内存碎片后,程序进入这个大循环。

  • 按照FIFO的顺序逐个遍历unsortedbin中的chunk

    • 如果是small request,考虑是否满足需求,如果是,则返回该chunk
    • 如果不是,就放入相应大小的bin中
  • 尝试从largebin中分配用户所需的内存

  • Unsorted Bin遍历 - 前置检查#

for (;;)
	{
		int iters = 0;
	//如果UnsortedBin不为空,进入while循环
		while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
			{
				bck = victim -> bk; //victim为unsortedbin最后一个chunk,bck为倒数第二
				size = chunksize (victim); //victim的大小
				mchunkptr next = chunk_at_offset (victim, size); //获取下一个chunk
				//接下来是判断获取的chunk是否满足要求
				if (__glibc_unlikely (size <= CHUNK_HDR_SZ)
					|| __glibc_unlikely (size > av->system_mem))
				//检查当前chunk的大小是否有效
				//检查当前chunk的大小是否小于或等于chunk头的大小
				//检查当前chunk的大小是否大于arena
				malloc_printerr ("malloc(): invalid size (unsorted)");
				if (__glibc_unlikely (chunksize_nomask (next) < CHUNK_HDR_SZ)
					|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
				//检查next chunk的大小(去除mask后)是否小于chunk头的大小。
				//检查next chunk的大小(去除mask后)是否大于arena总的系统内存大小。
				malloc_printerr ("malloc(): invalid next size (unsorted)");
				if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
				//获取next chunk的前一个chunk的大小,并去除SIZE_BITS
				//检查next chunk的前一个chunk的大小是否与当前chunk的大小匹配。
				malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
				if (__glibc_unlikely (bck->fd != victim)
					|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
				//检查当前chunk的前一个chunk的fd字段是否指向当前chunk。
				//检查当前chunk的fd字段是否指向unsorted bin的头。
				malloc_printerr ("malloc(): unsorted double linked list corrupted");
				if (__glibc_unlikely (prev_inuse (next)))
				//检查next chunk的前一个chunk是否被标记为已使用
				malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
			}
	}
  • Unsorted Bin遍历 - Small Request#

if (in_smallbin_range (nb) && //用户申请的大小命中smallbin
	bck == unsorted_chunks (av) && //检查victim是unsortedbin中的一个chunk
	victim == av -> last_remainder && //检查当前内存块是否是最近分配内存块的剩余
	//检查victim是否可以被分割
	(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
	{
		//计算分割后的大小
		remainder_size = size - nb;
		//分割small request chunk
		remainder = chunk_at_offset (victim, nb);
		//将分割出来的块纳入链表
		unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
		//更新last remainder
		av->last_remainder = remainder;
		//维护remainder的链表
		remainder->bk = remainder->fd = unsorted_chunks (av);
		//如果分割后的remainder不在smallbin中,则维护其在largebin的链表
		if (!in_smallbin_range (remainder_size))
			{
				remainder -> fd_nextsize = NULL;
				remainder -> bk_nextsize = NULL;
			}
		//设置chuck header
		set_head (victim, nb | PREV_INUSE |
				(av != &main_areana ? 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;
	}
  • Unsorted Bin遍历 - 初始取出#

//检查链表完整性
if (__glibc_unlikely (bck->fd != victim))
	malloc_printerr ("malloc(): corrupted unsorted chunks 3");
//移除victim
unsorted_chunk (av) -> bk = bck; 
bck -> fd = unsorted_chunk ; 
  • Unsorted Bin遍历 - Exact Fit#

//恰好合适的情况,直接取出
if (size == nb)
	{
		set_inuse_bit_at_offset (victim, size);
		if (av != &main_arena)
	set_non_main_arena (victim);
		check_malloced_chunk (av, victim, nb);
		void *p = chunk2mem (victim);
		alloc_perturb (p, bytes);
		return p;
	}
  • Unsorted Bin遍历 - Place Chunk In Bin#

//取用的chunk命中smallbin范围时
if (in_smallbin_range (size))
	{
		victim_index = smallbin_index (size);
		bck = bin_at (av, victim_index);
		fwd = bck->fd;
	}
//否则,在Largebin
else
	{
		//放入Large Bin中
		victim_index = largebin_index (size);
		bck = bin_at (av, victim_index);
		fwd = bck->fd;
		//如果LargeBin不为空
		if (fwd != bck)
			{
				//加速比较,应该不仅仅有这个考虑,因为链表里的 chunk 都会设置该位。
				size |= PREV_INUSE;
				//bck->bk 存储着相应 large bin 中最小的chunk。
				//如果遍历的chunk比当前最小的chunk还要小,直接插入尾部
				//判断是否在main_arena
				assert (chunk_main_arena (bck->bk));
				if ((unsigned long) (size)
			< (unsigned long) chunksize_nomask (bck->bk))
				{
					//令fwd指向largebin头部
					fwd = bck;
					//令 bck 指向 largin bin 尾部 chunk
					bck = bck -> bk;

					//插入victim到链表尾部
					victim->fd_nextsize = fwd->fd;
					victim->bk_nextsize = fwd->fd->bk_nextsize;
					fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
				}
				else //当前要插入的 victim 的大小大于最小的 chunk
				{
					assert (chunk_main_arena (fwd));
					//从链表头部开始找到不比 victim 大的 chunk
					while ((unsigned long) size < chunksize_nomask (fwd))
					{
						fwd = fwd->fd_nextsize;
					assert (chunk_main_arena (fwd));
					}

					if ((unsigned long) size  //如果找到了一个和 victim 一样大的 chunk,
					//那就直接将 chunk 插入到该chunk的后面,并不修改 nextsize 指针。
				== (unsigned long) chunksize_nomask (fwd))
					fwd = fwd->fd;
					else //如果找到的chunk和当前victim大小不一样
					{
						//那么就需要构造 nextsize 双向链表了
						victim->fd_nextsize = fwd;
						victim->bk_nextsize = fwd->bk_nextsize;
						//检查双向链表完整性
						if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
							malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
						fwd->bk_nextsize = victim;
						victim->bk_nextsize->fd_nextsize = victim;
					}
					bck = fwd->bk;
					if (bck->fd != fwd) //检查bck和fwd是否匹配
						malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
						
				}
			}
		else  //如果空的话,直接简单使得 fd_nextsize 与 bk_nextsize 构成一个双向链表即可。
			victim->fd_nextsize = victim->bk_nextsize = victim;
	}
  • Unsorted Bin遍历 - 最终取出#

//构成bck <-> victim <-> fwd
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

总结#

在UnsortedBin大遍历后,如果找到了对应的chunk,会返回,如果没有找到,依旧会把UnsortedBin中所有的chunk放入chunk对应的bin中。

Large Chunk部分#

如果请求的 chunk 在 large chunk 范围内,就在对应的 bin 中从小到大进行扫描,找到第一个合适的。

//如果请求的chunk在large chunk部分,则进入判断
if (!in_smallbin_range(nb))
	{
		bin = bin_at (av, idx)
		// 如果对应的 bin 为空或者其中的chunk最大的也很小,那就跳过
		// first(bin)=bin->fd 表示当前链表中最大的chunk
		if ((victim = first (bin)) != bin //这行代码检查链表是否为空
	&& (unsigned long) chunksize_nomask (victim)
		>= (unsigned long) (nb)) // 这行代码检查链表中第一个内存块的大小是否大于或等于请求的内存大小
			{
				victim = victim -> bk_nextsize;
				//反向遍历链表,直到找到第一个不小于所需chunk大小的chunk
				while (((unsigned long) (size = chunksize (victim)) <
					(unsigned long) (nb)))
				victim = victim->bk_nextsize;
				//如果最终取到的chunk不是该bin中的最后一个chunk,并且该chunk与其前面的chunk
				//的大小相同,那么我们就取其前面的chunk,这样可以避免调整bk_nextsize,fd_nextsize
				//链表。因为大小相同的chunk只有一个会被串在nextsize链上。
				if (victim != last (bin)
				&& chunksize_nomask (victim)
				== chunksize_nomask (victim->fd))
					victim = victim->fd;

				remainder_size = size - nb;
				//取出
				unlink_chunk (av, victim);
				// 剩余不足以当作一个chunk
				// 直接使用整个victim,不进行分割
				if (remainder_size < MINSIZE)
					set_inuse_bit_at_offset (victim, size);
					if (av != &main_arena)
				set_non_main_arena (victim);
				//分割的情况
				else
					{
						//获取剩余的指针
						remainder = chunk_at_offset (victim, nb);
						//插入unsortedbin
						bck = unsorted_chunks (av);
						fwd = bck->fd;
						// 判断 unsorted bin 是否被破坏。
					if (__glibc_unlikely (fwd->bk != bck))
						malloc_printerr ("malloc(): corrupted unsorted chunks");
						remainder->bk = bck;
						remainder->fd = fwd;
						bck->fd = remainder;
						fwd->bk = remainder;
						// LargeChunk的情况
						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;
			}
	}
  • 寻找较大chunk#

++idx; 
bin = bin_at(av, idx);//获取对应的bin
block = idx2block(idx);//Binmap按block管理,每个block为一个int,共32个bit,可以表示32个bin中是否有空闲chunk存在
map = av->binmap[block];
bit = idx2bit (idx);

  

for (;; )

找到一个合适的map

//如果bit>map,则表示该 map 中没有比当前所需要chunk大的空闲块
//如果bit为0,那么说明,上面idx2bit带入的参数为0。
if (bit > map || bit == 0)
	{
		do
			{
			//寻找下一个block,直到其对应的map不为0。如果已经不存在的话,那就只能使用top chunk了
				if (++block >= BINMAPSIZE)
					goto use_top;
			}
		while ((map = av->binmap[block]) == 0);
		bin = bin_at (av, (block << BINMAPSHIFT));
		bit = 1;
	}

找到合适的bin

// 从当前map的最小的bin一直找,直到找到合适的bin。 
// 这里是一定存在的 
while ((bit & map) == 0) 
	{ 
		bin = next_bin(bin); 
		bit <<= 1; 
		assert(bit != 0); 
	}

检查并取出chunk

victim = last (bin);
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_chunk (av, victim);

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

		else
			{
				remainder = chunk_at_offset (victim, nb);

				bck = unsorted_chunks (av);

				fwd = bck->fd;

		if (__glibc_unlikely (fwd->bk != bck))

			malloc_printerr ("malloc(): corrupted unsorted chunks 2");

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

使用 Top Chunk#

如果所有的 bin 中的 chunk 都没有办法直接满足要求(即不合并),或者说都没有空闲的 chunk。那么我们就只能使用 top chunk 了。

use_top:
	//获取当前的topchunk计算大小
	victim = av->top;
	size = chunksize(victim);
	//检查内存是否正确
	if (__glibc_unlikely (size > av->system_mem))
		malloc_printerr ("malloc(): corrupted top size");
	//如果分割后top chunk 大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
	if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
		{
			//计算剩余大小
			remainder_size = size -nb;
			//获取剩余指针
			remainder = chunk_at_offset(victim, nb);
			//更新top指针
			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;
		}
	//清理内存碎片
	else if (atomic_load_relaxed (&av->have_fastchunks))
		{
			malloc_consolidate (av);
			if (in_smallbin_range (nb))
				idx = smallbin_index (nb);
			else
				idx = largebin_index (nb);
		}
	//堆内存不够时,sysmalloc申请到libc上
	else
		{
			void *p = sysmalloc (nb , av);
			if(p != NULL)
				alloc_perturb (p, bytes);
			return p;
		}
glibc-2.35 malloc.c源码审计笔记
https://k4per-blog.xyz/posts/glibc-235-mallocc源码审计笔记/
作者
K4per
发布于
2025-04-25
许可协议
CC BY-NC-SA 4.0