What is 堆?

明确一下堆的概念,堆不同于栈,堆是动态分配的(由操作系统内核或者堆管理器),只有在程序中需要时才会分配。在 CTF 的 pwn 程序中,栈是程序加载进内存后就会出现,而堆是由 malloc、alloc、realloc 函数分配内存后才会出现。

  • windows 和 linux 下的堆分配、管理方式都不同,这里主要讲到的是 CTF 中常出现的 linux 下的堆分配知识
    先看看堆在虚拟内存中的位置
    Pasted image 20250225175129
  • 堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的。
    实际上堆可以申请到的内存空间比栈要大很多,在 linux 的 4G 的虚拟内存空间里最高可以达到 2.9 G 的空间

    对堆操作的是由堆管理器(ptmalloc2)来实现的,而不是操作系统内核。因为程序每次申请或者释放堆时都需要进行系统调用,系统调用的开销巨大,当频繁进行堆操作时,就会严重影响程序的性能

下面的分析都是以 glibc 库下的 ptmalloc2 堆管理器来讲解的。

一、堆的基本结构

先简单的画一个图吧:
Pasted image 20250225175239

堆的结构

malloc_chunk的结构

malloc_chunk结构

每个程序分配的内存(这里指的是malloc函数)在内部被一个叫做”堆块”的所替代。一个堆块是由元数据和程序返回的内存组成的(实际上内存是malloc的返回值)。所有的这些堆块都是保存在堆上,这块内存区域在申请新的内存时会不断的扩大。同样,当一定数量的内存释放时,堆可以收缩。在glibc源码中定义的堆块如下:

/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T 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;
};

假设内存中没有堆块释放,新分配的内存区域紧随之前申请的堆块后。因此如果一个程序依次调用malloc(256),malloc(512),以及malloc(1024), 内存布局如下:
Pasted image 20250225200003
在堆块之间的”.”是虚拟的边界,实际当中他们是彼此相邻的。你可能会问,为何我要在布局当中包含一个”顶块”元数据(top chunk)。顶级块表示堆中可利用的内存,而且是唯一的可以大小可以生长的堆块。当申请新的内存时,顶块分成两个部分:第一个部分变成所申请的堆块,第二个部分变为新的顶块(因此顶块大小可以收缩)。如果顶块不能够满足申请的内存区域大小,程序就会要求操作系统扩大顶块(让堆继续生长)。被释放的chunk被记录在链表中(可能是循环双向链表,也可能是单向链表)。具体结构如下
Pasted image 20250225200210
一般情况下,物理相邻的两个空闲chunk会被合并为一个chunk。堆管理器会通过prev_size字段以及size字段合并两个物理相邻的空闲chunk块。

1. pre size字段

全称previous size

