什么是堆
在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序 虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。
一般用一个堆指针使用申请得到的内存读 写 释放 都通过这个指针来完成 ,使用完毕后需要把堆指针传给堆指针释放函数回收这片内存,否则会造成内存泄露
我们一般称管理堆的那部分程序为堆管理器。 因为程序每次申请或者释放堆时都需要进行系统调用,系统调用的开销巨大,当频繁进行堆操作时,就会严重影响程序的性能
堆管理器是处于用户程序与内核中间,主要做以下工作:
响应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户程序。同时,为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存,然后让堆管理器通过某种算法管理这块内存。只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互。管理用户所释放的内存。一般来说,用户释放的内存并不是直接返还给操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用户新申请的内存的请求。
需要注意的是,在内存分配与使用的过程中,Linux有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。
实际上堆可以申请到的内存空间比栈要大很多,在 linux 的 4G 的虚拟内存空间里最高可以达到 2.9 G 的空间
堆和栈的区别
堆的管理策略和数据结构
对于堆管理来说,响应程序的内存使用申请就意味着要在杂乱的堆区中辨别出哪些内存正在被使用,哪些内存时空闲的,并最终寻找到一片恰当空闲内存区域,以指针形式返回给程序,那么杂乱呢时堆区经过反复的申请 释放操作指挥,原本大片空闲内存区可能呈现出 大小不等且空闲块,占用块相间隔的凌乱状态
- 下面的分析都是以 glibc 库下的 ptmalloc2 堆管理器来讲解的。
堆的基本结构
无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同
下面这张图是一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。
当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。
chunk被free之后,其usrdata区域被复用,作为bin中的链表指针,后续我会讲解
堆块
出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识而不是传统的按字节标识,一个堆块包括两个部分:块首和块身
块首是一个堆块头部的几个字节,用来标识这个堆块自身的信息,块身是紧跟在块首后面的部分,也是最终分配给用户使用的数据区
注意:堆管理系统所返回的指针一般指向块首的起始位置,在程序中是感觉不到块首的存在的,然而连续的进行内存申请时,可能会发现返回的内存之间存在空隙 那就是块首
1.pre size 字段。只有在前面一个堆块是空闲的时候才有指,用来指示前一个堆块的大小。前面一个堆块在使用时,他的值始终为 0
2.size 字段。是用来指示当前堆块的大小的(头部加上 user data 的大小)。但是这个字段的最后三位相当于三个 flag ,有另外的作用。
1 | 这三位flag的作用分别是: |
这里重点讲解最后一位:用来记录前一个 chunk 块是否被分配,被分配的话这个字段的值为 1,所以经常会在已分配的堆块中的 size 字段中发现值比原来大 1 个字节。
所以前一个堆块的释放与否都和这两个字段(pre_size、size)的值有关,这是因为便于内存的释放操作
使用 malloc 函数分配到的内存的返回值指针是指向 user data (用户数据区),在后面的例子中也会讲到这个问题。
例如在 64 位程序中:
malloc(8)
申请到的堆块总大小为 16 + 8 + 8 + 1 = 0x21
1.第一个 16 字节是系统最小分配的内存,也就是说你如果想要申请的内存小于系统最小分配的内存的话,就会按照最小的内存来分配。
- 在 64 位系统中这个值是 16 个字节,在 32 位系统中是 8 个字节
- 例如,如果代码中是 malloc(0) 的话,堆管理器也会分配最小内存空间给你
2.第二个 8 字节是 pre size 字段的大小(32 位的为 4 字节)
3.第三个 8 字节为 size 字段的大小(32 位的为 4 字节)
4.最后一个 1 字节是 PREV_INUSE 的值,只有 0 或 1两个值
这里的举一个最简单的例子:
1 |
|
我们在ubantu终端先用touch 1.c 创建一个文件 ,然后写入,编译是gcc 1.c ,可以看到生成了a.out文件
然后放入gdb中调试,走到call malloc函数之前 用vmmap指令查看内存分布
可以看到没有发现堆段
我们n 运行
那为什么这么大呢 我们只申请了10字节的大小
56000-77000 是21000个字节的大小
1kb是1024个字节 转化一下是132kb
原来这132KB的堆空间叫做arena,此时因为是主线程分配的,所以这个区域叫做 main arena
也就是说这 132 KB 是”厂家”(内核)批发给”中间商”(ptmalloc2)的货物,以便下次程序在向系统申请小内存的时候,直接去”中间商”去取就行了,他就会在这 132KB 中按照要申请”货物”的多少进行分配下去。若”中间商”缺货了话,ptmalloc2 就继续去找”厂家”(系统内核)去取货
在上面我们动态调试的时候已经执行了 malloc 函数,申请到的堆指针是保存在 eax 中的 , 我们这里使用下面这个命令来查看内存堆块情况:
1 | x/32gx 0x602010-0x10 |
然后输入x/32gx 0x555555756260-0x10
观察
top chunk
顾名思义,是堆中第一个堆块。相当于一个”带头大哥”,程序以后分配到的内存到要放在他的后面。
在系统当前的所有 free chunk(无论那种 bin),都无法满足用户请求的内存大小的时候,将此 chunk 当做一个应急消防员,分配给用户使用。
简单点说,也就是在程序在向堆管理器申请内存时,没有合适的内存空间可以分配给他,此时就会从 top chunk 上”剪切”一部分作为 chunk 分配给他
Top chunk不属于任何bin。它的作用是满足bin中没有空闲的chunk时,用户申请内存空间的请求。
- 当top chunk的大小大于用户申请的空间时,它将拆分为两个chunk
- User chunk (大小等于用户申请的大小)
- Remainder chunk (剩余的空间),这部分chunk将成为新的top chunk
- 当top chunk无法满足用户申请的空间时,使用sbrk(main arena)或mmap(thread arena)扩展top chunk的空间。
需要注意的是,top chunk 的 prev_inuse 比特位始终为 1,否则其前面的 chunk 就会被合并到 top chunk 中。
bin
我们曾经说过,用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk。当用户再一次请求分配内存时ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配的开销。
它会根据空闲的 chunk 的大小以及使用状态将 chunk 初步分为 4 类:fast bins,small bins,large bins,unsorted bin。
每类中仍然有更细的划分,相似大小的 chunk 会用双向链表链接起来。也就是说,在每类 bin 的内部仍然会有多个互不相关的链表来保存不同大小的 chunk。
数据结构中包含如下两类:
fastbinsY: 用来索引fast bin的数组;
bins: 一个用来索引unsorted, small, large三种bins的数组。总长度为126
- 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的 chunk 比较杂。
- 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
- small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。
此外,上述这些 bin 的排布都会遵循一个原则:任意两个物理相邻的空闲 chunk 不能在一起。
需要注意的是,并不是所有的 chunk 被释放后就立即被放到 bin 中。ptmalloc 为了提高分配的速度,会把一些小的 chunk 先放到 fast bins 的容器内。而且,fastbin 容器中的 chunk 的使用标记总是被置位的,所以不满足上面的原则。
- fd和bk指针分别指向bin中在其之前和之后的chunk,fd指向先进入bin者;bk指向后来者。
- fastbin中只有fd指针,使用单向链表进行维护。
- fd_nextsize和bk_nextsize只存在与large bin中(chunk的size不大时不需要这两个变量,也可能没有他们的空间),指向前/后一个更大size的chunk。
可以发现,如果一个 chunk 处于 free 状态,那么会有两个位置记录其相应的大小
- 本身的 size 字段会记录,
- 它后面的 chunk 会记录。
一般情况下,物理相邻的两个空闲 chunk 会被合并为一个 chunk 。堆管理器会通过 prev_size 字段以及 size 字段合并两个物理相邻的空闲 chunk 块。
free函数
free 函数的使用是和 bins 的分配息息相关的。用一个简单的例子来理解一下 free 函数的实现原理。
1 |
|
运行完memcpy函数之后
free掉之后
指针进入了main_areana的fastbin中,因为申请的块比较小,我们后续会讲解,只是留一个疑问
Fast bin
对于size较小(小于max_fast)的chunk,在释放之后进行单独处理,将其放入fastbin中。
fastbin是main_arena中的一个数组,每个元素作为特定size的空闲堆块的链表头,指向被释放并加入fastbin的chunk。
1 | max_fast: |
大多数程序经常会申请以及释放一些比较小的内存块。如果将一些较小的 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 才会做接下来的一系列操作。
默认情况下(32 位系统为例), fastbin 中默认支持最大的 chunk 的数据空间大小为 64 字节。但是其可以支持的 chunk 的数据空间最大为 80 字节。除此之外, fastbin 最多可以支持的 bin 的个数为 10 个,从数据空间为 8 字节开始一直到 80 字节(注意这里说的是数据空间大小,也即除去 prev_size 和 size 字段部分的大小)
需要特别注意的是,fastbin 范围的 chunk 的 inuse 始终被置为 1。因此它们不会和其它被释放的 chunk 合并。
每个bin都是一个单链表(只使用fd指针),链表的入队出队都在表头进行,所以是LIFO。如下图所示
大小:每个bin都包含同样大小的chunk,fastbinsY[0]储存数据空间大小为0 bytes的chunk,fastbinsY[1]储存大小为8 bytes的chunk,以此类推,每次增加8个byte。
注意,chunk的数据空间是指用户可以利用来储存数据的空间,总是小于chunk的实际大小的
在malloc
初始化时,最大的fast bin大小被设置为64byte,而不是80 bytes。
不合并:两个freed chunk可以相邻。与malloc_chunk学习时的说明有所冲突,主要是fast bin更注重分配的速度。
malloc(fast chunk):用户通过malloc请求的大小属于fast chunk的大小范围
- 在初始化的时候fast bin支持的最大chunk大小和所有的bins都是空的。所以一开始即使申请的大小时fast chunk的范围内,也不会交由fast bin处理,而是交由small bin,如果small bin也为空的话,交由unsorted bin处理。
- 当fast bin不为空时,会根据用户申请的空间大小计算索引,去对应的bin中获取chunk
- 第一个索引到的chunk会在bin中被移除,并返回给用户
Free(fast chunk):用户通过free释放的大小属于fast chunk的大小范围
- fast bin根据用户释放的空间大小计算索引,释放的chunk会被加入bin中。
- free掉的chunk,如果大小在0x20~0x80之间会直接放到fastbins上去,大于0x80的会放到unsortedbin上,然后进行整理。
采取后进先出(LIFO)的原因
- 因为在单链表中一个节点的bk成员是没有使用的。当新增/取走一个freechunk时,都是通过fastbin的bin头的fd指针所操作的。
实例程序
1 |
|
程序断到sleep处 此时如上图所示,然后继续往下运行
上图是程序运行完 bin的情况,我们再看看堆结构的情况
可以看到chunk2的fd指向的chunk1 fd指向先入bin者,此时chunk3的fd没有指,所以为0
fastbin attack
核心思想
- 通过fastbins链的管理,达到目标地址(target)读写。
Fastbin Double Free
Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。
Fastbin Double Free 能够成功利用主要有两部分的原因:
- fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
- fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。
1 |
|
如果你执行这个程序,不出意外的话会得到如下的结果,这正是 _int_free 函数检测到了 fastbin 的 double free。 如果我们在 chunk1 释放后,再释放 chunk2 ,这样 main_arena 就指向 chunk2 而不是 chunk1 了,此时我们再去释放 chunk1 就不再会被检测到。
1 |
|
调他 走到第二个free的位置
然后再第三个free chunk1
注意因为 chunk1 被再次释放因此其 fd 值不再为 0 而是指向 chunk2,就是那个白的0X602000
这时如果我们可以控制 chunk1 的内容,便可以写入其 fd 指针从而实现在我们想要的任意地址分配 fastbin 块。
House Of Spirit
介绍 ¶
House of Spirit 是 the Malloc Maleficarum
中的一种技术。
该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。
要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即
- fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
- fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
- fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
- fake chunk 的 next chunk 的大小不能小于
2 * SIZE_SZ
,同时也不能大于av->system_mem
。 - fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
至于为什么要绕过这些检测,可以参考 free 部分的源码。
这里就直接以 how2heap 上的例子进行说明,如下
1 |
|
Unsorted Bin
当small chunk和large chunk被释放的时候,这些chunk并不会添加到对应的small bin或者large bin中,而是被添加到unsroted bin中。这是符合局部性原则的。
unsort bin在bins[]中仅占有一个位置,除了fastbin外的其他块被释放后都会进入到这里来作为一个缓冲,每当进行malloc时会把堆块从unsort bin中取出并放到对于的bins[]中。
数量:只有一个bin。bin是双向链表,采用FIFO的规则
small bin
chunk大小小于512byte的属于small chunk,small bins 中每个 chunk 的大小与其所在的 bin 的 index 的关系为:chunk_size = 2 * SIZE_SZ *index,具体如下
small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致。比如对于 32 位系统来说,下标 2 对应的双向链表中存储的 chunk 大小为均为 16 字节。每个链表都有链表头结点,这样可以方便对于链表内部结点的管理。此外,small bins 中每个 bin 对应的链表采用 FIFO 的规则,所以同一个链表中先被释放的 chunk 会先被分配出去。
合并:两个空闲的chunk相邻时要合并成一个空闲的chunk。
malloc(small chunk):
- 类似于fast bins,最初所有的small bin都是空的,因此在对这些small bin完成初始化之前,即使用户请求的内存大小属于small chunk也不会交由small bin进行处理,而是交由unsorted bin处理,如果unsorted bin也不能处理的话,glibc malloc就依次遍历后续的所有bins,找出第一个满足要求的bin,如果所有的bin都不满足的话,就转而使用top chunk,如果top chunk大小不够,那么就扩充top chunk,这样就一定能满足需求了
- 之后再调用时,如果small bin不为空,则从small bin中获取chunk
free(small chunk):
- 释放small chunk时,先检查物理地址上相邻的chunk,若相邻的chunk为未分配状态,则合并这些chunk。合并的时候需要先把这些chunk从对应的链上unlink,之后把合并后chunk添加到unsorted bin中
或许,大家会很疑惑,那 fastbin 与 small bin 中 chunk 的大小会有很大一部分重合啊,那 small bin 中对应大小的 bin 是不是就没有什么作用啊? 其实不然,fast bin 中的 chunk 是有可能被放到 small bin 中去的,我们在后面分析具体的源代码时会有深刻的体会。
Large Bin
chunk大小大于512byte的属于large chunk,large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内。此外,这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致,具体如下:
为了增加内存管理的效率,large bin中的链表采用降序排列。
- malloc(large chunk)
- 初始化完成之前的操作类似于small bin
- 初始化完成之后调用malloc(large chunk)时,首先确定用户请求的大小属于哪一个large bin,然后判断该large bin中最大的chunk的size是否大于用户请求的size(只需要对比链表中front end的size即可)。
- 如果最大的chunk大于用户请求的空间,就从最小的(rear end)开始遍历该large bin,找到第一个size相等或接近的chunk,分配给用户。如果该chunk大于用户请求的大小的话,就将该chunk拆分为两个chunk:前者返回给用户,且size等同于用户请求的size;多出来的部分做为一个新的chunk,即remainder chunk,添加到unsorted bin中。
- 如果bin中最大的chunk小于用户请求的空间,尝试通过binmaps寻找下一个不为空的large bin,知道找到满足要求的bin。
- free(large chunk):类似samll chunk的释放。
练习调试和熟悉堆
练习一
这个程序并不展示如何攻击,而是展示glibc的一种分配规则.glibc使用一种first-fit算法去选择一个free-chunk.如果存在一个free-chunk并且足够大的话,malloc会优先选取这个chunk.这种机制就可以在被利用于use after free(简称uaf)的情形中.先分配两个buffer,可以分配大一点,是不是fastbin也无所谓.
1 |
|
首先通过malloc分配两个内存,返回内存指针地址-0x10是chunk块的真正头部。
这两块内存相邻,header之间的距离正好是0x520字节。
会发现这是一个UAF的分配原理,a被释放之后,变成了悬垂指针,又申请了c。使a和c同时指向的同一个堆的地址空间。这时候我们就可以通过这个指针来做一些事情了。
这是申请了ab之后
两个head字段相减正好是0x757780-0x757260=0x520
这是free了a之后
A内存没有被马上回收,而是链接到了unsorted bin中.可以看到a内存的fd 和bk都指向了cdca0
查看unsorted bin
这是free过后b的内存
然后我们malloc c,重新malloc一块内存chunk_C,发现分配的内存块正是之前被释放的chunk_A内存。
虽然fd和bk指针依然存在,但是size值已经被改变了。
查看一下c
再看一下b
此时 b的presize字段是不为零 说明他认为c是空闲状态
然后运行 memcpy(c,”cccccccc”,8); 再看
此时访问unsorted bin,发现此时挂载在unsorted bin是chunk_A分割出来的一部分,因为申请的C空间小于chunk_A,就从A中分配了一部分给C,剩下的部分继续挂载在bins上。
会发现这是一个UAF的分配原理,a被释放之后,变成了悬垂指针,又申请了c。使a和c同时指向的同一个堆的地址空间。这时候我们就可以通过这个指针来做一些事情了。
练习二
1 |
|
堆溢出
堆溢出 不像栈溢出可以直接劫持程序流,它主要利用漏洞获取一个任意写的chunk,通过该chunk进行内存关键地址(GOT表、hook改写)进而达到一定目的
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。
不难发现,堆溢出漏洞发生的基本前提是
- 程序向堆上写入数据。
- 写入的数据大小没有被良好地控制。
对于攻击者来说,堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。
堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是
- 覆盖与其物理相邻的下一个 chunk的内容
- prev_size
- size,主要有三个比特位,以及该堆块真正的大小。
- NON_MAIN_ARENA
- IS_MAPPED
- PREV_INUSE
- the True chunk size
- chunk content,从而改变程序固有的执行流。
- 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。
堆溢出实战详解
1 |
|
这个程序的主要目的是调用 malloc 分配一块堆上的内存,之后向这个堆块中写入一个字符串,如果输入的字符串过长会导致溢出 chunk 的区域并覆盖到其后的 top chunk 之中 (实际上 puts 内部会调用 malloc 分配堆内存,覆盖到的可能并不是 top chunk)。
输入aaaa循环 就可以 溢出到top chunk
*calloc和 realloc *
calloc 与 malloc 的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命
1 | calloc(0x20); |
除此之外,还有一种分配是经由 realloc 进行的,realloc 函数可以身兼 malloc 和 free 两个函数的功能。
1 |
|
*寻找危险函数 *
通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。
常见的危险函数如下
- 输入
- gets,直接读取一行,忽略
'\x00'
- scanf
- vscanf
- gets,直接读取一行,忽略
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到
'\x00'
停止 - strcat,字符串拼接,遇到
'\x00'
停止 - bcopy
- strcpy,字符串复制,遇到
*确定填充长度 *
这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。 一个常见的误区是 malloc 的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)
会返回用户区域为 16 字节的块。
如果我们申请的 chunk 大小是 24 个字节。但是我们将其编译为 64 位可执行程序时,实际上分配的内存会是 16 个字节而不是 24 个。
注意用户区域的大小不等于 chunk_head.size,chunk_head.size = 用户区域大小 + 2 * 字长
还有一点是之前所说的用户申请的内存大小会被修改,其有可能会使用与其物理相邻的下一个 chunk 的 prev_size 字段储存内容。回头再来看下之前的示例代码
参考资料太多,非完全原创
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1204342476@qq.com