pwn基础之堆基础知识超详解

内存申请

请内存的系统调用有brk和mmap两种:

  1. brk是将数据段(.data)的最高地址上方的指针_edata往高地址推。(并非修改数据段的最高地址上限)
  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

brk

对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,我们可以通过增加 brk 的大小来向操作系统申请内存。

初始时,堆的起始地址 start_brk 以及堆的当前末尾 brk 指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同。

  • l 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。
  • l 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处。
mmap

malloc 会使用 mmap 来创建独立的匿名映射段。匿名映射的目的主要是可以申请以 0 填充的内存,并且这块内存仅被调用进程所使用。

多线程

在原来的 dlmalloc 实现中,当两个线程同时要申请内存时,只有一个线程可以进入临界区申请内存,而另外一个线程则必须等待直到临界区中不再有线程。这是因为所有的线程共享一个堆。在 glibc 的 ptmalloc 实现中,比较好的一点就是支持了多线程的快速访问。在新的实现中,所有的线程共享多个堆。

你可能会问: 只要把_edata+?K就完成内存分配了?

事实是这样的,_edata+?K只是完成虚拟地址的分配, A这块内存现在还是没有物理页与之对应的, 等到进程第一次读写这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。 也就是说,如果用malloc分配了这块内容,然后从来不访问它,那么对应的物理页是不会被分配的。

两种申请的情况:

内存分配原理:

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  • brk是将数据段(.data)的最高地址上方的指针_edata往高地址推。
  • mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

下面以一个例子来说明内存分配的原理:

情况一:

malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

1、进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。

其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见, 省略了内存映射文件。

_edata指针(glibc里面定义)指向数据段的最高地址。

2、进程调用A=malloc(30K)以后,内存空间如图2:

malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。

3、进程调用B=malloc(40K)以后,内存空间如图3。

情况二:

malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

4、进程调用C=malloc(200K)以后,内存空间如图4:

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

这样子做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。

5、进程调用D=malloc(100K)以后,内存空间如图5;

6、进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。

7、进程调用free(B)以后,如图7所示:

B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?

当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。

8、进程调用free(D)以后,如图8所示:

B和D连接起来,变成一块140K的空闲内存。

9、默认情况下:

当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(malloc_trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

三、既然堆内内存brk和sbrk不能直接释放,为什么不全部使用 mmap 来分配,munmap直接释放呢?

既然堆内碎片不能直接释放,导致疑似“内存泄露”问题,为什么 malloc 不全部使用 mmap 来实现呢(mmap分配的内存可以会通过 munmap 进行 free ,实现真正释放)?而是仅仅对于大于 128k 的大块内存才使用 mmap ?

其实,进程向 OS 申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1M 空间,第一次调用产生了大量缺页中断 (1M/4K 次 ) ,当munmap 后再次分配 1M 空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU消耗较大。另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大。

同时堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。 因此, glibc 的 malloc 实现中,充分考虑了 sbrk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128k) 才使用 mmap 获得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>) 来修改这个临界值。

C语言跟内存申请相关的函数主要有 alloc,calloc,malloc,free,realloc,sbrk等.其中alloc是向栈申请内存,因此无需释放. malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间.calloc则将初始化这部分的内存,设置为0. 而realloc则对malloc申请的内存进行大小的调整.申请的内存最终需要通过函数free来释放. 而sbrk一种函数,能够修改程序BSS段的大小。 作用:将内核的brk指针增加incr来扩展和收缩堆。 返回值: 函数调用成功则返回旧的brk指针。

堆分配流程图:

malloc根据用户申请的内存块大小以及相应大小chunk通常使用的频度(fastbin chunk, small chunk, large chunk),依次实现了不同的分配方法。它由小到大依次检查不同的bin中是否有相应的空闲块可以满足用户请求的内存。当所有的空闲chunk都无法满足时,它会考虑top chunk。当 top chunk 也无法满足时,堆分配器才会进行内存块申请。

对于malloc申请一般大小(不超过现有空闲内存大小)的内存,其简化版流程如下。