如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的*大小* (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk。前面一个堆块在使用时并且pre_size为储存前面chunk的数据时,它的值始终为0**

2. size 字段

用来指示当前堆块的大小的(头部加上(pre_size+size) +user data 的大小)
大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 SIZE_SZ 的倍数。
32 位系统中,SIZE_SZ 是 4;
64 位系统中,SIZE_SZ 是 8。
该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
但是这个字段的最后*三位
相当于三个 flag ,这三位的作用分别是:

  1. NON_MAIN_ARENA,记录当前 chunk 是否不属于主分配区(主线程),1 表示不属于(是非主分配区/非主线程),0 表示属于。
  2. IS_MAPPED,表示当前chunk是从哪个内存区域获得的虚拟内存。为1表示该chunk是从mmap映射区域分配的,否则是从heap区域分配的
  3. PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
    Pasted image 20250225180103
    这里重点讲解最后一位:用来记录前一个 chunk 块是否被分配,被分配的话这个字段的值为 1,所以经常会在已分配的堆块中的 size 字段中发现值比原来大 1 个字节。

    所以前一个堆块的释放与否都和这两个字段(pre_size、size)的值有关,这是因为便于内存的释放操作(free)

3. user data

顾名思义就是用来存放用户数据的。
使用malloc函数分配到的内存的返回值指针是指向userdata(用户数据区)
例如在 64 位程序中:

malloc(8)

申请到的堆块总大小为16 + 8 + 8 + 1 = 0x21(byte)

  1. 第一个16字节是系统最小分配的内存,也就是说你如果想要申请的内存小于系统最小分配的内存的话,就会按照最小的内存来分配。
  • 在64位系统中这个值是16个字节,在32位系统中是8个字节
  • 例如,如果代码中是malloc(0)的话,堆管理器也会分配最小内存空间给你
  1. 第二个8字节是pre size字段的大小(32位的为4字节)
  2. 第三个8字节为 size字段的大小(32位的为4字节)
  3. 最后一个1字节是PREV_INUSE的值,只有0或1两个值

整理一下:堆的基本结构包括pre_size、size、userdata
size字段包括:头部(pre size+size)加上 user data 的大小
malloc出最小大小为:系统最小分配内存+pre_size字段+size字段+prev_inuse(此处存疑)

结构图

使用中(分配后)

如图

Pasted image 20250226142821

空闲中(释放后)

如图

  1. 空闲中的chunk不存在M状态,只有A|P状态
  2. user data头部被分配出两个成员,fd和bk
    Pasted image 20250226153753
    fdbk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
  • fd 指向前一个(非物理相邻)空闲的 chunk 的起始地址,32位占4字节,64位占8字节
  • bk 指向后一个(非物理相邻)空闲的 chunk 的起始地址,32位占4字节,64位占8字节
  • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理

事实上,释放后的large block中还存在另外两个成员:fd_nextsizebk_nextsize

fd_nextsizebk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。

  • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针
  • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针

    一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

堆块大小

  • 32位程序:

    1. 用户分配到的最小堆块大小为17Bprev_size(4B) + size(4B) + fd(4B) + bk(4B) + next_chunk->p(1B)
    2. 若用户申请的大小超过最小堆块大小,会与8B进行对齐
  • 64位程序:

    1. 用户分配到的最小堆块大小为33Bprev_size(8B) + size(8B) + fd(8B) + bk(8B) + next_chunk->p(1B)
    2. 若用户申请的大小超过最小堆块大小,会与16B进行对齐

      空间复用

      描述当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 无效。所以下一个 chunk 的 prev_size 也可以被当前 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) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

二、指针与地址

