0%

WindowsAPI - 内存管理

最近在学习 Win32编程,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习Win32开发的童鞋们提供一份参考。

本系列博文均根据学习《WindowsAPI开发详解》一书总结而来;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Visual Studio 2013

内存管理

内存主要存储着程序运行时所需的机器代码数据等。

内存管理原理

数据是存储与内存中,为了能够找到存储与内存中的数据,内存的最小存储单元是字节,内存中的每一个字节都有一个地址;

基本概念

  • 地址空间

    • 系统中所有可用的内存地址的集合称为地址空间;
  • 物理内存

    • 硬件系统中真实存在的内存称为物理内存,物理内存的访问必须通过硬件系统总线进行;
  • 虚拟地址空间

    • 为了访问内存的统一,操作系统允许其运行的程序访问所有的4GB内存空间的地址。
  • 进程内存空间

    • Windows操作系统每个进程都有属于自己的虚拟地址空间。32位系统上将4GB的虚拟内存划分为两个部分,进程使用低2gb,内存使用搞2gb。虚拟地址空间在进程上是封闭的,进程只能访问自己的地址空间。

分页与分段内存管理

X86体系CPU中,有若干的段寄存器,可以通过“选择器+偏移”的形式来标识内存,使用这种方式标识的地址称”逻辑地址”,这种机制称为”分段”。如果使用了分页,线性地址与物理的对应是通过分页的机制实现的。通常情况下,页大小为4KB;

进程的内存空间

  • 进程虚拟地址空间
    • Windows系统中每个进程都有一个私有的虚拟地址空间,系统需要将每个进程的虚拟地址空间都映射到物理内存地址上。为了实现系统每个进程都有一个私有的虚拟地址空间,系统为每个进程都创建一个页目录和一组页表。每个进程的页表是独立的,而内核空间的页表是所有进程共享的;
  • 数据保护共享
    • 在一个进程间共享数据、系统的可执行代码,在各个进程间都是一致的,因此没有必要再物理内存中为这些数据保留多分,不同进程的虚拟内存分页可以映射为同样的物理内存分页。
    • 为了保证映射到相同物理内存页上的内存分页在进程上是私有的,系统还提供了一些保护机制;如果对DLL中的数据进行写操作,那么在数据写入之前,将要写入的进程虚拟内存分页映射到一个新的内存分页。并将源系统DLL的内容复制到这个分页中,进程不共享这个新乌里内存分页,最后进程结束,将数据写入这个新的分页中;

虚拟内存布局

进程的虚拟内存空间分为两部分,低2gb由应用程序使用,高2gb由系统内核使用;

应用程序壳可使用的低地址空间中包括了应用程序代码、数据、系统和用户DLL的代码、各线程的栈、堆等;

进程的每个线程都有自己的栈,栈与函数的调用、执行和返回及局部变量的保存相关;

内存保护属性和存储权限

系统为每个内存分页提供保护属性和存取权限,内存的保护属性和存取权限的最小单位是分页,也就是说同一个分页中的内存必然具有相同的保护属性和存取权限;

堆管理

用户使用内存分配函数分配的内存都位于堆中,所以使用堆管理函数内存进行分配、释放是最为直接的方式;

获取堆句柄、分配与再分配

堆是一种内存管理对象,一个进程有若干个堆,在分配内存前需要指定从哪个堆上进行分配。对的句柄唯一标识了一个堆。在堆上分配内容前,需要获得索要进行分配的堆的句柄,获得堆的句柄有两种方式:获取在进程中时已经创建好的堆,进程自己再创建堆

HeapCreate

为进程创建新堆,请求分配虚拟内存分页;

1
2
3
HANDLE HeapCreate(DWORD flOptions, 		//你可以设定0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS、HEAP_CREATE_ENABLE_EXECUTE或者是这些标志的组合
DWORD dwInitialSize, //堆初始化大小
DWORD dwMaximumSize ); //堆最大值

GetProcessHeap

1
HANDLE GetProcessHeap(VOID);			//成功返回获得的堆句柄

GetProcessHeaps

获取进程中所有堆,包括堆的数量和各个堆的句柄

