仅供个人学习参考

0

堆块大小

chunk的大小(至少)都是8字节对齐,所以mchunk_ size的 低3位固定为0 (8D= 1000B)。为了充分利用内存空间,mchunk_ size的低3位分别存储PREV_ INUS、IS_MMAPPED、NON_MAIN_ARENA信息。NON_MAIN_ARENA用来记录当前chunk是否不属于主线程,1表示不属于,0表示属于。IS_ MAPPED用来记录当前chunk是否是由mmap分配的。 PRE_INUSE用来记录前个chunk块是否被分配, 如果与当前chunk向 上相邻的chunk为被释放的状 态,则PREV_ INUSE标志位为0。

32位:
用户分配到的最小堆块是17B: pre_size(4B) + size(4B) + fd(4B) + bk(4B) + pre_inuse (1B)
若用户申请的大小超过最小堆块大小,会与8B对齐(2size_sz)
64位:
用户分配到的最小堆块是33B: pre_size(8B) + size(8B) + fd(8B) + bk(8B) + pre_inuse (1B)
若用户申请的大小超过最小堆块大小,会与16B对齐(2
size_sz)

size_sz 32位为4B,64位为8B

1

空间复用

当一个chunk正在使用中,其下一个chunk的pre_size无效,可以被当前chunk使用

ida中常见指针形式

*(point) <==> [point] 伪代码<==>汇编

堆申请内存本质

1

free函数

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

bin

·与内存回收相关的概念,用户free掉的chunk并不是直接归还给操作系统,而是交由堆管理器(这里先谈ptmalloc)来管理,从而避免多次进行系统调用浪费大量资源
·ptmalloc将相似大小的chunk用双向链表连接起来,这样的一个链表被称为bin
·ptmalloc一共维护了128个bin,并使用一个数组来存储这些bin,如图
1
·堆管理器根据特点,将bin分为四类: fast bin | unsorted bin | small bin | large bin
·数组中 bin1 为 unsorted bin;bin2到63 为small bin;bin64到126为large bin

fast bin

包含chunk大小:16B 24B … 80B
fast bin只使用了fd,为单链表结构
fast bin 不会主动进行合并
fastbinY为管理fast bin的数组,每个成员分别管理不同大小的fast bin
当用户申请大小小于或等于 fastbin_MAXSIZE时,优先从fast bin 中找,遵循LIFO

small bin

当用户申请大小大于 fastbin_MAXSIZE时,考虑small bin
small bin 为双链表结构,FIFO
当满足small bin条件的chunk被free后,会优先放入unsorted bin,在一定情况下才会分配到small bin
相邻free chunk会合并

unsorted bin

双链表结构,对chunk大小没有限制
当fast bin 和 small bin 中的chunk都不满足要求时,会优先考虑unsorted bin,它会在分配large bin之前对堆中的碎片chunk进行合并,以减少堆中的碎片

top chunk

程序第一次进行 malloc 的时候,heap 会被分为两块,一块给用户,剩下的那块就是 top chunk。其实,所谓的 top chunk 就是处于当前堆的物理地址最高的 chunk。这个 chunk 不属于任何一个 bin,它的作用在于当所有的 bin 都无法满足用户请求的大小时,如果其大小不小于指定的大小,就进行分配,并将剩下的部分作为新的 top chunk。否则,就对 heap 进行扩展后再进行分配。在 main arena 中通过 sbrk 扩展 heap,而在 thread arena 中通过 mmap 分配新的 heap。

需要注意的是,top chunk 的 prev_inuse 比特位始终为 1,否则其前面的 chunk 就会被合并到 top chunk 中。

初始情况下,我们可以将 unsorted chunk 作为 top chunk。

last remainder

在用户使用 malloc 请求分配内存时,ptmalloc2 找到的 chunk 可能并不和申请的内存大小一致,这时候就将分割之后的剩余部分称之为 last remainder chunk ,unsort bin 也会存这一块。top chunk 分割剩下的部分不会作为 last remainder


