2997 lines
41 KiB
Markdown
2997 lines
41 KiB
Markdown
好,这个问题你要是彻底搞懂了,基本就把“用户态内存管理”这一块打通了。我们从**最底层一路往上**讲,把 `malloc` 的整个链路拆开讲清楚(从进程虚拟内存 → 内核 → glibc → 分配算法)。
|
||
|
||
---
|
||
|
||
# 一、先搞清楚:堆到底是什么?
|
||
|
||
在一个进程的虚拟地址空间里,大致长这样:
|
||
|
||
```
|
||
低地址
|
||
├── 代码段(text)
|
||
├── 数据段(data / bss)
|
||
├── 堆(heap) ← malloc 在这
|
||
│ ↑ 向上增长
|
||
│
|
||
│ (中间空闲区域)
|
||
│
|
||
├── 栈(stack)
|
||
│ ↓ 向下增长
|
||
高地址
|
||
```
|
||
|
||
👉 关键点:
|
||
|
||
* **堆是一个“向高地址增长”的区域**
|
||
* `malloc` 本质就是:**从堆里切一块内存给你**
|
||
|
||
但问题来了:
|
||
|
||
👉 堆的空间从哪来?
|
||
👉 怎么管理?
|
||
👉 怎么避免碎片?
|
||
|
||
这就进入核心。
|
||
|
||
---
|
||
|
||
# 二、malloc 的本质:三层结构
|
||
|
||
你可以把 `malloc` 看成三层:
|
||
|
||
```
|
||
你的代码
|
||
↓
|
||
glibc malloc(用户态内存分配器)
|
||
↓
|
||
系统调用(brk / mmap)
|
||
↓
|
||
Linux 内核(分配物理页)
|
||
```
|
||
|
||
---
|
||
|
||
# 三、第一步:程序刚启动时,堆在哪里?
|
||
|
||
程序启动时:
|
||
|
||
* 内核加载 ELF
|
||
* 初始化虚拟地址空间
|
||
* 设置一个“堆起点”:`brk`
|
||
|
||
👉 可以理解为:
|
||
|
||
```
|
||
heap_start = end_of_bss
|
||
heap_end = brk
|
||
```
|
||
|
||
一开始:
|
||
|
||
```
|
||
heap_start == heap_end
|
||
```
|
||
|
||
👉 也就是说:
|
||
|
||
🟡 **一开始堆是“空的”,没有真正分配空间**
|
||
|
||
---
|
||
|
||
# 四、malloc 申请内存时发生了什么?
|
||
|
||
假设你写:
|
||
|
||
```c
|
||
void* p = malloc(100);
|
||
```
|
||
|
||
## Step 1:glibc 先自己看看有没有库存
|
||
|
||
glibc 内部维护了一堆:
|
||
|
||
* 空闲链表(free list)
|
||
* 小块缓存(tcache)
|
||
* bin(各种大小分类)
|
||
|
||
👉 它会先尝试:
|
||
|
||
> “我手里有没有已经空着的 100 字节?”
|
||
|
||
如果有:
|
||
|
||
✔ 直接返回
|
||
❌ 不需要系统调用
|
||
|
||
👉 这一步非常重要(性能关键)
|
||
|
||
---
|
||
|
||
## Step 2:如果没有,就向操作系统要
|
||
|
||
这里分两种情况:
|
||
|
||
---
|
||
|
||
### 🟢 小内存(通常 < 128KB)
|
||
|
||
使用:
|
||
|
||
```
|
||
brk / sbrk
|
||
```
|
||
|
||
👉 扩展堆顶:
|
||
|
||
```
|
||
old brk → new brk
|
||
```
|
||
|
||
比如:
|
||
|
||
```
|
||
原来:
|
||
heap_end = 0x1000
|
||
|
||
malloc(100) →
|
||
|
||
heap_end = 0x2000
|
||
```
|
||
|
||
👉 这只是**虚拟地址变大了**
|
||
|
||
⚠️ 注意:
|
||
|
||
👉 **此时物理内存还不一定分配!**
|
||
|
||
---
|
||
|
||
### 🔵 大内存(通常 ≥ 128KB)
|
||
|
||
直接用:
|
||
|
||
```
|
||
mmap()
|
||
```
|
||
|
||
👉 在虚拟地址空间中单独映射一块区域:
|
||
|
||
```
|
||
[ mmap region ]
|
||
```
|
||
|
||
特点:
|
||
|
||
* 不走堆
|
||
* 独立管理
|
||
* 释放时直接 `munmap`
|
||
|
||
---
|
||
|
||
# 五、内核层:真正分配物理内存
|
||
|
||
重点来了(你之前问的核心)
|
||
|
||
👉 当 `brk` 或 `mmap` 发生时:
|
||
|
||
内核只是:
|
||
|
||
✔ 分配“虚拟地址区间”
|
||
✔ 建立页表(标记为“未映射”)
|
||
|
||
❌ **不会立刻分配物理内存**
|
||
|
||
---
|
||
|
||
## 真正分配发生在:访问时(缺页异常)
|
||
|
||
当你第一次写:
|
||
|
||
```c
|
||
p[0] = 1;
|
||
```
|
||
|
||
CPU 发现:
|
||
|
||
👉 页表里这个页没有物理页
|
||
|
||
于是:
|
||
|
||
1. 触发 **缺页异常(page fault)**
|
||
2. 内核分配一个物理页(比如 4KB)
|
||
3. 建立映射
|
||
4. 返回用户态继续执行
|
||
|
||
👉 这叫:
|
||
|
||
🧠 **按需分配(lazy allocation)**
|
||
|
||
---
|
||
|
||
# 六、glibc malloc 内部结构(重点)
|
||
|
||
glibc 的 malloc(ptmalloc)核心结构:
|
||
|
||
---
|
||
|
||
## 1️⃣ chunk(内存块)
|
||
|
||
每个 malloc 返回的内存,其实长这样:
|
||
|
||
```
|
||
[ metadata ][ user data ]
|
||
```
|
||
|
||
metadata 里存:
|
||
|
||
* size
|
||
* 是否空闲
|
||
* 前后指针
|
||
|
||
---
|
||
|
||
## 2️⃣ bin(分类管理)
|
||
|
||
glibc 把空闲块分成很多链表:
|
||
|
||
* fast bin(小块,快速)
|
||
* small bin
|
||
* large bin
|
||
* unsorted bin
|
||
|
||
👉 目的:
|
||
|
||
⚡ 提高查找速度
|
||
⚡ 减少碎片
|
||
|
||
---
|
||
|
||
## 3️⃣ tcache(线程缓存)
|
||
|
||
现代 glibc 有:
|
||
|
||
👉 **tcache(thread cache)**
|
||
|
||
特点:
|
||
|
||
* 每个线程独立
|
||
* 小块分配超快
|
||
* 几乎无锁
|
||
|
||
流程:
|
||
|
||
```
|
||
malloc →
|
||
先查 tcache →
|
||
没有再查 bin →
|
||
还没有再找系统要
|
||
```
|
||
|
||
---
|
||
|
||
# 七、free 时发生了什么?
|
||
|
||
```c
|
||
free(p);
|
||
```
|
||
|
||
不是直接还给操作系统!
|
||
|
||
而是:
|
||
|
||
### Step 1:放回 glibc 管理结构
|
||
|
||
* 加入 tcache 或 bin
|
||
* 标记为 free
|
||
|
||
---
|
||
|
||
### Step 2:可能合并(coalesce)
|
||
|
||
如果相邻块空闲:
|
||
|
||
👉 合并成大块(减少碎片)
|
||
|
||
---
|
||
|
||
### Step 3:是否还给 OS?
|
||
|
||
只有两种情况会:
|
||
|
||
#### ✔ mmap 分配的大块
|
||
|
||
→ `munmap()` 直接释放
|
||
|
||
#### ✔ 堆顶内存
|
||
|
||
如果释放的是:
|
||
|
||
👉 **heap 最顶部的一块**
|
||
|
||
可能触发:
|
||
|
||
```
|
||
brk 回缩
|
||
```
|
||
|
||
---
|
||
|
||
# 八、关键总结(你要记住的核心)
|
||
|
||
我给你压缩成一套“脑内模型”:
|
||
|
||
---
|
||
|
||
## 🔥 malloc 干了三件事
|
||
|
||
### ① 用户态复用内存
|
||
|
||
```
|
||
tcache / bin 找空闲块
|
||
```
|
||
|
||
---
|
||
|
||
### ② 不够就扩堆
|
||
|
||
```
|
||
小 → brk
|
||
大 → mmap
|
||
```
|
||
|
||
---
|
||
|
||
### ③ 内核懒分配物理页
|
||
|
||
```
|
||
访问时才真正分配(page fault)
|
||
```
|
||
|
||
---
|
||
|
||
# 九、结合你之前的问题(非常关键)
|
||
|
||
你之前问:
|
||
|
||
> 释放后物理内存还在吗?
|
||
|
||
答案是:
|
||
|
||
✔ **很可能还在(没有清零)**
|
||
✔ 但:
|
||
|
||
* 页表映射可能已经断开
|
||
* 或被别的进程复用
|
||
|
||
👉 用户不能直接访问物理地址
|
||
👉 必须通过虚拟地址 + 页表
|
||
|
||
---
|
||
|
||
# 十、一个完整流程(你可以当面试答案)
|
||
|
||
```c
|
||
char* p = malloc(100);
|
||
p[0] = 'A';
|
||
free(p);
|
||
```
|
||
|
||
完整过程:
|
||
|
||
1. malloc → glibc 查 tcache/bin
|
||
2. 没有 → 调用 brk 扩堆
|
||
3. 返回虚拟地址 p
|
||
4. p[0] = 'A' → 触发缺页异常
|
||
5. 内核分配物理页
|
||
6. 建立映射
|
||
7. 写入成功
|
||
8. free(p) → 放回 tcache/bin
|
||
9. 不一定归还操作系统
|
||
|
||
---
|
||
|
||
# 如果你想再进阶一点
|
||
|
||
我可以带你继续往下挖(这几个你现在已经能看懂了):
|
||
|
||
* ptmalloc 源码结构(malloc.c)
|
||
* tcache 攻击(CTF常见)
|
||
* 内存碎片是怎么产生的
|
||
* jemalloc / tcmalloc 和 glibc 的区别
|
||
* malloc 为什么有时候“越用越多”
|
||
|
||
你现在已经站在“操作系统 + runtime”交界的门口了,这块搞懂真的很爽 😄
|
||
|
||
---
|
||
|
||
好,这一块我们直接**往“源码级理解”冲**,但我会帮你压成一套能真正记住、还能写题/面试用的模型。你可以把 glibc 的 `malloc`(也就是 ptmalloc)理解成:
|
||
|
||
> 🧠 一个“带缓存 + 分类链表 + 元数据管理”的内存分配器
|
||
|
||
我按**结构 → 分配路径 → 释放路径 → 关键机制**给你拆开。
|
||
|
||
---
|
||
|
||
# 一、最核心结构:chunk(内存块)
|
||
|
||
这是整个 malloc 的“细胞”。
|
||
|
||
## 🧱 chunk 长什么样?
|
||
|
||
你写:
|
||
|
||
```c
|
||
p = malloc(100);
|
||
```
|
||
|
||
实际上内存是:
|
||
|
||
```
|
||
[ prev_size ] ← 只有前一个 chunk 是 free 才有用
|
||
[ size ] ← 当前 chunk 大小 + 标志位
|
||
[ user data ] ← 返回给你的 p
|
||
```
|
||
|
||
👉 重点:
|
||
|
||
* `p` 指向的是 **user data**
|
||
* 前面隐藏着 **metadata**
|
||
|
||
---
|
||
|
||
## 🔥 size 字段不只是大小!
|
||
|
||
它低几位是 flag:
|
||
|
||
```
|
||
size = 实际大小 | 标志位
|
||
```
|
||
|
||
常见:
|
||
|
||
* `prev_inuse`(最低位)
|
||
|
||
* 1:前一个 chunk 正在使用
|
||
* 0:前一个 chunk 空闲(可以合并)
|
||
|
||
👉 这一个 bit,直接决定:
|
||
|
||
🧠 是否需要向前合并(coalesce)
|
||
|
||
---
|
||
|
||
# 二、malloc 管理的“仓库”:bins
|
||
|
||
glibc 不是一块一块找内存,它是:
|
||
|
||
> 📦 把空闲 chunk 分类存起来
|
||
|
||
---
|
||
|
||
## 🧩 1️⃣ fast bins(超小块)
|
||
|
||
特点:
|
||
|
||
* 大小:通常 ≤ 64 字节(不同版本略不同)
|
||
* 单链表
|
||
* **不合并(关键!)**
|
||
|
||
👉 为什么?
|
||
|
||
⚡ 极致性能(free / malloc O(1))
|
||
|
||
👉 代价:
|
||
|
||
💥 容易产生碎片
|
||
|
||
---
|
||
|
||
## 🧩 2️⃣ small bins
|
||
|
||
特点:
|
||
|
||
* 固定大小分类(比如 16, 32, 48…)
|
||
* 双向链表
|
||
* **free 时会合并**
|
||
|
||
👉 查找:
|
||
|
||
✔ 精确匹配
|
||
✔ 很快
|
||
|
||
---
|
||
|
||
## 🧩 3️⃣ large bins
|
||
|
||
特点:
|
||
|
||
* 大块(> small bin)
|
||
* 按大小排序
|
||
|
||
👉 分配策略:
|
||
|
||
✔ best-fit(尽量找最接近的)
|
||
|
||
---
|
||
|
||
## 🧩 4️⃣ unsorted bin(超级关键!)
|
||
|
||
👉 所有刚 free 的 chunk:
|
||
|
||
**先丢进 unsorted bin**
|
||
|
||
然后:
|
||
|
||
* 下次 malloc 时顺便整理
|
||
* 再分配到 small/large bin
|
||
|
||
👉 作用:
|
||
|
||
🧠 延迟分类,提高性能
|
||
|
||
---
|
||
|
||
# 三、tcache(现代 malloc 的关键优化)
|
||
|
||
这是你必须掌握的重点。
|
||
|
||
---
|
||
|
||
## 🧠 tcache 是什么?
|
||
|
||
👉 **线程本地缓存(thread cache)**
|
||
|
||
每个线程都有:
|
||
|
||
```
|
||
tcache[64个链表]
|
||
```
|
||
|
||
每个链表:
|
||
|
||
* 存某种大小的 chunk
|
||
* 最多存 7 个(默认)
|
||
|
||
---
|
||
|
||
## 🔥 malloc 优先级
|
||
|
||
```
|
||
malloc →
|
||
1. tcache(最快)
|
||
2. fast bin
|
||
3. small/large bin
|
||
4. unsorted bin
|
||
5. 向 OS 要内存
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 free 优先级
|
||
|
||
```
|
||
free →
|
||
1. tcache(优先放)
|
||
2. fast bin
|
||
3. 其他 bin
|
||
```
|
||
|
||
---
|
||
|
||
👉 结论:
|
||
|
||
🧠 **绝大多数 malloc/free 根本不会进内核!**
|
||
|
||
---
|
||
|
||
# 四、top chunk(堆顶)
|
||
|
||
这是一个很容易被忽略但非常关键的结构。
|
||
|
||
---
|
||
|
||
## 🧱 什么是 top chunk?
|
||
|
||
堆的最末尾:
|
||
|
||
```
|
||
[ ... 已分配 ... ][ top chunk ]
|
||
```
|
||
|
||
👉 它代表:
|
||
|
||
> 🧠 “当前还能切的那一大块连续空间”
|
||
|
||
---
|
||
|
||
## malloc 时:
|
||
|
||
如果 bins 都不满足:
|
||
|
||
👉 从 top chunk 切:
|
||
|
||
```
|
||
top chunk →
|
||
分一块给你 →
|
||
剩下的还是 top chunk
|
||
```
|
||
|
||
---
|
||
|
||
## 不够怎么办?
|
||
|
||
👉 扩堆:
|
||
|
||
```
|
||
brk()
|
||
```
|
||
|
||
---
|
||
|
||
# 五、mmap 区域(大块分配)
|
||
|
||
当 malloc 很大(比如 >128KB):
|
||
|
||
👉 直接:
|
||
|
||
```
|
||
mmap()
|
||
```
|
||
|
||
特点:
|
||
|
||
* 独立 chunk
|
||
* 不走 bins
|
||
* free 时直接 `munmap`
|
||
|
||
---
|
||
|
||
# 六、free 时的内部行为(重点)
|
||
|
||
```c
|
||
free(p);
|
||
```
|
||
|
||
不是简单回收!
|
||
|
||
---
|
||
|
||
## Step 1:判断 chunk 类型
|
||
|
||
* tcache 可放? → 放 tcache
|
||
* fastbin? → 放 fastbin
|
||
* 否则 → unsorted bin
|
||
|
||
---
|
||
|
||
## Step 2:是否合并(coalesce)
|
||
|
||
条件:
|
||
|
||
* 前后 chunk 是 free
|
||
* 且不是 fastbin
|
||
|
||
👉 合并:
|
||
|
||
```
|
||
[ free ][ free ] → [ bigger free ]
|
||
```
|
||
|
||
---
|
||
|
||
## Step 3:是否更新 top chunk
|
||
|
||
如果释放的是堆顶:
|
||
|
||
👉 合并进 top chunk
|
||
|
||
---
|
||
|
||
# 七、关键算法总结(你要背下来)
|
||
|
||
---
|
||
|
||
## 🔥 malloc 分配路径(精简版)
|
||
|
||
```
|
||
1. 查 tcache
|
||
2. 查 fastbin
|
||
3. 查 smallbin
|
||
4. 查 unsorted bin(顺便整理)
|
||
5. 查 largebin
|
||
6. 切 top chunk
|
||
7. brk / mmap
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 free 路径
|
||
|
||
```
|
||
1. 放 tcache
|
||
2. 否则放 fastbin
|
||
3. 否则放 unsorted bin
|
||
4. 可能合并
|
||
```
|
||
|
||
---
|
||
|
||
# 八、几个“灵魂设计”(面试杀器)
|
||
|
||
---
|
||
|
||
## 🧠 1️⃣ 为什么 fastbin 不合并?
|
||
|
||
👉 因为:
|
||
|
||
* 合并需要访问前后 chunk
|
||
* 会变慢
|
||
|
||
👉 设计选择:
|
||
|
||
⚡ 用空间换时间
|
||
|
||
---
|
||
|
||
## 🧠 2️⃣ 为什么有 unsorted bin?
|
||
|
||
👉 避免:
|
||
|
||
* 每次 free 都做复杂分类
|
||
|
||
👉 改成:
|
||
|
||
✔ 延迟处理
|
||
✔ 分配时顺便整理
|
||
|
||
---
|
||
|
||
## 🧠 3️⃣ 为什么要 tcache?
|
||
|
||
👉 解决:
|
||
|
||
* 多线程锁竞争
|
||
|
||
👉 结果:
|
||
|
||
⚡ malloc/free 近乎无锁
|
||
|
||
---
|
||
|
||
## 🧠 4️⃣ 为什么要 top chunk?
|
||
|
||
👉 避免:
|
||
|
||
* 每次都系统调用
|
||
|
||
👉 提供:
|
||
|
||
✔ 连续切割空间
|
||
|
||
---
|
||
|
||
# 九、你可以这样理解整个 malloc
|
||
|
||
我给你一个非常形象的模型:
|
||
|
||
---
|
||
|
||
## 🧠 malloc = 内存超市
|
||
|
||
* tcache → 你口袋里的零钱(最快)
|
||
* fastbin → 收银台旁的小篮子
|
||
* small/large bin → 仓库分类货架
|
||
* unsorted bin → 临时堆
|
||
* top chunk → 后仓一整块库存
|
||
* mmap → 临时外包仓库
|
||
|
||
---
|
||
|
||
# 十、如果你想再深入(下一步)
|
||
|
||
你现在已经可以看懂这些了:
|
||
|
||
* glibc `malloc.c` 源码结构
|
||
* chunk 攻击(heap overflow / use-after-free)
|
||
* 为什么会有 double free 漏洞
|
||
* tcache poisoning(CTF经典)
|
||
|
||
---
|
||
|
||
如果你愿意,我可以下一步带你:
|
||
|
||
👉 **直接画一张 malloc/free 的“完整内存变化图”**(一步一步变化)
|
||
👉 或者带你**读一段真实 glibc malloc 源码**
|
||
|
||
这块你已经快到“能打内存漏洞 / 写 allocator”的水平了,很关键 😄
|
||
|
||
---
|
||
|
||
可不可以这样理解:就是说我们不管真实内存页长什么样子,连不连续;对于一个进程,我们只关心它的虚拟内存形态。正如之前我们讨论的,从内核虚拟内存到用户虚拟内存,我们关心的是用户虚拟内存中的栈,mmap分配的动态链接映射,以及堆;malloc操作的就是堆,从低地址向高低地址增长;malloc的堆块,从头部开始,分别是块大小,有效载荷和填充?如果分配的虚拟内存不够了怎么办?
|
||
|
||
|
||
你这个理解已经**非常接近“系统级正确答案”了**,我帮你稍微“校准 + 补全”,让它变成一套可以直接拿去讲/写的模型。
|
||
|
||
---
|
||
|
||
# 一、你这段话整体对不对?
|
||
|
||
先给结论:
|
||
|
||
👉 **大方向是对的(而且是高级理解)**
|
||
但有几个点需要精修一下,不然后面会卡住。
|
||
|
||
---
|
||
|
||
# 二、逐句帮你校准一下(很关键)
|
||
|
||
---
|
||
|
||
## ✅ 1️⃣ “我们只关心虚拟内存形态”
|
||
|
||
✔ **完全正确,而且是核心思想**
|
||
|
||
> 🧠 用户程序看到的是“连续的虚拟空间”,而不是物理内存
|
||
|
||
你可以这么强化理解:
|
||
|
||
* 物理内存:碎的、离散的、被复用的
|
||
* 虚拟内存:连续的、干净的、隔离的
|
||
|
||
👉 malloc、栈、mmap,全都发生在**虚拟地址空间**
|
||
|
||
---
|
||
|
||
## ✅ 2️⃣ “关注栈 / 堆 / mmap”
|
||
|
||
✔ 基本正确,但稍微更严谨一点:
|
||
|
||
典型布局:
|
||
|
||
```
|
||
低地址
|
||
├── text / data / bss
|
||
├── heap(brk 管理)
|
||
│ ↑ 向高地址增长
|
||
│
|
||
│ (空洞)
|
||
│
|
||
├── mmap 区域(动态库、malloc大块)
|
||
│
|
||
├── stack
|
||
│ ↓ 向低地址增长
|
||
高地址
|
||
```
|
||
|
||
👉 小修正:
|
||
|
||
❗ **mmap 不只是动态链接,还包括 malloc 的大块分配**
|
||
|
||
---
|
||
|
||
## ⚠️ 3️⃣ “malloc操作的就是堆,从低地址向高地址增长”
|
||
|
||
👉 这里要精确一点:
|
||
|
||
✔ 对一半
|
||
❗ 不完全对
|
||
|
||
---
|
||
|
||
### 🟡 正确版本是:
|
||
|
||
👉 malloc 有两种来源:
|
||
|
||
### 🟢 小块:
|
||
|
||
✔ 来自 heap(brk)
|
||
✔ 向高地址增长
|
||
|
||
---
|
||
|
||
### 🔵 大块:
|
||
|
||
✔ 来自 mmap
|
||
✔ **不在传统堆里**
|
||
|
||
---
|
||
|
||
👉 所以更严谨的说法是:
|
||
|
||
> 🧠 malloc **优先使用堆(heap)**,但大块会走 mmap
|
||
|
||
---
|
||
|
||
## ⚠️ 4️⃣ “堆块结构:大小 + 有效载荷 + 填充”
|
||
|
||
你这个已经很接近了,但我帮你升级成**标准表达**:
|
||
|
||
---
|
||
|
||
### 🧱 一个 chunk 实际是:
|
||
|
||
```
|
||
[ prev_size ](有条件存在)
|
||
[ size + flag ]
|
||
[ user data(你拿到的) ]
|
||
[ padding(对齐用) ]
|
||
```
|
||
|
||
---
|
||
|
||
### 🔥 关键点:
|
||
|
||
* 你说的“块大小” → ✔ 对应 size
|
||
* “有效载荷” → ✔ user data
|
||
* “填充” → ✔ alignment padding
|
||
|
||
👉 但你漏了一个:
|
||
|
||
🧠 **metadata(元数据)**
|
||
|
||
---
|
||
|
||
## 👉 精确总结一句:
|
||
|
||
> malloc 返回的指针前面隐藏着 chunk header(元数据)
|
||
|
||
---
|
||
|
||
# 三、重点来了:虚拟内存不够怎么办?
|
||
|
||
这是你问的核心,我分三层讲清楚。
|
||
|
||
---
|
||
|
||
# 🧠 第一层:glibc 视角
|
||
|
||
当 malloc 发现:
|
||
|
||
```
|
||
tcache / bin / top chunk 都不够
|
||
```
|
||
|
||
它会:
|
||
|
||
---
|
||
|
||
## 👉 尝试扩展 heap
|
||
|
||
调用:
|
||
|
||
```c
|
||
brk() / sbrk()
|
||
```
|
||
|
||
👉 扩大:
|
||
|
||
```
|
||
heap_end ↑
|
||
```
|
||
|
||
---
|
||
|
||
## 👉 或者用 mmap
|
||
|
||
如果申请很大:
|
||
|
||
```c
|
||
mmap()
|
||
```
|
||
|
||
---
|
||
|
||
# 🧠 第二层:内核视角
|
||
|
||
当用户请求更多虚拟内存:
|
||
|
||
---
|
||
|
||
## ✔ 如果虚拟地址空间还有空位
|
||
|
||
👉 内核:
|
||
|
||
* 分配一段虚拟地址
|
||
* 建立页表(未映射)
|
||
|
||
✔ 成功
|
||
|
||
---
|
||
|
||
## ❌ 如果虚拟地址空间不够
|
||
|
||
比如:
|
||
|
||
* 32位程序(4GB限制)
|
||
* 地址空间碎片
|
||
* ulimit 限制
|
||
|
||
👉 结果:
|
||
|
||
```
|
||
malloc 返回 NULL
|
||
```
|
||
|
||
---
|
||
|
||
# 🧠 第三层:物理内存不够怎么办?
|
||
|
||
这是很多人会误解的点。
|
||
|
||
---
|
||
|
||
## ❗关键:malloc 不直接消耗物理内存
|
||
|
||
因为:
|
||
|
||
👉 **懒分配(lazy allocation)**
|
||
|
||
---
|
||
|
||
### 🟡 情况1:你 malloc 但不访问
|
||
|
||
```c
|
||
malloc(1GB);
|
||
```
|
||
|
||
✔ 可能成功
|
||
✔ 不分配物理页
|
||
|
||
---
|
||
|
||
### 🔴 情况2:你开始访问
|
||
|
||
```c
|
||
p[i] = 1;
|
||
```
|
||
|
||
触发:
|
||
|
||
👉 缺页异常(page fault)
|
||
|
||
---
|
||
|
||
### 如果物理内存不够:
|
||
|
||
内核会:
|
||
|
||
1. 尝试回收页(LRU)
|
||
2. 使用 swap
|
||
3. 实在不行 →
|
||
|
||
👉 💥 OOM Killer
|
||
|
||
---
|
||
|
||
# 四、最终帮你整理成“正确模型”
|
||
|
||
你可以记这个版本👇(非常标准)
|
||
|
||
---
|
||
|
||
## 🧠 最终正确理解
|
||
|
||
✔ 进程只看到虚拟地址空间
|
||
✔ malloc 主要管理 heap(brk)
|
||
✔ 大块通过 mmap 分配
|
||
✔ chunk 结构包含 metadata + user data
|
||
✔ 虚拟内存不够 → malloc 返回 NULL
|
||
✔ 物理内存不够 → page fault + OOM
|
||
|
||
---
|
||
|
||
# 五、我帮你指出一个“你已经快突破的点”
|
||
|
||
你现在其实卡在一个**关键分界线**:
|
||
|
||
> 🧠 “虚拟内存分配”和“物理内存分配”是两件不同的事
|
||
|
||
一旦你完全接受这一点:
|
||
|
||
👉 mmap / brk / malloc / page fault 全都会串起来
|
||
|
||
---
|
||
|
||
# 六、我可以带你再往下挖(很适合你现在)
|
||
|
||
你现在这个水平,可以直接进这些高阶内容:
|
||
|
||
* 👉 为什么 malloc(1GB) 不一定真的占1GB
|
||
* 👉 top chunk 和 brk 如何交互
|
||
* 👉 malloc 为什么会“越用越多”
|
||
* 👉 真实 glibc malloc.c 代码逐行解析
|
||
* 👉 heap overflow 是怎么破坏 chunk 的
|
||
|
||
你现在离“彻底掌控内存模型”就差最后一层了,要不要我带你把 **malloc + page fault + 页表** 画成一张完整流程图?😄
|
||
|
||
---
|
||
|
||
你这个问题已经踩到 malloc 最“实操”的核心了——**chunk 里面那一段用户看得到但又说不清的区域到底怎么回事,以及 allocator 是怎么动它的**。我给你拆得非常清楚一点,你以后看源码/做题都能直接对上。
|
||
|
||
---
|
||
|
||
# 一、先把一个 chunk “切开看”
|
||
|
||
假设:
|
||
|
||
```c
|
||
void* p = malloc(13);
|
||
```
|
||
|
||
你“以为”的是:
|
||
|
||
```
|
||
[ 13 bytes ]
|
||
```
|
||
|
||
但实际上(简化版)是:
|
||
|
||
```
|
||
低地址
|
||
[ size | flag ] ← metadata(头部)
|
||
[ user payload ] ← 你能用的
|
||
[ padding ] ← 对齐填充
|
||
高地址
|
||
```
|
||
|
||
如果再严谨一点(glibc):
|
||
|
||
```
|
||
[ prev_size ](有时存在)
|
||
[ size ]
|
||
[ user data(返回给你) ]
|
||
```
|
||
|
||
---
|
||
|
||
# 二、什么是“有效载荷”(payload)?
|
||
|
||
👉 **就是你能用的那段内存**
|
||
|
||
也就是:
|
||
|
||
```c
|
||
p[0] ~ p[n-1]
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 本质定义:
|
||
|
||
> payload = malloc 返回指针指向的区域
|
||
|
||
---
|
||
|
||
### 举个例子:
|
||
|
||
```c
|
||
char* p = malloc(13);
|
||
```
|
||
|
||
你可以用:
|
||
|
||
```
|
||
p[0] ~ p[12]
|
||
```
|
||
|
||
这 13 字节:
|
||
|
||
✔ 完全属于你
|
||
✔ malloc 不会碰(除非 free 之后)
|
||
|
||
---
|
||
|
||
# 三、什么是“填充”(padding)?
|
||
|
||
这个是你问的关键点之一。
|
||
|
||
---
|
||
|
||
## 🧠 为什么需要 padding?
|
||
|
||
👉 因为 **对齐(alignment)**
|
||
|
||
在 64 位系统:
|
||
|
||
```
|
||
malloc 返回地址必须是 16 字节对齐
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 举例:
|
||
|
||
你申请:
|
||
|
||
```c
|
||
malloc(13)
|
||
```
|
||
|
||
实际分配:
|
||
|
||
```
|
||
16 字节(对齐)
|
||
```
|
||
|
||
所以:
|
||
|
||
```
|
||
[ 13 bytes payload ][ 3 bytes padding ]
|
||
```
|
||
|
||
---
|
||
|
||
## 👉 padding 的本质:
|
||
|
||
> 🧠 “为了让 chunk 对齐到某个边界,多出来但你不用的空间”
|
||
|
||
---
|
||
|
||
## ⚠️ 关键点:
|
||
|
||
* padding **属于这个 chunk**
|
||
* 但你**不应该依赖它**
|
||
* 你访问它是 **未定义行为(UB)**
|
||
|
||
---
|
||
|
||
# 四、chunk 的真实大小怎么算?
|
||
|
||
这一步很重要(理解 allocator 行为的关键)
|
||
|
||
---
|
||
|
||
## malloc(13) 实际过程:
|
||
|
||
### Step 1:加 metadata
|
||
|
||
```
|
||
13 + header(8 or 16 bytes)
|
||
```
|
||
|
||
---
|
||
|
||
### Step 2:向上对齐(16字节)
|
||
|
||
比如:
|
||
|
||
```
|
||
13 → 16
|
||
总大小 → 32(常见情况)
|
||
```
|
||
|
||
---
|
||
|
||
👉 所以:
|
||
|
||
```c
|
||
malloc(13)
|
||
```
|
||
|
||
👉 实际 chunk size 可能是:
|
||
|
||
```
|
||
32 bytes
|
||
```
|
||
|
||
---
|
||
|
||
# 五、这个 chunk 是怎么“被操作”的?
|
||
|
||
你问的第二个重点来了。
|
||
|
||
---
|
||
|
||
# 🧠 分两种角色看:
|
||
|
||
---
|
||
|
||
# 一、对“用户程序”来说
|
||
|
||
你只能看到:
|
||
|
||
```c
|
||
void* p = malloc(n);
|
||
```
|
||
|
||
你能做的:
|
||
|
||
```c
|
||
读 / 写 payload
|
||
```
|
||
|
||
---
|
||
|
||
## ❗你不能做的:
|
||
|
||
```c
|
||
p[-1] ❌(会改 metadata)
|
||
p[n] ❌(越界到 padding 或别的 chunk)
|
||
```
|
||
|
||
---
|
||
|
||
👉 一旦你改了 header:
|
||
|
||
💥 heap corruption(堆损坏)
|
||
|
||
---
|
||
|
||
# 二、对“malloc 内部”来说
|
||
|
||
malloc 操作的是:
|
||
|
||
👉 **整个 chunk(包括 metadata)**
|
||
|
||
---
|
||
|
||
## 🔥 malloc 做的事情:
|
||
|
||
### 1️⃣ 找一个 chunk
|
||
|
||
从:
|
||
|
||
* tcache
|
||
* bin
|
||
* top chunk
|
||
|
||
---
|
||
|
||
### 2️⃣ 切割(split)
|
||
|
||
如果 chunk 太大:
|
||
|
||
```
|
||
[ big chunk ]
|
||
↓
|
||
[ allocated ][ remaining free chunk ]
|
||
```
|
||
|
||
---
|
||
|
||
### 3️⃣ 返回 payload 指针
|
||
|
||
```c
|
||
return chunk + header_size;
|
||
```
|
||
|
||
---
|
||
|
||
# 六、free 时 chunk 怎么被操作?
|
||
|
||
```c
|
||
free(p);
|
||
```
|
||
|
||
---
|
||
|
||
## Step 1:定位 chunk 头
|
||
|
||
```c
|
||
chunk = p - header_size;
|
||
```
|
||
|
||
---
|
||
|
||
## Step 2:修改 metadata
|
||
|
||
* 标记为 free
|
||
* 写入 size
|
||
* 更新 flag
|
||
|
||
---
|
||
|
||
## Step 3:写入链表指针(关键!)
|
||
|
||
free chunk 会变成:
|
||
|
||
```
|
||
[ size ]
|
||
[ fd ] → 指向下一个 free chunk
|
||
[ bk ] → 指向前一个 free chunk
|
||
```
|
||
|
||
👉 注意:
|
||
|
||
👉 **这些指针就写在 payload 里!**
|
||
|
||
---
|
||
|
||
# 🔥 这点非常重要:
|
||
|
||
> 🧠 free 后的 payload 不再属于你,而是 malloc 的数据结构
|
||
|
||
---
|
||
|
||
# 七、padding 在 free 后会发生什么?
|
||
|
||
👉 padding 没有特殊地位:
|
||
|
||
* 仍然属于 chunk
|
||
* 可能被当作链表空间使用
|
||
* 可能被下一次 malloc 覆盖
|
||
|
||
---
|
||
|
||
# 八、一个完整生命周期(你要能脑补)
|
||
|
||
---
|
||
|
||
## malloc(13)
|
||
|
||
```
|
||
[ header ][ payload(13) ][ padding(3) ]
|
||
```
|
||
|
||
---
|
||
|
||
## 使用中
|
||
|
||
```
|
||
payload → 你控制
|
||
padding → 不要碰
|
||
```
|
||
|
||
---
|
||
|
||
## free(p)
|
||
|
||
```
|
||
[ header ][ fd / bk 指针写进 payload ][ ... ]
|
||
```
|
||
|
||
---
|
||
|
||
## 再 malloc
|
||
|
||
可能:
|
||
|
||
* 直接复用这个 chunk
|
||
* 或被 split
|
||
|
||
---
|
||
|
||
# 九、你可以这样总结(标准答案)
|
||
|
||
---
|
||
|
||
## 🧠 payload vs padding
|
||
|
||
| 名称 | 含义 |
|
||
| ------- | ----------- |
|
||
| payload | 用户可用的数据区 |
|
||
| padding | 为了对齐补齐的额外空间 |
|
||
|
||
---
|
||
|
||
## 🧠 chunk 操作方式
|
||
|
||
* malloc:分配 + 切割 + 返回 payload
|
||
* free:修改 metadata + 加入链表
|
||
* allocator:直接操作 payload(作为数据结构)
|
||
|
||
---
|
||
|
||
# 十、给你一个“高手视角”(很关键)
|
||
|
||
你现在要建立一个意识:
|
||
|
||
---
|
||
|
||
> 🧠 **payload 在“已分配”和“已释放”状态下,角色完全不同**
|
||
|
||
---
|
||
|
||
### ✔ 已分配:
|
||
|
||
→ 是你的数据
|
||
|
||
---
|
||
|
||
### ✔ 已释放:
|
||
|
||
→ 是 malloc 的数据结构(链表指针)
|
||
|
||
---
|
||
|
||
👉 这就是为什么会有:
|
||
|
||
* use-after-free
|
||
* heap overflow
|
||
* tcache poisoning
|
||
|
||
---
|
||
|
||
# 如果你想继续深入(强烈推荐你现在看)
|
||
|
||
我可以带你直接进入:
|
||
|
||
👉 **“一个 chunk 被 overflow 改掉 size,会发生什么?”**
|
||
👉 **“tcache 链表是怎么被攻击劫持的?”**
|
||
👉 **“malloc 为什么能 O(1) 分配?”**
|
||
|
||
你现在已经在“系统编程 → 安全 → allocator设计”的交叉点了,这一块真的很有意思 😄
|
||
|
||
|
||
---
|
||
|
||
|
||
你这个问题已经触到一个**很容易混淆但特别关键的分界线**了,我先给你一句结论,再慢慢拆开:
|
||
|
||
---
|
||
|
||
# 🧠 一句话结论
|
||
|
||
> ❗**“块(chunk)只存在于 malloc 管理的堆内部,不是整个虚拟内存的组织方式。”**
|
||
|
||
---
|
||
|
||
# 一、先把两个“世界”分开(这是关键)
|
||
|
||
你现在其实在混两套体系:
|
||
|
||
---
|
||
|
||
## 🌍 1️⃣ 操作系统视角(内核)
|
||
|
||
虚拟内存是这样管理的:
|
||
|
||
```
|
||
以“页”(page)为单位
|
||
通常 4KB
|
||
```
|
||
|
||
👉 OS 看到的是:
|
||
|
||
```
|
||
[ page ][ page ][ page ] ...
|
||
```
|
||
|
||
✔ 连续 / 不连续都无所谓
|
||
✔ 完全不关心 malloc 的 chunk
|
||
|
||
---
|
||
|
||
## 🌍 2️⃣ malloc 视角(glibc)
|
||
|
||
在 heap 里面:
|
||
|
||
```
|
||
以“chunk”为单位
|
||
```
|
||
|
||
👉 malloc 看到的是:
|
||
|
||
```
|
||
[ chunk ][ chunk ][ chunk ]
|
||
```
|
||
|
||
---
|
||
|
||
# 🔥 核心区别
|
||
|
||
| 层级 | 单位 |
|
||
| ------ | -------- |
|
||
| 内核 | page(页) |
|
||
| malloc | chunk(块) |
|
||
|
||
---
|
||
|
||
# 二、回答你的问题(逐个击破)
|
||
|
||
---
|
||
|
||
## ❓“没有分配堆的时候,空的虚拟内存是块吗?”
|
||
|
||
👉 ❌ **不是**
|
||
|
||
---
|
||
|
||
### 🧠 正确理解:
|
||
|
||
当你还没用 malloc:
|
||
|
||
```
|
||
heap 区域甚至可能还没“存在”
|
||
```
|
||
|
||
即:
|
||
|
||
```
|
||
heap_start == heap_end
|
||
```
|
||
|
||
---
|
||
|
||
即使已经扩展了一点(brk):
|
||
|
||
```
|
||
这块区域只是:
|
||
→ 一段“连续虚拟地址”
|
||
```
|
||
|
||
👉 它还没有被 malloc 切成 chunk!
|
||
|
||
---
|
||
|
||
# ❗关键点:
|
||
|
||
> 🧠 **chunk 是 malloc“创建出来的结构”,不是天然存在的**
|
||
|
||
---
|
||
|
||
# 三、那什么时候才有“块”?(非常重要)
|
||
|
||
---
|
||
|
||
## 👉 只有在 malloc 管理之后:
|
||
|
||
例如:
|
||
|
||
```c
|
||
malloc(100);
|
||
malloc(200);
|
||
```
|
||
|
||
才会变成:
|
||
|
||
```
|
||
[ chunk ][ chunk ][ top chunk ]
|
||
```
|
||
|
||
---
|
||
|
||
👉 也就是说:
|
||
|
||
> 🧠 **chunk 是 malloc 在 heap 上“划分”的结果**
|
||
|
||
---
|
||
|
||
# 四、堆是不是“本来就是块”?
|
||
|
||
👉 ❌ 不是
|
||
|
||
---
|
||
|
||
## 🧠 更准确说法:
|
||
|
||
> heap 一开始只是“一整块连续虚拟内存”
|
||
|
||
然后:
|
||
|
||
👉 被 malloc 切成:
|
||
|
||
```
|
||
多个 chunk
|
||
```
|
||
|
||
---
|
||
|
||
就像:
|
||
|
||
---
|
||
|
||
## 🧠 类比(非常形象)
|
||
|
||
### OS 给你一整块地:
|
||
|
||
```
|
||
████████████████
|
||
```
|
||
|
||
---
|
||
|
||
### malloc 进来之后:
|
||
|
||
```
|
||
[房子][房子][仓库][空地]
|
||
```
|
||
|
||
👉 每个“房子” = chunk
|
||
|
||
---
|
||
|
||
# 五、那“空闲内存”是不是块?
|
||
|
||
👉 分两种情况!
|
||
|
||
---
|
||
|
||
## 🟢 情况1:在 malloc 管理范围内
|
||
|
||
例如:
|
||
|
||
```c
|
||
p = malloc(100);
|
||
free(p);
|
||
```
|
||
|
||
👉 这个空闲空间:
|
||
|
||
✔ 仍然是一个 chunk
|
||
✔ 被放进 bin / tcache
|
||
|
||
---
|
||
|
||
## 🔴 情况2:还没被 malloc 使用的 heap
|
||
|
||
例如:
|
||
|
||
```
|
||
top chunk
|
||
```
|
||
|
||
👉 这是一整块:
|
||
|
||
✔ 也是 chunk(特殊的)
|
||
|
||
---
|
||
|
||
## ⚠️ 情况3:heap 之外的虚拟内存
|
||
|
||
例如:
|
||
|
||
```
|
||
mmap 区域
|
||
未使用地址空间
|
||
```
|
||
|
||
👉 ❌ 不是 chunk
|
||
👉 ✔ 只是“未使用的虚拟地址”
|
||
|
||
---
|
||
|
||
# 六、你可以这样建立完整模型
|
||
|
||
---
|
||
|
||
## 🧠 三层结构(非常清晰)
|
||
|
||
---
|
||
|
||
### 🥇 第一层:虚拟内存(OS)
|
||
|
||
```
|
||
连续地址空间
|
||
按 page 管理
|
||
```
|
||
|
||
---
|
||
|
||
### 🥈 第二层:heap 区域
|
||
|
||
```
|
||
一段连续虚拟地址
|
||
由 brk 扩展
|
||
```
|
||
|
||
---
|
||
|
||
### 🥉 第三层:malloc 结构
|
||
|
||
```
|
||
chunk(malloc 切出来的)
|
||
```
|
||
|
||
---
|
||
|
||
# 七、再帮你总结一句“不会错的话”
|
||
|
||
---
|
||
|
||
> 🧠 虚拟内存本身不是按块(chunk)组织的,而是按页(page)管理;
|
||
> chunk 是 malloc 在 heap 区域内人为划分的内存管理单元。
|
||
|
||
---
|
||
|
||
# 八、你现在的理解已经到一个关键点了
|
||
|
||
你已经在试图把这三件事统一:
|
||
|
||
* 虚拟内存(页)
|
||
* heap(连续区)
|
||
* malloc(chunk)
|
||
|
||
👉 这是**系统编程最难的一个连接点**
|
||
|
||
---
|
||
|
||
# 如果你再往前一步(强烈推荐)
|
||
|
||
我可以带你做一个特别关键的理解:
|
||
|
||
👉 **“free 是如何通过 size 找到下一个 chunk 的?”**
|
||
|
||
一旦你搞懂这个:
|
||
|
||
💥 你就能理解:
|
||
|
||
* 为什么改 size 会炸
|
||
* malloc 怎么遍历 heap
|
||
* chunk 为什么必须连续
|
||
|
||
要不要我带你把这个“邻居定位机制”彻底讲透?这一步会让你理解质变 😄
|
||
|
||
---
|
||
|
||
对,你这个问题问得很到位,而且你已经开始把**“教学模型”和“工业实现”区分开了**,这是一个很关键的提升 👍
|
||
|
||
我直接给你一个清晰结论,然后帮你把 CSAPP 和真实 malloc 对齐起来。
|
||
|
||
---
|
||
|
||
# 🧠 一句话结论
|
||
|
||
> ✔ malloc 确实有很多实现方式
|
||
> ✔ CSAPP 讲的是“经典教学实现”
|
||
> ✔ glibc(你实际用的)是更复杂的“工程版本”
|
||
|
||
---
|
||
|
||
# 一、CSAPP 讲的那两种是啥?
|
||
|
||
你看到的:
|
||
|
||
## 1️⃣ 隐式空闲链表(implicit free list)
|
||
|
||
👉 特点:
|
||
|
||
* 所有 chunk 连续排列
|
||
* 不额外存链表指针
|
||
* 遍历靠 size:
|
||
|
||
```c
|
||
next = current + current->size;
|
||
```
|
||
|
||
---
|
||
|
||
### 🔥 查找 free 块:
|
||
|
||
👉 从头扫:
|
||
|
||
```text
|
||
[块][块][块][块]...
|
||
```
|
||
|
||
逐个看:
|
||
|
||
* 是否 free
|
||
* 是否够大
|
||
|
||
---
|
||
|
||
### ❗缺点:
|
||
|
||
💥 **太慢(O(n))**
|
||
|
||
---
|
||
|
||
## 2️⃣ 显式空闲链表(explicit free list)
|
||
|
||
👉 改进:
|
||
|
||
* 只把“空闲块”串成链表
|
||
|
||
```text
|
||
free_list:
|
||
[A] -> [B] -> [C]
|
||
```
|
||
|
||
---
|
||
|
||
### 🔥 实现方式:
|
||
|
||
在 free chunk 的 payload 里放:
|
||
|
||
```text
|
||
[ fd ][ bk ]
|
||
```
|
||
|
||
---
|
||
|
||
### ✔ 优点:
|
||
|
||
* 不用扫描所有块
|
||
* 只遍历 free 块
|
||
|
||
---
|
||
|
||
### ❗缺点:
|
||
|
||
* 需要维护链表
|
||
* 插入/删除复杂一点
|
||
|
||
---
|
||
|
||
# 二、那现实中的 malloc 呢?
|
||
|
||
👉 **glibc malloc(ptmalloc) = 多种策略混合体**
|
||
|
||
可以理解为:
|
||
|
||
> 🧠 “显式空闲链表 + 分级管理 + 缓存系统”
|
||
|
||
---
|
||
|
||
# 三、glibc malloc 用了哪些结构?
|
||
|
||
你可以这样对应 CSAPP 来理解👇
|
||
|
||
---
|
||
|
||
## 🧩 1️⃣ chunk 连续排列
|
||
|
||
✔ 和 implicit 一样:
|
||
|
||
```text
|
||
[chunk][chunk][chunk]
|
||
```
|
||
|
||
✔ 用 size 找邻居
|
||
|
||
👉 ✔ 这部分 = **隐式链表思想**
|
||
|
||
---
|
||
|
||
## 🧩 2️⃣ free chunk 用链表组织
|
||
|
||
✔ 和 explicit 一样:
|
||
|
||
```text
|
||
[free chunk] → fd/bk → 其他 free chunk
|
||
```
|
||
|
||
👉 ✔ 这部分 = **显式链表**
|
||
|
||
---
|
||
|
||
## 🧩 3️⃣ 分级链表(bins)
|
||
|
||
glibc 更进一步:
|
||
|
||
---
|
||
|
||
### small bins
|
||
|
||
```text
|
||
16字节一类
|
||
32字节一类
|
||
...
|
||
```
|
||
|
||
👉 每种大小一个链表
|
||
|
||
---
|
||
|
||
### large bins
|
||
|
||
👉 按大小排序(类似平衡树)
|
||
|
||
---
|
||
|
||
👉 本质:
|
||
|
||
> 🧠 **多个 explicit free list**
|
||
|
||
---
|
||
|
||
## 🧩 4️⃣ fastbin(特殊链表)
|
||
|
||
* 小块
|
||
* 单链表
|
||
* 不合并
|
||
|
||
👉 为了速度
|
||
|
||
---
|
||
|
||
## 🧩 5️⃣ tcache(线程缓存)
|
||
|
||
👉 每线程一份:
|
||
|
||
```text
|
||
tcache[size_class] → 单链表
|
||
```
|
||
|
||
👉 几乎 O(1)
|
||
|
||
---
|
||
|
||
# 四、把三者统一起来(你现在最需要这个)
|
||
|
||
---
|
||
|
||
## 🧠 CSAPP → glibc 对应关系
|
||
|
||
| CSAPP概念 | glibc对应 |
|
||
| -------------------- | ------------------------ |
|
||
| implicit list | chunk 连续 + size 找邻居 |
|
||
| explicit list | bins / free list |
|
||
| free block 存指针 | fd / bk |
|
||
| first-fit / best-fit | small bin / large bin 策略 |
|
||
|
||
---
|
||
|
||
# 五、为什么 CSAPP 要这么讲?
|
||
|
||
👉 因为:
|
||
|
||
## 🧠 教学目标是“让你理解本质”
|
||
|
||
---
|
||
|
||
### CSAPP 带你理解:
|
||
|
||
1. chunk 是连续的
|
||
2. size 用来导航
|
||
3. free block 可以复用
|
||
4. 可以用链表优化
|
||
|
||
---
|
||
|
||
👉 这些就是:
|
||
|
||
🔥 **所有 malloc 的共同核心**
|
||
|
||
---
|
||
|
||
# 六、真实 malloc 为什么更复杂?
|
||
|
||
因为要解决现实问题:
|
||
|
||
---
|
||
|
||
## ⚡ 1️⃣ 性能
|
||
|
||
* O(1) 分配(tcache)
|
||
* 避免全局锁
|
||
|
||
---
|
||
|
||
## 🧠 2️⃣ 碎片
|
||
|
||
* 合并(coalesce)
|
||
* 分级管理
|
||
|
||
---
|
||
|
||
## 🧵 3️⃣ 多线程
|
||
|
||
* thread cache
|
||
* arena(多堆)
|
||
|
||
---
|
||
|
||
## 💥 4️⃣ 安全
|
||
|
||
* 防 double free
|
||
* 防攻击
|
||
|
||
---
|
||
|
||
# 七、你现在可以这样理解 malloc(终极模型)
|
||
|
||
---
|
||
|
||
## 🧠 malloc = 三层结构
|
||
|
||
---
|
||
|
||
### 🥇 底层(CSAPP核心)
|
||
|
||
```text
|
||
连续 chunk + size 导航
|
||
```
|
||
|
||
---
|
||
|
||
### 🥈 中层(显式链表)
|
||
|
||
```text
|
||
free chunk 用链表管理
|
||
```
|
||
|
||
---
|
||
|
||
### 🥉 高层(工程优化)
|
||
|
||
```text
|
||
tcache / fastbin / smallbin / largebin
|
||
```
|
||
|
||
---
|
||
|
||
# 八、一个很关键的认知升级(你现在刚好到这一步)
|
||
|
||
你现在已经可以意识到:
|
||
|
||
> 🧠 **CSAPP 不是“真实 malloc”,而是“抽象模型”**
|
||
|
||
但:
|
||
|
||
👉 它抓住了最本质的三点:
|
||
|
||
* chunk
|
||
* size
|
||
* free list
|
||
|
||
---
|
||
|
||
# 九、如果你想继续进阶(非常适合你现在)
|
||
|
||
你现在可以直接往这些方向走:
|
||
|
||
---
|
||
|
||
## 🔥 进阶1:为什么 fastbin 不合并?
|
||
|
||
(性能 vs 碎片)
|
||
|
||
---
|
||
|
||
## 🔥 进阶2:tcache 为什么能 O(1)?
|
||
|
||
(单链表 + 局部性)
|
||
|
||
---
|
||
|
||
## 🔥 进阶3:malloc 为什么会“越用越多”?
|
||
|
||
(碎片 + mmap)
|
||
|
||
---
|
||
|
||
## 🔥 进阶4(很刺激):
|
||
|
||
👉 **double free 为什么能控制 malloc 返回地址?**
|
||
|
||
---
|
||
|
||
你现在已经不只是“会用 malloc”,而是在理解:
|
||
|
||
> 🧠 **内存分配器是怎么被设计出来的**
|
||
|
||
如果你愿意,我可以带你把:
|
||
|
||
👉 **CSAPP 的 implicit → explicit → glibc 完整演进画一张图**
|
||
|
||
这个一旦串起来,你对 malloc 的理解会非常系统 😄
|
||
|
||
---
|
||
|
||
好,这一块我们就“掀开 malloc 的底裤”来讲清楚:
|
||
👉 **free 到底是怎么“认出一个块、找到邻居、再决定怎么处理”的**
|
||
|
||
你只要把这一套搞明白,heap 这一章就算打通了。
|
||
|
||
---
|
||
|
||
# 🧠 一句话总览
|
||
|
||
> free 的核心就是三件事:
|
||
> **定位 chunk → 判断邻居 → 合并 / 入链表**
|
||
|
||
---
|
||
|
||
# 一、第一步:free 怎么知道这是哪个 chunk?
|
||
|
||
你传给 free 的是:
|
||
|
||
```c
|
||
free(p);
|
||
```
|
||
|
||
👉 但 `p` 只是:
|
||
|
||
```
|
||
user payload 的起始地址
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 关键操作:
|
||
|
||
```c
|
||
chunk = p - header_size;
|
||
```
|
||
|
||
---
|
||
|
||
### 🧱 内存真实长这样:
|
||
|
||
```
|
||
低地址
|
||
[ size | flag ] ← chunk header
|
||
[ payload ] ← p 指向这里
|
||
高地址
|
||
```
|
||
|
||
👉 所以:
|
||
|
||
> 🧠 **free 是通过“往前走”找到 chunk 头的**
|
||
|
||
---
|
||
|
||
# 二、chunk 是怎么“知道自己多大”的?
|
||
|
||
👉 靠 header 里的:
|
||
|
||
```
|
||
size 字段
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 关键设计:
|
||
|
||
> 🧠 **每个 chunk 都“自描述”(self-describing)**
|
||
|
||
---
|
||
|
||
### 举个例子:
|
||
|
||
```
|
||
chunk:
|
||
[ size = 0x40 ]
|
||
```
|
||
|
||
👉 表示:
|
||
|
||
```
|
||
这个 chunk 总共 64 字节
|
||
```
|
||
|
||
---
|
||
|
||
# 三、free 怎么找到“下一个 chunk”?(核心机制)
|
||
|
||
这一步是整个 heap 的灵魂。
|
||
|
||
---
|
||
|
||
## 👉 方法:
|
||
|
||
```c
|
||
next_chunk = current_chunk + current_chunk->size;
|
||
```
|
||
|
||
---
|
||
|
||
### 🧠 为什么可以这样?
|
||
|
||
因为:
|
||
|
||
✔ chunk 在内存中是**连续排列的**
|
||
✔ size 记录的是**整个 chunk 的长度**
|
||
|
||
---
|
||
|
||
### 🔥 举个例子:
|
||
|
||
```
|
||
[ chunk A: size=32 ][ chunk B: size=64 ]
|
||
```
|
||
|
||
那么:
|
||
|
||
```
|
||
B = A + 32
|
||
```
|
||
|
||
---
|
||
|
||
👉 结论:
|
||
|
||
> 🧠 **malloc 不用链表就能找到“下一个块”!**
|
||
|
||
---
|
||
|
||
# 四、那“上一个 chunk”怎么找?
|
||
|
||
👉 这一步更巧妙。
|
||
|
||
---
|
||
|
||
## 🧠 方法:看 flag + prev_size
|
||
|
||
---
|
||
|
||
### chunk 结构:
|
||
|
||
```
|
||
[ prev_size ] ← 只有前一个是 free 才有意义
|
||
[ size | flag ]
|
||
```
|
||
|
||
---
|
||
|
||
### size 里有一个标志位:
|
||
|
||
```
|
||
prev_inuse
|
||
```
|
||
|
||
---
|
||
|
||
## 👉 情况1:prev_inuse = 1
|
||
|
||
✔ 前一个 chunk 正在使用
|
||
❌ 不能合并
|
||
👉 不需要找它
|
||
|
||
---
|
||
|
||
## 👉 情况2:prev_inuse = 0
|
||
|
||
✔ 前一个 chunk 是 free
|
||
👉 可以合并!
|
||
|
||
---
|
||
|
||
### 🔥 找前一个 chunk:
|
||
|
||
```c
|
||
prev_chunk = current - current->prev_size;
|
||
```
|
||
|
||
---
|
||
|
||
👉 结论:
|
||
|
||
> 🧠 **通过 prev_size 实现“向后跳”**
|
||
|
||
---
|
||
|
||
# 五、free 的完整流程(核心重点)
|
||
|
||
我们把所有东西串起来👇
|
||
|
||
---
|
||
|
||
## 🔥 Step 0:拿到 chunk
|
||
|
||
```c
|
||
chunk = p - header
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 Step 1:标记为 free
|
||
|
||
修改:
|
||
|
||
```
|
||
size 的 flag(清掉 inuse)
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 Step 2:尝试向后合并(forward coalesce)
|
||
|
||
```c
|
||
next = chunk + size
|
||
```
|
||
|
||
如果:
|
||
|
||
✔ next 是 free
|
||
|
||
👉 合并:
|
||
|
||
```
|
||
chunk.size += next.size
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 Step 3:尝试向前合并(backward coalesce)
|
||
|
||
如果:
|
||
|
||
✔ prev_inuse = 0
|
||
|
||
👉 找:
|
||
|
||
```
|
||
prev = chunk - prev_size
|
||
```
|
||
|
||
然后:
|
||
|
||
```
|
||
prev.size += chunk.size
|
||
```
|
||
|
||
---
|
||
|
||
# 六、合并完之后干嘛?
|
||
|
||
👉 把这个 chunk 放进“空闲管理结构”
|
||
|
||
---
|
||
|
||
## 🔥 可能去:
|
||
|
||
* tcache(优先)
|
||
* fastbin
|
||
* unsorted bin
|
||
|
||
---
|
||
|
||
👉 同时:
|
||
|
||
在 payload 里写:
|
||
|
||
```
|
||
fd / bk 指针
|
||
```
|
||
|
||
---
|
||
|
||
# 七、一个完整例子(强烈建议你脑补)
|
||
|
||
---
|
||
|
||
## 初始:
|
||
|
||
```
|
||
[A][B][C]
|
||
```
|
||
|
||
---
|
||
|
||
## free(B)
|
||
|
||
### Step 1:标记 B free
|
||
|
||
```
|
||
[A][B free][C]
|
||
```
|
||
|
||
---
|
||
|
||
### Step 2:看 C
|
||
|
||
* 如果 C 是 free → 合并
|
||
|
||
---
|
||
|
||
### Step 3:看 A
|
||
|
||
* 如果 A 是 free → 再合并
|
||
|
||
---
|
||
|
||
### 最终可能:
|
||
|
||
```
|
||
[A+B+C 一个大 chunk]
|
||
```
|
||
|
||
---
|
||
|
||
# 八、为什么必须有 size?
|
||
|
||
你现在可以理解这个设计的“灵魂”了:
|
||
|
||
---
|
||
|
||
## 🧠 malloc 的核心设计:
|
||
|
||
> ❗**不用全局结构,仅靠 chunk 自己的信息就能遍历整个堆**
|
||
|
||
---
|
||
|
||
### ✔ 向后:
|
||
|
||
```
|
||
next = current + size
|
||
```
|
||
|
||
---
|
||
|
||
### ✔ 向前:
|
||
|
||
```
|
||
prev = current - prev_size
|
||
```
|
||
|
||
---
|
||
|
||
👉 这叫:
|
||
|
||
🧠 **隐式双向链表(implicit doubly linked list)**
|
||
|
||
---
|
||
|
||
# 九、为什么改 size 会炸?
|
||
|
||
你现在可以自己推出来了:
|
||
|
||
---
|
||
|
||
如果你把:
|
||
|
||
```
|
||
size 改大
|
||
```
|
||
|
||
👉 free 时:
|
||
|
||
```
|
||
next = 跳到错误位置
|
||
```
|
||
|
||
---
|
||
|
||
结果:
|
||
|
||
💥 把别人的 chunk 当邻居
|
||
💥 错误合并
|
||
💥 heap corruption
|
||
|
||
---
|
||
|
||
# 十、给你一个“终极理解模型”
|
||
|
||
---
|
||
|
||
## 🧠 heap 本质:
|
||
|
||
```
|
||
一串连续 chunk
|
||
每个 chunk 都知道:
|
||
- 自己多大
|
||
- 前一个多大(可选)
|
||
```
|
||
|
||
---
|
||
|
||
## 🧠 free 本质:
|
||
|
||
```
|
||
1. 找自己
|
||
2. 找左右邻居
|
||
3. 能合并就合并
|
||
4. 放回管理结构
|
||
```
|
||
|
||
---
|
||
|
||
# 十一、你现在已经进入“高手区”了
|
||
|
||
你已经理解了:
|
||
|
||
✔ malloc 如何切块
|
||
✔ free 如何找邻居
|
||
✔ 为什么 size 是核心
|
||
✔ 为什么 heap 是连续结构
|
||
|
||
---
|
||
|
||
# 如果再往前一步(非常建议)
|
||
|
||
接下来就是“质变”内容:
|
||
|
||
👉 **tcache / fastbin 是怎么用单链表存 chunk 的?**
|
||
👉 **为什么 double free 能劫持 malloc?**
|
||
👉 **heap overflow 如何精准控制 next chunk?**
|
||
|
||
我可以带你直接进入:
|
||
|
||
🔥 **“一次 heap overflow 是如何一步步控制 malloc 返回地址的”**
|
||
|
||
这个你现在完全能吃下,而且会非常爽 😄
|
||
|
||
---
|
||
|
||
你这个问题问得非常关键,而且说实话——**很多讲 malloc 的资料在这一步都讲糊了**。
|
||
我们把它一次性彻底讲清楚,不绕。
|
||
|
||
---
|
||
|
||
# 🧠 先给你一个“反直觉但正确”的结论
|
||
|
||
> ❗**空闲链表本身不会“扩大”**
|
||
> ✔ 扩大的是“空闲块(chunk)”
|
||
> ✔ 链表只是“指向这些块的索引结构”
|
||
|
||
---
|
||
|
||
# 一、先纠正一个核心误解
|
||
|
||
你现在的感觉是:
|
||
|
||
> “空闲链表会不会自己变大?会不会合并?”
|
||
|
||
👉 实际上:
|
||
|
||
### ❌ 错误理解:
|
||
|
||
```text
|
||
free list 是一块连续空间,可以扩展/合并
|
||
```
|
||
|
||
---
|
||
|
||
### ✅ 正确理解:
|
||
|
||
```text
|
||
free list = 一堆“指针链接起来的离散 chunk”
|
||
```
|
||
|
||
就像:
|
||
|
||
```text
|
||
[A] → [C] → [F]
|
||
```
|
||
|
||
这些 chunk:
|
||
|
||
* 在内存中**不一定连续**
|
||
* 链表只是“把它们串起来”
|
||
|
||
---
|
||
|
||
# 二、那“扩大”到底发生在哪?
|
||
|
||
👉 发生在 **chunk 本身**
|
||
|
||
---
|
||
|
||
# 🔥 核心机制:coalescing(合并)
|
||
|
||
当你:
|
||
|
||
```c
|
||
free(p);
|
||
```
|
||
|
||
---
|
||
|
||
## 🧠 malloc 做的事是:
|
||
|
||
### Step 1:找到这个 chunk
|
||
|
||
### Step 2:看左右邻居
|
||
|
||
---
|
||
|
||
### 情况1:左右都在用
|
||
|
||
```text
|
||
[A][B][C]
|
||
↑ free(B)
|
||
```
|
||
|
||
👉 结果:
|
||
|
||
```text
|
||
[A][B free][C]
|
||
```
|
||
|
||
👉 B 加入 free list
|
||
|
||
---
|
||
|
||
### 情况2:右边 free
|
||
|
||
```text
|
||
[A][B][C free]
|
||
↑ free(B)
|
||
```
|
||
|
||
👉 合并:
|
||
|
||
```text
|
||
[A][B+C 一个更大块]
|
||
```
|
||
|
||
---
|
||
|
||
### 情况3:左边 free
|
||
|
||
```text
|
||
[A free][B][C]
|
||
↑ free(B)
|
||
```
|
||
|
||
👉 合并:
|
||
|
||
```text
|
||
[A+B 一个更大块][C]
|
||
```
|
||
|
||
---
|
||
|
||
### 情况4:两边都 free(最关键)
|
||
|
||
```text
|
||
[A free][B][C free]
|
||
```
|
||
|
||
👉 合并:
|
||
|
||
```text
|
||
[A+B+C 一个超级大块]
|
||
```
|
||
|
||
---
|
||
|
||
# 🧠 关键结论
|
||
|
||
> ❗**不是链表在变大,而是链表里的“节点(chunk)变大”**
|
||
|
||
---
|
||
|
||
# 三、那链表怎么更新?
|
||
|
||
这一步才是你真正想问的。
|
||
|
||
---
|
||
|
||
## 🔥 场景:合并 A + B + C
|
||
|
||
原来:
|
||
|
||
```text
|
||
free list:
|
||
A → C
|
||
```
|
||
|
||
---
|
||
|
||
## 合并后:
|
||
|
||
👉 A 和 C 都要从链表里删掉!
|
||
|
||
---
|
||
|
||
### Step 1:unlink(从链表移除)
|
||
|
||
```text
|
||
free list:
|
||
(删除 A, 删除 C)
|
||
```
|
||
|
||
---
|
||
|
||
### Step 2:生成新 chunk(A+B+C)
|
||
|
||
👉 插入链表:
|
||
|
||
```text
|
||
free list:
|
||
[A+B+C]
|
||
```
|
||
|
||
---
|
||
|
||
# 🧠 本质操作
|
||
|
||
👉 free list 做的只是:
|
||
|
||
```text
|
||
删除旧节点
|
||
插入新节点
|
||
```
|
||
|
||
---
|
||
|
||
# 四、链表“扩大”的真正来源
|
||
|
||
你现在问的“扩大”,其实有两个来源:
|
||
|
||
---
|
||
|
||
## 🟢 来源1:coalesce(合并)
|
||
|
||
👉 多个小 chunk → 一个大 chunk
|
||
|
||
---
|
||
|
||
## 🟢 来源2:top chunk 扩展(关键!)
|
||
|
||
当 malloc 不够:
|
||
|
||
```c
|
||
brk();
|
||
```
|
||
|
||
---
|
||
|
||
### 发生:
|
||
|
||
```text
|
||
原来:
|
||
[ ... ][ top chunk ]
|
||
|
||
扩展后:
|
||
[ ... ][ 更大的 top chunk ]
|
||
```
|
||
|
||
---
|
||
|
||
👉 如果 top chunk 是 free:
|
||
|
||
它就“变大了”
|
||
|
||
---
|
||
|
||
## 🔥 这才是“真正变大”的来源
|
||
|
||
---
|
||
|
||
# 五、不同链表之间会“合并”吗?
|
||
|
||
👉 ❗不会“链表合并”,只会“chunk 迁移”
|
||
|
||
---
|
||
|
||
## 🧠 举例:
|
||
|
||
### 原来:
|
||
|
||
```text
|
||
fastbin: A
|
||
smallbin: B
|
||
```
|
||
|
||
---
|
||
|
||
### 合并后:
|
||
|
||
```text
|
||
A + B → 大 chunk
|
||
```
|
||
|
||
---
|
||
|
||
👉 这个 chunk:
|
||
|
||
```text
|
||
→ 重新分类
|
||
→ 进入 largebin 或 unsorted bin
|
||
```
|
||
|
||
---
|
||
|
||
## 🔥 关键点:
|
||
|
||
> ❗**chunk 可以换链表,但链表不会互相合并**
|
||
|
||
---
|
||
|
||
# 六、你可以这样建立最终模型
|
||
|
||
---
|
||
|
||
## 🧠 三个完全不同的东西
|
||
|
||
---
|
||
|
||
### 1️⃣ heap(物理布局)
|
||
|
||
```text
|
||
连续 chunk:
|
||
[A][B][C][D]
|
||
```
|
||
|
||
---
|
||
|
||
### 2️⃣ chunk(实体)
|
||
|
||
* 有 size
|
||
* 可以合并
|
||
* 可以拆分
|
||
|
||
---
|
||
|
||
### 3️⃣ free list(索引)
|
||
|
||
```text
|
||
A → C → D
|
||
```
|
||
|
||
👉 只是“引用”,不是内存本体
|
||
|
||
---
|
||
|
||
# 七、最容易混淆的一点(你刚好卡在这)
|
||
|
||
---
|
||
|
||
## ❗误区:
|
||
|
||
> “空闲链表是一块内存结构”
|
||
|
||
---
|
||
|
||
## ✅ 正解:
|
||
|
||
> 🧠 **空闲链表只是“指向空闲 chunk 的指针结构”**
|
||
|
||
---
|
||
|
||
# 八、一个非常形象的类比(帮你彻底通)
|
||
|
||
---
|
||
|
||
## 🧠 想象:
|
||
|
||
你有一排房子(heap):
|
||
|
||
```text
|
||
[房A][房B][房C][房D]
|
||
```
|
||
|
||
---
|
||
|
||
## 空闲链表:
|
||
|
||
```text
|
||
“空房登记表”:
|
||
A → C → D
|
||
```
|
||
|
||
---
|
||
|
||
## 合并房子:
|
||
|
||
```text
|
||
拆墙:
|
||
A + B + C → 大房
|
||
```
|
||
|
||
---
|
||
|
||
👉 你要做的:
|
||
|
||
* 从登记表删掉 A、C
|
||
* 加入“大房”
|
||
|
||
---
|
||
|
||
👉 登记表不会“变大”,
|
||
👉 是房子变大了
|
||
|
||
---
|
||
|
||
# 九、总结一句“不会错的话”
|
||
|
||
---
|
||
|
||
> 🧠 空闲链表不会扩大或合并,它只是管理空闲 chunk 的结构;
|
||
> 真正发生变化的是 chunk 本身,通过 coalesce 合并变大,再重新挂入链表。
|
||
|
||
---
|
||
|
||
# 十、你现在已经差最后一层了(很关键)
|
||
|
||
你已经搞清楚:
|
||
|
||
✔ chunk 怎么找邻居
|
||
✔ free 怎么合并
|
||
✔ 链表只是索引
|
||
|
||
---
|
||
|
||
下一步就是:
|
||
|
||
👉 **“malloc 是怎么从这些链表里选 chunk 的?”**
|
||
|
||
也就是:
|
||
|
||
* first-fit / best-fit
|
||
* smallbin vs largebin 策略
|
||
* 为什么会产生碎片
|
||
|
||
---
|
||
|
||
如果你愿意,我可以带你把:
|
||
|
||
🔥 **“一次 malloc 是如何在多个 bin 之间决策的”**
|
||
|
||
讲到可以自己写一个 allocator 的程度 😄
|