每个进程都有自己独立的4G内存空间,高2G操作系统内核使用,低2G用户使用。
每个进程中我们使用的都是虚拟地址,虚拟机地址到物理地址的转换由操作系统内核完成,故而我们无法在自己的进程当中访问其他进程的内存。
对于不同进程windows尽量保证同一份数据,在物理内存中只有一份 ,分别映射到多个进程中,达到节约内存的目的。
当各个进程所使用的内存数量超出物理内存大小后,操作系统还能够将物理内存中暂时用不到的数据交换到硬盘中。
Windows是如何管理内存数据的?
Windows提供了以下3种方式来管理内存数据:
堆
:适合用来管理大量的小型对象,使用堆管理方式可以非常方便的帮我们管理所需要的内存空间,缺点是效率低,控制不够灵活。
虚拟内存
:适合用于管理大型的对象数组或大型的结构数组,使用虚拟内存管理方式有丰富的内存管理接口,可以使我们更加精确的控制内存数据。
文件映射
:适合用于管理大型的数据流,以及在同一个机器上的不同进程间共享数据。
堆在windows中是作为一个对象来管理的,我们可以创建一个堆对象
,之后在堆对象所在的内存空间上分配内存,销毁内存等,C/C++中的new malloc其实也是使用的windows中的堆对象来分配空间的。
当windows系统创建一个进程后,会为此进程创建一个默认的堆,这个默认堆是不能够被销毁的。
堆内存管理相关API
API名称 | 备注 |
---|---|
HeapCreate | 在进程中创建一个堆对象,返回一个句柄 |
GetProcessHeap | 获取当前进程中的一个堆,返回一个句柄 |
GetProcessHeaps | 获取进程中的所有堆 |
HeapAlloc | 从指定的堆上分配块 |
HeapReAllocHeapFree | 重新分配内存,改变已经分配好的堆内存大小 |
HeapSize | 获取堆的大小 |
HeapDestroy | 销毁堆对象 |
HeapCreate:
HANDLE HeapCreate(
[in] DWORD flOptions, //堆分配选项
[in] SIZE_T dwInitialSize,//堆的初始大小 NULL为默认
[in] SIZE_T dwMaximumSize//堆的最大大小 为0则是可以增长的,否则是固定的。
);
HeapAlloc :
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap, //堆对象句柄
[in] DWORD dwFlags, //堆分配选项 ;指明了堆分配错误时的处理办法或者初始化堆空间为0
[in] SIZE_T dwBytes //分配的堆空间字节数
);
HeapFree:释放堆所处的内存空间
HeapDestroy:销毁堆对象
GetProcessHeaps:
DWORD GetProcessHeaps(
[in] DWORD NumberOfHeaps, //可以存储到ProcessHeaps的最大堆的句柄数量
[out] PHANDLE ProcessHeaps //指向可以存储堆句柄数组的缓冲区指针
);
//如果返回值小于或等于 NumberOfHeaps,则函数将堆句柄数存储在 ProcessHeaps 指向的缓冲区中。
//如果返回值大于 NumberOfHeaps,使用返回值分配足够大的缓冲区来接收所有句柄,并再次调用该函数。
代码测试:
#include
#include
void func1();
int main()
{
//创建可由调用进程使用的专用堆对象。
HANDLE Heap = HeapCreate(NULL, NULL, NULL);// 需要分配大型内存块的应用程序应将 dwMaximumSize 设置为 0。
if (Heap == 0)
{
printf("创建堆对象失败!\n");
return 0;
}
//分配内存块
char* str = nullptr;
str = (char*)HeapAlloc(Heap, HEAP_ZERO_MEMORY, 0x4);
strcpy_s(str, 0x4,"ylh");
printf("%s\n", str);
HeapFree(Heap, NULL, str);
HeapDestroy(Heap);
return 0;
}
给一个字符数组申请三个字节的堆内存,注意:字符数组大小包括最后的空字符,因此应该分配的确切的内存大小是 n-1 个字节。
获取进程堆 的句柄:
int main()
{
HANDLE hHeap = GetProcessHeap(); //获取当前进程的一个堆
HANDLE Handles[0x10]{};
//粗略的分配了0x10个句柄,但其实只有2个堆句柄
DWORD num = GetProcessHeaps(0x10, (PHANDLE)Handles);
//利用第一次的返回值来分配准确的堆数组的空间大小
HANDLE* RealHandles = new HANDLE[num]{};
GetProcessHeaps(0x10, (PHANDLE)RealHandles);
printf("处于活动状态的堆的句柄数:%d\n", num);
return 0;
}
内存分页的概念:
操作系统管理内存是将内存分成一页一页来管理的,每一页的大小是4K也就是0x1000
4G的内存共有1M个页
使用了分页机制之后,4G的地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西。对于一般程序来说,4G的地址空间,只有一小部分映射了物理内存,大片大片的部分是没有映射任何东西。CPU用来把虚拟地址转换成物理地址的信息存放在叫做页目录和页表的结构里。
虚拟内存状态:
状态
空闲
(FREE): 内存页不可用
保留(
Reserve) : 内存页被预定了,但为与物理内存做映射,还是不可用
提交
( Commit ) : 内存被分配,并且与物理内存进行了映射,进程可以使用了
当虚拟内存映射到物理内存时,有三种映射方式
内存映射方式:
映射方式 描述
Private
: 进程私有内存,不被其他进程所共享,一般是堆,栈
Mapped
: 从别的内存映射而来
Image
: 从程序的PE映像映射而来。
虚拟内存管理-内存属性:
Windows中,内存管理的最小单元是一个内存页 通常是0x1000=4kb
内存分页属性:
ReadOnly
只读
READ_WRITE
读写
EXECUTE
执行
EXECUTE_READ_WRITE
可读可写可执行
WRITE_COPY
写时拷贝
页交换文件逻辑:
程序访问虚拟内存地址,操作系统判断数据是否在内存中,如果在就从虚拟地址映射到的物理地址,如果不在就判断是否在页交换文件当中,如果在就查看物理内存是否有闲置空间,有的话,就将页交换文件载入到物理内存,如果没有闲置内存,就从物理内存中找到一个可以释放的页,然后将页保存到页交换文件中。
虚拟内存相关API
函数 | 作用 |
---|---|
VirtualAlloc | 分配或者预定一块虚拟内存 |
VirtualAllocEx | 可以在其他进程分配或者预定一块内存 |
VirtualFree | 释放内存 |
VirtualFreeEx | 可以释放其他进程内存 |
VirtualLock | 锁定内存不能交换到硬盘 |
VirtualUnLock | 解锁 |
VirtualProtect | 修改内存读写执行属性 |
VirtualProtectEx | 可以修改其他进程内存属性 |
ReadProcessMemory | 读取远程进程内存 |
WriteProcessMemory | 写入数据到远程进程内存 |
VirtualQuery | 查询内存状态 |
VirtualQueryEx |
VirtualAlloc:
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, //要分配的区域的起始地址,为NULL,则默认选择区域
[in] SIZE_T dwSize, //区域的大小
[in] DWORD flAllocationType,//内存分配的类型 预定,保留,提交等
[in] DWORD flProtect //要分配的页面区域的内存保护属性(可读,可写? ....)
);
VirtualFree :释放使用VirtualAlloc分配的内存。
VirtualProtect :
BOOL VirtualProtect(
[in] LPVOID lpAddress, //要修改的内存的起始地址
[in] SIZE_T dwSize, //要修改的内存大小
[in] DWORD flNewProtect,//要修改的新的内存保护属性(可读,可写....)
[out] PDWORD lpflOldProtect//保留旧的内存属性,便于恢复
);
代码测试,VirtualAlloc分配内存:
#include
#include
#include
int main()
{
char* str = (char*)VirtualAlloc(NULL, 0x5, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
strcpy_s(str, 0x5, "abcd"); //最后一个字符是空字符
printf("%s\n", str);
VirtualFree(str, NULL, MEM_RELEASE);
return 0;
}
VirtualAlloc: 以先RESERVE预定,再COMMIT提交的方式分配内存。
修改内存属性:
int main()
{
char* str=(char*)"abcdefg";
str[0]='6'
return 0;
}
在我们一开始的认识中,这样的代码是错误的,因为char* 再直接赋初始值的情况下是只读的,我们无法再次修改str的内容(数组可以,指针不行)。
使用VirtualProtect修改内存属性:
int main()
{
char* OnlyReadSpace = (char*)"woaini";
DWORD OldProtect = 0;
VirtualProtect(OnlyReadSpace, sizeof(OnlyReadSpace), PAGE_READWRITE, &OldProtect);
OnlyReadSpace[0] = '6';
printf("%s\n",OnlyReadSpace ); // 6oaini
VirtualProtect(OnlyReadSpace, sizeof(OnlyReadSpace), OldProtect, &OldProtect);
return 0;
}
我们把OnlyReadSpace 的内存属性修改为可读可写
的,这样我们就可以直接操作内存,对其进行修改了,我们修改了只读内存中的值。
别忘了保存原来的内存属性,并且再修改完成之后还原!!!
文件映射的概念:
文件映射(Mapping)是一种将文件内容映射到进程虚拟内存
的技术。
映射成功的文件可以用视图,来引用这段内存,从而达到操作位于此段内存中的文件的目的。
文件映射最大的一个特点 : 可以在不同的进程间共享数据。(跨进程)
正常操作文件:
文件映射
:
函数 | 作用 |
---|---|
CreateFileMapping | 创建一个Mapping对象 |
OpenFileMapping | 打开一个Mapping对象 |
MapViewOfFile | 将maping对象的文件映射到内存中 |
UnmapViewOfFile | 取消文件映射 |
FlushViewOfFile | 刷新缓存区,将映射在内存中的文件写回到硬盘中 |
CreateFileMapping :为指定文件创建或打开命名或未命名的文件映射对象。
HANDLE CreateFileMappingW(
[in] HANDLE hFile,//文件句柄
[in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,//NULL
[in] DWORD flProtect, //文件映射的内存属性
[in] DWORD dwMaximumSizeHigh,//文件映射对象的最大大小的高阶 DWORD 。
[in] DWORD dwMaximumSizeLow,//文件映射对象的最大大小的低阶 DWORD 。
[in, optional] LPCWSTR lpName//名称
);
MapViewOfFile : 将maping对象的文件映射到内存中
LPVOID MapViewOfFile(
[in] HANDLE hFileMappingObject,//文件映射对象的句柄
[in] DWORD dwDesiredAccess, //文件映射对象的内存属性
[in] DWORD dwFileOffsetHigh, //视图开始位置距离文件开头的偏移:高位
[in] DWORD dwFileOffsetLow, //低位
[in] SIZE_T dwNumberOfBytesToMap //要映射到视图的文件映射的字节数:NULL则到文件尾
);
测试代码:
int main()
{
HANDLE FileHandle = CreateFileA("666.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE FileMapping = CreateFileMappingA(FileHandle, NULL, PAGE_READWRITE, NULL, 0x100, NULL);
char* filemappingView = (char*) MapViewOfFile(FileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0x100);
printf("666");
return 0;
}
如图:将MapViewOfFile返回的 filemappingView 拖入内存窗口,可以得到此文件所处的内存区域,我们可以直接修改这个文件的内存,在内存中。因为我们已经创建了这个文件的文件内存映射。
可以指定最大的文件映射的内存区域的大小,这里我指定0x100,即最大修改0x100个字节的大小在文件中,当然你也可以使用GetFileSize获取文件大小,避免超过文件本身大小。