堆利用/glibc内存管理
什么是堆?
首先明确一下堆的概念,堆不同于栈,堆是(由操作系统内核或者堆管理器)动态分配的,**只有在程序需要时才会分配。**在CTFpwn程序中,栈是程序加载进内存后就会出现,而堆是由malloc,alloc,realloc函数分配内存之后才会出现。
堆(chunk)内存是一种允许程序在运行过程中动态分配和使用的内存区域。相比于栈内存和全局内存,堆内存没有固定的生命周期和固定的内存区域,程序可以动态地申请和释放不同大小的内存。被分配后,如果没有进行明确的释放操作,该堆内存区域都是一直有效的。
windows和linux下的堆分配,管理方式都不同,这里主要是linux的堆分配知识
堆分配
首先需要明确堆在虚拟内存中的位置
堆的生长方式是从低地址向高地址生长的,而栈是从高地址向低地址生长的。
实际上堆可以申请到的内存空间比栈要大很多,在linux的4G虚拟内存空间里最高可以达到2.9G的空间
chunk的结构
堆的基本结构
pre_size和size统称为chunk头(chunk header),每一次malloc申请得到的内存指针其实都指向user data的起始处
堆的结构
malloc_chunk结构
每个程序分配的内存(这里指的是malloc函数)在内部被一个叫做“堆块”的东西所替代。一个堆块是由元数据和程序返回内存组成的(实际上内存是malloc的返回值)。所有的堆块都是保存在堆上,这块内存区域在申请新的内存时会不断地扩大。同样,当一定数量内存释放时,堆可以收缩,在glibc源码中定义的堆块如下
1 | struct malloc_chunk{ |
在计算机科学中,chunk 是一种用于管理内存的基本数据结构,特别是在堆内存分配中。chunk 结构在内存分配和释放过程中起着关键作用,尤其是在 malloc 和 free 函数的实现中。
当进程动态分配内存时,系统会在堆中创建一个chunk(堆块);chunk包含chunk头和chunk体两个部分。当一个chunk被释放后,它不会立即归还给操作系统,而是被保留以供后续使用。(空闲chunk)
1 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
1 | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
一般情况下,物理相邻的两个空闲chunk会被合并为一个chunk。堆管理器会通过prev_size字段以及size字段合并两个物理相邻的空闲chunk块.堆由低地址向高地址生长
prcv_size 字段:
也可以说是pre_size。如果该chunk的相邻物理的前一个地址chunk**(两个指针的地址差为前一个chunk大小)**是空闲的话,那么该字段记录的是前一个chunk的大小(包括chunk头)。否则该字段可以用来存储物理相邻的前一个chunk的数据。这里的前一个chunk指的是较低地址的chunk。前面一个堆块在使用时名且pre_size为储存前面chunk的数据时,它的值始终为0
size 字段/AMP:
size字段是用来指示当前堆块大小的** [ 头部 (pre_size+size) + user_data_size ] **。
size字段的低三位AMP不用于计算size内存大小
- 其中_A(NON_MAIN_ARENA)_:为0表示该chunk属于主分配去(主线程);为1表示非主分配区(非主线程)
- M(IS_MAPPED):表示当前chunk获取虚拟内存的内存区域。为1表示chunk从mmap去映射分配,否则从heap区域分配
- 末位的_P(PREV_INUSE)_:字段表示前一堆块是否正在使用(1为使用0为空闲) ;一般来说,堆中第一个分配的内存块的size字段的P位都会被设置成1,以便于防止访问前面的非法内存。当一个chunk的size的P位为0时,我们能通过prev_size字段来获取上一个chunk的大小以及地址。这也方便进行空闲chunk之间的合并
所以前一个堆块的释放与否都和pre_size,size->prev_inuse这两个值有关,这是因为便于内存的释放操作free
user_data 字段:
顾名思义就是来存放用户数据的,使用malloc函数分配到的内存返回值指针指向user_data(用户数据区),在后面的例子中也会讲到这个问题。
_存放内存空间大小: _例如在64位程序中申请一块空间
1 | malloc(8) |
申请到的堆块总大小为16+8+8+1=0x21byte
16字节是系统最小分配内存,也就是说如果你想要申请的内存小于系统最小分配内存的话,就会按照最小的内存来分配,64位是16个字节,32位是八个字节,同样malloc( 0 )也会分配最小内存空间给你
8字节是pre_size字段的大小(32位为4字节)
8字节是size字段的大小(32位位4字节)
最后一个1字节是PREV_INUSE的值,只有0或1两个值
总结:堆的基本结构包括pre_size , size(大小:pre_size+size+userdata) , userdata
具体结构图
使用中(分配后)
空闲中(使用后)
- 空闲中的chunk不存在M状态,只有A| |P状态
- user_data头部被分配出两个成员->fd和bk
chunk体分为两种情况:
- 对于非空闲/正在使用的堆块,chunk体就是当前堆块存放的数据
fd & bk
空闲的堆块会被一个双向链表(空闲链表)连接:
chunk体的前两个size_t的空间存放两个指针fd和bk _** fd **_指向链表中后一个(非物理相邻)空闲chunk的起始地址,32位占4字节,64位占8字节 _** bk **_指向链表中前一个(非物理相邻)空闲chunk的起始地址,32位占4字节,64位占8字节
_**fd[Forward pointer] 向前指针 **_指向下一个空闲chunk
_**bk[Back pointer] 向后指针 **_指向前一个空闲chunk
通过fd和bk可以将空闲的chunk块加入到空闲的chunk链进行统一管理
堆块大小
32位操作系统:
用户分配到最小的堆块大小位17B:prev_size(4B)+size(4B)+fd(4B)+bk(4B)+next_chunk->p(1B)
若用户申请大小超过最小堆块大小,会与8B进行对齐
64位操作系统:
用户分配到最小的堆块大小位33B:prev_size(8B)+size(8B)+fd(8B)+bk(8B)+next_chunk->p(1B)
若用户申请大小超过最小堆块大小,会与16B进行对齐
chunk的空间复用
描述:当一个chunk处于使用状态时,他的下一个chunk的prev_size无效。所以下一个chunk的prev_size也可以被当前chunk使用,这就是chunk的空间复用。
chunk的的结构十分巧妙,通过空间复用,能在占用最少的空间的情况下实现malloc的内存管理算法。 空闲时,一个 chunk 中至少需要 4个size_t大小的空间,用来存储[mchunk最小空间],mchunk_size,fd和bk。 当一个 chunk 处于使用状态时,它的下一个chunk的prev_size域肯定是无效的。 所以实际上,这个空间也可以被当前chunk使用。
指针与地址
结构体指针
动态分配内存函数malloc原型
1 | void *malloc(unsigned int size) |
连续动态分配内存calloc原型
1 | void *calloc(unsigned int n, unsigned nt size) |
内存分配调整函数realloc原型
1 | void *realloc(void *ptr/*已经分配的首地址*/, unsigned int size/*存储空间的字节数*/) |
释放存储空间free
1 | void free(void *block) |
free函数只释放动态申请的储存空间,如果block为指针,则该指针仍然存在,即该指针占用的储存单元仍然存在,释放的是该指针指向的内存空间。
指针内存图
首先明确用户在调用malloc函数时返回的值为一个指针,指向分配到对空间(用户数据区)
first_chunk&second_chunk表示第一个和第二个结构,每个结构中都有一个point_heap指针来指向存储用户数据的堆块(chunk)
左边的本身就是一个堆块,用来存放一些全局信息,比如max_size存储了能够存储的最大结构数量;exit_num表示已经存储的结构的数量
IDA中常见的针表示形式
在 IDA 伪代码中指针形式形如下面的情况:
*(qword_6020A8 + 8)表示取到qword_6020A8这个地址加8偏移道德哪个地址存储的值
汇编代码等同于:.text:00000000400F85 mov rax, cs:qword_6020A8
.text:00000000400F8C mov rax, [rax+8]
简单转换一下也就是*(addr) = [addr]
链表表示
在pwn的堆题目里面,经常会有像一些管理系统之类的题目例如
这种有最基本的增删改查的功能的数据结构通常就是使用链表连接起来的
申请堆块的本质
堆管理器ptmalloc2主要是通过 malloc/free 函数来分配和释放内存块
通俗来讲ptmalloc2就相当于是一个中间商,当程序想要申请向程序申请堆空间时,这里的ptmalloc2就会申请一块很大的空间,并根据算法从这些内存中把空间真正的分配给程序
1 |
|
这里堆段会给132kB大小的的空间,一个地址就是1byte的内容
这132kB的堆空间叫做arena,此时因为是主线程分配的,所以这个区域叫做main arena
(厂家)内核会批发给(中间商)ptmalloc2的货物,以便下次程序在向系统是申请小内存的时候,直接去中间商去取就可以了,它就会在这132kB中按照要申请的货物的多少进行分配下去,如果中间商缺货,ptmalloc就会继续去找(厂家)内核去取货
main_arena与top chunk
chunk 在 管 理 器 中 有 3 种 形 式 , 分 别 为 allocated chunk 、 free chunk和top chunk。当用户申请一块内存后,堆管理器会返回一个allocated chunk , 其 结 构 为mchunk_prev_size+mchunk_size+user_memory 。 user_memory 为 可 被用户使用的内存空间。free chunk为allocated chunk被释放后的存在形式。top chunk是一个非常大的free chunk,如果用户申请内存大小比top chunk小,则由top chunk分割产生。在64位系统中,chunk结构最小为32(0x20)字节。
main_arena
top chunk
顾名思义,就是堆中的第一个堆块,相当于一个带头大哥,程序以后分配到的内存要放在他的后面。当系统当前所有free chunk(无论那种bin)都无法满足用户请求的内存大小的时候,将此chunk当作一个应急消防员,分配给用户使用。
简单来说就是在程序向堆管理器申请内存的时候,没有合适的内存空间分配给他,此时就会从top chunk上剪切一部分作为chunk给他。
free函数与bins
free函数
free函数的使用是和bins的分配是息息相关的。
调用完free函数以后程序做了两件事
- 清空此堆块的user_data
- 将此堆块的指针存储到了main_arena(或者fast bin)中了
bin
bins这个改建适合内存回收相关的,也就是堆管理器会根据用户已经申请到的内存空间大小进行释放,来决定放入哪类bins。bins可以直译为垃圾桶的意思,所以系统在决定使用哪个bins的时候可以看作为“垃圾的分类”
描述:
1.用户free掉的内存并不是都会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中空闲的chunk
2.当用户进行下一次分配请求时,ptmalloc会首先试图在空闲的chunk中条船一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销
3.ptmalloc将相似大小的chunk用双向链表连接起来,这样的一个链表被称为bin
4.ptmalloc一共维护了128个bin,并使用一个数组来存储这些bin
5.根据堆管理器的特点,将堆分为四种:fastbin | un__sortbin | smallbin | largebin
为了高效地分配内存并尽量避免内存碎片,Ptmalloc2将不同大小的 free chunk 分 为 不 同 bin结构 , 分别为Fast Bin 、Small Bin 、Unsorted Bin、Large Bin。
为了进一步提升速度还建立了fastbin这个特殊的容器
fastbin
顾名思义,就是为了快速重新分配回内存而存在的一个结构
描述:
1)在32位操作系统中,当用户释放的堆块大小小于64B时使用fastbin进行管理,即chunk空间最大为80字节
2)fastbin只使用了fd成员,他是一个单链表结构
3)chunk并不改变fastbin的使用标志P,也就是说它不会主动进行合并;只有在某些特定条件下,堆管理器才会对fastbin进行合并
4)fastbinY为管理fastbin的数组,每个成员分别管理不同大小的fastbin链表,且均指向量当前链表的尾节点,当尾节点被分配时,通过其fd指针指向前一个结点
5)当用户申请chunk大小小于或等于MAX_FAST_SIZE时,有限从fastbins中查找相应的空闲块,且规则为后进先出
- 这里横向排列的就是main_arene(fastbin)的内存地址
加入此时0x804a000处的堆块已经被free了,那么它就会被存储在表示40bytes的fastbin的内存地址里
fastbin特性
1.使用单链表来维护释放的堆块
从main_arena到free第一个块的地方是采用单链表形式存储的,若还有free掉的堆块,则这个堆块的fk指针域就会指向前一个堆块
再次释放一个堆块
2.采用后进先出的方式维护链表(类似于栈的结构)
当程序需要重新malloc内存并且需要从fastbin中挑选堆块时,后选择后面新加入的堆块拿来先进行内存分配
如上图,如果程序请求和上面堆块大小一样的时候,堆管理器就会直接使用fastbin里的堆块
这里的话时直接使用第二次释放的堆块,然后将这个堆块从链表里移除,接着根据堆块的fk指针找到这个堆块,此时main_arena就指向了这里,也就是恢复到了第一个图的情况
Small Bin
顾名思义这是一个small chunk,满足的内存空间比fastbin大一点。如果程序请求的内存范围不在fastbin内就会考虑smallbin。
描述:
1)在32位操作系统中,当用户释放的堆块大小大于64B,小于等于512B时使用smallbin进行管理
2)smallbin为双向循环链表,且使用先入先出算法
3)当满足smallbin条件的chunk被释放后,会优先被放入unsorted bin,只有在一定情况下才会被分配到smallbin中
4)相邻的free chunk将会被合并成一个更大的free chunk,增加内存利用率
_**Large Bin **_大于1024(0x400)字节的的chunk使用Large Bin进行管理 。Large Bin的结构相对于其他Bin是最复杂的,速度也是最慢的,相同大小的Large Bin使用fd和bk指针连接,不同大小的Large Bin通过fd_nextsize和bk_nextsize按大小排序连接。
Unsorted Bin
相当于Ptmalloc2堆管理器的垃圾桶。chunk被释放后,会先加入Unsorted Bin中,等待下次分配使用。在堆管理器的Unsroted Bin 不 为 空 时 , 用 户 申 请 非 Fast Bin 大 小的内存会先从Unsorted Bin中查找,如果找到符合该申请大小要求的chunk(等于或者大于),则直接分配或者分割该chunk。
描述:
1)释放较小或者较大的chunk的时候,为了增加分配效率,系统会先将最近释放的chunk添加到unsorted bin中
2)unsorted bin为一个双向循环链表,对chunk的大小没有限制,任何大小的chunk都可以放入unsorted bin中
3)当fastbin,smallbin中的chunk都不能满足用户请求chunk大小时,堆管理器就会考虑使用unsorted bin。他会在分配large chunk之前对堆中碎片chunk进行合并,以便减少堆中的碎片
sbrk和mmap
内存的分配与回收
内存分配的具体步骤


内存回收概述




更新: 2026-03-07 20:21:20
原文: https://www.yuque.com/idcm/wnemg9/tnxcosgeyt8267gg
