<学习笔记>Windows驱动开发技术详解__Windows内存管理

本文介绍Windows驱动程序中的内存管理概念,包括物理内存、虚拟内存、分页与非分页内存等,探讨驱动程序如何有效利用内存资源,以及如何在驱动程序中使用链表和Lookaside结构来提高内存管理效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作为开发Windows驱动程序的程序员,需要比普通程序员更多了解Windows内部的内存管理机制,并在驱动程序中有效地使用内存。在驱动程序的编写中,分配和管理内存不能使用熟知的Win32 API函数,取而代之的是DDK提供的高效内核函数。C语言和C++中大多数关于内存操作的运行时函数,大多在内核模式下是无法使用的。例如,C语言中的malloc函数和C++中的new操作符等。

 

内存管理的概念:

 

1.物理内存:

PC上有三条总线,分别是数据总线,地址总线和控制总线。32位的CPU寻址能力是4GB。用户最多可以使用4GB的真实物理内存。PC中会拥有许多设备,其中很多设备都拥有自己的设备内存,这部分的设备内存会映射到PC机的物理内存上,读写这段物理地址其实会读写设备内存地址。

 

2虚拟内存:

.虽然可以寻址4GB的内存,而PC里往往没有如此多的真实物理内存。操作系统和硬件为使用者提供了虚拟内存的概念。虚拟内存和物理内存之间的转换暂不讨论。

 

3.用户模式地址和内核模式地址:

虚拟地址在0~0X7FFFFFFF范围内的虚拟内存,即低2GB的虚拟地址,被称为用户模式地址。而0X80000000~0XFFFFFFFF范围内的虚拟内存,即高2GB的虚拟内存,被称为内核模式地址。

 

4.Windows驱动程序和进程的关系:

驱动程序可以看成一个特殊的DLL文件被应用程序加载到虚拟内存中,只不过加载地址是内核模式地址,而不是用户模式地址。

 

5.分页和非分页内存:

Windows规定有些虚拟内存页面是可以交换到文件中的,这类内存成为分页内存。而有些虚拟内存页永远不会交换到文件中,这些内存被称为非分页内存。

当程序的中断请求级在DISPATCH_LEVEL之上时,程序只能使用非分页内存,否则会导致蓝屏死机。

在编译DDK提供的例程时,可以指定某个例程和某个全局变量是载入分页内存还是非分页内存,需要做如下定义:

[cpp]  view plain  copy
  1. #define PAGEDCODE code_seg("PAGE")  
  2. #define LOCKEDCODE code_seg()  
  3. #define INITCODE code_seg("INIT")  
  4.   
  5. #define PAGEDDATA data_seg("PAGE")  
  6. #define LOCKEDDATA data_seg()  
  7. #define INITDATA data_seg("INIT")  


如果将某个函数在入到分页内存,我们需要在函数的实现中加入如下代码:

[cpp]  view plain  copy
  1. #pragma INITCODE  
[cpp]  view plain  copy
  1. VOID SomeFunction()  
  2. {  
  3.       PAGED_CODE();  
  4.       //do something  
  5. }  

如果要让程序加载到非分页内存,需要在函数的实现中加入如下代码:

[cpp]  view plain  copy
  1. #pragma LOCKEDCODE  
  2. VOID SomeFunction()  
  3. {  
  4.       //do something  
  5. }  

还有一种特殊情况,就是某个例程初始化的时候载入内存,然后就可以从内存中卸载掉,例如DriverEntry

[cpp]  view plain  copy
  1. #pragma INITCODE  
  2. extern "C" NTSTATUS DriverEntry(  
  3.                            IN PDRIVER_OBJECT pDriverObject,  
  4.             IN PUNICODE_STRING pRegistryPath  
  5.                              )  
  6. {  
  7.   
  8. }  


 

6.内存的分配:

Windows驱动程序使用的内存资源非常珍贵,局部变量是在栈(stack)空间中,但是驱动程序的栈空间不会像应用程序那么大,所以不适合进行递归调用或者局部变量是大型的结构体,否则请在堆(Heap)中申请。

堆中申请内存的函数有以下几个:

