堆相关数据结构学习


前言

#include "stdafx.h"
#include<stdlib.h>



int main(int argc, char* argv[])
{
	int arr[1000*1000];
	return 0;
}

写如上代码,定义一个arr[1000*1000]的数组
在这里插入图片描述
发现编译器没有报错,但是程序直接崩溃,包括写递归,不停止,也会崩溃


一、崩溃原因

栈内存的空间不够,一直申请栈内存而不释放,就会崩溃
所以栈:容量小,主要分配给局部变量

堆内存:程序运行的时候分配,需要程序员向操作系统申请和释放,一次申请内存不要超过内存条的容量
申请:new
类型* 变量 = new 类型
类型* 变量 = new 类型(初始值)//这是初始化
delete 堆内存首地址

二、堆的使用

在这里插入图片描述代码:

#include "stdafx.h"
#include<stdlib.h>
#include<iostream>


int main(int argc, char* argv[])
{
	//new int; 申请堆内存,如果申请成功就会返回堆内存的首地址
	int* heapaddr = new int(233);//初始化,寻找四个连续的内存(因为是int),然后把233赋给这个int,就是四个连续的内存
	printf("变量heapaddr保存的内存地址:%x\n",heapaddr);
	printf("变量heapaddr指向地址的数据:%d\n",*heapaddr);

	delete heapaddr;//释放heapaddr保存的地址代表的内存,此时heapaddr为野指针
	heapaddr = NULL;//搞成空指针
	system("pause");

	return 0;
}


打印结果
在这里插入图片描述
一定要保存好内存地址,如果丢失,就delete不了了,这种就会造成内存泄漏

也可以用malloc和free


三、 反汇编

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
有点难懂

四、概念

堆是每个程序被分配到的一块内存区域,堆是动态分配的,也就是说程序可以从heap段请求一块内存,或者释放一块内存,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器。

五、glibc heap利用

1.malloc和free

malloc
在 glibc 的 malloc.c 中,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.
*/

当 n=0 时,返回当前系统允许的堆的最小内存块。其实就是一个chunk,chunk就是最小操作单元
当 n 负数时,由于在大多数系统上,size_t 是无符号数,所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

free
在 glibc 的 malloc.c 中,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.
*/

当 p 为空指针时,函数不执行任何操作。
当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free。
除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。
而malloc和free在底层上使用brk()和mmap()这两个系统调用来完成管理内存

2.brk()和mmap()

来自CTFwiki的图
在这里插入图片描述
堆往上涨可以覆盖栈
但是堆覆盖BSS段呢?在32位下面整数溢出,一直往上走,走到kernel后再往上走,回到最下面的text,再往上就到了bss段,听大佬说这就是house of false的思路
brk就是break locaiton获取内存
堆的起点,图上标识的start_brk,终点就是program brk,程序开始的时候这两个是在一起的,原因是程序这个时候没有申请堆,申请内存就是program指针往上移动,也就是说低地址是startbrk,高地址是program,program往上移动,令heap变大
但是sbrk()是C语言库函数,不是这个brk()

mmap就是进行标记映射,匿名映射的目的主要是可以申请以 0 填充的内存,并且这块内存仅被调用进程所使用。

3.多线程和arena

每个线程必定维护一些独立的数据结构,而对于这些结构是要进行加锁的,每个线程都有自己的freelist,也就是维护空闲的链表,以及自己的一段连续的堆区域arena。主线程的arena叫做main_arena,只有main_arena可以访问heap段和mmap段的映射,non_main_arena只能访问mmap映射
第一次申请时没有heap段,因此第一次申请的heap就是main_arena。它是通过brk()创建的。如果后面再次申请,就从这里取出,如果不够的话brk()往上移动
non_main_arena是由mmap创建,虽然non_main_arena可能很大,但是其中只有main_arena的大小可读写,这个可读写就是non_main_arena

free的时候不会给kernel,给分配器,丢给bin当中,如果后面又要申请小于等于的内存,直接从bin中取出

多线程维护

如果non_main_arena里的内存不够了,只能在mmap一次,所以我们要考虑在non_main_arena里维护多个堆的问题
heap_info:每个堆头部,main_arena没有
malloc_state:arena的头部,main_arena的这个部分时全局变量而不属于堆段,属于libc,如果能知道这个结构体位置,就能泄露出libc的地址
malloc_chunk:就是每个chunk的头部

4.chunk的结构

(1)已经分配的chunk

chunk类型:
已分配
空闲
topchunk
last remainchunk

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|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第一个部分prev_size,前一个chunk空闲的时候表示前一个块的大小,如果已经分配的话,这里无效,这里填充前一个块的用户数据,如果这里存了用户数据,溢出,覆盖到size_chunk的低位,这里的前一 chunk 指的是较低地址的 chunk 。(在32位上4B,在64位上8B)
第二个部分高位存储当前chunk的大小,低位分别表示:
P:之前的chunk已经分配则为1
M当前的chunk是mmap得到的则为1
N:当前chunk在non_main_arena里则为1
fd啥的都没有用,就用来填充用户数据,所以后面就是user data
NMP原本也是chunck_size,因为是8字节对齐,低三位没有用
申请内存的时候,把user data大小申请,然后生成前面两个部分(32位4+4,64位8+8),还要考虑chunk size是否8字节对齐,调用malloc是mem指针,但实际上chunk开始是在chunk指针开始

(2)free过后chunk

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' |             Size of chunk, in bytes                     |A|0|P|
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Forward pointer to next chunk in list             |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Back pointer to previous chunk in list            |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Unused space (may be 0 bytes long)                .
        .                                                               .
 next   .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' |             Size of chunk, in bytes                           |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|0|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

