虚拟内存与物理内存
这里有两个进程,他们都有属于自己的4GB的内存空间,这个在我们以前的学习中了解到了,那每个内存都是真实存在的吗?
并不是,假设在进程A中的123地址存储了一个10,那在B中的123地址不也能存储个20吗,在实际的工作中,这两个位置互不影响
所以,虚拟内存就是windows为进程分配了假的4GB的内存
但在实际运用中,A进程的数据我不但能读,还能改,说明这个值是真实存在的,但存在哪里了呢?存在了物理内存上
就如上的图
所以,每个进程的4GB是假的内存,开的空头支票,每当用的时候,操作系统才会真正的给你分配一块物理内存来存储里面的数据,这个虚拟内存才会有意义。没用的地方就没有物理内存
操作系统为了方便管理物理内存,他把物理内存按照一定的单元分类,在英特尔 x86的CPU架构里,物理内存是按照4KB的方式来分配的,也就是说把所有物理内存按照4KB的大小分成一页一页的,这也是我们为什么称管物理内存为页式管理
但4KB不是绝对的,不同的架构可能不一样,arm架构是2KB,我们只用知道就行了
虚拟地址划分
每个进程都有自己的4GB的虚拟内存,但是高2G是内核空间,是共享的,所以只有低2G才属于用户,但是,也不是全部的用户空间都能使用(现阶段不行)
分区 |
x86 32位Windows |
空指针赋值区 |
0x00000000 ~ 0x0000FFFF |
用户模式区 |
0x00010000 ~ 0x7FFEFFFF |
64KB禁入区 |
0x7FFF0000 ~ 0x7FFFFFFF |
内核 |
0x80000000 ~ 0xFFFFFFFF |
如这个表,就能说明,在用户区,前64KB和后64KB都不是能用的,所以只有上表的用户模式区才能使用
理解后,那么是不是所有的用户模式区都会被挂上物理内存?
并不是,即使你又这么大的虚拟内存,但是你的程序只用了一点,那么就只会给你一点的物理内存
所以,可以使用的虚拟内存并不代表一定对应物理页,只有当申请使用了,才会有自己对应的物理页
实验
这里就展示一下,在虚拟机中,启动一个程序,看一下这个程序的虚拟内存和物理内存
实验准备
这里我用的是win xp,和windbg,双机调试。环境配置的话可以自行搜索,建议使用已经激活的xp系统,因为我上个xp就是这么没滴(T_T)
这里不强制需要你明白,了解就行了
在虚拟机里启动一个程序,就记事本
在windbg里用一个指令去查看当前内存的占用情况
!process 0 0
显示所有进程
找到记事本的进程
查看记事本进程的结构体
dt _ERPOCRSS 898ea020
(此处可以理解为内核对象的地址)
然后查看物理页的指令,也就是记录虚拟内存的空间那些被使用了,在上图中11c的位置
!vad 0x894f1f98
这里是以页为单位,我们知道一页是4KB,即0~0xFFF,即0x1000
4KB = 2^12B = 2^4 2^4 2^4 = 16 16 16 = 0x1000
所以这里的start的10并不是线性地址,而是10000才对,后面的也同理
看后面的end也是10000,说明这个10000被占用了,20000也是一样的,但10000~20000(不包含头和尾)之间没有被占用
后面的30000~12F000就是被占用的
start和end可以看成两个节点,每个节点内都是占用的状态,而节点与节点之间确是没有占用的状态
物理页有多大?
在虚拟机的设置中可以看到内存有多大
在从管理器上看是2096552KB
这里的单位是KB,且是10进制的,识别不一定准确,但绝大部分都是
2GB = 2(2^10)\(2^10) = 2097152 KB
这里有多少的物理页,2096552 / 4 = 524138 = 0x7FF6A
验证一下,windows用一个内存存储着
MmNumberOfPhysicalPages
dd就是查看以DWORD为单位指定的地址的值
可以看到跟我算的没问题
物理内存
可供使用的物理内存
MmNumberOfPhysicalPages * 4 = 物理内存
如果不够用,操作系统就会把硬盘当成内存来使用,允许我们设置虚拟内存(这个虚拟内存跟我们上面说的虚拟内存没有关系,这个是虚拟内存是把硬盘划出来一块当内存使用)
右键【我的电脑】,选择属性,再按照图中的数字去操作,这里我显示的是2046MB
可以在C盘的根目录下找到pagefile文件,查看,是1.99GB,2064/1024 = 1.99,如果修改那个值,那这个文件也会发生变化
能够识别的物理内存
32位系统最多可以识别物理内存为64GB,但由于操作系统的限制,比如说XP,只能识别4G
物理内存原理
既然知道了虚拟内存和物理内存,那这个物理内存是否永远存在?
当然不是,假设A进程原来申请了一块物理内存,但是操作系统发现这个进程运行的并不是很频繁,所以这块物理内存原来的数据就会存入硬盘,腾出来的新物理页就会用作他处,谁紧急谁就会优先处理
所以通过这个方式,明明物理内存不充足,却能运行多个程序的原因所在
我访问一个分配过的虚拟内存,操作系统会检查,读的时候发现没有物理页,出异常了,操作系统会接管这个异常,查原来有没有数据,一查发现分配了,就会给他分配一个新的物理页,把pagefile.sys里的数据读出来,放到物理页里,挂上后,你的数据正常返回,你也就是正常读到数据
私有内存的申请释放
在上一章中了解了虚拟内存和物理内存的对应关系,以及用内核调试来查看虚拟内存的使用情况
首先在看一个程序
#include<stdio.h>
#include<windows.h>
int main()
{
return 0; //下个断点
}
然后运行,在windbg里写上命令,查找我们创建的程序,建议写英文名字
!process 0 0
然后查看他的结构体
dt _EPROCESS 8954fda0
查看他的物理页
!vad 0x8960b840
我们申请的内存,只有两种,一个是私有(Private),一个是映射(Mapped)
就如上的图,Private内存是私有的,别的访问不了
而mapped内存就是共享内存,多个进程都可以访问
代码
接下来就是私有内存的申请与释放代码了
申请内存的两种方式
- 通过VirtualAlloc/VirtualAllocEx申请的:Private Memory
- 通过CreateFileMapping映射的:Mapped Memory
VirtualAlloc/VirtualAllocEx
这两个实现的方法都是一样的,但是VirtualAlloc是在当前进程申请的,而VirtualAllocEx是在别的进程申请的,给个进程ID就可以了
以VirtualAlloc为例
#include<stdio.h>
#include<windows.h>
int main()
{
LPVOID p = VirtualAlloc(
NULL,
0x1000*2,
MEM_COMMIT,
PAGE_READWRITE
);
return 0;//断点
}
VirtualAlloc
LPVOID VirtualAlloc(
LPVOID lpAddress, //要分配的内存区域地址
SIZE_T dwSize, //分配的大小
DWORD flAllocationType, //分配的类型
DWORD flProtect //该内存的初始保护属性
);
Parameters
lpAddress
指定一个地址,但是通常不用指定,原因是,假如我指定了一个已经被占用的地址,那一定是失败的,因为这个地址已经被占用了,所以需要指定一个还未被占用的空间,所以通常不指定,填空,除非有特殊需求
dwSize
[in] Specifies the size, in bytes, of the region. If the lpAddress parameter is NULL, this value is rounded up to the next page boundary. Otherwise, the allocated pages include all pages containing one or more bytes in the range from lpAddress to (lpAddress+dwSize). This means that a 2-byte range straddling a page boundary causes both pages to be included in the allocated region.
这里虽然说是以字节为单位,但是实际写的过程中要写成页的整数倍
即便你写了一个字节,也会给你分配一个页
4KB = 0x1000 = 4096 ,两页就乘以2
flAllocationType
分配的类型,这里的类型较多,只说常用的两个
MEM_COMMIT
他不仅会占用线性地址,并且还会需要物理内存
MEM_RESERVE
他会占用线性地址,但暂时不需要物理内存
flProtect
物理页的属性
这里就是可读、可写、可执行等,没有全部截完
执行后在windbg查看虚拟内存使用情况
可以看到,跟之前,多了3A0~3A1,私有的,可读可写的,commit代表改内存区域是否被提交,提交了才可以直接访问,这里是提交了的状态
如果是MEM_RESERVE状态就会是0
释放
VirtualFree
BOOL VirtualFree(
LPVOID lpAddress, // address of region
SIZE_T dwSize, // size of region
DWORD dwFreeType // operation type
);
这里的参数跟VirtualAlloc这里的参数意思是一样的,只不过需要注意一点,当第三个参数为MEM_RESERVE时,第二个参数就要为0
#include<stdio.h>
#include<windows.h>
int main()
{
LPVOID p = VirtualAlloc(
NULL,
0x1000*2,
MEM_COMMIT,
PAGE_READWRITE
);
VirtualFree(p,0x1000*2,MEM_COMMIT);
//或者 VirtualFree(p,0,MEM_RESERVE)
return 0;//断点
}
真假内存说
在上面说了,只有两个函数是申请内存的,一个是VirtualAlloc/VirtualAllocEx,另一个是CreateFileMapping
那malloc呢?不是吗?
其实他是在已经申请的内存中拿出来一块来分配的
#include<stdio.h>
#include<windows.h>
int main()
{
int x = 0x12345678; //栈
int* y = (int*)malloc(sizeof(int)*128); //堆
printf("栈:%x\n",&x);
printf("堆:%x\n",y);
getchar();
return 0;
}
这里代码都能理解,简单说一下栈和堆,栈是给运行中的程序一块内存,而堆是当我们想申请不确定大小的内存时,通常就会从堆里申请一块内存
在从windbg看该程序的内存使用情况
这里海哥说12ff7c是在30~12f区间的,但我认为不是,因为在上面也说了,12f也就是12f000,而12ff7c是超过了这个区间,所以我认为不是
如果有错误,还请帮忙指正,这里我是有点蒙圈
堆的话就没问题了
可以发现堆的这块内存是早以分配好的,不管你运不运行malloc
如果这样不能说服你的话,那就来看malloc的汇编
我们停到malloc的位置,跟进去看看
发现有个_nh_malloc_dbg函数,先不管,继续跟进去看看
会发现有个_heap_alloc_dbg函数,仍然不管,继续跟进去
这里有点长,就不截完了
看这个函数名猜测不是跟内存有关的,那就不管,继续向下走
遇到_heap_alloc_base,可能有点熟悉了,不明白的话,依然继续跟
这里依然太多,就不截完
看到这儿,明白了这是调用了windows的一个模块里的HeapAlloc函数,查一下
LPVOID HeapAlloc(
HANDLE hHeap, // handle to private heap block
DWORD dwFlags, // heap allocation control
SIZE_T dwBytes // number of bytes to allocate
);
Return Values
If the function succeeds, the return value is a pointer to the allocated memory block.
返回值
如果函数成功,则返回值是指向已分配内存块的指针。
这里就可以知道了,malloc经过了重重调用直到HeapAlloc,所以可以理解为malloc,就是调用了HeapAlloc,所以malloc并不是真正意义上的申请新内存,而是旧内存新用
共享内存的申请释放
上一章讲解了私有内存的申请与释放,接下来就是共享内存了
先看代码
#include<stdio.h>
#include<windows.h>
#define MapFileName "共享内存"
#define BUF_SIZE 0x1000
HANDLE hMapFile;
LPTSTR lpBuff;
int main()
{
getchar();
//内核对象:物理页
hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUF_SIZE,MapFileName);
//将物理页与线性地址进行映射
lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_WRITE,0,0,BUF_SIZE);
*(PDWORD)lpBuff = 0x12345678;
printf("%p",lpBuff);
getchar();
//关闭映射
UnmapViewOfFile(lpBuff);
//关闭句柄
CloseHandle(hMapFlie);
getchar();
return 0;
}
CreateFileMapping
看名字感觉这个函数与文件有联系,其实,不仅仅是文件,还与物理页有联系
HANDLE CreateFileMapping(
HANDLE hFile, // handle to file
LPSECURITY_ATTRIBUTES lpAttributes, // security
DWORD flProtect, // protection
DWORD dwMaximumSizeHigh, // high-order DWORD of size
DWORD dwMaximumSizeLow, // low-order DWORD of size
LPCTSTR lpName // object name
);
这个函数也是个内核对象,这个函数的作用,可以理解成,为我们准备物理内存
hFile
文件句柄。如果给我一个文件的句柄,可以把这个文件映射到物理页上。如果我只想要一个物理内存,不想要与文件关联,那就用INVALID_HANDLE_VALUE参数,这个宏的值是-1
lpAttributes
安全描述符,填空就可以了
flProtect
保护模式
为这个物理内存提供什么属性,第一个就是只读,第二个是可读可写,第三个会在以后的章节讲到
dwMaximumSizeHigh
高32位值,在32位计算机里这个值用不到,填空
dwMaximumSizeLow
你要指定多大的物理内存
lpName
物理内存的名称
这个函数解释完了,就是用来创建物理内存的,好了,进程有了,物理内存有了,但是这两者之间的联系还没建立起来,所以会用到一个新的API
MapViewOfFile
把物理页与虚拟地址或者线性地址关联起来,即映射
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // handle to file-mapping object
DWORD dwDesiredAccess, // access mode
DWORD dwFileOffsetHigh, // high-order DWORD of offset
DWORD dwFileOffsetLow, // low-order DWORD of offset
SIZE_T dwNumberOfBytesToMap // number of bytes to map
);
hFileMappingObject
你要映射的物理内存的句柄
dwDesiredAccess
指定虚拟内存的属性
这里有个要求,就是对虚拟地址的属性要求只能比物理内存的要求要严格,比如说,物理内存是可读可写的,那我虚拟内存是可读的,那么就能成功,当我去写的话就会报错
如果物理内存是只读,虚拟内存是可读可写,那么映射失败,这个很容易理解
dwFileOffsetHigh
高32位,用不到,填0
dwFileOffsetLow
从什么位置开始映射,写偏移地址。比如说,本代码的大小是1页,那我就从0开始映射,映射1页
dwNumberOfBytesToMap
大小,创建了多少就写多少
返回值
If the function succeeds, the return value is the starting address of the mapped view
如果函数成功执行,其返回值是映射视图的起始地址。
这个地址就可以根据你设定的属性来读或写
接下来就是释放了
UnmapViewOfFile
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress // starting address
);
这个函数的作用就是把虚拟地址与物理页之间的关联也就是映射给断掉,也就是映射没了,但是这个物理页还是存在的,所以后面有CloseHanle的函数,内核对象-1,但是执行这个函数也只是代表本进程不用这个物理页了,可能还有其它进程在使用这个物理页,所以这个物理页可能还活着
运行结果
首先遍历创建前的虚拟内存占用情况
由于我还没创建和映射,所以这里没有,然后继续执行,放开windbg,在控制台点个回车
可以看到打出了返回地址,在在windbg里看,这里就不用以前那么麻烦,只用!vad这个指令就行了,因为,这个程序还在跑,所以不用重新来
这里就可以看到属性等都对上了,这里的commit为0并不是没有物理页的意思,只是没有提交。
在放开windbg,点回车
发现我们映射的内存被摘掉了
多进程共享
在准备一份大差不差的代码
#include<stdio.h>
#include<windows.h>
#define MapFileName "共享内存"
#define BUF_SIZE 0x1000
HANDLE hMapFile;
LPTSTR lpBuff;
int main()
{
//内核对象:物理页
hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUF_SIZE,MapFileName);
//将物理页与线性地址进行映射
lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_WRITE,0,0,BUF_SIZE);
printf("进程B:%x",*(PDWOR)lpBuff);
//关闭映射
UnmapViewOfFile(lpBuff);
//关闭句柄
CloseHandle(hMapFile);
getchar();
return 0;
}
我直接读返回地址的内容
查看CreateFileMapping的饭返回值
Return Values
If the function succeeds, the return value is a handle to the file-mapping object. If the object existed before the function call, the function returns a handle to the existing object (with its current size, not the specified size) and [GetLastError](JavaScript:hhobj_4.Click()) returns ERROR_ALREADY_EXISTS.
If the function fails, the return value is NULL. To get extended error information, call GetLastError.
返回值
如果函数成功执行,返回值是一个指向文件映射对象的句柄。如果该函数调用之前对象已经存在,则函数返回指向现有对象的句柄(具有其当前大小,而不是指定的大小),并且GetLastError函数返回ERROR_ALREADY_EXISTS。
如果函数失败,返回值是NULL。要获取扩展的错误信息,请调用GetLastError函数。
运行结果
首先A进程都有返回地址
然后看B进程
成功读取到了