内存分配
在应用层编程时,系统提供了GlobalAlloc/HeapAlloc/LocalAlloc等函数。C/C++库提供了malloc函数,以及new操作符在堆上分配内存。
在我前面一个关于Windows页交换文件的博客中,介绍了虚拟内存,
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
堆内存是基于虚拟内存上更小粒度的分割,这个分割由堆管理器管理,根据需求,堆管理器会申请 一页(或多页虚拟内存),然后对这块虚拟内存进行更小粒度的内存分割与管理,以满足开发者对内存的需求。
堆的大小是在应用程序启动时设置,但可以随着空间的需要而增长(分配器从操作系统请求更多内存)。
与应用层的堆概念类似,在内核中有一种称为“池(Pool)"的概念,我们可以从Pool中申请内存
WDK提供了一系列内存分配函数,其中最基本的是ExAllocatePoolWithTag,函数原型如下:
1 NTKERNELAPI 2 PVOID 3 NTAPI 4 ExAllocatePoolWithTag ( 5 _In_ __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType, 6 _In_ SIZE_T NumberOfBytes, 7 _In_ ULONG Tag 8 );
PoolType:表示 需要申请哪种类型的内存,PoolType为POOL_TYPE枚举类型
POOL_TYPE定义如下:
1 typedef _Enum_is_bitflag_ enum _POOL_TYPE { 2 NonPagedPool, 3 NonPagedPoolExecute = NonPagedPool, 4 PagedPool, 5 NonPagedPoolMustSucceed = NonPagedPool + 2, 6 DontUseThisType, 7 NonPagedPoolCacheAligned = NonPagedPool + 4, 8 PagedPoolCacheAligned, 9 NonPagedPoolCacheAlignedMustS = NonPagedPool + 6, 10 MaxPoolType, 11 NonPagedPoolBase = 0, 12 NonPagedPoolBaseMustSucceed = NonPagedPoolBase + 2, 13 NonPagedPoolBaseCacheAligned = NonPagedPoolBase + 4, 14 NonPagedPoolBaseCacheAlignedMustS = NonPagedPoolBase + 6, 15 NonPagedPoolSession = 32, 16 PagedPoolSession = NonPagedPoolSession + 1, 17 NonPagedPoolMustSucceedSession = PagedPoolSession + 1, 18 DontUseThisTypeSession = NonPagedPoolMustSucceedSession + 1, 19 NonPagedPoolCacheAlignedSession = DontUseThisTypeSession + 1, 20 PagedPoolCacheAlignedSession = NonPagedPoolCacheAlignedSession + 1, 21 NonPagedPoolCacheAlignedMustSSession = PagedPoolCacheAlignedSession + 1, 22 23 NonPagedPoolNx = 512, 24 NonPagedPoolNxCacheAligned = NonPagedPoolNx + 4, 25 NonPagedPoolSessionNx = NonPagedPoolNx + 32, 26 27 } _Enum_is_bitflag_ POOL_TYPE;
常用的值是:NonPagedPool(非分页内存)与PagedPool(分页内存)。
在前面介绍过分页内存与非分页内存的概念。
非分页内存是指这块内存的内容不会被置换到磁盘上,非分页内存非常宝贵,一般用于高IRQL(>= DISPATCH_LEVEL) 的代码中。
分页内存是指这块内存的内容可以被转换到磁盘上。
除了NonPagedPool与PagedPool类型,我们还需要关心的类型是NonPagedPoolExecute与NonPagedPoolNx。
NonPagedPoolExecute类型的内存属性为“可执行”,意味着开发者可以将这块内存写入二进制指令然后执行,这个机制虽然很灵活,但存在一定的安全隐患︰对于一些存在漏洞的代码来说,攻击者可以使用“缓存区溢出攻击”技术,在目标内存(缓冲区)中写入可执行指令,由于这块内存具有“可执行“属性,所以攻击者可以成功实施攻击 。
从Win8开始,推荐使用NonPagedPoolNx类型来替代NonPagedPool类型。
从Windows 10 2004开始,使用POOL_FLAG_NON_PAGED类型
NumberOfTypes:表示 需要申请的内存大小
Tag:一个4个字节的标志,用于标志一块内存的使用者,这个Tag—般用于问题排查,如内存泄露,系统蓝屏等。对于内存泄露的情况,可以通过Windbg或PoolMon等一些小工具,查看系统中各Tag标志对应的内存大小,找到最大的或者持续增长的内存块。标记中的每个 ASCII 字符必须是0x7E (平铺) 0x20 空间范围内的值。
如果不需要使用Tag标志,可以传递0,或者调用ExAllocatePool函数。
返回值:成功,返回分配内存的首地址。失败返回NULL。
说明:在Windows 10 2004版本中,ExAllocatePoolWithTag函数已经弃用,替换为ExAllocatePool2函数,详细可以参考以下链接:
更新对 ExAllocatePool2 和 ExAllocatePool3 的已弃用 ExAllocatePool 调用 - Windows drivers | Microsoft Learn
内存使用完毕后需要释放,使用ExFreePoolWithTag函数,声明如下:
1 NTKERNELAPI 2 VOID 3 ExFreePoolWithTag ( 4 _Pre_notnull_ __drv_freesMem(Mem) PVOID P, 5 _In_ ULONG Tag 6 );
P:需要释放的内存地址
Tag:内存申请 时的标记,如果分配内存时使用的Tag等于0,释放时也传0即可。
后备列表(Lookaside Lists)
在频繁使用ExAllocatePoolWithTag函数分配内存时,容易 造成”内存碎片“。为了提高性能,系统提供了一种被称为”后备列表(Lookaside Lists)“的内存分配方法。
注意:Lookaside的翻译在不同的地方可能有出入,知道这个概念就行了。在《Windows内核编程》一书中,使用的是”旁视列表“,官方文档上显示的是”后备列表“。官方的中文文档是机翻的,但是我这里还是使用了这个名称。
使用”后备列表“的步骤如下:
1、初始化一个”后备列表“
这里以非分页内存为例,分页内存使用方法基本一样
使用ExInitializeNPagedLookasideList函数初始化”后备列表“对象,声明如下:
1 NTKERNELAPI 2 VOID 3 ExInitializeNPagedLookasideList ( 4 _Out_ PNPAGED_LOOKASIDE_LIST Lookaside, 5 _In_opt_ PALLOCATE_FUNCTION Allocate, 6 _In_opt_ PFREE_FUNCTION Free, 7 _In_ ULONG Flags, 8 _In_ SIZE_T Size, 9 _In_ ULONG Tag, 10 _In_ USHORT Depth 11 );
Lookaside:表示 被初始化的”后备列表“对象的指针,在64位系统下,这个指针必须以16字节对齐。ExInitializeNPagedLookasideList执行后,Lookaside会被初始化。
Allocate:一个函数指针(回调函数),当我们从”后备列表“对象分配内存时,系统会调用这个函数。
ALLOCATE_FUNCTION声明如下:
1 PVOID 2 ALLOCATE_FUNCTION ( 3 _In_ POOL_TYPE PoolType, 4 _In_ SIZE_T NumberOfBytes, 5 _In_ ULONG Tag 6 );
这个参数可以根据自己实际情况使用,不需要使用时,传NULL,系统会使用默认的内存分配函数。
Free:一个函数指针,当我们从”后备列表”释放申请的内存块时,系统会调用这个函数。
FREE_FUNCTION声明如下:
1 VOID 2 FREE_FUNCTION ( 3 _In_ __drv_freesMem(Mem) PVOID Buffer 4 );
这个参数可以根据自己实际情况使用,不需要使用时,传NULL,系统会使用默认的内存释放函数。
Flags:内存分配行为。可选以下值
POOL_NX_ALLOCATION:表示分配的非分页内存的属性为“不可执行”,类似上一节介绍的NonPagedPoolNx标志。
POOL_RAISE_IF_ALLOCATION_FAILURE:表示如果内存失败,将抛出一个异常。
0:如果没有特殊要求,可以把Flags参数设置为0。
Size:每次从“后备列表”对象中申请内存的固定大小,单位是字节,这个值不能小于LOOKASIDE_MINIMUM_BLOCK_SIZE
LOOKASIDE_MINIMUM_BLOCK_SIZE = ((((LONG)__builtin_offsetof(SLIST_ENTRY, Next)) + (sizeof(((SLIST_ENTRY*)0)->Next))))
在64位系统下,LOOKASIDE_MINIMUM_BLOCK_SIZE的值为8。
Tag:表示 内存分配时所使用的标记,与ExAllocatePoolWithTag中的Tag参数一样。
Depth:保留参数,传0即可。
2、需要内存时,直接从”后备列表“对象申请 内存
申请内存使用ExAllocateFromNPagedLookasideList函数,该函数声明如下:
1 PVOID 2 ExAllocateFromNPagedLookasideList ( 3 _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside 4 )
Lookaside:“后备列表”对象指针
返回值:执行成功,返回相应的内存块首地址,否则 返回NULL
ExAllocateFromNPagedLookasideList分配的内存大小为ExInitializeNPagedLookasideList函数所指定的Size
3、使用完成后,通过”后备列表“回收这些内存
释放内存使用ExFreeToNPagedLookasideList函数,该函数声明如下:
1 VOID 2 ExFreeToNPagedLookasideList ( 3 _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside, 4 _In_ __drv_freesMem(Mem) PVOID Entry 5 )
Lookaside:“后备列表“对象指针
Entry:表示需要释放的内存块
4、当不再需要”后备列表“时,将其对象删除。
删除”后备列表“时,使用ExDeleteNPagedLookasideList函数。该函数声明如下:
1 NTKERNELAPI 2 VOID 3 ExDeleteNPagedLookasideList ( 4 _Inout_ PNPAGED_LOOKASIDE_LIST Lookaside 5 );
Lookaside:表示需要删除的”后备列表“对象指针
5、完整的示例
下面使用简单的代码演示一下,如何使用”后备列表“
//直接申请内存 PVOID pAllocFromPool = ExAllocatePoolWithTag(NonPagedPoolNx, 10240, 0); if (pAllocFromPool != NULL) ExFreePoolWithTag(pAlloc, 0); //使用后备列表(Lookaside Lists) PNPAGED_LOOKASIDE_LIST pLookasideList = (PNPAGED_LOOKASIDE_LIST)ExAllocatePoolWithTag(NonPagedPool, sizeof(NPAGED_LOOKASIDE_LIST), 'urfh'); if (pLookasideList != NULL) { memset(pLookasideList, 0, sizeof(NPAGED_LOOKASIDE_LIST)); //初始化 ExInitializeNPagedLookasideList(pLookasideList, NULL, NULL, 0, 1024, 'urfh', 0); //分配 PVOID pAlloc = ExAllocateFromNPagedLookasideList(pLookasideList); if (pAlloc != NULL) { DbgPrint("Memory Allocate First:%p", pAlloc); //释放 ExFreeToNPagedLookasideList(pLookasideList, pAlloc); } //再次分配 pAlloc = ExAllocateFromNPagedLookasideList(pLookasideList); if (pAlloc != NULL) { DbgPrint("Memory Allocate Second:%p", pAlloc); //释放 ExFreeToNPagedLookasideList(pLookasideList, pAlloc); } //删除Lookaside Lists ExDeleteNPagedLookasideList(pLookasideList); //释放 ExFreePoolWithTag(pLookasideList, 'urfh');
注意:如果编译报错,请将ExAllocatePoolWithTag替换为ExAllocatePool2函数
1 PVOID pAllocFromPool = ExAllocatePool2(POOL_FLAG_NON_PAGED, 10240, 0);
运行结果:
参考资料
比较内存分配方法
https://learn.microsoft.com/zh-cn/windows/win32/memory/comparing-memory-allocation-methods
What and where are the stack and heap?
https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap
使用后备列表
使用后备列表 - Windows drivers | Microsoft Learn