首先要明确用户在调用 malloc 函数时返回的值为一个指针,指向分配到堆空间(用户数据区,这个在最前面的那个图片也已经标出来了。
有时候题目是以更复杂的情况,用指针来表示某个数据结构的,例如下面的这个图中的例子:
Pasted image 20250226190505
first chunk(second chunk)表示第一和第二个结构,每个结构中都有一个 point_heap 指针来指向存储用户数据的堆块(chunk)。
左边的这个本身就是一个堆块,用来存放一些全局信息。比如 max_size 存储了能够存储的最大结构数量;exist_num 表示已经存储的结构的数量。

IDA中常见的指针表示形式

在 IDA 伪代码中的指针形式形如下面的情况:

*(qword_6020A8 + 8)

表示取到 qword_6020A8 这个地址加 8 偏移的那个地址存储的值

汇编代码等同于:

.text:0000000000400F85                 mov     rax, cs:qword_6020A8
.text:0000000000400F8C mov rax, [rax+8]

简单转化一下,也就是:
*(addr) = [addr]

链表

在 pwn 的堆题目中,经常会有像一些”笔记管理系统”之类的题目,例如下面这里例子
Pasted image 20250226192122
代码提供了最基本的增删查改的功能。这个”笔记”的数据结构通常就是使用链表连接起来的,记录了当前 note 的大小、属性、内容等等。
例如,下面这个例子就是以指针为基础来存储这个 note 结构的。这里的 i 代表 note 的索引,若这里的 i = 0 时:
Pasted image 20250226192345
*(qword_6020A8 + 16) 就代表从 qword_6020A8 这个地址出再往后偏移 16 个字节,取到这个地址存储的值,接着把 1 赋值给这个地方(也就是把 1 存入这个地址)
同样的*(qword_6020A8 + 24) 就代表偏移 24 个字节处的值为 len
依次类推就可以在不连续的内存空间中,把整个 note 的数据结构存储下来了。

三、申请堆块的本质

堆管理器 ptmalloc2 主要是通过 malloc/free 函数来分配和释放内存块。

ptmalloc2 的作用通俗的讲就是相当于一个”中间商”,在程序想要申请向系统申请堆空间时,这里的 ptmalloc2 就会申请一块很大的空间,并根据算法从这些内存中把空间真正的分配给程序。

简单点说就是下面这个图中的情况:
Pasted image 20250226192641

简例

#include <stdlib.h> 
#include <malloc.h>
int main(){
char *p;
p = malloc(10);
return 0; }

在 gdb 中进行调试,在 call malloc 处下一个断点,在这里使用 vmmap 命令,查看内存分布。可以看到此时并没有发现堆段
Pasted image 20250226193759
单步 n ,vmmap 命令再次查看内存,发现出现了堆段
Pasted image 20250226193830**
只申请了 10 字节的大小,但是为什么这里的为什么给了这么大的堆段呢?

0x00602000     ~    0x00623000

计算一下,刚好是 132 kB

(0x00623000-0x00602000)/1024 = 132 kB

这132KB的堆空间叫做arena,此时因为是主线程分配的,所以这个区域叫做 main arena

也就是说这 132 KB 是”厂家”(内核)批发给”中间商”(ptmalloc2)的货物,以便下次程序在向系统申请小内存的时候,直接去”中间商”去取就行了,他就会在这 132KB 中按照要申请”货物”的多少进行分配下去。若”中间商”缺货了话,ptmalloc2 就继续去找”厂家”(系统内核)去取货

查看已分配的堆内存分布

在上面我们动态调试的时候已经执行了 malloc 函数,申请到的堆指针是保存在 eax 中的
Pasted image 20250226193941
我们这里使用下面这个命令来查看内存堆块情况:

x/32gx 0x602010-0x10
  • 32位的程序使用 x/32xw 比较直观一点
    这里减去 0x10 表示从堆块的头部开始观察(包含 pre size 和 size 字段)
    Pasted image 20250226194002

    四、main_arena与top chunk

    main_arena

    这个 main_arena 其实就是 ptmalloc2 堆管理器通过与操作系统内核进行交互申请到的,也就是相当于上面所说的”批发”到的一堆货物

    因为是主线程分配的,所以叫做main arena,通过增加 program break location 的方式来增加 main arena 的大小。

在 gdb 调试中,使用
使用

x/32gx &main_arena

可以看到 main_arena 的内存分配情况。

top chunk

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

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

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

五、free函数和bins

free

free 函数的使用是和 bins 的分配息息相关的。用一个简单的例子来理解一下 free 函数的实现原理。
代码如下:

#include <stdlib.h>
#include <string.h>
int main(){
char *p;
p = malloc(10);
memcpy(p,"Hello",5);
free(p);
return 0;
}

  • 程序将 “Hello” 字符串复制到申请到的堆内存空间中。
    编译后用 gdb 调试,在 call memcpy 处下一个断点,单步后将 “Hello” 复制到堆块中
    Pasted image 20250226194730
    继续使用 x/32gx 0x602010-0x10 命令查看堆块情况
    Pasted image 20250226194742
    继续单步 n,执行 free 函数之后,查看堆块情况
    Pasted image 20250226194753
    这里可以看出原本堆块中存储的内容已经被清空,然后查看一下 main_arena 的值,发现其中 +0x8 的偏移处,存储了指向已经 free 了的指针(指向头部,而不是 user data)

所以调用 free 函数以后程序做了两件事:
1.清空此堆块的 user data
2.将此堆块的指针存储到 main_arena 中了(或是 fast bin 中)

bin

bins 这个概念是与内存回收相关的,也就是堆管理器会根据用户已经申请到的内存空间大小进行释放,来决定放入哪类 bins 当作去。bins 直接翻译过来就是”垃圾桶”的意思,所以在系统在决定使用哪个 bins 时可以看作为”垃圾的分类”。
主要的 bins 分为以下几类,这里重点讲解一下 fast bin,因为 fast bin 是使用到的最多的一类,也是其中结构最为简单的。
描述:

  1. 用户free掉的内存并不是都会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中的空闲的chunk
  2. 当用户进行下一次分配请求时,ptmalloc会首先试图在空闲的chunk中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销
  3. ptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin
  4. ptmalloc一共维护了128个bin,并使用一个数组来存储这些bin
  5. 堆管理器根据特点,将堆分为四种:fastbin | unsortedbin | smallbin | largebin
  6. 数组中bin 1为unsorted bin;bin 2到63为small bin;bin 64到126为large bin
    Pasted image 20250226201008

    fast bin

    顾名思义,就是为了快速重新分配回内存而存在的一个结构。

    fastbin所包含chunk的大小为16 Bytes, 24 Bytes, 32 Bytes, … , 80 Bytes。当分配一块较小的内存(mem<=64 Bytes)时,会首先检查对应大小的fastbin中是否包含未被使用的chunk,如果存在则直接将其从fastbin中移除并返回;否则通过其他方式(剪切top chunk)得到一块符合大小要求的chunk并返回

描述:

  1. 在32位操作系统中,当用户释放的堆块大小小于64B时使用fastbin进行管理,即chunk空间最大为80字节
  2. fastbin只使用了fd成员,是个单链表结构
  3. fastbin不会对P位进行操作,也就是说它不会主动进行合并;只有在某些特定情况下,堆管理器才会对fastbin进行合并
  4. fastbinY为管理fastbin的数组,每个成员分别管理不同大小的fastbin链表,且均指向了当前链表的尾节点,当尾节点被分配时,通过其fd指针指向前一个结点
  5. 当用户申请chunk大小小于或等于MAX_FAST_SIZE时,优先从fastbins中查找相应的空闲块,且规则为LIFO(Last in, first out, 后进先出)
    Pasted image 20250226204112
    👇这里的横向排列的就是 main_arene(fast bin)的内存地址
    Pasted image 20250226204137
    假如此时 0x0804a000 处的堆块(实际堆块中的 size 字段要减去 PREV_INUSE 字段值 1,)已经被 free 了,那么他就会被存储在表示 40 bytes 的 fast bin 的内存地址里
  • 注意:这里把指针和地址区别开。地址存储的是指针,64 位的指针占 8 个字节
    假设我们现在还是以 64 位下的 malloc(10) 为例子。
    根据前面那个 free 函数的例子,查看 main_arena 地址中的指针值我们可以看出来,+0x8 偏移处才是指向 malloc(10) 的堆块的指针(这个堆块分配后的 user data 实际大小是 16 字节)
gdb-peda$ x/2gx &main_arena                           (16 bytes 的链表头)
0x7ffff7dd3760 <main_arena>: 0x0000000000000000 0x0000000000602000

所以这个 16 字节的堆块的指针会被插入属于他的这个链表队列中,也就是如下的情况
Pasted image 20250226204230
所以这也就印证了在 main_arena 中分别表示 16 Bytes, 24 Bytes, 32 Bytes, … , 80 Bytes 的内存地址中分别存储着已经 free 的而且满足这个大小的 chunk的指针。

fast bin的特性

1.使用单链表来维护释放的堆块
也就是和上图一样,从main_arena 到 free 第一个块的地方是采用单链表形式进行存储的,若还有 free 掉的堆块,则这个堆块的 fk 指针域就会指针前一个堆块。
如下图所示,此时就是一个单链表结构
Pasted image 20250226204508
2.采用后进先出的方式维护链表(类似于栈的结构)
当程序需要重新 malloc 内存并且需要从fastbin 中挑选堆块时,会选择后面新加入的堆块拿来先进行内存分配
如上图,如果程序重新请求和上面的堆块大小一样时候(malloc),堆管理器就会直接使用 fast bin 里的堆块。
这里的话也就是直接使用第二次释放的这个堆块,然后将这个堆块从链表中移除,接着根据堆块的 fk 指针找到这个堆块,此时 main_arena 就指向了这里。也就是恢复到了上面第一个图中的情况。

small bin

顾名思义,这个是一个 small chunk ,满足的内存空间比 fast bin 大一点。
如果程序请求的内存范围不在 fast bin 的范围内,就会考虑small bin。简单点说就是大于 80 Bytes 小于某一个值时,就会选择他。
描述:

  1. 在32位操作系统中,当用户释放的堆块大小大于64B,小于等于512B时使用small bin进行管理
  2. small bin 为双向循环链表,且使用 FIFO(First in, first out, 先入先出)算法
  3. 当满足small bin条件的chunk被释放后,会优先被放入unosrted bin,只有在一定情况下,才会被分配到small bin中
  4. 相邻的free chunk将会被合并成一个更大的fee chunk,增加内存利用率
    Pasted image 20250226204717

    unsorted bin

    1. 当释放较小或较大的chunk的时候,为了增加分配效率,系统会先将最近释放的chunk添加到unsorted bin中
    2. unsorted bin 为一个双向循环链表,对chunk的大小没有限制,即任何大小的chunk都可以放入unsorted bin链表中
    3. 当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,堆管理器就会考虑使用 unsorted bin 。它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。
  • unsorted bin 与 fast bin 不同,他使用双向链表对 chunk 进行连接
  • unsorted 的字面意思就是”不可回收”的意思,可以看作将不可回收的垃圾(不满足能够进行内存分配的堆块)都放到这个”垃圾桶”中。

深入理解堆的概念

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器。
堆管理器处于用户程序与内核中间,主要做以下工作:

  1. 响应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户程序。同时,为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存,然后让堆管理器通过某种算法管理这块内存。只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互
  2. 管理用户所释放的内存。一般来说,用户释放的内存并不是直接返还给操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用户新申请的内存的请求
    Linux 中早期的堆分配与回收由 Doug Lea 实现,但它在并行处理多个线程时,会共享进程的堆内存空间。因此,为了安全性,一个线程使用堆时,会进行加锁。然而,与此同时,加锁会导致其它线程无法使用堆,降低了内存分配和回收的高效性。同时,如果在多线程使用时,没能正确控制,也可能引起内存分配和回收的正确性。Wolfram Gloger 在 Doug Lea 的基础上进行改进使其可以支持多线程,这个堆分配器就是 ptmalloc 。在 glibc-2.3.x. 之后,glibc 中集成了ptmalloc2。
    目前 Linux 标准发行版中使用的堆分配器是 glibc 中的堆分配器:ptmalloc2。ptmalloc2 主要是通过 malloc/free 函数来分配和释放内存块。
    需要注意的是,在内存分配与使用的过程中,Linux有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。

堆的基本操作

这里我们主要介绍

  • 基本的堆操作,包括堆的分配,回收,堆分配背后的系统调用
  • 介绍堆目前的多线程支持。

    malloc

    在 glibc 的malloc.h中,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 函数返回对应大小字节的内存块的指针。此外,该函数还对一些异常情况进行了处理
  • 当 n=0 时,返回当前系统允许的堆的最小内存块
  • 当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

    free

    在 glibc 的 malloc.h 中,free 的说明如下
    /*
    free(void* p)
    Releases the chunk of memory pointed to by p, that had been previously
    allocated using malloc or a related routine such as realloc.
    It has no effect if p is null. It can have arbitrary (i.e., bad!)
    effects if p has already been freed.

    Unless disabled (using mallopt), freeing very large spaces will
    when possible, automatically trigger operations that give
    back unused memory to the system, thus reducing program footprint.
    */
    可以看出,free 函数会释放由 p 所指向的内存块。这个内存块有可能是通过 malloc 函数得到的,也有可能是通过相关的函数 realloc 得到的。
    此外,该函数也同样对异常情况进行了处理
  • 当 p 为空指针时,函数不执行任何操作
  • 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free
  • 除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间

内存分配背后的系统调用

在前面提到的函数中,无论是 malloc 函数还是 free 函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数。这些函数背后的系统调用主要是 (s)brk 函数以及 mmap, munmap 函数。
如下图所示,我们主要考虑对堆进行申请内存块的操作。

(s)brk

对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,我们可以通过增加 brk
brk 和 sbrk的作用:malloc的底层实现,用于分配开辟内存,但是brk是系统调用 而sbrk不是 ,sbrk调用了brk
(program break location, the program break is the address of the first location beyond the current end of the data region https://en.wikipedia.org/wiki/Sbrk)的大小来向操作系统申请内存%E7%9A%84%E5%A4%A7%E5%B0%8F%E6%9D%A5%E5%90%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%94%B3%E8%AF%B7%E5%86%85%E5%AD%98)。

关于program break :程序间断点

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

  • 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。
  • 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处。
    具体效果如下图所示

    mmap

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

在执行mmap之前

Pasted image 20250226213125

mmap后

Pasted image 20250226213144

munmap

Pasted image 20250226213155

多线程支持

Pasted image 20250226213304
Pasted image 20250226213327
Pasted image 20250226213356
Pasted image 20250226213413