Linux堆管理基础知识(一)

2 篇文章 0 订阅

本文中若无特殊标识,则默认指linux中glibc环境下的堆管理。

后面一段时间将系统的学习下堆相关的知识,本文为系列的第一篇学习笔记

什么是堆

堆内存是一种允许程序在运行过程中动态分配内存和使用的区域。和栈的主要不同在于动态分配,堆的内存区域是程序运行时申请和释放的。

堆和栈的对比如下表所示

申请程序在运行过程中动态分配,由程序控制申请程序运行前分配
释放不能自动释放,由程序控制释放自动释放
特点地址由低 => 高地址由高=>低
内存分配非线性,无序线性,有序

堆的基本数据结构

堆的基本数据结构主要包含堆块和堆表两部分。

  • 堆块(chunk)
    • 堆块进一步分为块首和块身。
    • 块首(malloc_chunk):包含当前堆块的主要信息例如:此堆块的大小,前一个堆块的大小,是否是空闲态还是占用态等状态表信息。
    • 块身:块身就是本堆块存放数据的位置,即最终分配给用户的数据区。
      • 在内存中紧跟在mallco_chunk后面。
      • 当申请堆块成功后,返回的指针指向块身。
    • 堆块拥有不同的状态
      • allocated:已分配给用户使用的chunk
      • Unallocated/free:已释放/未分配给用户使用的chunk
  • 堆表(bin)
    • 堆表用来索引堆块。堆表中包含索引堆块的大小,位置,状态等信息。
    • 在glibc中使用四种不同的bin管理堆块。
      • Fast bin
      • Unsorted bin
      • Small bin
      • Large bin

Malloc_CHUNK

堆管理器最基本的数据结构是malloc,在源码中的定义如下

struct malloc_chunk {
  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk, if it is free. */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;                /* double links -- used only if this chunk is 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 this chunk is free. */
  struct malloc_chunk* bk_nextsize;
};

typedef struct malloc_chunk* mchunkptr;
  • mchunk_prev_size : 当物理地址相邻的前一个chunk为未分配状态时,该字段表示前一个chunk的大小。不会出现两个free状态的chunk相邻的情况,当两个free的chunk相邻时,会合并成一个chunk。因此对于一个free状态的chunk,它的前一个chunk总是已分配的,prev_size字段并不起作用,而是用来储存前一个chunk的用户数据。

  • mchink_size :记录了当前chunk的大小,chunk的大小都是8字节对齐,所以mchunk_size的低3位并不用来表示地址。为了充分利用内存空间,这三位被当作了标志位。

    ---------------------------------------------------
    |...........................................|A|M|P|
    ---------------------------------------------------
    
    • A (NON_MAIN_ARENA) : 记录当前chunk是否不属于主线程。1 = 不属于,0 = 属于。
    • M (IS_MMAPED): 记录当前chunk是否是由mmap分配的。1 = 是, 0 = 否。
    • P (PREV_INUSE): 记录前一个chunk块是否被分配。1 = 是,0 = 否。这里的前一个chunk指的是物理地址上相邻的前一个chunk。
  • 其他字段:对于已分配的chunk,并不包含其他的字段(fd, bk等)。

为什么mallco_chunk的结构是这样呢?我们来了解下最基本的堆管理方法 - 隐式链表

隐式链表

堆内存管理器都是以堆块(chunk)为最小单位进行堆内存管理的。为了高效的分配和使用内存,有很多种堆块的管理方法,下面介绍常见的几种方法

  • 隐式链表(Implict List):通过一个内部链表将所有区块的头字段链接起来
    在这里插入图片描述

  • 显式链表(Explicit List):通过一个链表将所有空闲区块的头字段连接起来。

在这里插入图片描述

  • 隔离闲置的链表(Segregated Free List):可以跟踪不同尺寸的置区块之间的链表,也可以说可以跟踪闲置区块之间是已分配的区块
  • 根据尺寸已排序的链表(Blocks Sorted List by sizes):可以使用平衡二叉树,指针位于每个空闲块中,长度用作关key值

后三种方法我们暂时了解,不去深入的学习,重点来看一下隐式链表的方法。

