本篇文章摘自华庭大佬的Glibc 内存管理一书,只是插入了一些记录

什么是堆

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

堆管理器处于用户程序与内核中间,主要做以下工作

  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.c 中,malloc 的说明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
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个字节大小的内存块,并返回指向该内存块的指针。如果没有足够的空间可用,则返回空指针。在失败的情况下,对于 ANSI C 系统,errno 会被设置为 ENOMEM。

如果 n 为零,则malloc返回一个最小尺寸的内存块。在大多数 32 位系统上,最小尺寸为 16 个字节,在 64 位系统上为 24 或 32 个字节。在大多数系统中,size_t 是无符号类型,因此带有负参数的调用会被解释为对大量空间的请求,通常会失败。n 的最大支持值因系统而异,但在所有情况下都小于 size_t 可表示的最大值
*/

可以看出,malloc 函数返回对应大小字节的内存块的指针。此外,该函数还对一些异常情况进行了处理

  • 当 n=0 时,返回当前系统允许的堆的最小内存块。
  • 当 n 为负数时,由于在大多数系统上,size_t 是无符号数(这一点非常重要),所以程序就会申请很大的内存空间,但通常来说都会失败,因为系统没有那么多的内存可以分配。

free

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
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的内存块,该内存块之前使用malloc或类似的realloc例程分配。如果p为null,则没有影响。如果p已经被释放,可能会产生任意(即不好的)影响。除非禁用(使用mallopt),释放非常大的空间将在可能的情况下自动触发操作,将未使用的内存返回给系统,从而减少程序占用空间。
*/

可以看出,free 函数会释放由 p 所指向的内存块。这个内存块有可能是通过 malloc 函数得到的,也有可能是通过相关的函数 realloc 得到的。

此外,该函数也同样对异常情况进行了处理

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

内存分配背后的系统调用

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

如下图所示,我们主要考虑对堆进行申请内存块的操作。

img

(s)brk

对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,我们可以通过增加 brk 的大小来向操作系统申请内存。

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

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

具体效果如下图(这个图片与网上流传的基本一致,这里是因为要画一张大图,所以自己单独画了下)所示

img

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* sbrk and brk example */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
void *curr_brk, *tmp_brk = NULL;

printf("Welcome to sbrk example:%d\n", getpid()); //输出当前进程的PID(进程标识符)

/* sbrk(0) gives current program break location */
tmp_brk = curr_brk = sbrk(0); //使用 sbrk(0) 获取当前程序堆区的边界
printf("Program Break Location1:%p\n", curr_brk);
getchar();

/* brk(addr) increments/decrements program break location */
brk(curr_brk+4096); // 调用 brk(curr_brk+4096) 来将程序的堆区边界地址向上移动 4096 字节(4KB)。brk 函数设置新的 break 的位置。

curr_brk = sbrk(0);
printf("Program break Location2:%p\n", curr_brk);
getchar();

brk(tmp_brk); //调用 brk(tmp_brk) 将程序的堆区边界重置回最初的位置

curr_brk = sbrk(0);
printf("Program Break Location3:%p\n", curr_brk);
getchar();

return 0;
}

需要注意的是,在每一次执行完操作后,都执行了 getchar() 函数,这是为了我们方便我们查看程序真正的映射。

在第一次调用 brk 之前

从下面的输出可以看出,并没有出现堆。因此

  • start_brk = brk = end_data = 0x804b000
1
2
3
4
5
6
7
8
9
10
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ ./sbrk
Welcome to sbrk example:6141
Program Break Location1:0x804b000
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ cat /proc/6141/maps
...
0804a000-0804b000 rw-p 00001000 08:01 539624 /home/sploitfun/ptmalloc.ppt/syscalls/sbrk
b7e21000-b7e22000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$

第一次增加 brk 后

从下面的输出可以看出,已经出现了堆段

  • start_brk = end_data = 0x804b000
  • brk = 0x804c000
1
2
3
4
5
6
7
8
9
10
11
12
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ ./sbrk
Welcome to sbrk example:6141
Program Break Location1:0x804b000
Program Break Location2:0x804c000
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ cat /proc/6141/maps
...
0804a000-0804b000 rw-p 00001000 08:01 539624 /home/sploitfun/ptmalloc.ppt/syscalls/sbrk
0804b000-0804c000 rw-p 00000000 00:00 0 [heap]
b7e21000-b7e22000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$

其中,关于堆的那一行

  • 0x0804b000 是相应堆的起始地址
  • rw-p 表明堆具有可读可写权限,并且属于隐私数据。
  • 00000000 表明文件偏移,由于这部分内容并不是从文件中映射得到的,所以为 0。
  • 00:00 是主从 (Major/mirror) 的设备号,这部分内容也不是从文件中映射得到的,所以也都为 0。
  • 0 表示着 Inode 号。由于这部分内容并不是从文件中映射得到的,所以为 0。

mmap

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

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Private anonymous mapping example using mmap syscall */
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

void static inline errExit(const char* msg)
{
printf("%s failed. Exiting the process\n", msg);
exit(-1);
}

int main()
{
int ret = -1;
printf("Welcome to private anonymous mapping example::PID:%d\n", getpid());
printf("Before mmap\n");
getchar();
char* addr = NULL;
addr = mmap(NULL, (size_t)132*1024, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");
printf("After mmap\n");
getchar();

/* Unmap mapped region. */
ret = munmap(addr, (size_t)132*1024);
if(ret == -1)
errExit("munmap");
printf("After munmap\n");
getchar();
return 0;
}

在执行 mmap 之前

我们可以从下面的输出看到,目前只有. so 文件的 mmap 段。

1
2
3
4
5
6
7
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ cat /proc/6067/maps
08048000-08049000 r-xp 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
08049000-0804a000 r--p 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
0804a000-0804b000 rw-p 00001000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
b7e21000-b7e22000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$

mmap 后

从下面的输出可以看出,我们申请的内存与已经存在的内存段结合在了一起构成了 b7e00000 到 b7e21000 的 mmap 段。

1
2
3
4
5
6
7
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ cat /proc/6067/maps
08048000-08049000 r-xp 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
08049000-0804a000 r--p 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
0804a000-0804b000 rw-p 00001000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
b7e00000-b7e22000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$

munmap

从下面的输出,我们可以看到我们原来申请的内存段已经没有了,内存段又恢复了原来的样子了。

1
2
3
4
5
6
7
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$ cat /proc/6067/maps
08048000-08049000 r-xp 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
08049000-0804a000 r--p 00000000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
0804a000-0804b000 rw-p 00001000 08:01 539691 /home/sploitfun/ptmalloc.ppt/syscalls/mmap
b7e21000-b7e22000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/syscalls$

多线程支持

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

这里给出一个例子。

pthread_create 是一个函数,用于在 POSIX 线程库中创建一个新的线程。

函数原型如下:

1
2
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

参数说明:

  • thread:指向 pthread_t 类型的指针,用于存储新创建线程的标识符。
  • attr:指向 pthread_attr_t 类型的指针,用于指定线程的属性。可以传递 NULL,表示使用默认属性。
  • start_routine:指向线程函数的指针,该函数是线程的入口点,线程将从该函数开始执行。
  • arg:传递给线程函数 start_routine 的参数。

返回值:

  • 如果成功创建线程,则返回 0,表示成功。
  • 如果创建线程失败,则返回一个非零的错误码,表示失败的原因。

pthread_create 函数用于创建一个新的线程,并在指定的线程函数 start_routine 中执行。新线程的执行将从 start_routine 函数开始,该函数接受一个 void* 类型的参数 arg。线程函数可以执行任意操作,包括计算、I/O 操作、同步等。

使用 pthread_create 创建的线程在执行完毕后,可以通过调用 pthread_join 函数来等待线程的结束,并获取线程的返回值。此外,还可以使用其他线程相关的函数来管理和操作线程,例如 pthread_detachpthread_cancel 等。

需要注意的是,pthread_create 函数是 POSIX 标准中定义的线程创建函数,在不同的操作系统和编译器中可能有所差异。在使用时,应仔细阅读相关文档并遵循相应的使用规范。

pthread_join 是一个函数,用于等待指定的线程结束,并获取线程的返回值。

函数原型如下:

1
int pthread_join(pthread_t thread, void **retval);

参数说明:

  • thread:要等待的线程标识符,通常由 pthread_create 返回。
  • retval:指向 void* 类型指针的指针,用于存储线程的返回值。

返回值:

  • 如果成功等待线程结束,则返回 0,表示成功。
  • 如果等待线程失败,则返回一个非零的错误码,表示失败的原因。

pthread_join 函数用于等待指定的线程结束。当调用该函数时,当前线程将被阻塞,直到被等待的线程执行完毕。在线程结束后,可以通过 retval 参数获取线程的返回值,该返回值是线程函数 start_routine 的返回值。

需要注意的是,如果线程被成功等待并成功获取返回值,那么线程的资源将被释放,不再占用系统资源。但是,如果不关心线程的返回值,可以将 retval 参数设置为 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* threadFunc(void* arg) {
printf("Before malloc in thread 1\n");
getchar();
char* addr = (char*) malloc(1000);
printf("After malloc and before free in thread 1\n");
getchar();
free(addr);
printf("After free in thread 1\n");
getchar();
}

int main() {
pthread_t t1;
void* s;
int ret;
char* addr;

printf("Welcome to per thread arena example::%d\n",getpid());
printf("Before malloc in main thread\n");
getchar();
addr = (char*) malloc(1000);
printf("After malloc and before free in main thread\n");
getchar();
free(addr);
printf("After free in main thread\n");
getchar();
ret = pthread_create(&t1, NULL, threadFunc, NULL); //
if(ret)
{
printf("Thread creation error\n");
return -1;
}
ret = pthread_join(t1, &s);
if(ret)
{
printf("Thread join error\n");
return -1;
}
return 0;
}

第一次申请之前, 没有任何任何堆段。

1
2
3
4
5
6
7
8
9
10
11
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

第一次申请后, 从下面的输出可以看出,堆段被建立了,并且它就紧邻着数据段,这说明 malloc 的背后是用 brk 函数来实现的。同时,需要注意的是,我们虽然只是申请了 1000 个字节,但是我们却得到了 0x0806c000-0x0804b000=0x21000 个字节的堆。这说明虽然程序可能只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。我们称这一块连续的内存区域为 arena。此外,我们称由主线程申请的内存为 main_arena。后续的申请的内存会一直从这个 arena 中获取,直到空间不足。当 arena 空间不足时,它可以通过增加 brk 的方式来增加堆的空间。类似地,arena 也可以通过减小 brk 来缩小自己的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
...
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
...
sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e05000-b7e07000 rw-p 00000000 00:00 0
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

在第一个线程 malloc 之前,我们可以看到并没有出现与线程 1 相关的堆,但是出现了与线程 1 相关的栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

第一个线程 malloc 后, 我们可以从下面输出看出线程 1 的堆段被建立了。而且它所在的位置为内存映射段区域,同样大小也是 132KB(b7500000-b7521000)。因此这表明该线程申请的堆时,背后对应的函数为 mmap 函数。同时,我们可以看出实际真的分配给程序的内存为 1M(b7500000-b7600000)。而且,只有 132KB 的部分具有可读可写权限,这一块连续的区域成为 thread arena。

注意:

当用户请求的内存大于 128KB 时,并且没有任何 arena 有足够的空间时,那么系统就会执行 mmap 函数来分配相应的内存空间。这与这个请求来自于主线程还是从线程无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
After malloc and before free in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7500000-b7521000 rw-p 00000000 00:00 0
b7521000-b7600000 ---p 00000000 00:00 0
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

在第一个线程释放内存后, 我们可以从下面的输出看到,这样释放内存同样不会把内存重新给系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread
Welcome to per thread arena example::6501
Before malloc in main thread
After malloc and before free in main thread
After free in main thread
Before malloc in thread 1
After malloc and before free in thread 1
After free in thread 1
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps
08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7500000-b7521000 rw-p 00000000 00:00 0
b7521000-b7600000 ---p 00000000 00:00 0
b7604000-b7605000 ---p 00000000 00:00 0
b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594]
...
sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

glibc内存管理篇

5. 源代码分析

5.1 边界标记法
1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). 这个字段存储前一个内存块的大小(以字节为单位),但仅当这个内存块是“空闲”的(也就是当前没有被分配)时才有意义。*/
INTERNAL_SIZE_T mchunk_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.这个字段是为了管理较大的内存块而设置的,它是指向上一个geng'xiao内存块的指针。这个指针也只在当前内存块空闲时使用。 */
struct malloc_chunk* bk_nextsize; /* 这个字段类似于`fd_nextsize`,但它指向的是下一个更大的内存块。*/
};

prev_size:如果前一个 chunk 是空闲的,该域表示前一个 chunk 的大小,如果前一个 chunk 不空闲,该域无意义

size当前 chunk 的大小,并且记录了当前 chunk 和前一个 chunk 的一些属性,包括前一个 chunk 是否在使用中,当前 chunk 是否是通过 mmap 获得的内存,当前 chunk 是否属于 非主分配区。

fd 和 bk:指针 fd 和 bk 只有当该 chunk 块空闲时才存在,其作用是用于将对应的空闲 chunk 块加入到空闲 chunk 块链表中统一管理,如果该 chunk 块被分配给应用程序使用,那 么这两个指针也就没有用(该 chunk 块已经从空闲链中拆出)了,所以也当作应用程序的使用空间,而不至于浪费。 fd指向低地址,bk指向高地址

fd_nextsize 和 bk_nextsize:当当前的 chunk 存在于 large bins 中时,large bins 中的空闲 chunk 是按照大小排序的,但同一个大小的 chunk 可能有多个,增加了这两个字段可以加快遍历空闲 chunk,并查找满足需要的空闲 chunk,fd_nextsize 指向下一个比当前 chunk 大小 大的第一个空闲 chunkbk_nextszie 指向前一个比当前 chunk 大小小的第一个空闲 chunk如果该 chunk 块被分配给应用程序使用,那么这两个指针也就没有用(该 chunk 块已经从 size 链中拆出)了,所以也当作应用程序的使用空间,而不至于浪费。

一个已分配的块的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
chunk->  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前一个块的大小(如果已分配) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 块的大小(以字节为单位) |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 用户数据从这里开始... .
.
. (malloc_usable_size() 字节)
.
|
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 块的大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

在这里,”chunk”在大多数malloc代码中表示块的前部,而”mem”是返回给调用者的指针。块的前部包含了前一个块的大小(如果该块已分配),以及当前块的大小(以字节为单位)。”M”和”P”是用于表示块的状态(分配或空闲)的位标志。”mem”指针指向的位置是用户数据的起始位置。下一个块的位置由”nextchunk”指针表示,并且包含了下一个块的大小信息。

空闲块以循环双向链表的形式存储,结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
chunk->  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前一个块的大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | 块的大小(以字节为单位) |P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 指向链表中下一个块的前向指针 |fd
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 指向链表中前一个块的后向指针 |bk
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 未使用的空间(可能为0字节) .
.
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | 块的大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

