heap

堆简介

提供动态分配的内存

堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长(栈是高地址向低地址)。我们一般称管理堆的那部分程序为堆管理器。

只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。

堆基本操作

malloc

原型:void *malloc(size_t size);

对异常状况的处理:

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

free

free(void* p)

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

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

内存分配背后的系统调用

无论是maclloc还是free都不是直接与系统交互的函数,这些函数背后的调用主要是(s)brk函数和mmap,munmap函数

当使用内存块申请操作时

系统调用

(s)brk

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

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

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

结构

brk和sbrk

sbrkbrk是UNIX和类UNIX操作系统中用于管理进程数据段(堆)的两个系统调用。它们都用于改变进程的堆大小,但它们的使用方式和行为有所不同。

sbrk函数

sbrk函数用于申请或释放堆内存。它的原型是:

1
void *sbrk(intptr_t increment);
  • increment:指定堆应该增加或减少的字节数。正值表示增加堆的大小,负值表示减少堆的大小。
  • 返回值:sbrk返回新的堆顶地址。

sbrk的工作方式是将堆顶(program break)向上或向下移动指定的字节数。如果increment是正数,堆增长;如果是负数,堆缩小。sbrk的返回值是操作前的堆顶地址。

brk函数

brk函数用于设置堆的上限。它的原型是:

1
int brk(void *end_data_segment);
  • end_data_segment:指定新的堆顶地址。
  • 返回值:brk返回0表示成功,或者在出错时返回-1。

brk将堆顶设置为指定的地址。如果新的堆顶低于当前堆中已分配的内存(即堆缩小),brk可能会失败并返回-1。如果新的堆顶高于当前堆的大小(即堆增长),操作系统可能会分配更多的内存。

区别
  1. 操作方式

    • sbrk通过指定要增加或减少的字节数来改变堆的大小。
    • brk直接设置堆的新上限地址。
  2. 返回值

    • sbrk返回操作前的堆顶地址。
    • brk返回0表示成功,返回-1表示失败。
  3. 行为

    • sbrk在增加堆时,如果新的堆顶超过了系统为进程分配的最大堆大小,sbrk会失败。
    • brk在设置堆顶时,如果新的堆顶低于已分配的内存,brk会失败。

在现代操作系统中,mallocfree等高级内存管理函数通常是基于sbrkbrk实现的,但在大多数情况下,程序员不需要直接使用sbrkbrk,而是使用这些更高级的抽象。

例子:

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
/* 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());

/* sbrk(0) gives current program break location */
tmp_brk = curr_brk = sbrk(0);
//这里使这两个变量都是堆的初始地址,这里还没有出现堆
printf("Program Break Location1:%p\n", curr_brk);
getchar();

/* brk(addr) increments/decrements program break location */
brk(curr_brk+4096);
//开始出现堆,大小位4kb
curr_brk = sbrk(0);//sbrk本质是向上或向下控制堆的大小并返回结果地址,这里传参数0就是不对堆做任何的大小上的改变,返回的地址也就是此时堆顶部的地址了
//这里获取此时堆顶部的地址
printf("Program break Location2:%p\n", curr_brk);
getchar();

brk(tmp_brk);
//释放堆
curr_brk = sbrk(0);
printf("Program Break Location3:%p\n", curr_brk);
getchar();

return 0;
}

mmapmunmap 是在 Unix 和类 Unix 操作系统中用于内存管理的两个系统调用,它们允许程序映射文件或设备到进程的地址空间,以及撤销这种映射。这些调用通常用于实现内存映射文件,这是一种高效的文件 I/O 方法,也可以用于动态内存分配。

mmap

mmap

mmap 系统调用用于将一个文件或者其他对象映射到进程的地址空间。这允许程序像访问普通内存一样访问文件内容,而不需要使用传统的 read 和 write 系统调用。mmap 的原型如下:

1
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:映射区域的起始地址,通常设置为 NULL 让系统选择地址。
  • length:映射区域的长度。
  • prot:映射区域的保护方式,可以是以下几种方式的组合:PROT_EXEC(可执行)、PROT_READ(可读)、PROT_WRITE(可写)、PROT_NONE(不可访问)。
  • flags:映射类型和属性的标志,常用的标志包括 MAP_SHARED(对映射区域的修改会反映到文件上)、MAP_PRIVATE(私有的copy-on-write映射)、MAP_ANONYMOUS(匿名映射,不与任何文件关联)等。
  • fd:被映射文件的文件描述符,如果是匿名映射则设置为 -1。
  • offset:文件映射的起始位置,通常为文件大小的整数倍。

mmap 返回值是映射区域的起始地址,如果失败则返回 MAP_FAILED(通常是(void *)-1)。

munmap

munmap 系统调用用于撤销由 mmap 创建的内存映射。munmap 的原型如下:

1
int munmap(void *addr, size_t length);
  • addr:要撤销映射的起始地址。
  • length:要撤销映射的长度,必须与原始 mmap 调用时的长度一致。

munmap 返回值是 0 表示成功,-1 表示失败,并设置 errno 以指示错误原因。

区别和用途
  • mmap 用于创建内存映射,可以用于文件 I/O、动态内存分配、共享内存等。
  • munmap 用于撤销已有的内存映射,释放之前 mmap 分配的内存空间。
  • 使用 mmapmunmap 可以提高程序的性能,因为它们允许程序直接在内存中读写文件内容,减少了数据复制的开销。
  • mmap 还可以用于实现跨进程的内存共享,因为多个进程可以映射同一个文件,从而共享文件内容。
  • munmap 必须在不再需要映射时调用,以避免内存泄漏。

例子

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 实现中,比较好的一点就是支持了多线程的快速访问。在新的实现中,所有的线程共享多个堆

这里给出一个例子。

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;
}

pthread_create 是 POSIX 线程库中用于创建新线程的函数。它是 POSIX 线程(pthread)标准的一部分,该标准定义了在多线程程序中创建和管理线程的方法。pthread_create 函数允许程序并行地执行多个线程,从而提高程序的效率和响应能力。

函数原型如下:

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

参数说明:

  1. pthread_t *thread:这是一个指向 pthread_t 类型的指针,用于存储新创建线程的标识符。如果线程成功创建,这个变量将被赋予一个唯一的线程ID。
  2. const pthread_attr_t *attr:这是一个指向 pthread_attr_t 类型的指针,它指定了线程的属性。如果设置为 NULL,则使用默认的线程属性。
  3. void *(*startroutine)(void *):这是新线程开始执行时调用的函数。它是一个函数指针,指向一个接受 void* 类型参数并返回 void* 类型值的函数。
  4. void *arg:这是传递给 startroutine 函数的参数。

返回值:

  • 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$

http://yyyffff.github.io/2025/01/28/堆/
作者
yyyffff
发布于
2025年1月28日
许可协议