只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系 ,故上面的内存分配的是虚拟内存

ptmalloc2主要通过malloc和free来分配和释放内存块

malloc

在 glibc 的 malloc.c 中,malloc 的说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
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返回对应大小的指针,同时对一些异常情况做了处理
1.n=0时,返回当前系统允许的最小内存块
2.n<0时,由于n为无符号整数,此时会申请一个很大的内存块,而一般没有这么大内存块可申请,会报错

free

在 glibc 的 malloc.c 中,free 的说明如下:

1
2
3
4
5
6
7
8
9
10
/*
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 得到的。
同样对一些异常情况做了处理
1.当 p 为空指针时,函数不执行任何操作。
2.当 p 已经被释放之后,再次释放会出现乱七八糟的效果,这其实就是 double free。
除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

内存分配背后的系统调用

操作系统提供brk,glibc库提供sbrk,我们可以通过增加 brk 的大小来向操作系统申请内存。
初始时,堆的起始地址 start_brk 以及堆的当前末尾 brk 指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同
1.不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。
2.开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处

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

多线程支持

在原来的 dlmalloc 实现中,当两个线程同时要申请内存时,只有一个线程可以进入临界区申请内存,而另外一个线程则必须等待直到临界区中不再有线程。这是因为所有的线程共享一个堆。在 glibc 的 ptmalloc 实现中,比较好的一点就是支持了多线程的快速访问。在新的实现中,所有的线程共享多个堆。

arena

虽然程序可能只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存分配给程序。我们称这一块连续的内存区域为 arena。此外,我们称由主线程申请的内存为 main_arena。后续的申请的内存会一直从这个 arena 中获取,直到空间不足。当 arena 空间不足时,它可以通过增加 brk 的方式来增加堆的空间。类似地,arena 也可以通过减小 brk 来缩小自己的空间。

在主线程释放内存后,其对应的 arena 并没有进行回收,而是交由 glibc 来进行管理。当后面程序再次申请内存时,在 glibc 中管理的内存充足的情况下,glibc 就会根据堆分配的算法来给程序分配相应的内存。

brk&sbrk函数 (摘自评论区 作者:载酒)

【定义】
对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,brk是系统调用 而sbrk不是 ,sbrk调用了brk
内核维护一个进程控制块(PCB),该结构体中维护两个指针:start_brk和brk,他们分别表示堆块的开始地址和结束地址
【函数使用】
sbrk函数
定义:传参0时可以获取当前brk指针的值,传参num时可以将当前brk指针的值增加拓展num字节(传参整数是增加,传参负数是减少)
返回值:若成功,brk()会返回0,否则返回-1。
brk函数
定义:可以改变brk指针内存储的地址(即堆结束地址,又叫堆顶),传参多少就把brk设置为多少
返回值:若成功,brk()会返回0,否则返回-1。
示例
同样是将堆地址拓展4096字节,有以下两种写法
● 1、使用sbrk函数进行拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
#include <stdio.h>

int main(){
void original_brk = (void*)sbrk(0); //获取当前堆顶地址
printf("Original break: %p\n",original_brk);

if(sbrk(4096) != (void*)-1){ //扩展堆顶地址4096字节
void* updated_brk = (void*)sbrk(0); //获取更新过后的堆顶地址
printf("updated break: %p\n",updated_brk);
}else{
printf("failed to update break\n")
}
return 0;
}

2、使用brk函数进行拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
#include <stdio.h>

int main(){
void new_brk = (void*)sbrk(0); //获取当前堆顶地址
printf("Original break: %p\n",new_brk);

if(brk(new_brk+4096)==0){ //扩展堆顶地址4096字节
void* updated_brk = (void*)sbrk(0); //获取更新过后的堆顶地址
printf("updated break: %p\n",updated_brk);
}else{
printf("failed to update break\n")
}
return 0;
}