首先将size按照一定规则对齐,得到最终要分配的大小size_real,具体如下。

  • .x86:size+4按照0x10字节对齐。
  • .x64:size+8按照0x20字节对齐。
  1. 检查size_real是否符合fast bin的大小,若是则查看fast bin中对应size_real的那条链表中是否存在堆块,若是则分配返回,否则进入第2步。
  2. 检查size_real是否符合small bin的大小,若是则查看small bin中对应size_real的那条链表中是否存在堆块,若是则分配返回,否则进入第3步。
  3. 检查size_real是否符合large bin的大小,若是则调用malloc_consolidate函数对fast bin中所有的堆块进行合并,其过程为将fast bin中的堆块取出,清除下一块的p标志位并进行堆块合并,将最终的堆块放入unsorted bin。然后在small bin和large bin中找到适合size_real大小的块。若找到则分配,并将多余的部分放入unsorted bin,否则进入第4步。
  4. 检查top chunk的大小是否符合size_real的大小,若是则分配前面一部分,并重新设置top chunk,否则调用malloc_consolidate函数对fast bin中的所有堆块进行合并,若依然不够,则借助系统调用来开辟新空间进行分配,若还是无法满足,则在最后返回失败。

这里面值得注意的点如下。

  1. fast bin的分配规则是LIFO。
  2. malloc_consolidate函数调用的时机:它在合并时会检查前后的块是否已经释放,并触发unlink。

在 glibc 的 malloc.c 中,malloc 的说明如下:

/*
  malloc(size_t n)
  Returns a pointer to a newly allocated chunk of at least n bytes, or null
  if no space is available. Additionally, on failure, errno is
  set to ENOMEM on ANSI C systems.
  If n is zero, malloc returns a minumum-sized chunk. (The minimum
  size is 16 bytes on most 32bit systems, and 24 or 32 bytes on 64bit
  systems.)  On most systems, size_t is an unsigned type, so calls
  with negative arguments are interpreted as requests for huge amounts
  of space, which will often fail. The maximum supported value of n
  differs across systems, but is in all cases less than the maximum
  representable value of a size_t.

  malloc(大小_t n)
  返回指向新分配的至少 n 字节块的指针,或 null
  如果没有可用空间。此外,失败时,errno 为
  在 ANSI C 系统上设置为 ENOMEM。
  如果 n 为零,则 malloc 返回一个最小大小的块。 (最低
  大多数 32 位系统上的大小为 16 字节,64 位系统上的大小为 24 或 32 字节
  系统。)在大多数系统上,size_t 是无符号类型,因此调用
  带有否定参数的被解释为巨额请求
  空间,这通常会失败。 n的最大支持值
  不同系统有所不同,但在所有情况下都小于最大值
  size_t 的可表示值。
*/

free

free函数将用户暂且不用的chunk回收给堆管理器,适当的时候还会归还给操作系统。它依据chunk大小来优先试图将free chunk链入tcache或者是fast bin。不满足则链入usorted bin中。在条件满足时free函数遍历usorted bin并将其中的物理相邻的free chunk合并,将相应大小的chunk分类放入small bin或large bin中。除了tcache chunk与fast bin chunk,其它chunk在free时会与其物理相邻的free chunk合并。

一个简易的内存释放流程如下。


相关宏如下。

堆块在释放时会有一系列的检查,可以与源码进行对照。在这里,将对一些关键的地方进行说明。

  1. 释放(free)时首先会检查地址是否对齐,并根据size找到下一块的位置,检查其p标志位是否置为1。
  2. 检查释放块的size是否符合fast bin的大小区间,若是则直接放入fast bin,并保持下一堆块中的p标志位为1不变(这样可以避免在前后块释放时进行堆块合并,以方便快速分配小内存),否则进入第3步。
  3. 若本堆块size域中的p标志位为0(前一堆块处于释放状态),则利用本块的pre_size找到前一堆块的开头,将其从bin链表中摘除(unlink),并合并这两个块,得到新的释放块。
  4. 根据size找到下一堆块,如果是top chunk,则直接合并到top chunk中去,直接返回。否则检查后一堆块是否处于释放状态(通过检查下一堆块的下一堆块的p标志位是否为0)。将其从bin链表中摘除(unlink),并合并这两块,得到新的释放块。
  5. 将上述合并得到的最终堆块放入unsorted bin中去。

这里有以下几个值得注意的点:

  1. 合并时无论向前向后都只合并相邻的堆块,不再往更前或者更后继续合并。
  2. 释放检查时,p标志位很重要,大小属于fast bin的堆块在释放时不进行合并,会直接被放进fast bin中。在malloc_consolidate时会清除fast bin中所对应的堆块下一块的p标志位,方便对其进行合并。