​ 在这里,空闲块的结构与已分配块的结构类似,但有一些差别。空闲块的前部包含了前一个块的大小。head:标记表示这是一个空闲块的头部。在mem指针指向的位置,存储了块的大小信息和状态标志(P表示空闲)。空闲块还包含了指向链表中下一个块和前一个块的指针,形成了循环双向链表的结构。链表的头部指针指向第一个空闲块。空闲块的尾部有一个foot:标记,标识块的大小

P(PREV_INUSE)位存储在chunk大小的未使用的低位上(chunk大小总是二字倍数),它是用于标记前一个chunk是否在使用的一个位。如果该位被清除,则当前chunk大小之前的字包含了前一个chunk的大小,并且可以用来找到上一个chunk的开头。第一次分配的chunk总是设置了这个位,这样可以防止访问不存在的(或不属于自己的)内存。如果任何给定的chunk的prev_inuse被设置,则你无法确定前一个chunk的大小,甚至在尝试这样做时可能会遇到内存寻址错误。

​ 值得注意的是,当前chunk的foot实际上表示为下一个chunk的prev_size。这样做可以更容易地处理对齐等问题,但在尝试扩展或适应这段代码时可能会非常困惑。

​ 所有这些都有两个异常情况:

  1. 特殊的chunk 顶端(top)并不使用它的后续大小字段,因为没有紧接着的下一个chunk需要依赖它的索引。在初始化后,顶端chunk强制被设置为一直存在。如果它变得小于MINSIZE字节,则会被重新填充。
  2. 通过mmap分配的chunks,在其大小字段中设置了第二低位的M(IS_MMAPPED)位。因为它们是单独一个一个分配的,每一个都必须包含自己的后续大小字段。
1
2
3
4
5
6
7
8
9
10
11
12
13
/* conversion from malloc headers to user pointers, and back */
#define chunk2mem(p) ((Void_t*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
/* The smallest size we can malloc is an aligned minimal chunk */
#define MINSIZE
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
/* Check if m has acceptable alignment */
#define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)
#define misaligned_chunk(p)
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem (p))
& MALLOC_ALIGN_MASK)

​ 对于已经分配的 chunk通过 chunk2mem 宏根据 chunk 地址获得返回给用户的内存地址,反过来通过 mem2chunk 宏根据 mem 地址得到 chunk 地址,chunk 的地址是按 2 * SIZE_SZ 对齐的,而 chunk 结构体的前两个域刚好也是 2*SIZE_SZ 大小,所以,mem 地址也是 2 * SIZE_SZ 对齐的。宏 aligned_OK 和misaligned_chunk(p)用于校验地址是否是按 2 * SIZE_SZ 对齐的。 MIN_CHUNK_SIZE 定义了最小的 chunk 的大小,32 位平台上位 16 字节,64 位平台为 24 字节或是 32 字节MINSIZE 定义了最小的分配的内存大小,是对 MIN_CHUNK_SIZE 进行了 2*SIZE_SZ 对齐,地址对齐后与 MIN_CHUNK_SIZE 的大小仍然是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
检查一个请求是否过大,以至于在填充和对齐时会绕回零。为了简化其他代码,边界被设置得足够低,以至于添加MINSIZE也不会绕回零。
*/
#define REQUEST_OUT_OF_RANGE(req)
((unsigned long)(req) >=
(unsigned long)(INTERNAL_SIZE_T)(-2 * MINSIZE))
/* 将请求的字节数填充为可用的大小 - 内部版本 */
#define request2size(req)
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)
? MINSIZE
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
/* 相同功能,同时执行参数检查 */
#define checked_request2size(req, sz)
if (REQUEST_OUT_OF_RANGE(req)) {
MALLOC_FAILURE_ACTION;
return 0;
}
(sz) = request2size(req);
*/

这几个宏用于将用户请求的分配大小转换成内部需要分配的 chunk 大小,这里需要注意 的在转换时不但考虑的地址对齐,还额外加上了 SIZE_SZ,这意味着 ptmalloc 分配内存需要 一个额外的overhead,为SIZE_SZ字节,通过chunk的空间复用,我们很容易得出这个overhead 为 SIZE_SZ。

wiki: 当一个 chunk 处于已分配状态时,它的物理相邻的下一个 chunk 的 prev_size 字段必然是无效的,故而这个字段就可以被当前这个 chunk 使用。这就是 ptmalloc 中 chunk 间的复用。具体流程如下

  1. 首先,利用 REQUEST_OUT_OF_RANGE 判断是否可以分配用户请求的字节大小的 chunk。
  2. 其次,需要注意的是用户请求的字节是用来存储数据的,即 chunk header 后面的部分。与此同时,由于 chunk 间复用,所以可以使用下一个 chunk 的 prev_size 字段。因此,这里只需要再添加 SIZE_SZ 大小即可以完全存储内容。
  3. 由于系统中所允许的申请的 chunk 最小是 MINSIZE,所以与其进行比较。如果不满足最低要求,那么就需要直接分配 MINSIZE 字节。
  4. 如果大于的话,因为系统中申请的 chunk 需要 2 * SIZE_SZ 对齐,所以这里需要加上 MALLOC_ALIGN_MASK 以便于对齐。

以 Linux X86_64 平台为例,假设 SIZE_SZ 为 8 字节,空闲时,一个 chunk 中至少要 4 个 size_t(8B)大小的空间,用来存储 prev_size,size,fd 和 bk,也就是 MINSIZE(32B),chunk 的大小要对齐到 2 * SIZE_SZ(16B)。当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域肯定是无效的。所以实际上,这个空间也可以被当前 chunk 使用。这听起来有点 不可思议,但确实是合理空间复用的例子。故而实际上,一个使用中的 chunk 的大小的计算公式应该是:in_use_size = (用户请求大小+ 16 - 8 ) align to 8B,这里加 16 是因为需要存储 prev_size 和 size,但又因为向下一个 chunk“借”了 8B,所以要减去 8,每分配一个 chunk 的 overhead 为 8B,即 SIZE_SZ 的大小。最后,因为空闲的 chunk 和使用中的 chunk 使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即**最终的分配空间 chunk_size = max(in_use_size, 32)**。这就是当用户请求内存分配时,ptmalloc 实际需要分配的内存大小。

注意:如果 chunk 是由 mmap ()直接分配的,则该 chunk 不会有前一个 chunk 和后一个 chunk,所有本 chunk 没有下一个 chunk 的 prev_size 的空间可以“借”,所以对于直接 mmap() 分配内存的 overhead 为 2 * SIZE_SZ,因为每个mmap()分配的内存块都有一个额外的头部和尾部,用于管理和维护内存的相关信息。

对于使用mmap()直接分配的内存块,其内存布局通常是独立的,与其他内存块没有前后关系。因此,这些内存块不会有前一个块或后一个块,也就无法利用前一个块的prev_size字段来存储额外的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 当前一个相邻的内存块正在使用时,将 size 字段与 PREV_INUSE 进行按位或操作 */
#define PREV_INUSE 0x1
/* 提取前一个内存块的使用位 */
#define prev_inuse(p) ((p)->size & PREV_INUSE)
/* 当内存块是通过 mmap() 函数获取时,将 size 字段与 IS_MMAPPED 进行按位或操作 */
#define IS_MMAPPED 0x2
/* 检查内存块是否是通过 mmap() 函数获取的 */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
/* 当内存块来自非主要堆区时,将 size 字段与 NON_MAIN_ARENA 进行按位或操作。
这仅在必要时在将内存块交给用户之前设置。 */
#define NON_MAIN_ARENA 0x4
/*检查内存块是否来自非主要堆区 */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

解释:

  1. PREV_INUSE(0x1):用于表示前一个相邻内存块是否正在使用的标志位。当前一个内存块处于使用状态时,将该标志位设置为1。
  2. prev_inuse(p):用于提取给定内存块(p)的前一个内存块的使用状态。通过与PREV_INUSE进行按位与操作,可以获取前一个内存块的使用状态。
  3. IS_MMAPPED(0x2):用于表示内存块是否是通过mmap()函数获得的标志位。当内存块是通过mmap()函数分配的时,将该标志位设置为1。
  4. chunk_is_mmapped(p):用于检查给定内存块(p)是否是通过mmap()函数分配的内存块。通过与IS_MMAPPED进行按位与操作,可以判断内存块是否是通过mmap()函数分配的。
  5. NON_MAIN_ARENA(0x4):用于表示内存块是否来自非主要堆区(non-main arena)的标志位。非主要堆区是glibc中用于管理多个堆的机制,当内存块来自非主要堆区时,将该标志位设置为1。
  6. chunk_non_main_arena(p):用于检查给定内存块(p)是否来自非主要堆区。通过与NON_MAIN_ARENA进行按位与操作,可以判断内存块是否来自非主要堆区。

chunk 在分割时总是以地址对齐(默认是 8 字节,可以自由设置,但是 8 字节是最小值 并且设置的值必须是 2 为底的幂函数值,即是 alignment = 2^n,n 为整数且 n>=3)的方式来 进行的,所以用 chunk->size 来存储本 chunk 块大小字节数的话,其末 3bit 位总是 0,因此 这三位可以用来存储其它信息,比如:

以第 0 位作为 P 状态位,标记前一chunk 块是否在使用中,为 1 表示使用,为 0 表示空闲

以第 1 位作为 M 状态位,标记本 chunk 块是否是使用 mmap()直接从进程的 mmap 映射区域分配的为 1 表示是,为 0 表示否。

以第 2 位作为 A 状态位,标记本 chunk 是否属于非主分配区为 1 表示是,为 0 表示 否

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Bits to mask off when extracting size
Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
/* Get size, ignoring use bits */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)( ((char*)(p)) + ((p)->size & ~SIZE_BITS) ))
/* Ptr to previous physical malloc_chunk */
#define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_size) ))
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
提取大小时要屏蔽的位
注意:在那些不应该看到 mmapped 块的宏中,意图上并没有从大小字段中屏蔽 IS_MMAPPED。
如果由于意外而尝试这样做,这应该导致有用的核心转储发生,以帮助扩展或调整此 malloc 实现的人员。
*/
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
/* 获取大小,忽略使用位 */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
/* 指向下一个物理 malloc_chunk 的指针 */
#define next_chunk(p) ((mchunkptr)( ((char)(p)) + ((p)->size & ~SIZE_BITS) ))
/* 指向前一个物理 malloc_chunk 的指针 */
#define prev_chunk(p) ((mchunkptr)( ((char)(p)) - ((p)->prev_size) ))
/* 将 ptr + offset 处的空间视为一个块 */
#define chunk_at_offset(p, s) ((mchunkptr)(((char)(p)) + (s)))

这些宏的作用如下:

  1. SIZE_BITS用于屏蔽提取大小时不需要的位。其中包括 PREV_INUSEIS_MMAPPEDNON_MAIN_ARENA。这些位用于标记内存块的状态和属性。
  2. chunksize(p)用于获取给定内存块(p)的大小。通过将内存块的 size 字段与 SIZE_BITS 进行按位与操作,可以得到实际的内存块大小,忽略了使用位。
  3. next_chunk(p)用于获取给定内存块(p)的下一个物理 malloc_chunk 的指针。通过将内存块的地址加上内存块的大小(忽略了使用位),可以得到下一个物理内存块的地址。
  4. prev_chunk(p)用于获取给定内存块(p)的前一个物理 malloc_chunk 的指针。通过将内存块的地址减去前一个内存块的 prev_size 字段的值,可以得到前一个物理内存块的地址。
  5. chunk_at_offset(p, s)将给定内存块(p)的地址加上偏移量 s,将结果视为一个块的指针。这个宏用于在指定偏移量处处理内存块。

prev_size 字段虽然在当前 chunk 块结构体内,记录的却是前一个邻接 chunk 块的信息, 这样做的好处就是我们通过本块 chunk 结构体就可以直接获取到前一 chunk 块的信息,从而 方便做进一步的处理操作。相对的,当前 chunk 块的 foot 信息就存在于下一个邻接 chunk 块的结构体内。字段 prev_size 记录的什么信息呢?有两种情况:

1)如果前一个邻接 chunk 块空闲,那么当前 chunk 块结构体内的 prev_size 字段记录的 是前一个邻接 chunk 块的大小。这就是由当前 chunk 指针获得前一个空闲 chunk 地址的依据。 宏 prev_chunk(p)就是依赖这个假设实现的。

2)如果前一个邻接 chunk 在使用中,则当前 chunk 的 prev_size 的空间被前一个 chunk 借用中,其中的值是前一个 chunk 的内存内容,对当前 chunk 没有任何意义。

字段 size 记录了本 chunk 的大小,无论下一个 chunk 是空闲状态或是被使用状态,都可以通过本 chunk 的地址加上本 chunk 的大小,得到下一个 chunk 的地址,由于 size 的低 3 个 bit 记录了控制信息,需要屏蔽掉这些控制信息,取出实际的 size 在进行计算下一个 chunk 地址,这是 next_chunk(p)的实现原理。

浅浅理解:标志位是不占用内存大小的,实际size包含例如pre_size之类的管理信息

宏 chunksize(p)用于获得 chunk 的实际大小,需要屏蔽掉 size 中的控制信息。

宏 chunk_at_offset(p, s)将 p+s 的地址强制看作一个 chunk。

注意:按照边界标记法,可以有多个连续的并且正在被使用中的 chunk 块,但是不会有 多个连续的空闲 chunk 块,因为连续的多个空闲 chunk 块一定会合并成一个大的空闲 chunk 块。

补充:合并空闲块的目标通常是为了提高内存利用率和减少内存碎片化

1
2
3
4
5
6
7
8
/* 提取 p 的 inuse 位 */
#define inuse(p)
((((mchunkptr)(((char)(p))+((p)->size & ~SIZE_BITS)))->size) & PREV_INUSE)
/* 设置/清除块的 inuse 位,同时不影响其他属性 */
#define set_inuse(p)
((mchunkptr)(((char)(p)) + ((p)->size & ~SIZE_BITS)))->size |= PREV_INUSE
#define clear_inuse(p)
((mchunkptr)(((char*)(p)) + ((p)->size & ~SIZE_BITS)))->size &= ~(PREV_INUSE)
  • inuse(p) 宏用于提取给定内存块 p 的 inuse 位。通过对 p 进行位运算和指针偏移,获取下一个内存块的起始位置,并从其 size 字段中提取出 PREV_INUSE 位的值,以确定 p 是否被标记为已使用。
  • set_inuse(p)用于将给定内存块 p 标记为已使用,而不影响其他属性。通过对 p 进行位运算和指针偏移,获取下一个内存块的起始位置,并将其 size 字段中的 PREV_INUSE 位置为 1,表示该内存块已被使用。
  • clear_inuse(p)用于将给定内存块 p 标记为未使用,而不影响其他属性。通过对 p 进行位运算和指针偏移,获取下一个内存块的起始位置,并将其 size 字段中的 PREV_INUSE 位清零,表示该内存块未被使用。

上面的这一组宏用于 check/set/clear 当前 chunk 使用标志位,当前 chunk 的使用标志位存储在下一个 chunk 的 size 的第 0 bit(P 状态位),所以首先要获得下一个 chunk 的地址, 然后 check/set/clear 下一个 chunk 的 size 域的第 0 bit。

1
2
3
4
5
6
7
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size & PREV_INUSE)
#define set_inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size |= PREV_INUSE)
#define clear_inuse_bit_at_offset(p, s)
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))

