前言
对于内存空间有两种描述方式,一种是物理内存的角度,所有地址只分为两类,挂了物理页的地址与没有挂物理页的地址,其属性由PDE/PTE决定。另一种是线性地址的角度,分为私有内存与映射内存。前者,在介绍段、页时已有了解,近来这几篇主要是从线性地址的角度,来学习内存空间。本篇就来学习其中的一种类型,私有内存。
私有内存与映射内存
在前一篇用指令打印Vad树时,可以看到,同一个进程中,就存在私有内存(Private)与映射内存(Mapped)两种类型的内存空间。
![](/2020/08/31/Private-Memory/privateAndMapped.png)
这两类内存的区别主要有2点不同:
- 申请内存的方式不同:
- 私有内存:通过VirtualAlloc/VirtualAllocEx申请的。
- 映射内存:通过CreateFileMapping映射的。
- 使用方式不同:
- 私有内存:独享物理页。
- 映射内存:可能要与其它进程共享物理页。
VirtualAlloc
函数原型
本篇主要介绍私有内存,就先从它的申请内存函数开始。首先是VirtualAlloc的函数原型:
1 | LPVOID VirtualAlloc{ |
这4个参数都挺重要的,除注释外,再额外介绍一下:
- lpAddress:申请内存地址,自然要有起点,终点,这个参数就是指申请地址的起点。结合第二个参数dwSize,例如申请起始地址0x123000,大小0x1000,那么就会给你分配0x123000~0x123fff这个物理页。但是如果这块内存已经被占用了,自然就无法申请了,由于需要查找Vad树才能知道这块地址是否被占用,过程较为麻烦,因此,通常来说,这个值填NULL,让系统自动去分配一块没被占用的内存地址。
- dwSize:就是想要分配的大小,如果提供了lpAddress,那么lpAddress就是地址的起点,lpAddress+dwSize-1就是地址的终点。这个值必须是0x1000(4KB,即一个物理页)的整数倍。
- flAllocationType:分配的类型,有两种:
- MEM_COMMIT:创建节点并分配物理页。
- MEM_RESERVE:只创建节点,不分配物理页。
- flProtect:内存的初始保护属性,例如READWRITE,READONLY这样的。
与VirtualAllocEx的差异
还有一个类似的函数VirtualAllocEx,参数什么的都和VirtualAlloc一样,唯一不同的VirtualAllocEx可以跨进程申请内存,VirtualAlloc只能在当前进程中申请内存。
VirtualAlloc实验
下面用一个实验来了解VirtualAlloc申请内存的过程:
编译运行如下代码(环境:Windows XP,编译器:VC++6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char* argv[])
{
printf("申请内存前\n");
getchar();
LPVOID address = VirtualAlloc(NULL, 0x3000, MEM_COMMIT, PAGE_READWRITE);
printf("内存地址:%X\n", address);
getchar();
return 0;
}第一次运行时,会在第一个getchar()处停下,此时进入Windbg查看当前进程的Vad树
在用VirtualAlloc函数申请完内存后,再来观察Vad树,情况就有些不一样了
可以看到,在执行完VirtualAlloc后,0x3a0000~0x3a2fff处新分配了一个大小为0x3000的内存空间,这在执行前是没有的,同时Vad树的总结点树也增加了1。同时新分配的内存空间,属性也是对应了VirtualAlloc的各个参数。
这样,使用VirtualAlloc函数申请内存的过程就好理解了,它会在低2G中,还没有使用的内存空间中,分配一个指定大小的私有内存空间,然后将其对应的_MMVAD结构添加到创建它进程的Vad树中。
堆与栈
malloc与new
文章开头提到,对于以线性地址为角度描述内存时,分为私有内存与映射内存。申请私有内存的函数为VirtualAlloc/VirtualAllocEx,申请映射内存的函数为CreateFileMapping。那么问题了,曾经我们在C语言中学习到的malloc函数,与C++中学习的new函数,难道就不能申请内存吗。这部分,就来讨论一下这个问题。
在初级班,海哥曾经说过malloc与new的底层调用过程,具体如下:
1 | malloc -> _nh_malloc_dbg -> _heap_alloc_dbg -> _heap_alloc_base -> HeapAlloc |
1 | new -> _nh_malloc -> _nh_malloc_dbg -> _heap_alloc_dbg -> _heap_alloc_base -> HeapAlloc |
可以看到,malloc和new本质都是一样的,底层都调用了HeapAlloc这么一个函数。接下来就来看看这个函数。
HeapAlloc与堆
HeapAlloc作用是在堆中分配内存的一个函数,但是它真正申请内存了吗?其实并没有,HeapAlloc函数甚至没有进入0环,一个没有进入0环的函数,它自然没有权限去申请内存。既然没法申请内存,那么HeapAlloc又是如何在堆中分配内存的呢?
这里就要了解一下堆的概念,什么是堆呢?堆其实就是操作系统通过调用VirtualAlloc函数预先分配好的一大块内存。HeapAlloc的作用就是在这一大块已经预先分配好的内存里面,分一些小份出来用。作个比喻,可以认为VirtualAlloc就是批发市场,一次必须批量从操作系统那里购买内存,必须是4KB的整数倍才可以;而HeapAlloc就是零售商,从VirtualAlloc已经批来的货里面(堆)买一部分走。
栈
前面说到堆是OS调用VirtualAlloc预先分配好的一块内存,那么栈是什么呢?栈其实和堆一样,也是预先分配好的内存,但是栈甚至不需要HeapAlloc分配,就可以直接使用。最常见的,就是局部变量了。参考接下来的实验。
堆栈实验
下面就来做一个实验,观察进程是如何使用堆,栈的空间的。
编译运行如下代码(环境:Windows XP,编译器:VC++6.0)
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int z = 0x6666;
int main(int argc, char* argv[])
{
int x = 0x12345678;
int* y = (int*)malloc(sizeof(int)*128);
printf("栈:%x \n",&x);
printf("堆:%x \n",y);
printf("全局变量:%x\n", &z);
getchar();
return 0;
}运行后,分别打印全局变量的地址,局部变量的地址,以及malloc申请的内存首地址。
然后分别在局部变量初始化前后,以及malloc调用前后,观察Windbg中查看当前进程的Vad树
会发现,无论是全局变量,局部变量,或者调用malloc函数,它都没有分配新的内存空间,只不过是使用了当前进程已有的内存空间。
不过还是有几个需要说明一下,比如局部变量的地址是0x12ff7c,而它所在的内存块的范围是0x3000~0x12ffff,主要原因是,栈是从高地址向低地址延申的,因此刚开始使用的地址都是当前内存块的高地址。堆的话,就是直接使用了一块已有的内存,可以回想之前批发商与零售商的例子。全局变量,就比较与众不同了,它映射了当前进程的.exe文件,这部分下篇学习映射内存时会讲到。
总结
- VirtualAlloc/VirtualAllocEx是申请私有内存的唯一方式。
- new与malloc的内部调用是HeapAlloc,HeapAlloc不会进入0环,也不会申请内存,仅能分配一些已经经过VirtualAlloc/VirtualAllocEx申请好的内存。
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83722047 (My classmates-线性地址的管理学习笔记)