Linux动态内存管理机制
Linux动态内存管理机制
堆
堆是用于分配动态内存的一段内存区域,它独立的存在于内存中,介于程序内存基地址和libc地址之间,它是从低地址向高地址生长的
比如说:
一个留言板程序,用户可以输入自己的留言,留言长度由用户自己决定,那么栈上存储的是一个函数的局部变量,bss储存全局变量
那么我们怎么储存用户输入的留言,用户的留言可能是1个字符到几百几千个字符,这时候就需要堆来进行动态管理
在lib中,我们可以通过malloc来给用户分配一段长度位size的内存,通过free来释放这段内存空间
这些数据,被统一存放在堆中,维护这些数据的运行机制在glibc中,称为ptmalloc,也就是我们要重点学习的内容
堆的内存管理机制
堆的管理机制相比于栈是非复杂,但是堆的漏洞比栈有更多的形式和利用方式,而且堆漏洞所产生的条件比栈更少
一般情况下栈溢出起码需要16个字节,也就是说至少溢出到返回地址才能利用,但是堆的话只需要一个字节就可完成利用,甚至这个字节可以是个\x00,也就是空字节,nullbyte
栈基本都会关闭一两个保护机制,堆的话一般全开
堆块
在内存中,堆是一个个堆块构成的,这些堆块称为chunk
在64位中,堆块的大小是8字节对齐的,也就是说,我们申请一个15字节长度的堆块,实际到我们用户可控的数据大小位16字节
但是在管理过程中,一个堆块除用户数据区外,还有头部字段,长度为16字节,一个堆块最小的长度为32字节(包括头部),也就是说,我们分配一个1字节的堆块,它的实际长度为32字节
1 | [ 内存低地址 ] |
其中,pre_size和size字段分别代表前一个chunk的大小以及当前chunk的大小,大小都是8字节,两个一共16字节,称之为chunk的头部字段
后面的useer_data区域是用户可以输入数据的地方
size
为了节省空间,将size的最低三个bit设置为三个标志位,从低到高分别为non_main_arena、is_mmap、prev_inuse。
non_main_arena用来记录当前chunk是否不属于主线程,1表示不属于,0表示属于is_mmap表示当前chunk是否由mmap分配的,1表示属于,0表示不属于prev_inuse用来表示前面紧邻的那个chunk是否正在使用,0表示前面的chunk已经被释放,1表示正在被用户使用
presize
记录前一个chunk的大小。这里注意,presize只有在前面的chunk被free掉的时候才生效,也就是说,只有在prev_inuse为0的时候,系统才会把prev_size字段当作presize
如果chunk正在被使用,那么它会把下一个chunk的presize当作userdata。充分利用空间
也就是说,如果我们申请一个0x80长度的区域,系统实际会给我们0x90大小;如果申请了0x88大小的区域,系统同样也是给我吗0x90大小的区域,剩下的8个字节,使用nextchunk的prevsize区域。因为,只有当一个chunk被释放的时候,nextchunk的prevsize才真正代表前一个chunk的大小
第一次,申请 0x80 空间的时候1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17[ 内存低地址 ]
+---------------------+ <--- Chunk A 起始地址
| prev_size (8 bytes) |
+---------------------+
| size = 0x91 (P=1) | ---> 0x90 大小,P=1 表示前一块在用
+----> +---------------------+ <--- 返回给你的指针 (user_ptr)
| | |
| | |
| | 你的用户数据 (128B) | ---> 刚好填满 0x80 字节
| | |
| | |
+----> +---------------------+ <--- Chunk B 起始地址 (A起始地址 + 0x90)
| prev_size (8 bytes) | ---> 属于 B 的区域,目前闲置浪费,没被用到
+---------------------+
| size = 0x?? (P=1) | ---> B 的 size,P=1 表示 A 正在使用
+---------------------+
[ 内存高地址 ]
第二次,申请 0x88 空间的时候1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18[ 内存低地址 ]
+---------------------+ <--- Chunk A 起始地址
| prev_size (8 bytes) |
+---------------------+
| size = 0x91 (P=1) | ---> 依然是 0x90 大小!
+----> +---------------------+ <--- 返回给你的指针 (user_ptr)
| | |
| | 你的用户数据 |
| | (前 128 bytes) | ---> 填满了 A 本身的数据区
| | |
| | |
+----> +=====================+ <--- Chunk B 起始地址 (物理分界线)
| 你的用户数据 |
| (后 8 bytes) | ---> 【高能预警】你剩下的 8 字节,写在了这里!
+---------------------+
| size = 0x?? (P=1) | ---> Chunk B 的 size,P=1 保护了它
+---------------------+
[ 内存高地址 ]
topchunk
最开始的时候,程序的堆还未被使用,整个的堆区域属于一个很大的堆块叫做topchunk
当已经被使用的空间不够的时,程序就会从topchunk分割一块出来来给程序使用
堆块的管理
为了保证程序的快速运行,而且方便程序内存管理,所以ptmalloc将释放后的堆块根据大小分成不同的bin
fastbin:大小范围:0x20-0x80smallbin:大小范围:0x90-0x400largebin:大小范围:0x410以上unsortedbin:未被归类的bin,临时存储用,存放的堆块大小不确定
chunk被free后的样子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22[ 内存低地址 ]
+-------------------------+ <--- Chunk A 起始地址 (0x1000)
| prev_size (8 bytes) |
+-------------------------+
| size = 0x91 (P=1) | ---> 大小依然是 0x90
+----> +-------------------------+ <--- 曾经的 user_ptr (0x1010)
| | fd (Forward Pointer) | ---> 【巨变1】前8字节变成了指向下一个空闲块的指针!
| +-------------------------+
| | bk (Backward Pointer) | ---> 【巨变2】后8字节变成了指向上一个空闲块的指针!
| +-------------------------+
| | 旧的 User Data 残留 | ---> 剩下的空间没人管,可能还留着你以前的数据
| | ...... |
| +-------------------------+
| | 旧的 User Data 残留 |
+----> +=========================+ <--- Chunk B 起始地址 (0x1090)
| prev_size = 0x90 | ---> 【巨变3】A 的大小(0x90)被写入了 B 的 prev_size!
+-------------------------+
| size = 0x??0 (P=0) | ---> 【巨变4】Chunk B 的 P位被清零!(P=0)
+-------------------------+
| B 的 User Data |
+-------------------------+
[ 内存高地址 ]
由于chunk被free了,所以按常理说用户不应该能访问到在这个chunk
于是乎在userdata区域存放一些用于管理内存的指针信息
fastbin:单链表结构,只有fdsmall & unsortedbin:双向链表结构,fd和bk都有largebin:双向链表,fdbk都有,同时还有fd nextsize和bk next
堆块的合并操作
如果我们free掉一个堆块,可能会触发向前合并和向后合并
- 向前合并
- 检查当前
chunk的previnuse位,如果为0,则根据当前chunk的prev size找到prev chunk的头,两个模块合并1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16[ 内存低地址 ]
+-------------------------+ <--- Prev Chunk 起始地址 (假设 0x1000)
| prev_size |
+-------------------------+
| size = 0x90 (P=1) | ---> 大小是 0x90,处于空闲状态
+-------------------------+
| fd / bk | ---> 因为已释放,这里存着链表指针
| |
+----> +=========================+ <--- Current Chunk 起始地址 (0x1090) —— 【你正在释放它】
| | prev_size = 0x90 | ---> 【关键点 1】记录了前一块的大小是 0x90
| +-------------------------+
| | size = 0x??0 (P=0) | ---> 【关键点 2】P=0!系统一看这个 0,就知道前面那位大哥是空的!
| +-------------------------+
| | User Data | ---> 准备被清理的用户数据
| +-------------------------+
[ 内存高地址 ]
- 检查当前
- 向后合并
- 检查当前
chunk的next nextchunk的prev inuse位(因为一个堆块的状态由它后面的chunk的previnuse位决定,所以确定nextchunk的状态需要检查next next chunk的previnuse位,找size就行),然后根据nextchunk的状态决定是否合并1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18[ 内存低地址 ]
+-------------------------+ <--- Current Chunk 起始地址 (0x1000) —— 【你正在释放它】
| prev_size |
+-------------------------+
| size = 0x90 (P=1) | ---> 假设当前块大小是 0x90
+----> +=========================+ <--- Next Chunk 起始地址 (0x1090)
| | prev_size |
| +-------------------------+
| | size = 0x80 (P=?) | ---> 假设大小是 0x80。但注意:它自己并没有标记记录自己是否空闲!
+----> +-------------------------+
| | fd / bk |
| | |
+----> +=========================+ <--- Next Next Chunk 起始地址 (0x1110)
| prev_size = 0x80 |
+-------------------------+
| size = 0x??0 (P=0) | ---> 【关键点】P=0!系统通过这个 0,判定它前面的 Next Chunk 是空闲的!
+-------------------------+
[ 内存高地址 ]
- 检查当前