可以看出,free 函数会释放由 p 所指向的内存块。这个内存块有可能是通过 malloc 函数得到的,也有可能是通过相关的函数 realloc 得到的。

此外,该函数也同样对异常情况进行了处理:

  • 当 p 为空指针时,函数不执行任何操作。
  • 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free。
  • 除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

Arena

  • 一个线程申请的1个或多个堆包含很多的信息:二进制位信息,多个malloc_chunk信息等这些堆需要东西来进行管理,那么Arena就是来管理线程中的这些堆的,也可以理解为堆管理器所持有的内存池。
  • 堆管理器与用户的内存交易发生于arena中,可以理解为堆管理器向操作系统批发来的有冗余的内存库存。
  • 主线程无论一开始malloc多少空间,只要size<128KB,kernel都会给132KB的heap segment(rw)。这部分称为main arena。 main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。

可以看到在malloc后会出现heap的空间,且是在data的上面,地址增长方向是从start_brk到brk,也就是上面提到的:brk是将数据段(.data)的最高地址上方的指针_edata往高地址推。

Top chunk

top chunk 是堆中未分配内存的起点,通常位于堆的末尾。堆是进程的内存区域,专用于动态内存分配。堆的起始地址在进程的虚拟地址空间中是固定的,而堆的末尾地址(即 top chunk 的位置)会随着内存分配和释放而变化。

堆的末尾是指堆内存区域的最高地址,或者说是未分配内存区域的开始位置。堆的大小和位置在进程的虚拟地址空间中是动态调整的,由操作系统和运行时库(如 GNU C Library)管理。

在内存分配中,ptr1 和 ptr2 分别指向不同的内存块,具体哪个指针更接近 top chunk 取决于它们在堆中的分配顺序。通常情况下,后分配的内存块会位于前分配内存块的高地址处,这意味着 ptr2 比 ptr1 更接近 top chunk。 (关于top chunk的位置)!!!!!

Top chunk,在第一次malloc的时候,glibc就会将堆切成两块chunk,第一块chunk就是分配出去的chunk,剩下的空间视为top chunk,之后要是分配空间不足时将会由top chunk分配出去,它的size为表示top chunk还剩多少空间。假设 Top chunk 当前大小为 N 字节,用户申请了 K 字节的内存,那么 Top chunk 将被切割为:

  • 一个 K 字节的 chunk,分配给用户
  • 一个 N-K 字节的 chunk,称为 Last Remainder chunk

后者成为新的 Top chunk。如果连 Top chunk 都不够用了,那么:

  • 在 main_arena 中,用 brk() 扩张 Top chunk
  • 在 non_main_arena 中,用 mmap() 分配新的堆

top chunk的prev_inuse位总是1,否则其前面的 chunk 就会被合并到 top chunk 中。

顾名思义,是堆中第一个堆块。相当于一个”带头大哥”,程序以后分配到的内存到要放在他的后面.

在系统当前的所有 free chunk(无论那种 bin),都无法满足用户请求的内存大小的时候,将此 chunk 当做一个应急消防员,分配给用户使用。

简单点说,也就是在程序在向堆管理器申请内存时,没有合适的内存空间可以分配给他,此时就会从 top chunk 上”剪切”一部分作为 chunk 分配给他。

Bins

  • Bins为一个单向或者双向链表,存放着空闲的chunk(freed chunk)。glibc为了让malloc可以更快找到合适大小的chunk,因此在free掉一个chunk时,会把该chunk根据大小加入合适的bin中。
  • Bins一共可分为fast bin、small bin、large bin、unsorted bin和tcache bin。可分为:10个fast bins,存储在fastbinsY中;1个unsorted bin,存储在bins[1];62个small bins,存储在bins[2]至bins[63];63个large bins,存储在bins[64]至bins[126]。其中虽然定义了NBINS=128,但是bins[0]和bins[127]其实是不存在的。
  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