pre_size:上一个块如果是空闲的话就会合并,如果是已经分配的,那么这里无效,就会存储上一个块的用户数据
fd : 指向同一个bin中前一个free chunk的地址
fd:指向同一个bin中后一个free chunk的地址
bk:往后

(3)top chunk

程序第一次进行 malloc 的时候,heap 会被分为两块,一块给用户,剩下的那块就是 top chunk。
这个 chunk 不属于任何一个 bin
省情内存时,bin中都没有可用chunk时,就会分割top chunk。先把一部分给用户,剩下的做为新的top chunk,如果top chunk无法满足大小:

      在main_arena中brk()扩张
      non_main中mmap()分配新的堆

top chunk 的 prev_inuse 比特位始终为 1,否则其前面的 chunk 就会被合并到 top chunk 中。
初始情况下,将unsorted bin看成top chunk
在用户使用 malloc 请求分配内存时,ptmalloc 找到的 chunk 可能并不和申请的内存大小一致,这时候就将分割之后的剩余部分称之为 last remainder chunk ,unsort bin 也会存这一块。top chunk 分割剩下的部分不会作为 last remainder.

(4)堆的相关约束

指的是堆采用8字节对齐,所以堆的大小肯定是2*chunk_size的整数倍

5.bin的结构

free掉的chunk不会归还给系统,因为用户态和内存态来回切换很麻烦,就给到了ptmalloc管理器来管理malloc和mmap分配的内存。
其中就像垃圾工人一样,对free后的chunk实现“垃圾分类”,这些chunk分为四个大类的bin。也就是说,bin实际上就是被free后丢给ptmalloc进行管理的chunk
每一类bin中会有不同的内存
再次申请内存的时候,管理器就会从这里面找到一块合适的给出来bin是用来存储空闲chunk的
对于 small bins,large bins,unsorted bin 来说,ptmalloc 将它们维护在同一个数组中。也就是这个数组将这三个bin关联起来

bins[0] = bin1的fd/bin2的pre_size
bins[1] = bin1的bk/bin2的size
bins[2] = bin2的fd/bin3的pre_size
bins[4] = bin2的bk/bin3的size

此处说明了在这三类bin中,数据是复用的,前一个bin和后一个bin同时使用一个数据,但是因为要索引链表,所以说,实际上存储的还是前一个bin的fd和bk
所以说,三类bin在数组的分布如下:

索引0-2:unsorted bin
2-63:small bin。同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
后面:large bin

同时任意两个物理相邻的空闲 chunk 不能在一起。

(1)fastbin

向系统申请一些小内存时,避免释放时发现有相邻的chunk合并后,再申请小内存,又要分配,这样很麻烦,所以单独拿fastbin来处理小内存
所以为了高效利用,采用单向链表的形式来关联每一个bin,采取先进后出的方式,后进的chunk会先被分配。
向系统申请内存时,ptmalloc会判断是不是小于fastbin的最大大小,如果是,直接从fastbin中取出来了
默认情况下(32 位系统为例), fastbin 中默认支持最大的 chunk 的数据空间大小为 64 字节。但是其可以支持的 chunk 的数据空间最大为 80 字节。除此之外, fastbin 最多可以支持的 bin 的个数为 10 个,从数据空间为 8 字节开始一直到 80 字节(注意这里说的是数据空间大小,也即除去 prev_size 和 size 字段部分的大小)

(2)unsorted bin

像buffer缓冲区,超过fast bins的会被加入到这里,这是双向链表,如果在申请时unsorted bin没有空间,就在top chunk里面切割
就在之前所说数组的第0和第1位

(3)small bin

small bins 中每个 chunk 的大小与其所在的 bin 的 index 的关系为:chunk_size = 2 * SIZE_SZ *index
也就是之前提到bins的数组

bins数组索引32位size_z :464位size_t:8
21632
32448
43264
635041008

small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致。比如对于 32 位系统来说,下标 2 对应的双向链表中存储的 chunk 大小为均为 16 字节。此外,small bins 中每个 bin 对应的链表采用先进先出,所以同一个链表中先被释放的 chunk 会先被分配出去。

(4)large bin

large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内。此外,这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致
在这里插入图片描述

6.内存分配流程

malloc申请,获取arena锁,如果失败就开辟新的non_main_arena计算实际的chunk_size,属于fast_bin范围就从fast_bin取chunk,超过了就看是否属于small_bin范围,然后合并fast chunk并连接到unsorted_bin,在unsorted_bin取,如果失败了,根据这个chunk放到small或者large里,如果又失败,从top chunk中分内存,如果失败,如果这个chunk是main_arena,就用sbrk()增加top chunk的大小,如果不是main,看chunk大小是否在mmap阈值,不超过就要创建新的堆,增加大小,如果超过,就直接映射内存,获得了这一块内存
在这里插入图片描述

7.内存释放流程

在这里插入图片描述

先判断传入指针是不是0,然后判断mmap_chunk的size是否小于maxfast,如果是的话看是否和top chunk相邻,如果和top chunk相邻放入fast bin,如果否,看是不是空闲,是的话就合并,不是的话看下一块是不是top chunk,是的话和top chunk合并,不是的话下一块是否空闲,,就合并放入unsorted bin,判断是否大于fast bin阈值,如果大于,就会合并,放入unsorted,看top chunk是否过大,大于mmap阈值,会归还部分top chunk给内核

结语

看堆的wiki和blog看得一头雾水,省略了看不懂的…
知道名词就算成功
参考资料:
heap tutorial
wiki
堆利用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值