上面的三个宏用于 check/set/clear 指定 chunk 的 size 域中的使用标志位

1
2
3
4
5
6
/* Set size at head, without disturbing its use bit */
#define set_head_size(p, s) ((p)->size = (((p)->size & SIZE_BITS) | (s)))
/* Set size/use field */
#define set_head(p, s) ((p)->size = (s))
/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s) (((mchunkptr)((char*)(p) + (s)))->prev_size = (s))

宏 set_head_size(p, s)用于设置当前 chunk p 的 size 域并保留 size 域的控制信息。宏 set_head(p, s) 用于设置当前 chunk p 的 size 域并忽略已有的 size 域控制信息。宏 set_foot(p, s)用于设置当前 chunk p 的下一个 chunk 的 prev_size 为 s,s 为当前 chunk 的 size,只有当 chunk p 为空闲时才能使用这个宏当前 chunk 的 foot 的内存空间存在于下一个 chunk,即下一个 chunk 的 prev_size。

5.2 分箱式内存管理

​ 对于空闲的 chunk,ptmalloc 采用分箱式内存管理方式,根据空闲 chunk 的大小和处于的状态将其放在四个不同的 bin 中,这四个空闲 chunk 的容器包括 fast bins,unsorted bin, small bins 和 large binsFast bins小内存块的高速缓存,当一些大小小于 64 字节的 chunk 被回收时首先会放入 fast bins 中,在分配小内存时首先会查看 fast bins 中是否有合适的 内存块,如果存在,则直接返回 fast bins 中的内存块,以加快分配速度。Usorted bin 只有一个回收的 chunk 块必须先放到 unsorted bin 中分配内存时会查看 unsorted bin 中是否有合适的 chunk,如果找到满足条件的 chunk,则直接返回给用户,否则将 unsorted bin 的所有 chunk 放入 small bins 或是 large bins 中。Small bins 用于存放固定大小的 chunk,共 64 个 bin,最小的 chunk 大小为 16 字节或 32 字节,每个 bin 的大小相差 8 字节或是 16 字节,当 分配小内存块时,采用精确匹配的方式从 small bins 中查找合适的 chunk。Large bins 用于存 储大于等于 512B 或 1024B 的空闲 chunk,这些 chunk 使用双向链表的形式按大小顺序排序, 分配内存时按最近匹配方式从 large bins 中分配 chunk

问题小结

问题:为什么bin数组在遍历的时候会往前跨两个地址,并且将bin元素强转成chunk指针类型?

解答:因为chunk指针指向chunk,如果直接使用bin数组的fd和bk来寻址,那么链表会断掉,因为类型不符合

太强了!! 强转很重要!!!!

bin 通用的宏如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct malloc_chunk *mbinptr;

/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i)
(mbinptr)(((char *) &((m)->bins[ ((i) -1) * 2 ])) - //这里往前跨两个且强转
offsetof(struct malloc_chunk, fd))

/* analog of ++bin */
//获取下一个bin的地址
#define next_bin(b) ((mbinptr)((char *) (b) + (sizeof(mchunkptr) << 1)))

/* Reminders about list directionality within bins */
// 这两个宏可以用来遍历bin
// 获取 bin 的位于链表头的 chunk
#define first(b) ((b)->fd)
// 获取 bin 的位于链表尾的 chunk
#define last(b) ((b)->bk)
5.2.1 Small bins

​ ptmalloc使用small bins管理空闲小chunk,每个small bin中的chunk的大小与bin的index 有如下关系: Chunk_size=2 * SIZE_SZ * index

​ 在 SIZE_SZ 为 4B 的平台上,small bins 中的 chunk 大小是以 8B 为公差的等差数列,最大 的 chunk 大小为 504B,最小的 chunk 大小为 16B,所以实际共 62 个 bin。分别为 16B、24B、 32B,„„,504B。在 SIZE_SZ 为 8B 的平台上,small bins 中的 chunk 大小是以 16B 为公差的等差数列,最大的 chunk 大小为 1008B,最小的 chunk 大小为 32B,所以实际共 62 个 bin。 分别为 32B、48B、64B,…… ,1008B

ptmalloc 维护了 62 个双向环形链表(每个链表都具有链表头节点,加头节点的最大作用就是便于对链表内节点的统一处理,即简化编程),每一个链表内的各空闲 chunk 的大小一致,因此当应用程序需要分配某个字节大小的内存空间时直接在对应的链表内取就可以了, 这样既可以很好的满足应用程序的内存空间申请请求而又不会出现太多的内存碎片。我们可 以用如下图来表示在 SIZE_SZ 为 4B 的平台上 ptmalloc 对 512B 字节以下的空闲 chunk 组织方 式(所谓的分箱机制)

image-20231219202911816

5.2.2 Large bins

​ 在 SIZE_SZ 为 4B 的平台上,大于等于 512B 的空闲 chunk,或者,在 SIZE_SZ 为 8B 的平 台上,大小大于等于 1024B 的空闲 chunk,由 sorted bins 管理。Large bins 一共包括 63 个 bin每个 bin 中的 chunk 大小不是一个固定公差的等差数列,而是分成 6 组 bin每组 bin 是一个 固定公差的等差数列,每组的 bin 数量依次为 32、16、8、4、2、1公差依次为 64B、512B、 4096B、32768B、262144B 等

​ 以 SIZE_SZ 为 4B 的平台为例,第一个 large bin 的起始 chunk 大小为 512B,共 32 个 bin, 公差为 64B,等差数列满足如下关系:

Chunk_size=512 + 64 * index

​ 第二个 large bin 的起始 chunk 大小为第一组 bin 的结束 chunk 大小,满足如下关系:

Chunk_size=512 + 64 * 32 + 512 * index

​ 同理,我们可计算出每个 bin 的起始 chunk 大小和结束 chunk 大小。这些 bin 都是很有规律的,其实 small bins 也是满足类似规律,small bins 可以看着是公差为 8 的等差数列,一 共有 64 个 bin(第 0 和 1bin 不存在),所以我们可以将 small bins 和 large bins 存放在同一个包含 128 个 chunk 的数组上,数组的前一部分位 small bins,后一部分为 large bins,每个 bin 的 index 为 chunk 数组的下标,于是,我们可以根据数组下标计算出该 bin 的 chunk 大小(small bins)或是 chunk 大小范围(large bins),也可以根据需要分配内存块大小计算出所需 chunk 所属 bin 的 index,ptmalloc 使用了一组宏巧妙的实现了这种计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define NBINS 128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define MIN_LARGE_SIZE (NSMALLBINS * SMALLBIN_WIDTH)
#define in_smallbin_range(sz)
((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE)
#define smallbin_index(sz)
(SMALLBIN_WIDTH == 16 ? (((unsigned)(sz)) >> 4) : (((unsigned)(sz)) >> 3))
#define largebin_index_32(sz)
(((((unsigned long)(sz)) >> 6) <= 38)? 56 + (((unsigned long)(sz)) >> 6):
((((unsigned long)(sz)) >> 9) <= 20)? 91 + (((unsigned long)(sz)) >> 9):
((((unsigned long)(sz)) >> 12) <= 10)? 110 + (((unsigned long)(sz)) >> 12):
((((unsigned long)(sz)) >> 15) <= 4)? 119 + (((unsigned long)(sz)) >> 15):
((((unsigned long)(sz)) >> 18) <= 2)? 124 + (((unsigned long)(sz)) >> 18):
126)
// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz)
(((((unsigned long)(sz)) >> 6) <= 48)? 48 + (((unsigned long)(sz)) >> 6):
((((unsigned long)(sz)) >> 9) <= 20)? 91 + (((unsigned long)(sz)) >> 9):
((((unsigned long)(sz)) >> 12) <= 10)? 110 + (((unsigned long)(sz)) >> 12):
((((unsigned long)(sz)) >> 15) <= 4)? 119 + (((unsigned long)(sz)) >> 15):
((((unsigned long)(sz)) >> 18) <= 2)? 124 + (((unsigned long)(sz)) >> 18):
126)
#define largebin_index(sz)
(SIZE_SZ == 8 ? largebin_index_64 (sz) : largebin_index_32 (sz))
#define bin_index(sz)
((in_smallbin_range(sz)) ? smallbin_index(sz) : largebin_index(sz))

​ 注意:如果对于用户要分配的内存大小 size, 必须先使用 checked_request2size(req, sz)计算出 chunk 的大小,再使用 bin_index(sz)计算出 chunk 所属的 bin index。

​ 对于 SIZE_SZ 为 4B 的平台,bin[0]和 bin[1]是不存在的,因为最小的 chunk 为 16B,small bins 一共 62 个,large bins 一共 63 个,加起来一共 125 个 bin。而 NBINS 定义为 128,其实 bin[0]和 bin[127]都不存在bin[1]为 unsorted bin 的 chunk 链表头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
typedef struct malloc_chunk* mbinptr;

/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i)
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) - offsetof (struct malloc_chunk, fd))

/* analog of ++bin */
#define next_bin(b)
((mbinptr)((char*)(b) + (sizeof(mchunkptr)<<1)))

/* Reminders about list directionality within bins */
#define first(b)
((b)->fd)

#define last(b)
((b)->bk)

/* Take a chunk off a bin list */
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) {
assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

宏 bin_at(m, i)通过 bin index 获得 bin 的链表头chunk 中的 fb 和 bk 用于将空闲 chunk 链入链表中,而对于每个 bin 的链表头,只需要这两个域就可以了,prev_size 和 size 对链表都来说都没有意义,浪费空间,ptmalloc 为了节约这点内存空间,增大 cpu 高速缓存的命中率,在 bins 数组中只为每个 bin 预留了两个指针的内存空间用于存放 bin 的链表头的 fb 和 bk 指针

从 bin_at(m, i)的定义可以看出,bin[0]不存在,以 SIZE_SZ 为 4B 的平台为例,bin[1]的前 4B 存储的是指针 fb,后 4B 存储的是指针 bk,而 bin_at 返回的是 malloc_chunk 的指针,由 于 fb 在 malloc_chunk 的偏移地址为 offsetof (struct malloc_chunk, fd))=8,所以用 fb 的地址减去 8 就得到 malloc_chunk 的地址。但切记,对 bin 的链表头的 chunk,一定不能修改 prev_size 和 size 域这两个域是与其他 bin 的链表头的 fb 和 bk 内存复用的

宏 next_bin(b)用于获得下一个 bin 的地址,根据前面的分析,我们知道只需要将当前 bin 的地址向后移动两个指针的长度就得到下一个 bin 的链表头地址。

每个 bin 使用双向循环链表管理空闲 chunk,bin 的链表头的指针 fb 指向第一个可用的 chunk指针 bk 指向最后一个可用的 chunk宏 first(b)用于获得 bin 的第一个可用 chunk, 宏 last(b)用于获得 bin 的最后一个可用的 chunk,这两个宏便于遍历 bin,而跳过 bin 的链表头。

宏 unlink(P, BK, FD)用于将 chunk 从所在的空闲链表中取出来,注意 large bins 中的空闲 chunk 可能处于两个双向循环链表中,unlink 时需要从两个链表中都删除。(还有一个由fdnextsize,bknextsize组成的链表,因为large bin十分特殊,大小可能不相同)

注意:bins是声明在malloc_state的指针数组,长度为254,2个元素构成一个bin,bin数组元素指向的是所对应管理的chunk

5.2.3 Unsorted bin

Unsorted bin 可以看作是 small bins 和 large bins 的 cache(缓存),只有一个 unsorted bin,以双向链表管理空闲 chunk,空闲 chunk 不排序,所有的 chunk 在回收时都要先放到 unsorted bin 中分配时,如果在 unsorted bin 中没有合适的 chunk,就会把 unsorted bin 中的所有 chunk 分别加入到所属的 bin 中,然后再在 bin 中分配合适的 chunk。Bins 数组中的元素 bin[1]用于 存储 unsorted bin 的 chunk 链表头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
Unsorted chunks
All remainders from chunk splits, as well as all returned chunks,
are first placed in the "unsorted" bin. They are then placed
in regular bins after malloc gives them ONE chance to be used before
binning. So, basically, the unsorted_chunks list acts as a queue,
with chunks being placed on it in free (and malloc_consolidate),
and taken off (to be either used or placed in bins) in malloc.
The NON_MAIN_ARENA flag is never set for unsorted chunks, so it
does not have to be taken into account in size comparisons.
*/
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at(M, 1))
/*
Top
The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD). Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sYSMALLOc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/
/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M) (unsorted_chunks(M))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
未排序的块
所有从块分割中剩余的部分,以及所有返回的块,
首先放置在"未排序"的 bin 中。然后,在 malloc 给它们一次使用的机会之前,
将它们放置在常规的 bin 中。因此,基本上,未排序的块列表充当队列,
在释放时将块放置在其中(以及 malloc_consolidate 中),
并在 malloc 中取出(用于使用或放置在 bins 中)。
未排序的块从不设置 NON_MAIN_ARENA 标志,因此在大小比较中不需要考虑它。
*/
/*无法索引的 1-bin 用于保存未排序的块。*/
#define unsorted_chunks(M) (bin_at(M, 1))
/*
顶部
最顶部的可用块(即,与可用内存结束边界相邻的块)被特殊对待。
它从不包含在任何 bin 中,仅在没有其他块可用时使用,并且如果非常大(参见 M_TRIM_THRESHOLD)则释放回系统。
因为 top 最初指向自己的 bin,初始大小为零,因此在第一个 malloc 请求时强制扩展,
我们避免在 malloc 中添加任何特殊代码来检查它是否已经存在。
但是,在从系统获取内存时,我们仍然需要这样做,因此我们使 initial_top 在初始化和第一次调用 sYSMALLOc 之间的时间间隔内将 bin 视为合法但不可用的块。(这有些微妙,因为它还依赖于在此时间间隔内前两个字为零。)
*/
/* 方便的是,未排序的 bin 可以在第一次调用时用作虚拟顶部 */
#define initial_top(M) (unsorted_chunks(M))

上面的宏的定义比较明显,把 bin[1]设置为 unsorted bin 的 chunk 链表头,对 top chunk 的初始化,也暂时把 top chunk 初始化为 unsort chunk,仅仅是初始化一个值而已,这个 chunk 的内容肯定不能用于 top chunk 来分配内存,主要原因是 top chunk 不属于任何 bin但 ptmalloc 中的一些 check 代码,可能需要 top chunk 属于一个合法的 bin。

5.2.4 Fast bins

Fast bins 主要是用于提高小内存的分配效率,默认情况下,对于 SIZE_SZ 为 4B 的平台, 小于 64B 的 chunk 分配请求,对于 SIZE_SZ 为 8B 的平台,小于 128B 的 chunk 分配请求, 首先会查找 fast bins 中是否有所需大小的 chunk 存在(精确匹配),如果存在,就直接返回。

Fast bins 可以看着是 small bins 的一小部分 cache,默认情况下,fast bins 只 cache 了 small bins 的前 7 个大小的空闲 chunk,也就是说,对于 SIZE_SZ 为 4B 的平台,fast bins 有 7 个 chunk 空闲链表(bin),每个 bin 的 chunk 大小依次为 16B,24B,32B,40B,48B,56B,64B;对于 SIZE_SZ 为 8B 的平台,fast bins 有 7 个 chunk 空闲链表(bin),每个 bin 的 chunk 大小依 次为 32B,48B,64B,80B,96B,112B,128B。以 32 为系统为例,分配的内存大小与 chunk 大小和 fast bins 的对应关系如下表所示:

image-20231219224233213

Fast bins 可以看着是 LIFO 的栈,使用单向链表实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
Fastbins
An array of lists holding recently freed small chunks. Fastbins
are not doubly linked. It is faster to single-link them, and
since chunks are never removed from the middles of these lists,
double linking is not necessary. Also, unlike regular bins, they
are not even processed in FIFO order (they use faster LIFO) since
ordering doesn't much matter in the transient contexts in which
fastbins are normally used.

Chunks in fastbins keep their inuse bit set, so they cannot
be consolidated with other free chunks. malloc_consolidate
releases all chunks in fastbins and consolidates them with
other free chunks.
*/
typedef struct malloc_chunk* mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
1
2
3
4
5
6
7
8
9
10
11
/*
fast bins
一个包含最近释放的小块的链表数组。fast bins 不是双向链表。
使用单向链接可以更快地操作它们,并且由于这些链表中的块从不被移除,
所以不需要双向链接。此外,与常规 bins 不同,它们甚至不按照 FIFO 的顺序进行处理
(它们使用更快的 LIFO),因为在通常使用fast bins 的瞬时上下文中,顺序并不重要。
fast bins 中的块保持其 inuse 位设置,因此它们不能与其他空闲块合并。
malloc_consolidate 释放fast bins 中的所有块,并将它们与其他空闲块合并。
*/
typedef struct malloc_chunk mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

根据 fast bin 的 index,获得 fast bin 的地址。