Fast bins非常像高速缓存cache,主要用于提高小内存分配效率。相邻空闲chunk不会被合并,这会导致内存碎片增多但是free效率提升。注意:fast bins时10个LIFO的单链表,最后三个链表保留未使用。

  • fastbinsY[],fast bin存放在此数组中
  • 使用单链表来维护释放的堆块
    也就是和上图一样,从main_arena 到 free 第一个块的地方是采用单链表形式进行存储的,若还有 free 掉的堆块,则这个堆块的 fk 指针域就会指针前一个堆块。
  • 采用后进先出的方式维护链表(类似于栈的结构)
    当程序需要重新 malloc 内存并且需要从fastbin 中挑选堆块时,会选择后面新加入的堆块拿来先进行内存分配
  • 管理 16、24、32、40、48、56、64 Bytes 的 free chunks(32位下默认)

关于fastbin最大大小参见宏DEFAULT_MXFAST:

在初始化时,这个值会被复制给全局变量global_max_fast。申请fast chunk时遵循first fit原则。释放一个fast chunk时,首先检查它的大小以及对应fastbin此时的第一个chunk的大小是否合法,随后它会被插入到对应fastbin的链表头,此时其fd指向上一个被free的chunk。

Fast bin示意图如下。

Small bin

顾名思义,这个是一个 small chunk ,满足的内存空间比 fast bin 大一点。

  • 如果程序请求的内存范围不在 fast bin 的范围内,就会考虑small bin。简单点说就是大于 80 Bytes 小于某一个值时,就会选择他。

Small bins,chunk size小于0x200(64位下0x400)字节的chunk叫做small chunk,而small bins存放的就是这些small chunk。Chunk大小同样是从16字节开始每次+8字节。

small bins 是 62 个双向循环链表,并且是 FIFO 的,这点和 fast bins 相反。同样相反的是相邻的空闲 chunk 会被合并。chunk大小:0x10-0x1f0字节(64位下0x20-0x3f0),相邻bin存放的大小相差0x8(0x10)字节。

ptmalloc 维护了 62 个双向环形链表(每个链表都具有链表头节点,加头节点的最大作用就是便于对链表内节点的统一处理,即简化编程),每一个链表内的各空闲 chunk 的大小一致,因此当应用程序需要分配某个字节大小的内存空间时直接在对应的链表内取就可以了,这样既可以很好的满足应用程序的内存空间申请请求而又不会出现太多的内存碎片。

释放非 fast chunk 时,按以下步骤执行:

  1. 若前一个相邻chunk空闲,则合并,触发对前一个相邻 chunk的unlink操作
  2. 若下一个相邻chunk是top chunk,则合并并结束;否则继续执行 3
  3. 若下一个相邻 chunk 空闲,则合并,触发对下一个相邻chunk的unlink 操作;否则,设置下一个相邻 chunk 的 PREV_INUSE 为 0
  4. 将现在的chunk插入unsorted bin。
  5. 若size超过了FASTBIN_CONSOLIDATION_THRESHOLD,则尽可能地合并 fastbin中的chunk,放入unsorted bin。若top chunk大小超过了 mp_.trim_threshold,则归还部分内存给 OS。

Small bins图示如下。

large bins

Large bins存放的是大于等于0x200(64位下0x400)字节的chunk,它是63个双向循环链表,插入和删除可以发生在任意位置,相邻空闲chunk也会被合并。Chunk大小就比较复杂了:

  • 前32个bins:从0x400字节开始每次+0x40字节
  • 接下来的16个bins:每次+0x200字节
  • 接下来的8个bins:每次+0x1000字节
  • 接下来的4个bins:每次+0x8000字节
  • 接下来的2个bins:每次+0x40000字节
  • 最后的1个bin:只有一个chunk,大小和large bins剩余的大小相同

同一个bin中的chunks不是相同大小的,按大小降序排列。这和上面的几种 bins都不一样。而在取出chunk时,也遵循best fit原则,取出满足大小的最小 chunk。总结以下特点。

  • 双向循环链表(排好序了)
  • Chunk size > 0x400
  • Freed chunk多两个指针fd_nextsize、bk_nextsize指向前一块和后一块large chunk
  • 根据大小再分成63个bin但大小不再是固定大小增加
    • 前32个bin为0x400+0x40*i
    • 32~48bin为0x1380+0x200*i
    • …以此类推
  • 不再是每个bin中的chunk大小都固定,每个bin中存着该范围内不同大小的bin并在过程中进行排序用来加快寻找的速度,大的chunk会放在前面,小的chunk会放在后面
  • FIFO

Large bins示意图如下。