[cpp]  view plain  copy
  1. PVOID   
  2. ExAllocatePool(  
  3.         IN POOL_TYPE PoolType,   
  4.         IN SIZE_T NumberOfBytes  
  5.         );  
  6.   
  7. PVOID   
  8. ExAllocatePoolWithTag(  
  9.         IN POOL_TYPE PoolType,   
  10.         IN SIZE_T NumberOfBytes,   
  11.         IN ULONG Tag   
  12.         );  
  13.   
  14. PVOID   
  15. ExAllocatePoolWithQuota(   
  16.         IN POOL_TYPE PoolType,  
  17.         IN SIZE_T NumberOfBytes   
  18.         );  
  19.   
  20. PVOID   
  21. ExAllocatePoolWithQuotaTag(  
  22.         IN POOL_TYPE PoolType,  
  23.         IN SIZE_T NumberOfBytes,  
  24.         IN ULONG Tag   
  25.         );  


其中有些重要的参数:

PoolType: 是个枚举变量,如果此值为NonPagedPool,则分配非分页内存。如果次值为PagedPool,则分配内存分页内存。

NumberOfBytes:是分配内存的大小,注意最好是4的倍数。

以上四个函数功能类似,函数以WithQuota结尾的代表分配的时候按额分配。以WithTag结尾的函数和ExAllocatePool功能类似,唯一不同的是多了一个Tag参数,

系统要求的内存外又额外地多分配4个字节的标签。在调试的时候,可以找出是否有标有这个标签的内存没有被释放。

 

将分配的内存,进行回收的函数原型如下:

[cpp]  view plain  copy
  1. VOID  
  2. ExFreePool(   
  3.     IN PVOID P  
  4.          );  
  5.   
  6. NTKERNELAPI  
  7. VOID  
  8. ExFreePoolWithTag(  
  9.     IN PVOID P,  
  10.     IN ULONG Tag  
  11.   );  


参数P就是要释放的内存。

 

在内存中使用链表:

链表中可以记录整形,浮点型,字符型或者程序员自定义的数据结构。对于单链表,元素中有一个Next指针指向下一个元素。对于双链表,每个元素有两个指针:BLINK指向前一个元素,FLINK指向下一个元素。

 

1.链表的结构:

DDK提供了标准的双向链表。双向链表可以将链表形成一个环。以下是DDK提供的双向链表的数据结构:

[cpp]  view plain  copy
  1. typedef struct _LIST_ENTRY {  
  2.    struct _LIST_ENTRY *Flink;  
  3.    struct _LIST_ENTRY *Blink;  
  4. } LIST_ENTRY, *PLIST_ENTRY  


这个结构体只有指针没有数据。

 

2.链表的初始化:

每个双向链表都是以链表头作为链表的第一个元素。初次使用链表头需要进行初始化,主要将链表头的Flink和Blink两个指针都指向自己。初始化链表头用InitiallizeListHead宏实现。判断链表头是否为空,DDK提供了一个宏简化这种检查,这就是IsListEmpty。

[cpp]  view plain  copy
  1. IsListEmpty(&head);  


程序员需要自己定义链表中每个元素的数据类型,并将LIST_ENTRY结构作为自定义结构的一个子域。例如:

[cpp]  view plain  copy
  1. typedef struct _MYDATASTRUCT  
  2. {  
  3.     LIST_ENTRY ListEntry;  
  4.     ULONG x;  
  5.     ULONG y;  
  6. }MYDATASTRUCT,*PMYDATASTRCUT;  


3.插入链表:

对链表的插入有两种方式,一种是在链表的头部插入,一种是在链表的尾部插入。

在头部插入链表使用语句InsertHeadList,用法如下:

[cpp]  view plain  copy
  1. InsertHeadList(&head,&mydata->ListEntry);  


在尾部插入链表使用语句InsertTailList,用法如下:

[cpp]  view plain  copy
  1. InsertTailList(&head,&mydata->ListEntry);  

 

4.链表的删除:

和插入链表一样,删除链表也有两种方法。一种从头部删除,一种从尾部删除。分别对应RemoveHeadList和RemoveTailList函数。其使用方法如下:

[cpp]  view plain  copy
  1. PLIST_ENTRY = RemoveHeadList(&head);  
  2.   
  3. PLIST_ENTRY = RemoveTailList(&head);  


 

下面代码完整演示向链表进行插入,删除等操作,其主要代码如下:

[cpp]  view plain  copy
  1. typedef struct _MYDATASTRUCT  
  2. {  
  3.     ULONG number;  
  4.     LIST_ENTRY ListEntry;  
  5. }MYDATASTRUCT,*PMYDATASTRCUT;  
  6.   
  7. #pragma INITCODE  
  8. VOID LinkListTest()  
  9. {  
  10.     LIST_ENTRY linkListHead;  
  11.     //初始化链表  
  12.     InitializeListHead(&linkListHead);  
  13.       
  14.     PMYDATASTRCUT pData;  
  15.     ULONG i = 0;  
  16.     //在链表中插入10个元素  
  17.   
  18.     KdPrint(("Begain insert to link list"));  
  19.     for (i = 0 ; i < 10 ; i++)  
  20.     {  
  21.         pData = (PMYDATASTRCUT)  
  22.                  ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));  
  23.         pData->number = i;  
  24.         InsertHeadList(&linkListHead,&pData->ListEntry);  
  25.     }  
  26.     //从链表中取出,并显示  
  27.     KdPrint(("Begain remove from link list\n"));  
  28.     while (!IsListEmpty(&linkListHead))  
  29.     {  
  30.         PLIST_ENTRY pEntry = RemoveTailList(&linkListHead);  
  31.         pData = CONTAINING_RECORD(pEntry,  
  32.                           MYDATASTRUCT,  
  33.                           ListEntry);  
  34.         KdPrint(("%d\n",pData->number));  
  35.         ExFreePool(pData);  
  36.     }  
  37. }  


在DebugView中打印log信息:

 

Lookaside结构:

频繁申请和回收内存,会导致在内存上产生大量的内存“空洞”,从而导致最终无法申请内存。DDK为程序员提供了Lookaside结构来解决这个问题。

 

1.使用Lookaside:

Lookaside一般会在一下情况使用:

(1)程序员每次申请固定大小的内存

(2)申请和回收的操作十分频繁

使用Lookaside对象,首先要初始化Lookaside对象,有以下两个函数可以使用:

[cpp]  view plain  copy
  1. VOID  
  2. ExInitializeNPagedLookasideList(  
  3.             IN PNPAGED_LOOKASIDE_LIST Lookaside,   
  4.             IN PALLOCATE_FUNCTION Allocate,   
  5.             IN PFREE_FUNCTION Free,  
  6.             IN ULONG Flags,   
  7.             IN SIZE_T Size,  
  8.             IN ULONG Tag,  
  9.             IN USHORT Depth   
  10.             );  
  11.   
  12. VOID  
  13. ExInitializePagedLookasideList(   
  14.             IN PPAGED_LOOKASIDE_LIST Lookaside,  
  15.             IN PALLOCATE_FUNCTION Allocate,   
  16.             IN PFREE_FUNCTION Free,   
  17.             IN ULONG Flags,  
  18.             IN SIZE_T Size,  
  19.             IN ULONG Tag,  
  20.             IN USHORT Depth  
  21.             );  


这两个函数分别对非分页和分页Lookaside对象进行初始化。

在初始化完Lookaside对象后,可以进行申请内存的操作了,有以下两个函数:

[cpp]  view plain  copy
  1. PVOID  
  2. ExAllocateFromNPagedLookasideList(  
  3.             IN PNPAGED_LOOKASIDE_LIST Lookaside   
  4.                   );  
  5.   
  6. PVOID  
  7. ExAllocateFromPagedLookasideList(  
  8.             IN PPAGED_LOOKASIDE_LIST Lookaside  
  9.                  );  


这两个函数分别是对非分页和分页内存的申请。

对Lookaside对象进行回收内存的操作,有以下两个函数:

[cpp]  view plain  copy
  1. VOID  
  2. ExFreeToNPagedLookasideList(   
  3.                 IN PNPAGED_LOOKASIDE_LIST Lookaside,  
  4.                 IN PVOID Entry  
  5.                 );  
  6.   
  7. VOID  
  8. ExFreeToPagedLookasideList(   
  9.                 IN PPAGED_LOOKASIDE_LIST Lookaside,  
  10.                 IN PVOID Entry   
  11.                 );  

这两个函数分别是对非分页和分页内存的回收。

在使用完Lookaside对象后,需要删除Lookaside对象,有以下两个函数:

[cpp]  view plain  copy
  1. VOID  
  2. ExDeleteNPagedLookasideList(  
  3.                 IN PNPAGED_LOOKASIDE_LIST Lookaside  
  4.                 );  
  5.   
  6. VOID  
  7. ExDeletePagedLookasideList(   
  8.                 IN PPAGED_LOOKASIDE_LIST Lookaside  
  9.                 );  


下面代码完整展示Lookaside对象的使用:

[cpp]  view plain  copy
  1. #pragma INITCODE  
  2. VOID LookasideTest()  
  3. {  
  4.     //初始化Lookaside对象  
  5.     PAGED_LOOKASIDE_LIST pageList;  
  6.     ExInitializePagedLookasideList(&pageList,  
  7.                                  NULL,NULL,0,  
  8.                                  sizeof(MYDATASTRUCT),  
  9.                                  '1234',  
  10.                                  0);  
  11. #define ARRAY_NUMBER 50  
  12.     PMYDATASTRUCT MyObjectArray[ARRAY_NUMBER];  
  13.     //模拟频繁申请内存  
  14.     for (int i = 0 ; i < ARRAY_NUMBER ; i++)  
  15.     {  
  16.         MyObjectArray[i] =   
  17.             (PMYDATASTRUCT)ExAllocateFromPagedLookasideList(&pageList);  
  18.     }  
  19.     //模拟频繁回收内存  
  20.     for (i = 0 ; i < ARRAY_NUMBER ; i++)  
  21.     {  
  22.         ExFreeToPagedLookasideList(&pageList,MyObjectArray[i]);  
  23.         MyObjectArray[i] = NULL;  
  24.     }  
  25.     //删除Lookaside对象  
  26.     ExDeletePagedLookasideList(&pageList);  
  27. }  


运行时函数:

 

1.内存间复制(非重叠)

在驱动程序开发中,经常用到内存的复制。DDK为程序员提供了以下函数:

[cpp]  view plain  copy
  1. VOID  
  2. RtlCopyMemory(   
  3.         IN VOID UNALIGNED *Destination,   
  4.         IN CONST VOID UNALIGNED *Source,   
  5.         IN SIZE_T Length  
  6.         );  

Destination:表示要复制内存的目的地址

Source:表示要复制内存的源地址

Length:表示要复制内存的长度,单位是字节

 

2.内存间的复制(可重叠)

函数原型:

[cpp]  view plain  copy
  1. VOID  
  2. RtlMoveMemory(   
  3.         IN VOID UNALIGNED *Destination,  
  4.         IN CONST VOID UNALIGNED *Source,   
  5.         IN SIZE_T Length  
  6.         );  

Destination:表示要复制内存的目的地址

Source:表示要复制内存的源地址

Length:表示要复制内存的长度,单位是字节

 

3.内存填充:

驱动程序开发中,还经常用到对某段内存区域用固定字节填充。DDK为程序员提供了函数RtlFillMemory。它在IA32平台下也是个宏,实际是memset函数。

[cpp]  view plain  copy
  1. VOID  
  2. RtlFillMemory(   
  3.        IN VOID UNALIGNED *Destination,   
  4.        IN SIZE_T Length,  
  5.        IN UCHAR Fill  
  6.        );  


Destination:目的地址

Length:长度

Fill:需要填充的字节

 

在驱动开发中,还经常需要对某段内存填零,DDK提供的宏是RtZeroBytes和RtZeroMemory。

[cpp]  view plain  copy
  1. VOID  
  2. RtlZeroMemory(  
  3.        IN VOID UNALIGNED *Destination,   
  4.        in SIZE_T Length  
  5.        );  


Destination:目的地址

Length:长度

 

4.内存比较:

驱动开发中,还会用到比较两块内存是否一致。该函数是RtlCompareMemory,其申明是:

[cpp]  view plain  copy
  1. ULONG  
  2. RtlEqualMemory(  
  3.       CONST VOID *Source1,  
  4.       CONST VOID *Source2  
  5.       SIZE_T Length  
  6.        );  

Source1:比较的第一个内存地址

Source2:比较的第二个内存地址
Length:比较的长度,单位为字节

 

将这些运行时函数统一做一个实验,代码如下:

[cpp]  view plain  copy
  1. #define BUFFER_SIZE 1024  
  2. #pragma INITCODE  
  3. VOID RtTest()  
  4. {  
  5.     PUCHAR pBuffer = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);  
  6.     //用零填充内存  
  7.     RtlZeroMemory(pBuffer,BUFFER_SIZE);  
  8.   
  9.     PUCHAR pBuffer2 = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);  
  10.     //用固定字节填充内存  
  11.     RtlFillMemory(pBuffer,BUFFER_SIZE,0xAA);  
  12.   
  13.     //内存拷贝  
  14.     RtlCopyMemory(pBuffer,pBuffer2,BUFFER_SIZE);  
  15.   
  16.     //判断内存是否一致  
  17.     ULONG ulRet = RtlCompareMemory(pBuffer,pBuffer2,BUFFER_SIZE);  
  18.     if (ulRet == BUFFER_SIZE)  
  19.     {  
  20.         KdPrint(("The two blocks are same\n"));  
  21.     }  
  22. }  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值