参考链接:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_overview/#_5
1、Linux在内存分配与使用的过程中,核心思想是:只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。所以虽然操作系统已经给程序分配了很大的内存空间,但是这块内存其实只是虚拟内存。只有当用户使用相应的内存时,系统才会真正分配物理页面给用户使用。
2、Linux标准发行版使用的堆分配器是glibc中的堆分配器:ptmalloc2.ptmalloc2主要是通过malloc/free函数来分配和释放内存。
3、堆管理器:(1)响应用户请求,向操作系统申请内存,然后将其返回给用户。
(2)管理用户释放的内存
4、malloc(size_t n)返回指向n大小的内存的指针。
当n=0时,返回当前系统允许堆的最小的内存块;
当n<0时,因为size_t是无符号整数,因此意味着分配很大的内存空间,这将导致失败。因为系统没有那么大空间进行分配。
5、free(void * p)释放p指向的内存块。
当p=null,程序不执行任何操作;
当p已经被释放过,则会出现乱七八糟的错误,就是double free;
除非被禁用时(mallopt),当释放很大的内存空间时,程序将内存块还给系统,以便减少系统所使用的内存空间。
6、我们动态申请或释放内存时,会使用malloc和free函数,但他们并不是真正与系统交互的函数。真的的函数是(s)brk、mmap、munmap函数。
如下图所示,我们主要考虑申请内存分配的操作:
(s)brk函数:OS提供了brk函数,GLIBC提供了sbrk函数。可通过增加brk的大小向OS申请内存。
初始时,堆的起始位置start_brk和末尾位置brk指向同一位置。根据ASLR是否开启可能有所不同,
(1)ASLR关闭,start_brk/brk指向data/bss段的末尾。
(2)ASLR开启,start_brk/brk指向data/bss段的末尾的随机偏移位置。
7、例子
/* 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);
curr_brk = 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;
}
运行程序:
在第一次调用brk之前:
发现此时堆段的末尾就是0x0806c000。即sbrk(0)=0x0806c000
在调用brk之后:
start_brk = 0x806c000
brk = 0x806d000
内存映射和上图一样。
这和ctfwiki中不一样……
为啥?
brk()和sbrk()这两个函数又是干啥的?
8、malloc()利用mmap创建独立的匿名映射段。匿名映射的主要目的是创建用0填充的内存,并且这块内存仅被调用进程所使用。
例子:
/* 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;
}
(1)在执行mmap之前:
.so文件的mmap段为:0xf7dfe000-0xf7dff000
(2)mmap之后:
申请的内存与存在的内存,共同构成0xf7ddd000-0xf7dff000
(3)munmmap之后:
申请的内存段又没有了,内存又恢复到原来的样子
0xf7dfe000-0xf7dff000
关键点:
问:mmap()是干啥的??
munmmap()又是干啥的??
答:
参考链接:https://blog.csdn.net/l534145398/article/details/53283808
9、多线程支持
在原来的dlmalloc实现中,当两个线程同时要申请内存时,只有一个线程可以进入临界区申请内存,而另一个线程必须等待直到临界区中不再有线程。这是因为所有的线程共享一个堆。
在glibc的ptmalloc实现中,比较好的一点就是支持了多线程的快速访问。这样所有的线程共享多个堆。
例子:
/* 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)第一次申请之前和之后:
(1)可以看出堆段紧跟着数据段,说明malloc后面是用brk函数实现的。
(2)同时,虽然我们申请了1000字节的内存,但是分配了0x0806c000-0x0804b000=0x21000字节的堆。
这说明虽然程序向OS申请了小的内存,但是OS会把大的内存分给程序。这样的话避免了多次内核态与用户态的切换,提高了效率。我们称这一连续的内存为arena。此外,我们称由主线程申请的为main_arena。后续的申请内存会从这个arena中获取,直到空间不足。当arena大小不足时,可通过增加brk的方式增加arena的空间。类似的,arena可通过减少brk缩小它的空间。
2)主线程释放之后:
内存分布同上。说明对应的arena并没有被回收,而是交由glibc来管理。当后面程序再次申请内存时,在glibc管理内存充足的情况下,glibc会根据堆分配算法来给程序分配相应的内存。[why???不懂?从哪儿看出交给glibc管理了???]
3)在第一个线程malloc之前:
我们发现内存映射变了。并没有出现与内存映射相关的堆,而是出现了内存映射相关的栈。
(为什么就说明是栈了?因为ctfwiki中有【stack:pid】字段,我这儿没有……)
4)在线程malloc之后:
发现线程1的堆栈建立了,而它所在位置是内存映射位置。大小为0xf7421000-0xf74000=0x21000(132kb)。这说明线程申请堆时背后的函数是mmap函数。同时,实际真的分配给程序的内存大小为0xf7500000-0xf7400000=0x1000000,但只有132kb具有可读可写可执行权限。这一连续的内存区域为thread_arena
注意:当用户请求的内存大于128kb,并且没有任何arena满足要求时,系统会调用mmap函数分配相应的内存空间。这与请求来自主近程还是线程无关。
5)线程释放内存后:
同样不会把内存还给系统。