1
2
DWORD GetProcessHeaps(  DWORD NumberOfHeaps,  // maximum number of heap handles 
PHANDLE ProcessHeaps // buffer for heap handles);

HeapAlloc

从指定的堆上分配内存块

1
2
3
LPVOID HeapAlloc(  HANDLE hHeap,   // handle to private heap block
DWORD dwFlags, // heap allocation control
SIZE_T dwBytes // number of bytes to allocate);

HeapReAlloc

重新分配内存,改变已经分配好的堆内存块的大小

1
2
3
4
LPVOID HeapReAlloc(HANDLE hHeap, 	//堆句柄
DWORD dwFlags, //分配标志
LPVOID lpMem, //原内存块的地址
DWORD dwBytes ); //内存块调整后的值
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>

DWORD PrintHeapSize(HANDLE hHeap, LPVOID lpMem)
{
SIZE_T dwHeapSize;
dwHeapSize = HeapSize(hHeap, HEAP_NO_SERIALIZE, lpMem);
if (dwHeapSize == -1)
{
printf("Get HeapSize error :%d", GetLastError());
return 1;
}
printf("内存块大小为:0x%x\n", dwHeapSize);
return 0;
}

int main()
{
SYSTEM_INFO si; //系统信息
HANDLE hHeap; //堆句柄
HANDLE hHeap1;
LPVOID lpMem; //内存块指针
LPVOID lpReAlloc; //内存块大小调整后的指针
DWORD dwHeapSize; //堆内存埠大小
HANDLE hHeaps[MAX_PATH]; //用于保存进程中所有的堆句柄
DWORD dwHeapNum; //进程中堆的数量
GetSystemInfo(&si);
printf("系统内存页大小: 0x%x\n系统内存分配粒度:0x%x\n",
si.dwPageSize, si.dwAllocationGranularity);
//创建最大为10个分页的堆
hHeap = HeapCreate(HEAP_NO_SERIALIZE, si.dwPageSize, si.dwPageSize * 10);
printf("创建堆,初始化大小为1页,最大为10页\n");

//初始化堆
hHeap = GetProcessHeap(); //获取堆
printf("获取系统已经存在的堆\n");


//创建一个可增长的堆
hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
//hHeap1 = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
printf("创建堆,初始化大小为1页,大小可变\n");


dwHeapNum = GetProcessHeaps(MAX_PATH, hHeaps);
printf("当前进程一共有%d个堆\n", dwHeapNum);


lpMem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, si.dwPageSize * 3);
printf("在堆上成功分配内存,起始地址为:0x%x\n", lpMem);
PrintHeapSize(hHeap, lpMem); ////打印当前堆内存块的大小
lpReAlloc = HeapReAlloc(hHeap, HEAP_ZERO_MEMORY, lpMem, si.dwPageSize * 11); //堆分配内存
printf("在堆上再分配内存,地址为:0x%x,原地址:0x%x\n", lpReAlloc, lpMem);
PrintHeapSize(hHeap, lpReAlloc); //打印调整后的堆
if (!HeapFree(hHeap, HEAP_NO_SERIALIZE, lpReAlloc)) //释放堆内存
{
printf("HeapFree error: %d", GetLastError());
return 1;
}
printf("释放内存成功\n");
getchar();
return 0;
}

堆内存块大小信息

HeapSize

1
2
3
DWORD HeapSize( HANDLE hHeap, 	//堆句柄
DWORD dwFlags, //内存块标志
LPCVOID lpMem ); //内存块大小指针

demo:

1
2
3
4
5
6
7
8
9
10
11
12
DWORD PrintHeapSize(HANDLE hHeap,LPVOID lpMem)
{
SIZE_T dwHeapSize;
dwHeapSize = HeapSize(hHeap,HEAP_NO_SERIALIZE,lpMem);
if(dwHeapSize == -1)
{
printf("Get HeapSize error :%d",GetLastError());
return 1;
}
printf("内存块大小为:0x%x\n",dwHeapSize);
return 0;
}

释放内存销毁堆

分配的内存再使用完成后需要释放,否则内存将不能再次使用,造成内存泄漏

HeapFree

1
2
3
BOOL HeapFree(HANDLE hHeap, 		//堆句柄
DWORD dwFlags, //内存标志快
LPVOID lpMem ); //释放的内存块指针

HeapDestroy

1
BOOL HeapDestroy(  HANDLE hHeap   // handle to heap);

虚拟内存管理

虚拟地址空间与内存分页

进程的虚拟地址空间内存页面存在三种状态,分别为空闲的、保留的、提交的。一个页大小4KB

分配和释放可读写的虚拟内存页

使用VitrualAlloc和VirtualAllocEx分配虚拟内存时,可以指定分配的内存页面是保留的还是提交的。

VirtualAlloc

1
2
3
4
LPVOID VirtualAlloc(  LPVOID lpAddress,        // region to reserve or commit
SIZE_T dwSize, // size of region
DWORD flAllocationType, // type of allocation //确定页是保留还是提交
DWORD flProtect // type of access protection); //内存页保护属性

VirtualAllocEx

1
2
3
4
5
LPVOID VirtualAllocEx(  HANDLE hProcess,          // process to allocate memory
LPVOID lpAddress, // desired starting address
SIZE_T dwSize, // size of region to allocate
DWORD flAllocationType, // type of allocation
DWORD flProtect // type of access protection);

VirtualFree

1
2
3
BOOL VirtualFree(  LPVOID lpAddress,   // address of region
SIZE_T dwSize, // size of region
DWORD dwFreeType // operation type); //提交状态

VirtualQuery

使用VirtualQuery获取内存信息

1
2
3
DWORD VirtualQuery(  LPCVOID lpAddress,                   // address of region
PMEMORY_BASIC_INFORMATION lpBuffer, // information buffer
SIZE_T dwLength // size of buffer);
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <windows.h>
#include <stdio.h>

/*************************************
* int main(void)
* 功能 演示虚拟内存的使用
*
* 参数 未使用
**************************************/
int main(void)
{
SIZE_T sizeVirtual = 4000; //大小
LPVOID lpRound = (LPVOID)0x100000FF; //地址
MEMORY_BASIC_INFORMATION mbi; //内存信息

//分配内存,直接分配已提交的内存
LPVOID lpAddress = VirtualAlloc(
lpRound, sizeVirtual,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
if (lpAddress == NULL)
{
printf("VirtualAlloc error: %d\n", GetLastError());
return 1;
}
printf("Alloc:MEM_COMMIT|MEM_RESERVE\n");
//复制数据到内存中
CopyMemory(lpAddress, "hello,world", strlen("hello,world") + 1);
printf("分配、复制成功,地址:0x%.8x,内容:%s\n",
lpAddress, lpAddress);
//获取内存信息并打印
VirtualQuery(lpAddress, &mbi, sizeof(mbi));
printf("使用VirtualQuery获得的信息:\n"
"BaseAddress:0x%.8x\tAllocationBase:0x%.8x\t"
"AllocationProtect:0x%.8x\tRegionSize:%u\t"
"State:0x%.8x\tProtect:0x%.8x\tType:0x%.8x\n",
mbi.BaseAddress, mbi.AllocationBase,
mbi.AllocationProtect, mbi.RegionSize,
mbi.State, mbi.Protect, mbi.Type
);


//DECOMMIT释放,页面将变为保留状态
printf("Free: DECOMMIT\n");
if (!VirtualFree(lpRound, sizeVirtual, MEM_DECOMMIT))
{
printf("VirtualFree error: %d", GetLastError());
return 1;
}
//获取内存信息并打印
VirtualQuery(lpAddress, &mbi, sizeof(mbi));
printf("使用VirtualQuery获得的信息:\n"
"BaseAddress:0x%.8x\tAllocationBase:0x%.8x\t"
"AllocationProtect:0x%.8x\tRegionSize:%u\t"
"State:0x%.8x\tProtect:0x%.8x\tType:0x%.8x\n",
mbi.BaseAddress, mbi.AllocationBase,
mbi.AllocationProtect, mbi.RegionSize,
mbi.State, mbi.Protect, mbi.Type
);
//释放内存
printf("Free:RELEASE\n");
if (!VirtualFree(lpAddress, 0, MEM_RELEASE))
{
printf("VirtualFree error: %d", GetLastError());
return 1;
}
getchar();
return 0;
}

运行结果:

虽然我们是从0x0x100000FF开始分配,但是Windows自动对齐了,所以我们访问的是0x1000000;

MEMORY_BASIC_INFORMATION结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //查询内存块所占的第一个页面基地址
PVOID AllocationBase; //内存块所占的第一块区域基地址,小于等于BaseAddress,
//也就是说BaseAddress一定包含在AllocationBase分配的范围内
DWORD AllocationProtect; //区域被初次保留时赋予的保护属性
SIZE_T RegionSize; //从BaseAddress开始,具有相同属性的页面的大小,
DWORD State; //页面的状态,有三种可能值:MEM_COMMIT、MEM_FREE和MEM_RESERVE,
//这个参数对我们来说是最重要的了,从中我们便可知指定内存页面的状态了
DWORD Protect; //页面的属性,其可能的取值与AllocationProtect相同
DWORD Type; //该内存块的类型,有三种可能值:MEM_IMAGE、MEM_MAPPED、MEM_PRIVATE
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

修改内存页状态

VirtualProtect

1
2
3
4
BOOL VirtualProtect(  LPVOID lpAddress,       // 内存基地址
SIZE_T dwSize, // size of the region //内存大小
DWORD flNewProtect, // desired access protection //页的新属性
PDWORD lpflOldProtect // old protection); //原保护属性值的DWORD变量,可以为NULL

VirtualLock

1
2
BOOL VirtualLock(  LPVOID lpAddress,   // first byte in range		//锁定的基地址
SIZE_T dwSize // number of bytes in range); //内存大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//设置为READ-ONLY属性
//if(!VirtualProtect(lpAddress,0,PAGE_READONLY,NULL)) //设备只读属性
//{
// printf("VirtualProtect error: %d",GetLastError());
// return 1;
//}
////测试READ-ONLY属性,异常
//CopyMemory(lpAddress,"read only",strlen("read only"));
////printf(lpAddress);
////获取内存信息并打印
//VirtualQuery(lpAddress,&mbi,sizeof(mbi));
//printf("使用VirtualQuery获得的信息:\n"
// "BaseAddress:0x%.8x\tAllocationBase:0x%.8x\t"
// "AllocationProtect:0x%.8x\tRegionSize:%d\t"
// "State:0x%.8x\tProtect:0x%.8x\tType:0x%.8x\n",
// mbi.BaseAddress,mbi.AllocationBase,
// mbi.AllocationProtect,mbi.RegionSize,
// mbi.State,mbi.Protect,mbi.Type
// );

内存操作与内存信息管理

复制、清空内存块、防止溢出

CopyMemory

复制内存

1
2
3
VOID CopyMemory(  PVOID Destination,   // copy destination		//目的地址	
CONST VOID* Source, // memory block //源地址
SIZE_T Length // size of memory block); //大小

FillMemoey

填充内存,将一段内存填充为某一个值

1
2
3
4
VOID FillMemory (  PVOID Destination,  // //需要填充的内存
memory block //填充的大小
SIZE_T Length, // size of memory block BYTE Fill // fill value //填充的值
);

MoveMemory

复制内存

1
2
3
VOID MoveMemory (  PVOID Destination,   // move destination			//目的
CONST VOID *Source, // block to move //源
SIZE_T Length // size of block to move); //大小

源地址与目的地址可相同

ZeroMemory

清空指定内存

1
2
VOID ZeroMemory(  PVOID Destination,  // memory block		//目标
SIZE_T Length // size of memory block); //大小

demo:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <windows.h>
#include <stdio.h>

/* 常量定义 */
#define MEM_BLOCK_MAX_SIZE 32

/*************************************
* BOOL ShowMemContent(LPVOID lpMem,SIZE_T dwSize)
* 功能 显示内存中的内容
*
* 参数 LPVOID lpMem 需要显示的内存指针
* SIZE_T dwSize 大小
*
* 返回值 BOOL 如果数据过大可能溢出,则返回FALSE。
**************************************/
BOOL ShowMemContent(LPVOID lpMem, SIZE_T dwSize)
{
BYTE lpShow[MEM_BLOCK_MAX_SIZE];
INT i = 0;
//防止栈溢出
if (dwSize>MEM_BLOCK_MAX_SIZE)
{
printf("over-flow");
return FALSE;
}
//复制到数组
CopyMemory((LPVOID)lpShow, lpMem, dwSize);
//显示
for (; i<dwSize; i++)
{
printf("%.2X ", lpShow[i]);
if (!((i + 1) % 16))
{
printf("\n");
}
}
printf("\n");
return TRUE;
}

/*************************************
* int main(void)
* 功能 获取内存使用情况
*
* 参数 未使用
**************************************/
int main(void)
{
HANDLE hHeap = GetProcessHeap();
LPVOID lpSrc;
LPVOID lpDis;

//分配内存
lpSrc = HeapAlloc(hHeap, 0, MEM_BLOCK_MAX_SIZE);
lpDis = HeapAlloc(hHeap, 0, MEM_BLOCK_MAX_SIZE);
//显示新分配的内存
printf("HeapAlloc 分配但不清零:\n");
ShowMemContent(lpDis, MEM_BLOCK_MAX_SIZE);
//将内存清零并显示
ZeroMemory(lpDis, MEM_BLOCK_MAX_SIZE);
printf("ZeroMemory 清零:\n");
ShowMemContent(lpDis, MEM_BLOCK_MAX_SIZE);
//使用0xBB填充全部内存
FillMemory(lpSrc, MEM_BLOCK_MAX_SIZE, 0xBB);
//将内存块的前半部分使用0xAA填充
FillMemory(lpSrc, MEM_BLOCK_MAX_SIZE / 2, 0xAA);
CopyMemory(lpDis, lpSrc, MEM_BLOCK_MAX_SIZE);
printf("FillMemory 有规律的填充内存:\n");
ShowMemContent(lpDis, MEM_BLOCK_MAX_SIZE);

//释放内存
HeapFree(hHeap, 0, lpSrc);
HeapFree(hHeap, 0, lpDis);
getchar();
return 0;

}

获得当前内存使用情况

GlobalMemoryStatus

GlobalMemoryStatusEx

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
48
49
50
51

#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <iomanip>
using namespace std;
int main(){
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);//必须有这一句,否则函数错误!
bool res = GlobalMemoryStatusEx(&statex);
cout << "GlobalMemoryStatusEx(&) = " << res << endl;
cout << "GetLastError() = " << GetLastError() << endl << endl;

HANDLE hEvent;
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//初始值为nonsignaled,并且每次触发后自动设置为nonsignaled

const unsigned long long DIV = 1024;

while (1){
WaitForSingleObject(hEvent, 1000);//等待500毫秒
res = GlobalMemoryStatusEx(&statex);
cout << left << setw(17) << "内存占用率:" << setw(3) << statex.dwMemoryLoad << "%" << endl
<< "============================================" << endl
<< setw(17) << "总物理内存:" << setw(10) << statex.ullTotalPhys / DIV << "KB 折合 "
<< setw(6) << statex.ullTotalPhys / DIV / DIV << "MB" << endl
<< setw(17) << "闲置物理内存:" << setw(10) << statex.ullAvailPhys / DIV << "KB 折合 "
<< setw(6) << statex.ullAvailPhys / DIV / DIV << "MB" << endl
<< setw(17) << "物理内存闲置率:" << setw(3)
<< (statex.ullAvailPhys * 100) / statex.ullTotalPhys << "%" << endl
<< "--------------------------------------------" << endl
<< setw(17) << "总页面文件:" << setw(10) << statex.ullTotalPageFile / DIV << "KB 折合 "
<< setw(6) << statex.ullTotalPageFile / DIV / DIV << "MB" << endl
<< setw(17) << "闲置页面文件:" << setw(10) << statex.ullAvailPageFile / DIV << "KB 折合 "
<< setw(6) << statex.ullAvailPageFile / DIV / DIV << "MB" << endl
<< setw(17) << "页面文件闲置率:" << setw(3)
<< (statex.ullAvailPageFile * 100) / statex.ullTotalPageFile << "%" << endl
<< "--------------------------------------------" << endl
<< setw(17) << "总虚拟内存:" << setw(10) << statex.ullTotalVirtual / DIV << "KB 折合 "
<< setw(6) << statex.ullTotalVirtual / DIV / DIV << "MB" << endl
<< setw(17) << "闲置虚拟内存:" << setw(10) << statex.ullAvailVirtual / DIV << "KB 折合 "
<< setw(6) << statex.ullAvailVirtual / DIV / DIV << "MB" << endl
<< setw(17) << "虚拟内存闲置率:" << setw(3)
<< (statex.ullAvailVirtual * 100) / statex.ullTotalVirtual << "%" << endl
<< "--------------------------------------------" << endl
<< setw(17) << "可用已扩展内存:" << setw(10) << statex.ullAvailExtendedVirtual / DIV << "KB 折合 "
<< setw(6) << statex.ullAvailExtendedVirtual / DIV / DIV << "MB" << endl << endl << endl;
}
getchar();
return 0;
}