1
2
3
4
5
6
7
8
/* offset 2 to use otherwise unindexable first 2 bins */
/*
将大小转换为快速 bins 的索引。
这个宏定义用于将块的大小转换为在快速 bins 中的索引。
它使用了一个偏移量 2,以便使用起始的两个 bins,这样就可以处理无法索引的前两个 bins。
*/
#define fastbin_index(sz)
((((unsigned int)(sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

​ 宏 fastbin_index(sz)用于获得 fast bin 在 fast bins 数组中的 index由于 bin[0]和 bin[1]中 的chunk不存在,所以需要减2,对于SIZE_SZ为4B的平台,将sz除以8减2得到fast bin index, 对于 SIZE_SZ 为 8B 的平台,将 sz 除以 16 减去 2 得到 fast bin index。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* The maximum fastbin request size we support */
/* 我们支持的最大快速 bins 请求大小 */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index(request2size(MAX_FAST_SIZE))+1)
/*
FASTBIN_CONSOLIDATION_THRESHOLD is the size of a chunk in free()
that triggers automatic consolidation of possibly-surrounding
fastbin chunks. This is a heuristic, so the exact value should not
matter too much. It is defined at half the default trim threshold as a
compromise heuristic to only attempt consolidation if it is likely
to lead to trimming. However, it is not dynamically tunable, since
consolidation reduces fragmentation surrounding large chunks even
if trimming is not used.
*/
/*
FASTBIN_CONSOLIDATION_THRESHOLD 是在 free() 函数中触发自动合并可能周围的快速 bins 块的块大小阈值。
这是一个启发式值,所以具体的数值并不太重要。它被定义为默认修剪阈值的一半,作为一个折衷的启发式值,
只有当合并可能导致修剪时才尝试合并。然而,它不能动态调整,因为即使不使用修剪,合并也可以减少大块周围的碎片化。
*/
#define FASTBIN_CONSOLIDATION_THRESHOLD (65536UL)

​ 根据 SIZE_SZ 的不同大小,定义 MAX_FAST_SIZE 为 80B 或是 160B,fast bins 数组的大小 NFASTBINS 为 10,FASTBIN_CONSOLIDATION_THRESHOLD 为 64k,当每次释放的 chunk 与该 chunk 相邻的空闲 chunk 合并后的大小大于 64k 时,就认为内存碎片可能比较多了,就需要 把 fast bins 中的所有 chunk 都进行合并,以减少内存碎片对系统的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef DEFAULT_MXFAST
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)
#endif
/*
Set value of max_fast.
Use impossibly small value if 0.
Precondition: there are no existing fastbin chunks.
Setting the value clears fastchunk bit but preserves noncontiguous bit.
*/
/*
设置 max_fast 的值。
如果为 0,则使用不可能的小值。
前提条件:不存在现有的 fastbin 块。
设置该值会清除 fastchunk 位,但保留非连续 bit。
*/
#define set_max_fast(s)
global_max_fast = (((s) == 0) ? SMALLBIN_WIDTH: ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
#define get_max_fast() global_max_fast

​ 上面的宏 DEFAULT_MXFAST 定义了默认的 fast bins 中最大的 chunk 大小对于 SIZE_SZ 为 4B 的平台,最大 chunk 为 64B,对于 SIZE_SZ 为 8B 的平台,最大 chunk 为 128B。ptmalloc 默认情况下调用 set_max_fast(s)将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就 是设置 fast bins 中 chunk 的最大值,get_max_fast()用于获得这个全局变量 global_max_fast 的值

5.3 核心结构体分析

每个分配区是 struct malloc_state 的一个实例,ptmalloc 使用 malloc_state 来管理分配区, 而参数管理使用 struct malloc_par,全局拥有一个唯一的 malloc_par 实例。

5.3.1 malloc_state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct malloc_state
{
/* Serialize access. */
/* 序列化访问 */
__libc_lock_define (, mutex);

/* Flags (formerly in max_fast). */
/* 标志位(以前在 max_fast 中) */
int flags;

/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
/* 如果 fastbin 块包含最近插入的空闲块,则设置为 1 */
/* 注意,这是一个布尔值,但并非所有目标平台都支持对布尔值的原子操作 */
int have_fastchunks;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
/* 最顶层块的基址 -- 不在任何 bin 中 */
mchunkptr top;

/* The remainder from the most recent split of a small request */
/* 最近一次拆分小型请求产生的剩余块 */
mchunkptr last_remainder;

/* Normal bins packed as described above */
/* 正常 bins,按照上述描述进行打包 */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
/* bins 的位图 */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
/* 链表 */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
/* 用于空闲 arenas 的链表。访问该字段由 arena.c 中的 free_list_lock 进行序列化 */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
/* 附加到该 arena 的线程数。如果该 arena 在空闲列表中,则为 0。访问该字段由 arena.c 中的 free_list_lock 进行序列化 */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
/* 在该 arena 中从系统分配的内存 */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

Mutex 用于串行化访问分配区,当有多个线程访问同一个分配区时,第一个获得这个 mutex 的线程将使用该分配区分配内存分配完成后,释放该分配区的 mutex,以便其它线程使用该分配区。

Flags 记录了分配区的一些标志bit0 用于标识分配区是否包含至少一个 fast bin chunkbit1 用于标识分配区是否能返回连续的虚拟地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
FASTCHUNKS_BIT held in max_fast indicates that there are probably
some fastbin chunks. It is set true on entering a chunk into any
fastbin, and cleared only in malloc_consolidate.
The truth value is inverted so that have_fastchunks will be true
upon startup (since statics are zero-filled), simplifying
initialization checks.
*/
/*
在 max_fast 中持有的 FASTCHUNKS_BIT 表示可能存在一些 fastbin 块。
当将块插入任何 fastbin 中时,将其设置为 true,并且仅在 malloc_consolidate 中清除。
为了使 have_fastchunks 在启动时为 true(因为静态变量会被零填充),真值被取反,简化了初始化检查。
*/

该注释解释了在 max_fast 中持有的 FASTCHUNKS_BIT 表示可能存在一些 fastbin 块的含义。当将块插入任何 fastbin中时,会将该标志位设置为 true,并且只有在 malloc_consolidate 函数中才会清除该标志位。为了确保在启动时 have_fastchunkstrue因为静态变量会被零填充),真值被取反,简化了初始化检查

1
2
3
4
5
6
7
8
9
#define FASTCHUNKS_BIT (1U)
#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)
#ifdef ATOMIC_FASTBINS
#define clear_fastchunks(M) catomic_or (&(M)->flags, FASTCHUNKS_BIT)
#define set_fastchunks(M) catomic_and (&(M)->flags, ~FASTCHUNKS_BIT)
#else
#define clear_fastchunks(M) ((M)->flags |= FASTCHUNKS_BIT)
#define set_fastchunks(M) ((M)->flags &= ~FASTCHUNKS_BIT)
#endif

​ 上面的宏用于设置或是置位 flags 中 fast chunk 的标志位 bit0,如果 bit0 为 0,表示分配区中有 fast chunk如果为 1 表示没有 fast chunk初始化完成后的 malloc_state 实例中,flags 值为 0,表示该分配区中有 fast chunk,但实际上没有,试图从 fast bins 中分配 chunk 都会返回 NULL,在第一次调用函数 malloc_consolidate()对 fast bins 进行 chunk 合并时,如果 max_fast 大于 0,会调用 clear_fastchunks 宏标志该分配区中已经没有 fast chunk,因为函数 malloc_consolidate()会合并所有的 fast bins 中的 chunk。clear_fastchunks 宏只会在函数 malloc_consolidate()中调用当有 fast chunk 加入 fast bins 时,就是调用 set_fastchunks 宏标 46 识分配区的 fast bins 中存在 fast chunk。

1
2
3
4
5
6
7
8
9
10
11
12
/*
NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous
regions. Otherwise, contiguity is exploited in merging together,
when possible, results from consecutive MORECORE calls.
The initial value comes from MORECORE_CONTIGUOUS, but is
changed dynamically if mmap is ever used as an sbrk substitute.
*/
/*
NONCONTIGUOUS_BIT 表示 MORECORE 不返回连续的内存区域。
否则,在可能的情况下,连续的 MORECORE 调用结果会被合并在一起。
初始值来自 MORECORE_CONTIGUOUS,但如果 mmap 被用作 sbrk 的替代,则会动态更改。
*/

该注释解释了 NONCONTIGUOUS_BIT 的含义。**NONCONTIGUOUS_BIT 表示 MORECORE 函数不返回连续的内存区域。在正常情况下,如果连续的 MORECORE 调用返回的内存区域是相邻的,那么在合并内存块时可以利用这种连续性。**

初始情况下,**NONCONTIGUOUS_BIT 的值来自 MORECORE_CONTIGUOUS,但如果在后续的内存分配中使用了 mmap 作为 sbrk 的替代方式,那么 NONCONTIGUOUS_BIT 的值会动态地进行更改。**

1
2
3
4
5
#define NONCONTIGUOUS_BIT (2U)
#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

​ Flags 的 bit1 如果为 0,表示 MORCORE 返回连续虚拟地址空间bit1 为 1,表示 MORCORE 返回非连续虚拟地址空间,对于主分配区,MORECORE 其实为 sbr(),默认返回连续虚拟地 址空间,对于非主分配区,使用 mmap()分配大块虚拟内存,然后进行切分来模拟主分配区的行为,而默认情况下 mmap 映射区域是不保证虚拟地址空间连续的,所以非主分配区默认分配非连续虚拟地址空间。

​ Malloc_state 中声明了几个对锁的统计变量,默认没有定义 THREAD_STATS,所以不会对锁的争用情况做统计

fastbinsY 拥有 10(NFASTBINS)个元素的数组用于存放每个 fast chunk 链表头指针所以 fast bins 最多包含 10 个 fast chunk 的单向链表。

top 是一个 chunk 指针指向分配区的 top chunk

last_remainder 是一个 chunk 指针,分配区上次分配 small chunk 时,从一个 chunk 中分裂出一个 small chunk 返回给用户,分裂后的剩余部分形成一个 chunk,last_remainder 就是指向的这个 chunk。

bins 用于存储 unstored bin,small bins 和 large bins 的 chunk 链表头,small bins 一共 62 个,large bins 一共 63 个,加起来一共 125 个 bin。而 NBINS 定义为 128,其实 bin[0]和 bin[127] 都不存在,bin[1]为 unsorted bin 的 chunk 链表头,所以实际只有 126bins。Bins 数组能存放 了 254(NBINS * 2 – 2)个 mchunkptr 指针,而我们实现需要存储 chunk 的实例,一般情况下, chunk 实例的大小为 6 个 mchunkptr 大小,这 254 个指针的大小怎么能存下 126 个 chunk 呢? 这里使用了一个技巧,如果按照我们的常规想法,也许会申请 126 个 malloc_chunk 结构体指针元素的数组,然后再给链表申请一个头节点(即 126 个),再让每个指针元素正确指向而形成 126 个具有头节点的链表。事实上,对于 malloc_chunk 类型的链表“头节点”,其内的 prev_size 和 size 字段是没有任何实际作用的,fd_nextsize 和 bk_nextsize 字段只有 large bins 中的空闲 chunk 才会用到,而对于 large bins 的空闲 chunk 链表头不需要这两个字段,因此 这四个字段所占空间如果不合理使用的话那就是白白的浪费。我们再来看一看 128 个 malloc_chunk 结构体指针元素的数组占了多少内存空间呢?假设 SIZE_SZ 的大小为 8B,则指针的大小也为 8B,结果为 126 * 2 * 8=2016 字节。而 126 个 malloc_chunk 类型的链表“头节点” 需要多少内存呢?126 * 6 * 8=6048,真的是 6048B 么?不是,刚才不是说了,prev_size,size, fd_nextsize 和 bk_nextsize 这四个字段是没有任何实际作用的,因此完全可以被重用(覆盖), 47 因此实际需要内存为 12628=2016。Bins 指针数组的大小为,(128 * 2 - 2) 8=2032* , 2032 大 于 2016(事实上最后 16 个字节都被浪费掉了),那么这 254 个 malloc_chunk结构体指针元素数组所占内存空间就可以存储这 126 个头节点了。

binmap 字段是一个 int 数组,ptmalloc 用一个 bit 来标识该 bit 对应的 bin 中是否包含空 闲 chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
Binmap
To help compensate for the large number of bins, a one-level index
structure is used for bin-by-bin searching. `binmap' is a
bitvector recording whether bins are definitely empty so they can
be skipped over during during traversals. The bits are NOT always
cleared as soon as bins are empty, but instead only
when they are noticed to be empty during traversal in malloc.
*/
/*
位图(Binmap)
为了弥补大量的 bin(内存块链表),使用了一级索引结构来进行逐个 bin 的搜索。
binmap 是一个位向量,记录了哪些 bin 明确为空,以便在遍历过程中可以跳过这些空的 bin。
这些位在 bin 变为空时并不总是立即清除,而是在 malloc 的遍历过程中注意到它们为空时才进行清除。
*/
#define BITSPERMAP (1U << BINMAPSHIFT)
#define BINMAPSIZE (NBINS / BITSPERMAP)
#define idx2block(i) ((i) >> BINMAPSHIFT)
#define idx2bit(i) ((1U << ((i) & ((1U << BINMAPSHIFT)-1))))
#define mark_bin(m,i) ((m)->binmap[idx2block(i)] |= idx2bit(i))
#define unmark_bin(m,i) ((m)->binmap[idx2block(i)] &= ~(idx2bit(i)))
#define get_binmap(m,i) ((m)->binmap[idx2block(i)] & idx2bit(i))

binmap 一共 128bit,16 字节,4 个 int 大小,binmap 按 int 分成 4 个 block,每个 block 有 32 个 bit,根据 bin indx 可以使用宏 idx2block 计算出该 bin 在 binmap 对应的 bit 属于哪个 block。idx2bit 宏取第 i 位为 1,其它位都为 0 的掩码,举个例子:idx2bit(3) 为 “0000 1000” (只显示 8 位)。mark_bin 设置第 i 个 bin 在 binmap 中对应的 bit 位为 1;unmark_bin 设置 第 i 个 bin 在 binmap 中对应的 bit 位为 0;get_binmap 获取第 i 个 bin 在 binmap 中对应的 bit。

next 字段用于将分配区以单向链表链接起来。

next_free 字段空闲的分配区链接在单向链表中,只有在定义了 PER_THREAD 的情况下才 定义该字段。

system_mem 字段记录了当前分配区已经分配的内存大小。

max_system_mem 记录了当前分配区最大能分配的内存大小。

堆相关数据结构(大部分与上述重复)

!!!一些关于堆的约束,后面详细考虑!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
The three exceptions to all this are:
1. The special chunk `top' doesn't bother using the
trailing size field since there is no next contiguous chunk
that would have to index off it. After initialization, `top'
is forced to always exist. If it would become less than
MINSIZE bytes long, it is replenished.
2. Chunks allocated via mmap, which have the second-lowest-order
bit M (IS_MMAPPED) set in their size fields. Because they are
allocated one-by-one, each must contain its own trailing size
field. If the M bit is set, the other bits are ignored
(because mmapped chunks are neither in an arena, nor adjacent
to a freed chunk). The M bit is also used for chunks which
originally came from a dumped heap via malloc_set_state in
hooks.c.
3. Chunks in fastbins are treated as allocated chunks from the
point of view of the chunk allocator. They are consolidated
with their neighbors only in bulk, in malloc_consolidate.
*/
/*
在所有这些规则之外,有三个例外情况:
1. 特殊块 top 不使用尾部的大小字段,因为没有下一个连续的块需要引用它。在初始化之后,top 始终存在。如果它的长度变得小于 MINSIZE 字节,它会被重新填充。
2. 通过 mmap 分配的块,在其大小字段中设置了第二低位 M(IS_MMAPPED)。因为它们是逐个分配的,每个块必须包含自己的尾部大小字段。如果设置了 M 位,其他位将被忽略(因为 mmapped 块既不在一个 arena 中,也不与已释放的块相邻)。M 位还用于最初通过 hooks.c 中的 malloc_set_state 从已转储堆中获取的块。
3. 快速分配链表(fastbins)中的块在块分配器的视角中被视为已分配的块。它们仅在 malloc_consolidate 中批量与相邻块合并。
*/
Top Chunk

glibc 中对于 top chunk 的描述如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
Top

The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD). Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sysmalloc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/

/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M) (unsorted_chunks(M))

程序第一次进行 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.

宏观结构

arena

在我们之前介绍的例子中,无论是主线程还是新创建的线程,在第一次申请内存时,都会有独立的 arena。那么会不会每个线程都有独立的 arena 呢?下面我们就具体介绍。

arena 数量

对于不同系统,arena 数量的约束如下

1
2
3
4
For 32 bit systems:
Number of arena = 2 * number of cores.
For 64 bit systems:
Number of arena = 8 * number of cores.

显然,不是每一个线程都会有对应的 arena。至于为什么 64 位系统,要那么设置,我也没有想明白。此外,因为每个系统的核数是有限的,当线程数大于核数的二倍(超线程技术)时,就必然有线程处于等待状态,所以没有必要为每个线程分配一个 arena。

arena 分配规则(来自于不知名博客,不确定其完全正确性)

Ptmalloc2通过几种数据结构来进行管理,主要有arena,heap,chunk三种层级。arena和heap都是对chunk的一种组织方式,方便之后的分配,arena又是对heap的组织,arene是堆管理器。

  • arena: 有一个main_arena,是由主线程创建的,thread_arena则为各线程创建的,当arena满了之后就不再创建而是与其他arena共享一个arena,方法为依次给各个arena上锁(查看是否有其他线程正在使用该arena),如果上锁成功(没有其他线程正在使用),则使用该arena,之后一直使用这个arena,如果无法使用则阻塞等待。

  • heap的等级就比arena要低一些了,一个arena可以有多个heap,也是存储了堆相关的信息。

  • chunk为分配给用户的内存的一个单位,每当我们分配一段内存的时候其实就是分配得到了一个chunk,我们就可以在chunk当中进行一定的操作了。不过为了进行动态分配,chunk本身也有一些数据(元数据),是用来指示其分配等等的数据。

  • glibc的malloc源码中涉及三种最重要数据结构:Arena、Heap、Chunk ,分别对应结构体malloc_state、heap_info、malloc_chunk 。每个数据结构都有对应的结构体实现,如图:

    image-20231220232830154

    • Thread - Arena一个Arena对应多个线程Thread。即每个线程都有一个Arena,但是**有可能多个线程共用一个Arena(同一时间只能一对一)**。每个Arena都包含一个malloc_state结构体,保存bins, top chunk, Last reminder chunk等信息。
    • Arena - Heap:一个Arena可能拥有多个heap。Arena开始的时候只有一个heap,但是当这个heap的空间用尽时,就需要获取新的heap。(也可以理解为subheap子堆)
    • Heap - Chunk:一个Heap根据用户的请求会划分为多个chunk,每个chunk拥有自己的header - malloc_chunk。
区别

与 thread 不同的是,main_arena 并不在申请的 heap 中,而是一个全局变量,在 libc.so 的数据段。

heap_info

程序刚开始执行时,每个线程是没有 heap 区域的。当其申请内存时,就需要一个结构来记录对应的信息,而 heap_info 的作用就是这个。而且当该 heap 的资源被使用完后,就必须得再次申请内存了。此外,一般申请的 heap 是不连续的,因此需要记录不同 heap 之间的链接结构。

该数据结构是专门为从 Memory Mapping Segment 处申请的内存准备的,即为非主线程准备的。

主线程可以通过 sbrk() 函数扩展 program break location 获得(直到触及 Memory Mapping Segment),只有一个 heap,没有 heap_info 数据结构。

heap_info 的主要结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#define HEAP_MIN_SIZE (32 * 1024)
#ifndef HEAP_MAX_SIZE
# ifdef DEFAULT_MMAP_THRESHOLD_MAX
# define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)
# else
# define HEAP_MAX_SIZE (1024 * 1024) /* must be a power of two */
# endif
#endif

/* HEAP_MIN_SIZE and HEAP_MAX_SIZE limit the size of mmap()ed heaps
that are dynamically created for multi-threaded programs. The
maximum size must be a power of two, for fast determination of
which heap belongs to a chunk. It should be much larger than the
mmap threshold, so that requests with a size just below that
threshold can be fulfilled without creating too many heaps. */

/***************************************************************************/

/* A heap is a single contiguous memory region holding (coalesceable)
malloc_chunks. It is allocated with mmap() and always starts at an
address aligned to HEAP_MAX_SIZE. */

typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */
struct _heap_info *prev; /* Previous heap. */
size_t size; /* Current size in bytes. */
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define HEAP_MIN_SIZE (32 * 1024)
#ifndef HEAP_MAX_SIZE
# ifdef DEFAULT_MMAP_THRESHOLD_MAX
# define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)
# else
# define HEAP_MAX_SIZE (1024 * 1024) /* 必须是2的幂次方 */
# endif
#endif

/* HEAP_MIN_SIZE和HEAP_MAX_SIZE限制了为多线程程序动态创建的mmap()堆的大小。
最大大小必须是2的幂次方,以便快速确定一个内存块属于哪个堆。
它应该比mmap阈值大得多,这样可以满足大小接近阈值的请求而不会创建太多堆。 */

/***************************************************************************/

/* 堆(heap)是一个连续的内存区域,包含(可合并的)malloc_chunk。
它是通过mmap()分配的,并且始终从HEAP_MAX_SIZE对齐的地址开始。 */

typedef struct _heap_info
{
mstate ar_ptr; /* 该堆所属的Arena。 */
struct _heap_info *prev; /* 前一个堆。 */
size_t size; /* 当前大小(以字节为单位)。 */
size_t mprotect_size; /* 已通过mprotect设置为PROT_READ|PROT_WRITE的内存大小(以字节为单位)。 */
/* 确保以下数据正确对齐,特别是sizeof(heap_info) + 2 * SIZE_SZ是MALLOC_ALIGNMENT的倍数。 */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

该结构主要是描述堆的基本信息,包括

  • 堆对应的 arena 的地址
  • 由于一个线程申请一个堆之后,可能会使用完,之后就必须得再次申请。因此,一个线程可能会有多个堆。prev 即记录了上一个 heap_info 的地址。这里可以看到每个堆的 heap_info 是通过单向链表进行链接的。
  • size 表示当前堆的大小
  • 最后一部分确保对齐

pad 里负数的缘由是什么呢?

pad 是为了确保分配的空间是按照 MALLOC_ALIGN_MASK+1 (记为 MALLOC_ALIGN_MASK_1) 对齐的。在 pad 之前该结构体一共有 6 个 SIZE_SZ 大小的成员, 为了确保 MALLOC_ALIGN_MASK_1 字节对齐, 可能需要进行 pad,不妨假设该结构体的最终大小为 MALLOC_ALIGN_MASK_1*x,其中 x 为自然数,那么需要 pad 的空间为 MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ = (MALLOC_ALIGN_MASK_1 * x - 6 * SIZE_SZ) % MALLOC_ALIGN_MASK_1 = 0 - 6 * SIZE_SZ % MALLOC_ALIGN_MASK_1=-6 * SIZE_SZ % MALLOC_ALIGN_MASK_1 = -6 * SIZE_SZ & MALLOC_ALIGN_MASK

看起来该结构应该是相当重要的,但是如果如果我们仔细看完整个 malloc 的实现的话,就会发现它出现的频率并不高。

malloc_state

该结构用于管理堆记录每个 arena 当前申请的内存的具体状态,比如说是否有空闲 chunk,有什么大小的空闲 chunk 等等。无论是 thread arena 还是 main arena,它们都只有一个 malloc state 结构。由于 thread 的 arena 可能有多个,malloc state 结构会在最新申请的 arena 中。

注意,main arena 的 malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段。

其结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct malloc_state
{
/* Serialize access. */
/* 序列化访问 */
__libc_lock_define (, mutex);

/* Flags (formerly in max_fast). */
/* 标志位(以前在 max_fast 中) */
int flags;

/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
/* 如果 fastbin 块包含最近插入的空闲块,则设置为 1 */
/* 注意,这是一个布尔值,但并非所有目标平台都支持对布尔值的原子操作 */
int have_fastchunks;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
/* 最顶层块的基址 -- 不在任何 bin 中 */
mchunkptr top;

/* The remainder from the most recent split of a small request */
/* 最近一次拆分小型请求产生的剩余块 */
mchunkptr last_remainder;

/* Normal bins packed as described above */
/* 正常 bins,按照上述描述进行打包 */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
/* bins 的位图 */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
/* 链表 */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
/* 用于空闲 arenas 的链表。访问该字段由 arena.c 中的 free_list_lock 进行序列化 */
struct malloc_state *next_free;

/*
Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c.
*/
/* 附加到该 arena 的线程数。如果该 arena 在空闲列表中,则为 0。访问该字段由 arena.c 中的 free_list_lock 进行序列化 */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
/* 在该 arena 中从系统分配的内存 */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
  • _libc_lock_define(, mutex);
    • 该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。
  • flags
    • flags 记录了分配区的一些标志,比如 bit0 记录了分配区是否有 fast bin chunk ,bit1 标识分配区是否能返回连续的虚拟地址空间。具体如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
FASTCHUNKS_BIT held in max_fast indicates that there are probably
some fastbin chunks. It is set true on entering a chunk into any
fastbin, and cleared only in malloc_consolidate.
The truth value is inverted so that have_fastchunks will be true
upon startup (since statics are zero-filled), simplifying
initialization checks.
*/

#define FASTCHUNKS_BIT (1U)

#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)
#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)
#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)

/*
NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous
regions. Otherwise, contiguity is exploited in merging together,
when possible, results from consecutive MORECORE calls.
The initial value comes from MORECORE_CONTIGUOUS, but is
changed dynamically if mmap is ever used as an sbrk substitute.
*/

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

/* ARENA_CORRUPTION_BIT is set if a memory corruption was detected on the
arena. Such an arena is no longer used to allocate chunks. Chunks
allocated in that arena before detecting corruption are not freed. */

#define ARENA_CORRUPTION_BIT (4U)

#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))
#define set_arena_corrupt(A) ((A)->flags |= ARENA_CORRUPTION_BIT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
FASTCHUNKS_BIT在max_fast中表示可能存在一些fastbin块。
它在将块放入任何fastbin时设置为true,并且仅在malloc_consolidate中清除。
真值取反,以便在启动时have_fastchunks为true(因为静态变量被零填充),
简化了初始化检查。
*/

#define FASTCHUNKS_BIT (1U)

#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)
#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)
#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)

/*
NONCONTIGUOUS_BIT指示MORECORE不返回连续的内存区域。
否则,在可能的情况下,利用连续性合并连续MORECORE调用的结果。
初始值来自MORECORE_CONTIGUOUS,但如果mmap被用作sbrk替代品,则会动态更改。
*/

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

/* ARENA_CORRUPTION_BIT如果在arena上检测到内存损坏,则设置。
这样的arena不再用于分配块。在检测到损坏之前在该arena中分配的块不会被释放。 */

#define ARENA_CORRUPTION_BIT (4U)

#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))
#define set_arena_corrupt(A) ((A)->flags |= ARENA_CORRUPTION_BIT)

  • fastbinsY[NFASTBINS]
    • 存放每个 fast chunk 链表头部的指针
  • top
    • 指向分配区的 top chunk
  • last_reminder
    • 最新的 chunk 分割之后剩下的那部分
  • bins
    • 用于存储 unstored bin,small bins 和 large bins 的 chunk 链表。
  • binmap
    • ptmalloc 用一个 bit 来标识某一个 bin 中是否包含空闲 chunk 。
malloc_par

!!待补充!!

基础操作

unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来,可能在以下地方使用

  • malloc
    • 从恰好大小合适的 large bin 中获取 chunk。
      • 这里需要注意的是 fastbin 与 small bin 就没有使用 unlink,这就是为什么漏洞会经常出现在它们这里的原因。
      • 依次遍历处理 unsorted bin 时也没有使用 unlink 。
    • 从比请求的 chunk 所在的 bin 大的 bin 中取 chunk。
  • free
    • 后向合并,合并物理相邻低地址空闲 chunk。
    • 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
  • malloc_consolidate
    • 后向合并,合并物理相邻低地址空闲 chunk。
    • 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
  • realloc
    • 前向扩展,合并物理相邻高地址空闲 chunk(除了 top chunk)。

由于 unlink 使用非常频繁,所以 unlink 被实现为了一个宏,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) {
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
// 防止攻击者简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
// 下面主要考虑 P 对应的 nextsize 双向链表的修改
if (!in_smallbin_range (chunksize_nomask (P))
// 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中。
// 那么其实也就没有必要对 nextsize 字段进行修改了。
// 这里没有去判断 bk_nextsize 字段,可能会出问题。
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
// 类似于小的 chunk 的检查思路
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
// 这里说明 P 已经在 nextsize 链表中了。
// 如果 FD 没有在 nextsize 链表中
if (FD->fd_nextsize == NULL) {
// 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
// 令 FD 为 nextsize 串起来的
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
// 否则我们需要将 FD 插入到 nextsize 形成的双链表中
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
// 如果在的话,直接拿走即可
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

这里我们以 small bin 的 unlink 为例子介绍一下。对于 large bin 的 unlink,与其类似,只是多了一个 nextsize 的处理。

img

可以看出, P 最后的 fd 和 bk 指针并没有发生变化,但是当我们去遍历整个双向链表时,已经遍历不到对应的链表了。这一点没有变化还是很有用处的,因为我们有时候可以使用这个方法来泄漏地址

  • libc 地址
    • P 位于双向链表头部,bk 泄漏
    • P 位于双向链表尾部,fd 泄漏
    • 双向链表只包含一个空闲 chunk 时,P 位于双向链表中,fd 和 bk 均可以泄漏
  • 泄漏堆地址,双向链表包含多个空闲 chunk
    • P 位于双向链表头部,fd 泄漏
    • P 位于双向链表中,fd 和 bk 均可以泄漏
    • P 位于双向链表尾部,bk 泄漏

注意

  • 这里的头部指的是 bin 的 fd 指向的 chunk,即双向链表中最新加入的 chunk
  • 这里的尾部指的是 bin 的 bk 指向的 chunk,即双向链表中最先加入的 chunk

同时,无论是对于 fd,bk 还是 fd_nextsize ,bk_nextsize,程序都会检测 fd 和 bk 是否满足对应的要求。

1
2
3
4
5
6
7
8
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

// next_size related
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,"corrupted double-linked list (not small)", P, AV);

看起来似乎很正常。我们以 fd 和 bk 为例,P 的 forward chunk 的 bk 很自然是 P ,同样 P 的 backward chunk 的 fd 也很自然是 P 。如果没有做相应的检查的话,我们可以修改 P 的 fd 与 bk,从而可以很容易地达到任意地址写的效果。关于更加详细的例子,可以参见利用部分的 unlink 。

注意:堆的第一个 chunk 所记录的 prev_inuse 位默认为 1。

malloc_printerr

在 glibc malloc 时检测到错误的时候,会调用 malloc_printerr 函数。

1
2
3
4
static void malloc_printerr(const char *str) {
__libc_message(do_abort, "%s\n", str);
__builtin_unreachable();
}

主要会调用 __libc_message 来执行abort 函数,如下

1
2
3
4
5
6
7
if ((action & do_abort)) {
if ((action & do_backtrace))
BEFORE_ABORT(do_abort, written, fd);

/* Kill the application. */
abort();
}

abort 函数里,在 glibc 还是 2.23 版本时,会 fflush stream

1
2
3
4
5
6
7
/* Flush all streams.  We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
堆初始化

堆初始化是在用户第一次申请内存时执行 malloc_consolidate 再执行 malloc_init_state 实现的。这里不做过多讲解。可以参见 malloc_state 相关函数。

申请内存块

__libc_malloc

一般我们会使用 malloc 函数来申请内存块,可是当仔细看 glibc 的源码实现时,其实并没有 malloc 函数。其实该函数真正调用的是 __libc_malloc 函数。为什么不直接写个 malloc 函数呢,因为有时候我们可能需要不同的名称。此外,__libc_malloc 函数只是用来简单封装 _int_malloc 函数。_int_malloc 才是申请内存块的核心。下面我们来仔细分析一下具体的实现。

该函数会首先检查是否有内存分配函数的钩子函数(__malloc_hook),这个主要用于用户自定义的堆分配函数,方便用户快速修改堆分配函数并进行测试。这里需要注意的是,用户申请的字节一旦进入申请内存函数中就变成了无符号整数

1
2
3
4
5
6
7
8
9
10
// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
mstate ar_ptr; //表示堆管理器的状态
void * victim; //表示分配的内存块的指针
// 检查是否有内存分配钩子,如果有,调用钩子并返回.
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));
//钩子函数接受两个参数:分配的字节数和返回地址。这里使用RETURN_ADDRESS(0)获取当前函数的返回地址作为参数传递给钩子函数
//如果不存在内存分配钩子,则继续执行后续的内存分配操作

接着会寻找一个 arena 来试图分配内存。

1
arena_get(ar_ptr, bytes);

然后调用 _int_malloc 函数去申请对应的内存。

1
victim = _int_malloc(ar_ptr, bytes);

如果分配失败的话,ptmalloc 会尝试再去寻找一个可用的 arena,并分配内存。

1
2
3
4
5
6
/* Retry with another arena only if we were able to find a usable arena before.  */
if (!victim && ar_ptr != NULL) {
LIBC_PROBE(memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry(ar_ptr, bytes); //用于在给定的堆管理器中查找另一个可用的堆管理器的函数
victim = _int_malloc(ar_ptr, bytes);
}

如果申请到了 arena,那么在退出之前还得解锁

1
if (ar_ptr != NULL) __libc_lock_unlock(ar_ptr->mutex);

判断目前的状态是否满足以下条件

  • 要么没有申请到内存
  • 要么是 mmap 的内存
  • 要么申请到的内存必须在其所分配的 arena 中
1
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) ||ar_ptr == arena_for_chunk(mem2chunk(victim)));  //断言语句,假则中断程序,确保在特定情况下,victim指针的状态与堆管理器的状态相符合

最后返回内存。

1
2
    return victim;
}
_int_malloc

_int_malloc 是内存分配的核心函数,其核心思路有如下

  1. 它根据用户申请的内存块大小以及相应大小 chunk 通常使用的频度(fastbin chunk, small chunk, large chunk),依次实现了不同的分配方法。
  2. 它由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求的内存。
  3. 当所有的空闲 chunk 都无法满足时,它会考虑 top chunk。
  4. 当 top chunk 也无法满足时,堆分配器才会进行内存块申请。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在进入该函数后,函数立马定义了一系列自己需要的变量,并将用户申请的内存大小转换为内部的 chunk 大小。


static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size */
unsigned int idx; /* associated bin index */
mbinptr bin; /* associated bin */

mchunkptr victim; /* inspected/selected chunk */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */

mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */

unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */

mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */

const char *errstr = NULL;

/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/

checked_request2size(bytes, nb);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* 规范化后的请求大小 */
unsigned int idx; /* 相关联的bin索引 */
mbinptr bin; /* 相关联的bin */

mchunkptr victim; /* 被检查/选择的chunk */
INTERNAL_SIZE_T size; /* chunk的大小 */
int victim_index; /* chunk的bin索引 */

mchunkptr remainder; /* 分割后的剩余部分 */
unsigned long remainder_size; /* 剩余部分的大小 */

unsigned int block; /* 位图遍历器 */
unsigned int bit; /* 位图遍历器 */
unsigned int map; /* 当前binmap的字 */

mchunkptr fwd; /* 用于链接的临时变量 */
mchunkptr bck; /* 用于链接的临时变量 */

const char *errstr = NULL;

/*
通过添加SIZE_SZ字节的开销,将请求大小转换为内部形式,
可能还需要更多的字节以获得必要的对齐和/或至少为MINSIZE的大小,
MINSIZE是最小可分配大小。此外,checked_request2size函数
会检查并阻止(返回0)请求大小太大,以至于在填充和对齐时
四舍五入为零。
*/

checked_request2size(bytes, nb);

// 其他代码...
}

arena
1
2
3
4
5
6
/* There are no usable arenas.  Fall back to sysmalloc to get a chunk from mmap.  */
if (__glibc_unlikely(av == NULL)) {
void *p = sysmalloc(nb, av);
if (p != NULL) alloc_perturb(p, bytes);
return p;
}
fast bin

如果申请的 chunk 的大小位于 fastbin 范围内,需要注意的是这里比较的是无符号整数此外,是从 fastbin 的头结点开始取 chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    /*
If the size qualifies as a fastbin, first check corresponding bin.
This code is safe to execute even if av is not yet initialized, so we
can try it without checking, which saves some time on this fast path.
*/
/*
如果大小符合快速分配块的条件,首先检查相应的bin。
即使av(内存管理状态结构体)尚未初始化,这段代码也可以安全执行,因此我们可以在不检查的情况下尝试执行它,
这在这个快速路径上节省了一些时间。
*/

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast())) {
// 得到对应的fastbin的下标
idx = fastbin_index(nb);
// 得到对应的fastbin的头指针
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp = *fb;
// 利用fd遍历对应的bin内是否有空闲的chunk块,
do {
victim = pp;
if (victim == NULL) break;
} while ((pp = catomic_compare_and_exchange_val_acq(fb, victim->fd,victim)) != victim);
// 存在可以利用的chunk
if (victim != 0) {
// 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。
// 根据取得的 victim ,利用 chunksize 计算其大小。
// 利用fastbin_index 计算 chunk 的索引。
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr(check_action, errstr, chunk2mem(victim), av);
return NULL;
}
// 细致的检查。。只有在 DEBUG 的时候有用
check_remalloced_chunk(av, victim, nb);
// 将获取的到chunk转换为mem模式
void *p = chunk2mem(victim); //将内存块的起始地址转换为指向实际可用内存的指针
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes); //对内存块进行初始化
return p;
}
}
small bin

如果获取的内存块的范围处于 small bin 的范围,那么执行如下流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    /*
If a small request, check regular bin. Since these "smallbins" hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
/*
如果是小的内存请求,检查常规的空闲块链表。由于这些"smallbins"只包含一种大小的内存块,所以不需要在链表中进行搜索。
(对于大的内存请求,我们需要等待未排序的内存块被处理以找到最佳匹配。但对于小的请求,匹配是精确的,所以我们可以立即检查,这样更快。)
*/
if (in_smallbin_range(nb)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。//自循环
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena) set_non_main_arena(victim);
// 细致的检查,非调试状态没有作用
check_malloced_chunk(av, victim, nb);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}
large bin

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,就会考虑是不是 large bin。但是,其实在 large bin 中并没有直接去扫描对应 bin 中的 chunk,而是先利用 malloc_consolidate(参见 malloc_state 相关函数) 函数处理 fast bin 中的 chunk,将有可能能够合并的 chunk 先进行合并后放到 unsorted bin 中,不能够合并的就直接放到 unsorted bin 中,然后再在下面的大循环中进行相应的处理。为什么不直接从相应的 bin 中取出 large chunk 呢?这是 ptmalloc 的机制,它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    /*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
/*
如果这是一个大的内存请求,在继续之前先合并 fastbins。
尽管在查看是否有可用空间之前杀死所有 fastbins 看起来可能有些过度,
但这样可以避免通常与 fastbins 相关的碎片化问题。
此外,在实践中,程序往往会有连续的小内存请求或大内存请求的运行,
但很少有混合请求,因此在大多数程序中不经常调用合并操作。
而对于频繁调用合并操作的程序,通常会发生碎片化问题。
*/
else {
// 获取large bin的下标。
idx = largebin_index(nb);
// 如果存在fastbin的话,会处理 fastbin
if (have_fastchunks(av)) malloc_consolidate(av);
}

bk是前驱节点,fd是后继节点,别记错了咩

大循环 - 遍历 unsorted bin

如果程序执行到了这里,那么说明 与 chunk 大小正好一致的 bin (fast bin, small bin) 中没有 chunk 可以直接满足需求 ,但是 large chunk 则是在这个大循环中处理

在接下来的这个循环中,主要做了以下的操作

  • 按照 FIFO 的方式逐个将 unsorted bin 中的 chunk 取出来
    • 如果是 small request,则考虑是不是恰好满足,是的话,直接返回。
    • 如果不是的话,放到对应的 bin 中。
  • 尝试从 large bin 中分配用户所需的内存

该部分是一个大循环,这是为了尝试重新分配 small bin chunk,这是因为我们虽然会首先使用 large bin,top chunk 来尝试满足用户的请求,但是如果没有满足的话,由于我们在上面没有分配成功 small bin,我们并没有对 fast bin 中的 chunk 进行合并,所以这里会进行 fast bin chunk 的合并,进而使用一个大循环来尝试再次分配 small bin chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    /*
Process recently freed or remaindered chunks, taking one only if
it is exact fit, or, if this a small request, the chunk is remainder from
the most recent non-exact fit. Place other traversed chunks in
bins. Note that this step is the only place in any routine where
chunks are placed in bins.

The outer loop here is needed because we might not realize until
near the end of malloc that we should have consolidated, so must
do so and retry. This happens at most once, and only when we would
otherwise need to expand memory to service a "small" request.
*/
/*
处理最近释放或剩余的内存块,只有在精确匹配时才取一个块,或者如果这是一个小的请求,并且该块是最近一个非精确匹配的剩余块。将其他经过遍历的块放入 bins 中。请注意,这一步是任何例程中唯一将块放入 bins 中的地方。

外部循环在这里是必需的,因为直到接近 malloc 的末尾,我们可能才意识到应该进行合并,所以必须这样做并重试。这最多发生一次,仅当我们否则需要扩展内存来处理一个 "小" 请求时。
*/
for (;;) {
int iters = 0;
unsorted bin 遍历

先考虑 unsorted bin再考虑 last remainder(是最近一次非精确匹配的剩余块) ,但是对于 small bin chunk 的请求会有所例外。

注意 unsorted bin 的遍历顺序为 bk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果 unsorted bin 不为空
// First In First Out
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
// victim 为 unsorted bin 的最后一个 chunk
// bck 为 unsorted bin 的倒数第二个 chunk
bck = victim->bk;
// 判断得到的 chunk 是否满足要求,不能过小,也不能过大
// 一般 system_mem 的大小为132K
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
// 得到victim对应的chunk大小。
size = chunksize(victim);
SMALL REQUEST

如果用户的请求为 small bin chunk,那么我们首先考虑 last remainder,如果 last remainder 是 unsorted bin 中的唯一一块的话, 并且 last remainder 的大小分割后还可以作为一个 chunk ,为什么没有等号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        /*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
/*
如果是一个小的内存请求,如果未排序的 bin 中只有一个块,并且它是上一个剩余块,尝试使用它。
这有助于提高连续小内存请求的局部性。这是对最佳适配算法的唯一例外,仅适用于没有完全匹配的小块。
*/

if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&victim == av->last_remainder &&(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
// 获取新的 remainder 的大小
remainder_size = size - nb;
// 获取新的 remainder 的位置
remainder = chunk_at_offset(victim, nb);
// 更新 unsorted bin 的情况
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
// 更新 av 中记录的 last_remainder
av->last_remainder = remainder;
// 更新last remainder的指针
remainder->bk = remainder->fd = unsorted_chunks(av);
if (!in_smallbin_range(remainder_size)) {
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 设置victim的头部,
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
// 设置 remainder 的头部
set_head(remainder, remainder_size | PREV_INUSE);
// 设置记录 remainder 大小的 prev_size 字段,因为此时 remainder 处于空闲状态。
set_foot(remainder, remainder_size);
// 细致的检查,非调试状态下没有作用
check_malloced_chunk(av, victim, nb);
// 将 victim 从 chunk 模式转化为mem模式
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
初始取出
1
2
3
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
EXACT FIT

如果从 unsorted bin 中取出来的 chunk 大小正好合适,就直接使用。这里应该已经把合并后恰好合适的 chunk 给分配出去了。

1
2
3
4
5
6
7
8
9
/* Take now instead of binning if exact fit */
if (size == nb) {
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena) set_non_main_arena(victim);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
PLACE CHUNK IN SMALL BIN

把取出来的 chunk 放到对应的 small bin 中。

1
2
3
4
5
6
/* place chunk in bin */

if (in_smallbin_range(size)) {
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
PLACE CHUNK IN LARGE BIN

把取出来的 chunk 放到对应的 large bin 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
} else {
// large bin 范围
victim_index = largebin_index(size);
bck = bin_at(av, victim_index); // 当前 large bin 的头部
fwd = bck->fd;

/* maintain large bins in sorted order */
/* 从这里我们可以总结出,largebin 以 fd_nextsize 递减排序。
同样大小的 chunk,后来的只会插入到之前同样大小的 chunk 后,
而不会修改之前相同大小的fd/bk_nextsize,这也很容易理解,
可以减低开销。此外,bin 头不参与 nextsize 链接。*/
// 如果 large bin 链表不空
if (fwd != bck) {
/* Or with inuse bit to speed comparisons */
// 加速比较,应该不仅仅有这个考虑,因为链表里的 chunk 都会设置该位。
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
// bck->bk 存储着相应 large bin 中最小的chunk。
// 如果遍历的 chunk 比当前最小的还要小,那就只需要插入到链表尾部。
// 判断 bck->bk 是不是在 main arena。
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) <
(unsigned long) chunksize_nomask(bck->bk)) {
// 令 fwd 指向 large bin 头
fwd = bck;
// 令 bck 指向 largin bin 尾部 chunk
bck = bck->bk;
// victim 的 fd_nextsize 指向 largin bin 的第一个 chunk
victim->fd_nextsize = fwd->fd;
// victim 的 bk_nextsize 指向原来链表的第一个 chunk 指向的 bk_nextsize
victim->bk_nextsize = fwd->fd->bk_nextsize;
// 原来链表的第一个 chunk 的 bk_nextsize 指向 victim
// 原来指向链表第一个 chunk 的 fd_nextsize 指向 victim
fwd->fd->bk_nextsize =
victim->bk_nextsize->fd_nextsize = victim;
} else {
// 当前要插入的 victim 的大小大于最小的 chunk
// 判断 fwd 是否在 main arena
assert(chunk_main_arena(fwd));
// 从链表头部开始找到不比 victim 大的 chunk
while ((unsigned long) size < chunksize_nomask(fwd)) {
fwd = fwd->fd_nextsize;
assert(chunk_main_arena(fwd));
}
// 如果找到了一个和 victim 一样大的 chunk,
// 那就直接将 chunk 插入到该chunk的后面,并不修改 nextsize 指针。
if ((unsigned long) size ==
(unsigned long) chunksize_nomask(fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else {
// 如果找到的chunk和当前victim大小不一样
// 那么就需要构造 nextsize 双向链表了
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
} else
// 如果空的话,直接简单使得 fd_nextsize 与 bk_nextsize 构成一个双向链表即可。
victim->fd_nextsize = victim->bk_nextsize = victim;
}
最终取出
1
2
3
4
5
6
// 放到对应的 bin 中,构成 bck<-->victim<-->fwd。
mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
WHILE 迭代次数

while 最多迭代 10000 次后退出。

1
2
3
    // #define MAX_ITERS 10000
if (++iters >= MAX_ITERS) break;
}
large chunk

注: 或许会很奇怪,为什么这里没有先去看 small chunk 是否满足新需求了呢?这是因为 small bin 在循环之前已经判断过了,这里如果有的话,就是合并后的才出现 chunk。但是在大循环外,large chunk 只是单纯地找到其索引,所以觉得在这里直接先判断是合理的,而且也为了下面可以再去找较大的 chunk。

如果请求的 chunk 在 large chunk 范围内,就在对应的 bin 中从小到大进行扫描,找到第一个合适的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
if (!in_smallbin_range(nb)) {
bin = bin_at(av, idx);
/* skip scan if empty or largest chunk is too small */
// 如果对应的 bin 为空或者其中的chunk最大的也很小,那就跳过
// first(bin)=bin->fd 表示当前链表中最大的chunk//双向链表的首部是最大的
if ((victim = first(bin)) != bin &&
(unsigned long) chunksize_nomask(victim) >=
(unsigned long) (nb)) {
// 反向遍历链表,直到找到第一个不小于所需chunk大小的chunk
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize(victim)) <(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
// 如果最终取到的chunk不是该bin中的最后一个chunk,并且该chunk与其前面的chunk
// 的大小相同,那么我们就取其前面的chunk,这样可以避免调整bk_nextsize,fd_nextsize
// 链表。因为大小相同的chunk只有一个会被串在nextsize链上。
if (victim != last(bin) &&
chunksize_nomask(victim) == chunksize_nomask(victim->fd))
victim = victim->fd;
// 计算分配后剩余的大小
remainder_size = size - nb;
// 进行unlink
unlink(av, victim, bck, fwd);

/* Exhaust */
// 剩下的大小不足以当做一个块
// 很好奇接下来会怎么办?
if (remainder_size < MINSIZE) {
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena) set_non_main_arena(victim);
}
/* Split */
// 剩下的大小还可以作为一个chunk,进行分割。
else {
// 获取剩下那部分chunk的指针,称为remainder
remainder = chunk_at_offset(victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
// 插入unsorted bin中
bck = unsorted_chunks(av);
fwd = bck->fd;
// 判断 unsorted bin 是否被破坏。
if (__glibc_unlikely(fwd->bk != bck)) {
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
// 如果不处于small bin范围内,就设置对应的字段
if (!in_smallbin_range(remainder_size)) {
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 设置分配的chunk的标记
set_head(victim,
nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));

// 设置remainder的上一个chunk,即分配出去的chunk的使用状态
// 其余的不用管,直接从上面继承下来了
set_head(remainder, remainder_size | PREV_INUSE);
// 设置remainder的大小
set_foot(remainder, remainder_size);
}
// 检查
check_malloced_chunk(av, victim, nb);
// 转换为mem状态
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
寻找较大 chunk

如果走到了这里,那说明对于用户所需的 chunk,不能直接从其对应的合适的 bin 中获取 chunk,所以我们需要来查找比当前 bin 更大的 fast bin , small bin 或者 large bin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
        /*
Search for a chunk by scanning bins, starting with next largest
bin. This search is strictly by best-fit; i.e., the smallest
(with ties going to approximately the least recently used) chunk
that fits is selected.

The bitmap avoids needing to check that most blocks are nonempty.
The particular case of skipping all bins during warm-up phases
when no chunks have been returned yet is faster than it might look.
*/
/*
通过扫描 bin,从下一个更大的 bin 开始,搜索一个块。这个搜索严格按照最佳适配原则进行;
也就是说,选择适合的最小块(如果有多个相同大小的块,则选择近期最少使用的块)。

位图的使用避免了需要检查大多数块是否非空的情况。
在热身阶段(warm-up phases)中,当还没有返回任何块时,跳过所有 bin 的特殊情况比看起来快得多。
*/
++idx;
// 获取对应的bin
bin = bin_at(av, idx);
// 获取当前索引在binmap中的block索引
// #define idx2block(i) ((i) >> BINMAPSHIFT) ,BINMAPSHIFT=5
// Binmap按block管理,每个block为一个int,共32个bit,可以表示32个bin中是否有空闲chunk存在
// 所以这里是右移5
block = idx2block(idx);
// 获取当前块大小对应的映射,这里可以得知相应的bin中是否有空闲块
map = av->binmap[ block ];
// #define idx2bit(i) ((1U << ((i) & ((1U << BINMAPSHIFT) - 1))))
// 将idx对应的比特位设置为1,其它位为0
bit = idx2bit(idx);
for (;;) {
找到一个合适的 MAP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Skip rest of block if there are no more set bits in this block.
*/
// 如果bit>map,则表示该 map 中没有比当前所需要chunk大的空闲块
// 如果bit为0,那么说明,上面idx2bit带入的参数为0。
if (bit > map || bit == 0) {
do {
// 寻找下一个block,直到其对应的map不为0。
// 如果已经不存在的话,那就只能使用top chunk了
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;
} while ((map = av->binmap[ block ]) == 0);
// 获取其对应的bin,因为该map中的chunk大小都比所需的chunk大,而且
// map本身不为0,所以必然存在满足需求的chunk。
bin = bin_at(av, (block << BINMAPSHIFT));
bit = 1;
}
找到合适的 BIN
1
2
3
4
5
6
7
8
/* Advance to bin with set bit. There must be one. */
// 从当前map的最小的bin一直找,直到找到合适的bin。
// 这里是一定存在的
while ((bit & map) == 0) {
bin = next_bin(bin);
bit <<= 1;
assert(bit != 0);
}
简单检查 CHUNK
1
2
3
4
5
6
7
8
9
10
11
12
/* Inspect the bin. It is likely to be non-empty */
// 获取对应的bin
victim = last(bin);

/* If a false alarm (empty bin), clear the bit. */
// 如果victim=bin,那么我们就将map对应的位清0,然后获取下一个bin
// 这种情况发生的概率应该很小。
if (victim == bin) {
av->binmap[ block ] = map &= ~bit; /* Write through */
bin = next_bin(bin);
bit <<= 1;
}
真正取出 CHUNK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
else {
// 获取对应victim的大小
size = chunksize(victim);

/* We know the first chunk in this bin is big enough to use. */
assert((unsigned long) (size) >= (unsigned long) (nb));
// 计算分割后剩余的大小
remainder_size = size - nb;

/* unlink */
unlink(av, victim, bck, fwd);

/* Exhaust */
// 如果分割后不够一个chunk怎么办?
if (remainder_size < MINSIZE) {
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena) set_non_main_arena(victim);
}

/* Split */
// 如果够,尽管分割
else {
// 计算剩余的chunk的偏移
remainder = chunk_at_offset(victim, nb);

/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
// 将剩余的chunk插入到unsorted bin中
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck)) {
errstr = "malloc(): corrupted unsorted chunks 2";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;

/* advertise as last remainder */
// 如果在small bin范围内,就将其标记为remainder
if (in_smallbin_range(nb)) av->last_remainder = remainder;
if (!in_smallbin_range(remainder_size)) {
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 设置victim的使用状态
set_head(victim,
nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
// 设置remainder的使用状态,这里是为什么呢?
set_head(remainder, remainder_size | PREV_INUSE);
// 设置remainder的大小
set_foot(remainder, remainder_size);
}
// 检查
check_malloced_chunk(av, victim, nb);
// chunk状态转换到mem状态
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
使用 top chunk

如果所有的 bin 中的 chunk 都没有办法直接满足要求(即不合并),或者说都没有空闲的 chunk。那么我们就只能使用 top chunk 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).

We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
/*
如果足够大,将与内存末尾(保存在 av->top 中)相邻的块分割出来。
注意,这符合最佳适配搜索规则。实际上,av->top 被视为比任何其他可用块都更大(因此更不适配),
因为它可以扩展到任意大小(受系统限制)。

我们要求 av->top 在初始化后始终存在(即大小 >= MINSIZE),
因此如果当前请求会耗尽它,它将被重新补充。
(确保其存在的主要原因是我们可能需要 MINSIZE 的空间来放置 sysmalloc 中的栅栏。)
*/
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果分割之后,top chunk 大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) {
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
// 这里设置 PREV_INUSE 是因为 top chunk 前面的 chunk 如果不是 fastbin,就必然会和
// top chunk 合并,所以这里设置了 PREV_INUSE。
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
// 否则,判断是否有 fast chunk
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks(av)) {
// 先执行一次fast bin的合并
malloc_consolidate(av);
/* restore original bin index */
// 判断需要的chunk是在small bin范围内还是large bin范围内
// 并计算对应的索引
// 等待下次再看看是否可以
if (in_smallbin_range(nb))
idx = smallbin_index(nb);
else
idx = largebin_index(nb);
}
堆内存不够

如果堆内存不够,我们就需要使用 sysmalloc 来申请内存了。

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
// 否则的话,我们就只能从系统中再次申请一点内存了。
else {
void *p = sysmalloc(nb, av);
if (p != NULL) alloc_perturb(p, bytes);
return p;
}
_libc_calloc

calloc 也是 libc 中的一种申请内存块的函数。在 libc中的封装为 _libc_calloc,具体介绍如下

1
2
3
4
5
6
7
8
9
10
/*
calloc(size_t n_elements, size_t element_size);
Returns a pointer to n_elements * element_size bytes, with all locations
set to zero.
*/
/*
calloc(size_t n_elements, size_t element_size);
返回一个指向 n_elements * element_size 字节的指针,其中所有位置都被设置为零。
*/
void* __libc_calloc(size_t, size_t);
sysmalloc

正如该函数头的注释所言,该函数用于当前堆内存不足时,需要向系统申请更多的内存。

1
2
3
4
5
6
7
8
9
10
11
/*
sysmalloc handles malloc cases requiring more memory from the system.
On entry, it is assumed that av->top does not have enough
space to service request for nb bytes, thus requiring that av->top
be extended or replaced.
*/
/*
sysmalloc 处理需要从系统获取更多内存的 malloc 情况。
在进入函数时,假设 av->top 没有足够的空间来满足 nb 字节的请求,
因此需要扩展或替换 av->top。
*/
基本定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void *sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
mchunkptr old_top; /* incoming value of av->top */
INTERNAL_SIZE_T old_size; /* its size */
char *old_end; /* its end address */

long size; /* arg to first MORECORE or mmap call */
char *brk; /* return value from MORECORE */

long correction; /* arg to 2nd MORECORE call */
char *snd_brk; /* 2nd return val */

INTERNAL_SIZE_T front_misalign; /* unusable bytes at front of new space */
INTERNAL_SIZE_T end_misalign; /* partial page left at end of new space */
char *aligned_brk; /* aligned offset into brk */

mchunkptr p; /* the allocated/returned chunk */
mchunkptr remainder; /* remainder frOm allocation */
unsigned long remainder_size; /* its size */

size_t pagesize = GLRO(dl_pagesize);
bool tried_mmap = false;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void *sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
mchunkptr old_top; /* av->top 的初始值 */
INTERNAL_SIZE_T old_size; /* 其大小 */
char *old_end; /* 其结束地址 */

long size; /* 传递给第一次 MORECORE 或 mmap 调用的参数 */
char *brk; /* MORECORE 的返回值 */

long correction; /* 传递给第二次 MORECORE 调用的参数 */
char *snd_brk; /* 第二次 MORECORE 的返回值 */

INTERNAL_SIZE_T front_misalign; /* 新空间前面不可用的字节数 */
INTERNAL_SIZE_T end_misalign; /* 新空间末尾剩余的部分页面 */
char *aligned_brk; /* 对齐后的 brk 偏移量 */

mchunkptr p; /* 分配/返回的块 */
mchunkptr remainder; /* 剩余的分配块 */
unsigned long remainder_size; /* 剩余块的大小 */

size_t pagesize = GLRO(dl_pagesize);
bool tried_mmap = false;

我们可以主要关注一下 pagesize,其

1
2
3
4
5
#ifndef EXEC_PAGESIZE
#define EXEC_PAGESIZE 4096
#endif
# define GLRO(name) _##name
size_t _dl_pagesize = EXEC_PAGESIZE;

所以,pagesize=4096=0x1000

考虑 mmap

正如开头注释所言如果满足如下任何一种条件

  1. 没有分配堆。
  2. 申请的内存大于 mp_.mmap_threshold,并且 mmap 的数量小于最大值,就可以尝试使用 mmap。

默认情况下,临界值为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct malloc_par mp_ = {
.top_pad = DEFAULT_TOP_PAD,
.n_mmaps_max = DEFAULT_MMAP_MAX,
.mmap_threshold = DEFAULT_MMAP_THRESHOLD,
.trim_threshold = DEFAULT_TRIM_THRESHOLD,
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof(long) == 4 ? 2 : 8))
.arena_test = NARENAS_FROM_NCORES(1)
#if USE_TCACHE
,
.tcache_count = TCACHE_FILL_COUNT,
.tcache_bins = TCACHE_MAX_BINS,
.tcache_max_bytes = tidx2usize(TCACHE_MAX_BINS - 1),
.tcache_unsorted_limit = 0 /* No limit. */
#endif
};

DEFAULT_MMAP_THRESHOLD 为 128*1024 字节,即 128 K。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef DEFAULT_MMAP_THRESHOLD
#define DEFAULT_MMAP_THRESHOLD DEFAULT_MMAP_THRESHOLD_MIN
#endif
/*
MMAP_THRESHOLD_MAX and _MIN are the bounds on the dynamically
adjusted MMAP_THRESHOLD.
*/

#ifndef DEFAULT_MMAP_THRESHOLD_MIN
#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
#endif

#ifndef DEFAULT_MMAP_THRESHOLD_MAX
/* For 32-bit platforms we cannot increase the maximum mmap
threshold much because it is also the minimum value for the
maximum heap size and its alignment. Going above 512k (i.e., 1M
for new heaps) wastes too much address space. */
#if __WORDSIZE == 32
#define DEFAULT_MMAP_THRESHOLD_MAX (512 * 1024)
#else
#define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
#endif
#endif

下面为这部分代码,目前不是我们关心的重点,可以暂时跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/

if (av == NULL ||
((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) &&
(mp_.n_mmaps < mp_.n_mmaps_max))) {
char *mm; /* return value from mmap call*/

try_mmap:
/*
Round up size to nearest page. For mmapped chunks, the overhead
is one SIZE_SZ unit larger than for normal chunks, because there
is no following chunk whose prev_size field could be used.

See the front_misalign handling below, for glibc there is no
need for further alignments unless we have have high alignment.
*/
if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
size = ALIGN_UP(nb + SIZE_SZ, pagesize);
else
size = ALIGN_UP(nb + SIZE_SZ + MALLOC_ALIGN_MASK, pagesize);
tried_mmap = true;

/* Don't try if size wraps around 0 */
if ((unsigned long)(size) > (unsigned long)(nb)) {
mm = (char *)(MMAP(0, size, PROT_READ | PROT_WRITE, 0));

if (mm != MAP_FAILED) {
/*
The offset to the start of the mmapped region is stored
in the prev_size field of the chunk. This allows us to adjust
returned start address to meet alignment requirements here
and in memalign(), and still be able to compute proper
address argument for later munmap in free() and realloc().
*/

if (MALLOC_ALIGNMENT == 2 * SIZE_SZ) {
/* For glibc, chunk2mem increases the address by 2*SIZE_SZ and
MALLOC_ALIGN_MASK is 2*SIZE_SZ-1. Each mmap'ed area is page
aligned and therefore definitely MALLOC_ALIGN_MASK-aligned. */
assert(((INTERNAL_SIZE_T)chunk2mem(mm) & MALLOC_ALIGN_MASK) == 0);
front_misalign = 0;
} else
front_misalign = (INTERNAL_SIZE_T)chunk2mem(mm) & MALLOC_ALIGN_MASK;
if (front_misalign > 0) {
correction = MALLOC_ALIGNMENT - front_misalign;
p = (mchunkptr)(mm + correction);
set_prev_size(p, correction);
set_head(p, (size - correction) | IS_MMAPPED);
} else {
p = (mchunkptr)mm;
set_prev_size(p, 0);
set_head(p, size | IS_MMAPPED);
}

/* update statistics */

int new = atomic_exchange_and_add(&mp_.n_mmaps, 1) + 1;
atomic_max(&mp_.max_n_mmaps, new);

unsigned long sum;
sum = atomic_exchange_and_add(&mp_.mmapped_mem, size) + size;
atomic_max(&mp_.max_mmapped_mem, sum);

check_chunk(av, p);

return chunk2mem(p);
}
}
}
mmap 失败或者未分配堆
1
2
3
/* There are no usable arenas and mmap also failed.  */
if (av == NULL)
return 0;

如果是这两种情况中的任何一种,其实就可以退出了。。

记录旧堆信息
1
2
3
4
5
6
7
/* Record incoming configuration of top */
/* 记录传入的 top 的配置信息 */
old_top = av->top;
old_size = chunksize(old_top);
old_end = (char *)(chunk_at_offset(old_top, old_size));

brk = snd_brk = (char *)(MORECORE_FAILURE);
检查旧堆信息 1
1
2
3
4
5
6
7
8
9
10
/*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/
/*
如果不是第一次执行,我们要求 old_size 至少为 MINSIZE,并且设置了 prev_inuse。
*/
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long)(old_size) >= MINSIZE && prev_inuse(old_top) &&
((unsigned long)old_end & (pagesize - 1)) == 0));

这个检查要求满足其中任何一个条件

  1. old_top == initial_top(av) && old_size == 0,即如果是第一次的话,堆的大小需要是 0。
  2. 新的堆,那么
    1. (unsigned long)(old_size) >= MINSIZE && prev_inuse(old_top),堆的大小应该不小于 MINSIZE,并且前一个堆块应该处于使用中。
    2. ((unsigned long)old_end & (pagesize - 1)) == 0),堆的结束地址应该是页对齐的,由于页对齐的大小默认是 0x1000,所以低 12 个比特需要为 0。
检查旧堆信息 2
1
2
/* Precondition: not enough current space to satisfy nb request */
assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));

根据 malloc 中的定义

1
2
static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size */

nb 应该是已经加上 chunk 头部的字节,为什么还要加上 MINSIZE呢?这是因为 top chunk 的大小应该至少预留 MINSIZE 空间,以便于合并。

tcache(gblic>=2.26)

tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术(see commit),目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。

主要参考了 glibc 源码,angelboy 的 slide 以及 tukan.farm,链接都放在最后了。

相关结构体

tcache 引入了两个新的结构体,tcache_entrytcache_perthread_struct

这其实和 fastbin 很像,但又不一样。

tcache_entry

1
2
3
4
5
6
7
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
/* 当块存储在每个线程的缓存中时,我们将此结构叠加在块的用户数据部分上。 */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

tcache_entry 用于链接空闲的 chunk 结构体,其中的 next 指针指向下一个大小相同的 chunk。

需要注意的是这里的 next 指向 chunk 的 user data,而 fastbin 的 fd 指向 chunk 开头的地址。

而且,tcache_entry 会复用空闲 chunk 的 user data 部分。

tcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
/*
每个线程都有一个这样的结构,其中包含每个线程的缓存(因此称为 "tcache_perthread_struct")。保持整体大小较小略为重要。请注意,COUNTS 和 ENTRIES 是冗余的(我们每次都可以计算链表的长度),这是出于性能考虑。
*/
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

每个 thread 都会维护一个 tcache_perthread_struct,它是整个 tcache 的管理结构,一共有 TCACHE_MAX_BINS 个计数器和 TCACHE_MAX_BINS项 tcache_entry,其中

  • tcache_entry 用单向链表的方式链接了相同大小的处于空闲状态(free 后)的 chunk,这一点上和 fastbin 很像。
  • counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk。(为了在tcache中保持较小的内存开销和更好的性能,可能在不同libc上有不同的限制,例如较早版本有的4或者8)

用图表示大概是:(next指针存在fd区域)

img

基本工作方式

  • 第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct
  • free 内存,且 size 小于 small bin size 时
  • tcache 之前会放到 fastbin 或者 unsorted bin 中
  • tcache 后:
    • 先放到对应的 tcache 中,直到 tcache 被填满(默认是 7 个)
    • tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
    • tcache 中的 chunk 不会合并(不取消 inuse bit)
  • malloc 内存,且 size 在 tcache 范围内
  • 先从 tcache 取 chunk,直到 tcache 为空
  • tcache 为空后,从 bin 中找
  • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来

Tcache的相关知识

在32位系统中,bin会存放12-512字节的chunk,在64位系统中bin会存放24-1032字节的chunk。也就是说符合这些大小的chunk被释放后不会先加入fastbin,而是会先放入TcacheBin中。**(和fastbin的存储大小差不多一致)**

TcacheBin以单链表构成

什么时候用到Tcache:

free:在释放chunk的时候,如果chunk符合Tcachebin的大小,并且该bin还没有被装满(没有七个),则会优先放入TcacheBin中

malloc:

  1. 当我们使用malloc的时候,返回一个chunk,并且该chunk是从fastbin中返回的,那么该chunk所对应下标的所有chunk都会被放入TcacheBin(当然,前提是Tcachebin没有被装满),而且由于fastbin和Tcachebin都是先进后出,所以就会导致chunk在移动完以后chunk的顺序和fastbin中的顺序相反。
  2. smallbin中的也一样,返回的一个chunk属于smallbin,那么smallbin中对应的chunk就会全部放入Tcachebin(前提是没有装满)
  3. 当出现堆块的合并等其它情况的时候,每一个符合条件的chunk都会优先放入TcacheBin中,而不是直接返回(除非Tcache已满)。寻找结束后Tcache会返回其中一个

比较重要的一点,当我们调用malloc的时候,其实是先调用libc_malloc然后调用int_malloc,但是如果我们请求的大小在Tcachebin中有符合的chunk那么就会在libc_malloc中返回该chunk,而不会调用int_malloc

我们在处理chunk的过程中,如果在Tcache中的chunk已满,那么会直接返回最后一个chunk;binning code 结束后,如果没有直接返回(如上),那么如果有至少一个符合要求的 chunk 被找到,则返回最后一个。

tcache 中的 chunk 不会被合并,无论是相邻 chunk,还是 chunk 和 top chunk。因为这些 chunk 会被标记为 inuse。

源码分析

接下来从源码的角度分析一下 tcache。

__libc_malloc

第一次 malloc 时,会进入到 MAYBE_INIT_TCACHE ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void *
__libc_malloc (size_t bytes)
{
......
......
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
/*int_free 函数还会调用 request2size 函数,请注意不要重复进行填充。(提醒开发者在释放内存块时,注意在调用 request2size 函数时避免重复进行填充操作) */
size_t tbytes;
// 根据 malloc 传入的参数计算 chunk 实际大小,并计算 tcache 对应的下标
checked_request2size (bytes, tbytes); //tbytes应该是对应tcache的大小
size_t tc_idx = csize2tidx (tbytes); //计算下标

// 初始化 tcache
MAYBE_INIT_TCACHE (); //检查当前线程的 tcache 是否已经初始化,如果没有初始化,则进行初始化
DIAG_PUSH_NEEDS_COMMENT; //在编译器诊断信息中添加一个注释没有实质性作用
if (tc_idx < mp_.tcache_bins // 根据 size 得到的 idx 在合法的范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL) // tcache->entries[tc_idx] 有 chunk
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
......
......
}

__tcache_init()

附:arena是一块连续的内存区域(包括Heap Arena[Small bin and Large bin]和Thread-specific Arena[Fast bin and Tcache])

其中 **MAYBE_INIT_TCACHE () 在 tcache 为空(即第一次 malloc)时调用了 tcache_init()**,直接查看 tcache_init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes); // 找到可用的 arena
victim = _int_malloc (ar_ptr, bytes); // 申请一个 sizeof(tcache_perthread_struct) 大小的 chunk
if (!victim && ar_ptr != NULL) //没有可用的arena或者没有申请chunk成功
{
ar_ptr = arena_get_retry (ar_ptr, bytes); //重试
victim = _int_malloc (ar_ptr, bytes); //重试
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex); //解锁arena
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
/* 在内存紧张的情况下,我们可能无法分配内存 - 在这种情况下,我们稍后会继续尝试。
不过,通常我们会在很早的阶段进行这个尝试,
因此要么有足够的内存,要么根本没有足够的内存来进行非平凡的分配操作。
*/
if (victim) // 初始化 tcache
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}

tcache_init() 成功返回后,tcache_perthread_struct 就被成功建立了。

申请内存

接下来将进入申请内存的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // 从 tcache list 中获取内存
if (tc_idx < mp_.tcache_bins // 由 size 计算的 idx 在合法范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL) // 该条 tcache 链不为空
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
// 进入与无 tcache 时类似的流程
if (SINGLE_THREAD_P)
{
victim = _int_malloc (&main_arena, bytes); //tcache为空就直接_int_malloc
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}

tcache->entries 不为空时,将进入 tcache_get() 的流程获取 chunk,否则与 tcache 机制前的流程类似,这里主要分析第一种 tcache_get()。这里也可以看出 tcache 的优先级很高,比 fastbin 还要高( fastbin 的申请在没进入 tcache 的流程中)。

tcache_get()

看一下 tcache_get()

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}

tcache_get() 就是获得 chunk 的过程了。可以看出这个过程还是很简单的,从 tcache->entries[tc_idx] 中获得第一个 chunk,tcache->counts 减一,几乎没有任何保护。

__libc_free()

看完申请,再看看有 tcache 时的释放

1
2
3
4
5
6
7
8
9
void
__libc_free (void *mem)
{
......
......
MAYBE_INIT_TCACHE ();
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}

__libc_free() 没有太多变化,**MAYBE_INIT_TCACHE () 在 tcache 不为空失去了作用**。

_int_free()

跟进 `_int_free()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
......
......
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins // 64
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7
{
tcache_put (p, tc_idx);
return;
}
}
#endif
......
......

判断 tc_idx 合法,tcache->counts[tc_idx] 在 7 个以内时,就进入 tcache_put(),传递的两个参数是要释放的 chunk 和该 chunk 对应的 size 在 tcache 中的下标。

tcache_put()

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
/* 调用者必须确保我们知道 tc_idx 是有效的,并且还有足够的空间来存放更多的块。 */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx]; //FIFO
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_puts() 完成了把释放的 chunk 插入到 tcache->entries[tc_idx] 链表头部的操作,也几乎没有任何保护。并且 没有把 p 位置零。(P位是指前一个内存是否分配)