隐式链表之所以叫隐式,是因为它并没有真的拥有一个链表结构,但是可以通过特殊的构造实现类似链表的数据结构。

对于内存空间中的堆块,堆管理器需要知道堆块的大小使用状态来决定如何分配。

典型的堆块设计如下图所示

在这里插入图片描述

  • 在x86_64中,内存是按照8字节对齐的,因此chunk_size的低3位可以节省出来用来标识状态。最低位用来标识chunk是否被分配。

将堆块连接起来后,链表隐式地由每个chunk的size字段链接起来。对于chunk p来说,它可以方便的用自己的size定位到下一个chunk,从而可以向链表一样的进行遍历等操作,也拥有链表单向的特征。典型示意图如下所示:

在这里插入图片描述

那么在这种情况下malloc是如何进行堆块的分配和释放呢?

释放

释放相对来说比较简单,直接将chunk_size的最低位置0即可完成释放。

分配

在进行分配操作的时候,堆内存管理器遍历整个链表,比较用户申请的空间大小与chunk_size大小,当用户申请的空间大小小于等于chunk_size时,即对chunk进行分割操作,分配给申请的程序。

这样一来会带来一个问题,随着chunk的消耗,会产生越来越多的碎片化、无法继续使用的chunk,最终导致内存消耗殆尽。因此在分配切割的同时,堆管理器还需要对空闲的chunk进行合并。

在隐式链表结构下,进行后向合并是很方便的 。比如将上图中的堆块p2与相邻的空闲堆块合并,p2可以通过自身的size得到空闲区块的起始位置,从而得到空闲区块的大小,最终合并空闲区块。但是前向合并很艰难,对于p3来说,它无法直接获取前一个空闲堆块的起始位置和大小,必须再从头遍历一次才可以。

为了解决前向合并的困难,Knuth提出了一种聪明而通用的技术——边界标记。

Knuth在每个chunk的最后添加了一个脚部(Footer),它就是该chunk 头部(header)的一个副本,我们称之为边界标记:

在这里插入图片描述

增加了边界标记后,当p3要合并前一个空闲chunk时,访问上一个地址即可获取前一个chunk的大小,从而可以找到前一个chunk的起始位置并进行合并。

上面的方法虽然解决了合并的问题,但是每一个chunk都要有一个header一个footer来表示同一个size值,这显然是浪费了内存空间的。我们回到最开始要解决的问题,是要解决前向合并,也就是前一个chunk是free时,才需要前一个chunk有footer。因此,只有状态是free的chunk才需要footer,allocated的chunk并不需要footer。此时如何知道前一个chunk的状态呢,我们进一步把原来表示chunk状态的字段改为表示前一个chunk状态,新的allocated chunk将如下所示

在这里插入图片描述

而free chunk如下所示

在这里插入图片描述

进一步分析发现 free状态还是有两个chunk size,同时footer的标志位也没有什么作用。如果我们将整个结构体的分界上移一个单位的地址,即将当前free chunk的footer分配给下一个footer,变成下一个chunk的起始地址,我们就得到了新的chunk结构。

在这里插入图片描述

如上图所示,当前一个结构体为未分配状态时,chunk_size的最低位为0,此时结构体的起始地址表示前一个chunk的大小。而当前一个chunk为allocated状态时,这个地址可以用来给前一个chunk作为存储空间,即前一个chunk的padding段。

此时我们基本已经演化出来了malloc_chunk的形态,在glibc中的chunk结构如下所示。

Allocated Chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |If previous chunk is free, this filed is size of previous chunk|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             user data of chunk.                               |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|1|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Free Chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             user data of previous chunk			                  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    'head:' |             Size of chunk, in bytes                     |A|0|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward pointer to next chunk in list             |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Back pointer to previous chunk in list            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Unused space (may be 0 bytes long)                .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    'foot:' |             Size of chunk, in bytes                           |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|0|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

对于fd和bk两个指针的作用,我们将在后面学习中再来深入研究。


Reference

  1. https://zhuanlan.zhihu.com/p/24753861
  2. https://zhuanlan.zhihu.com/p/185826940
  3. https://heap-exploitation.dhavalkapil.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morphy_Amo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值