Unsorted bin

unsorted bin也是以链表的方式进行组织的,和fast bin不同的是其分配方式是FIFO,即一个chunk放入unsorted bin链时将该堆块插入链表头,而从这个链取堆块的时候是从尾部开始的,因此unsorted bin遍历堆块的时候使用的是bk指针。(非常重要的)!!!!!!

Unsorted bin非常像缓冲区buffer,大小超过fast bins阈值的chunk被释放时会加入到这里,这使得ptmalloc2可以复用最近释放的chunk,从而提升效率。

所有的大小超过fast bins阈值的 chunk 在回收时都要先放到 unsorted bin中,分配时,如果在 unsorted bin 中没有合适的 chunk,就会把 unsorted bin 中的所有 chunk分别加入到所属的 bin 中,然后再在 bin 中分配合适的 chunk。Bins 数组中的元素 bin[1]用于存储 unsorted bin 的 chunk 链表头。

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,堆管理器就会考虑使用 unsorted bin 。它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。

  • unsorted bin 与 fast bin 不同,他使用双向链表对 chunk 进行连接
  • unsorted 的字面意思就是”不可回收”的意思,可以看作将不可回收的垃圾(不满足能够进行内存分配的堆块)都放到这个”垃圾桶”中。

  1. 如果 unsorted chunk 满足以下四个条件,它就会被切割为一块满足申请大小的 chunk 和另一块剩下的 chunk,前者返回给程序,后者重新回到 unsorted bin。
    • 申请大小属于 small bin 范围
    • unosrted bin 中只有该 chunk
    • 这个 chunk 同样也是 last remainder chunk
    • 切割之后的大小依然可以作为一个 chunk
  1. 否则,从 unsorted bin 中删除 unsorted chunk。
    • 若 unsorted chunk 恰好和申请大小相同,则直接返回这个 chunk
    • 若 unsorted chunk 属于 small bin 范围,插入到相应 small bin
    • 若 unsorted chunk 属于 large bin 范围,则跳转到 3。
  1. 此时 unsorted chunk 属于 large bin 范围。
    • 若对应 large bin 为空,直接插入 unsorted chunk,其 fd_nextsize 与 bk_nextsize 指向自身。
    • 否则,跳转到 4。
  1. 到这一步,我们需按大小降序插入对应 large bin。
    • 若对应 large bin 最后一个 chunk 大于 unsorted chunk,则插入到最后
    • 否则,从对应 large bin 第一个 chunk 开始,沿 fd_nextsize(即变小)方向遍历,直到找到一个 chunk 命名为c,其大小小于等于 unsorted chunk 的大小
    • 若c大小等于unsorted chunk大小,则插入到c后面
    • 否则,插入到c前面

直到找到满足要求的unsorted chunk,或无法找到,去top chunk切割为止。总结以下特点。

  • 双向循环链表
  • 当free的chunk大小大于等于144(0x90=0x80+0x10)字节时,为了效率,glibc并不会马上将chunk放到相对应的bin中,而会先放到unsorted bin
  • 而下次mallocs时将会先找找看unsorted bin中是否有合适的chunk,找不到才会去对应的bin中寻找,此时会顺便把unsorted bin的chunk放到对应的bin中,但small bin除外,为了效率,反而先从small bin找

仅有两个chunk的链表结构为下图

下面我就pwngdb演示一下:

unsorted bin的内部结构:

前提先说一下:我先申请的4个1200字节的堆块空间。他们的下标分别是0,1,2,3。在堆空间的排列是这样的。

然后删除了下标位0,2的堆块大小。释放的堆块会出现在unsorted bin结构中:

在gdb中可以查看到unsorted bin中的结构,如下图:unsorted bin中有两个自由堆块,就是我们刚刚释放的0,2号

我们查看每个堆块内的内容如下:

解释一下图三中的意思:

  • tcachebins、fastbins、smallbins、largebins
  • 这些bin都为空,意味着目前没有小型或大型的空闲块在这些bin中。
  • unsortedbin
  • 该bin包含两个自由块,地址分别是 0x555555559c10 和 0x555555559290。

  • 检查0x555555559c10的内容
pwndbg> x/4gx 0x555555559c10
0x555555559c10: 0x0000000000000000      0x00000000000004c1
0x555555559c20: 0x0000555555559290      0x00007ffff7fa5ce0
  • 0x555555559c10: 前4个字(64位)中,第一个字是0x0000000000000000,表示此块之前没有其他自由块。
  • 0x555555559c18: 第二个字是0x00000000000004c1,表示块的大小为0x4c0(包括标志位)。
  • 0x555555559c20: 第三个字是0x0000555555559290,表示前向指针(fd)。
  • 0x555555559c28: 第四个字是0x00007ffff7fa5ce0,表示后向指针(bk)。

  • 检查0x555555559290的内容
pwndbg> x/6gx 0x555555559290
0x555555559290: 0x0000000000000000      0x00000000000004c1
0x5555555592a0: 0x00007ffff7fa5ce0      0x0000555555559c10
0x5555555592b0: 0x0000000000000000      0x0000000000000000
  • 0x555555559290: 前4个字(64位)与上面的解释相同。
  • 0x555555559298: 第三个字是0x00007ffff7fa5ce0,表示前向指针(fd)。
  • 0x5555555592a0: 第四个字是0x0000555555559c10,表示后向指针(bk)。

  • 检查0x7ffff7fa5ce0的内容
pwndbg> x/6gx 0x7ffff7fa5ce0
0x7ffff7fa5ce0 <main_arena+96>: 0x000055555555a590      0x0000000000000000
0x7ffff7fa5cf0 <main_arena+112>:        0x0000555555559c10      0x0000555555559290
0x7ffff7fa5d00 <main_arena+128>:        0x00007ffff7fa5cf0      0x00007ffff7fa5cf0
  • 0x7ffff7fa5ce0: 是main_arena的指针,指向堆管理器的主要数据结构。

各bins的范围:

malloc_chunk的对齐属性

在glibc中,malloc_chunk以 2*sizeof(size_t)对齐,在32位系统中以8字节对齐,在64位系统中一般以16字节对齐。Malloc_chunk的定义如下:

既然malloc_chunk以2*sizeof(size_t)对齐,那么malloc返回给用户的指针数值也是以2*sizeof(size_t)对齐。

Glibc中最小的chunk是多大呢?

最小的chunk需要保证能放下prev_size、size、fd以及bk字段并保证对齐。在32位系统中,即16字节,在64位系统中,一般为32字节。在64位系统中也可能定义INTERNAL_SIZE_T也即size_t为4字节,这种情况下最小的chunk位24字节。如下:

Fastbin里有多少bin呢?

将上面的宏计算出来,会发现NFASTBINS为10,不论32位系统还是64位系统。

但是在32位系统中的nfastbins不为10,下面会说到:

在32位系统中,fastbin里相邻的两个bin大小差距8个字节;在64位系统中,则是差距16个字节。


 

既然有10个fastbin,在32位系统中,fastbin的chunk的范围是从16,24,32,...,88字节吗?不对!在malloc_init_state函数中,会将fastbin最大的chunk设置为64,并没有达到88。

因此,在32位系统中,fastbin里chunk的大小范围从16到64(0x10-0x40);在64位系统中,fastbin里chunk的大小范围从32到128(0x20-0x80)。

Small bins里chunk大小范围?有多少bins?

在32位系统中,small bins里的chunk大小从16到504字节;在64位系统中,small bins里的chunk大小从32到1016字节。

根据small bins里的chunk大小范围以及每个chunk递增的大小得知,small bins里有62个bin。

怎么根据p=malloc(m)里的m来判断分配多大的chunk呢?

将申请的内存大小加上每个chunk的overhead,也就是chunk结构体里的size字段。然后对齐,就是需要分配的chunk的大小。

在64位系统中,确定chunk的大小为0x88+0x16=0x98。所以每一次申请,都会分配一个大小为0x98的chunk。

Tcache

Tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有7个chunk,free的时候当tcache满了才放入fastbin或unsorted bin,malloc的时候优先去tcache找。

基本工作方式:

  • malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct 。
  • free 内存,且 size 小于 small bin size 时
    • 先放到对应的 tcache 中,直到 tcache 被填满(默认是 7 个)
    • tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
    • tcache 中的 chunk 不会合并(不取消 inuse bit)
  • malloc 内存,且 size 在 tcache 范围内
    • 先从 tcache 取 chunk,直到 tcache 为空
    • tcache 为空后,从 bin 中找
    • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来。

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值