当前位置: 代码迷 >> 驱动开发 >> Window XP驱动开发(十九)Window驱动的内存储器管理
  详细解决方案

Window XP驱动开发(十九)Window驱动的内存储器管理

热度:89   发布时间:2016-04-28 10:50:24.0
Window XP驱动开发(十九)Window驱动的内存管理

转载请标明是引用于 http://blog.csdn.net/chenyujing1234 

欢迎大家拍砖!

 

参考书籍<<Windows驱动开发技术详解>>

 

 在驱动程序编写中,分配和管理内存不能使用熟知的Win32 API函数,取而代之的是DDK提供的高效的内核函数。程序员
必须小心地使用这些内存相关的内核函数,因为在内核模式下,OS不会检查内存使用的合法性。

 

1、 内存管理概念

1、1  物理内存概念(Physical Memory Address)

PC上有三条总线,分别是数据总线地址总线控制总线。32位的CPU的寻址能力为4G(2的32次方)个字节。用户最多可以使用4G的真实的物理内存。

PC中会拥有很多设备,其中很多设备都提供了自己的设备内存。例如,显卡就会提供自己的显存。这部分内在会映射到PC的物理内存上,

也就是读写这段物理地址,其实会读写的设备内存地址,而是不会写物理内存地址。

下图是我PC中显卡的显存地址,一个设备可以有好几块设备内存映射到物理内存上:

1、2 虚拟内存地址概念(Virtual Memory Address)

虽然可以寻址4G的内存,而在PC 往往没有如此多的真实物理内存。操作系统和硬件(这里指的是CPU中的内存管理单元MMU)为使用者提供了虚拟内在的根据。

Windows的所有程序(包括Ring0层和Ring3层的程序)可以操作的都是虚拟内存。之所以称为虚拟内存,是因为对它的所有操作,最终会变成一系列对真实内存的操作

转换的过程是,在CPU中有一个重要的寄存器CR0,它是32位的寄存器,其中一个位(PG位)是负责告诉系统是否分页的。

Windows在启动前会将它的PG位置置1,即Windows允许分页。DDK中有个宏PAGE_SIZE记录着分页的大小,一般为4KB.

其中有一部分单元会和物理内存对应起来,即虚拟内存中第N个分页单元对应着物理内存第M个分页单元。这种对应不是一一对应,而是多对一映射的,多个虚拟内存页

可以映射到同一个物理内存页

还有一部分单元会被映射成磁盘上的文件,并标记为脏的(Dirty)。读取这段虚拟内存时,系统会发出一个异常,此时会触发异常处理函数,异常处理函数会将

这个分页的磁盘文件读入内存,并将标记设置为不脏。当让经常不读写的内存页,可以交替(Swap)成文件,并将此页设置为脏。

还有一部分单元什么也没有对应,即空的。

1、3  用户模式地址和内核模式地址

虚拟地址在0-0x7FFFFFFF范围内的虚拟内存,即低2GB的虚拟地址,被称为用户棋式地址,而0x80000000-0xFFFFFFFF范围内的虚拟内存,即高2G的虚拟内存,

被称为内核模式地址。Windows规定运行在用户态(Ring3层)的程序,只能访问用户模式地址,而运行在核心态(Ring0层)的程序,可以访问整个4G的虚拟内存,即

用户模式地址和内核模式地址。

Windows的核心代码和Windows的驱动程序加载的位置都是在高2G的内核地址里,所以一般应用程序是不能访问到这些核心代和重要数据的。这大大提高了系统的稳健性。

同时Windows操作系统在进程切换时,保持内核模式的地址是完全相同的,也就是说,所有进程的内核地址映射完全一致,进程切换时,只改变用户模式地址的映射。

1、4  Windows驱动程序和进程的关系

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

它能访问的只是这个进程的虚拟内存,而不能是其他进程的虚拟地址。

Window驱动程序里的不同例程运行在不同的进程中。

DriverEntry例程和AddDevice例程是运行在系统(System)进程中的,这个进程是Window中非常重要的进程,也是Windos第一个运行的进程

当需要加载的时候,这个进程中会有一个线程将驱动程序加载到内核模式地址空间内,并调用DriverEntry例程。

而其它一些例程,如IRP_MJ_READ和IRP_MJ_WRITE的派遣函数会运行于应用程序的“上下文”中,所谓运行在进程的“上下文”,指的是运行于某个进程的环境中,

所能访问的虚拟地址是这个进程的虚拟地址

有个技巧可以看出代码是运行于某个进程的上下文中,在代码中打印一行log信息,这行信息打印出当前进程的进程名。如果当前进程是发起I/O请求的进程,

则说明在进程的“上下文”中。下面给出的函数可以显示出当前进程的进程名,我们可以在任意的例程中调用这个函数,然后用DebugView软件查看LOG信息。

VOID DisplayItsProcessName(){	// 得到当前进程	PEPROCESS pEProcess = PsGetCurrentProcess();	// 得到当前进程名称	PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);	KdPrint(("%s\n", ProcessName));}


其中,PsGetCurrentProcess函数是得到当前运行的进程,它是EPROCESS的结构体,然而EPROCESS这个结构体是微软没有公开的结构体,其中0x174偏移的位置,

记录一个字符串指针。有兴趣的,可以利用WinDbg工具,查看该结构体的内容。和版本Windows的该结构,可能有些差别,下面是在Winows XP SP2的机器上用

WindDbg查看该结构的结果。如下:

 

1、5  分页和非分页内存

 Windows规定有些虚拟内存页面是可以交换到文件中的,这类内存被称为分页内存;

而有些虚拟内存页永远不会交换到文件中,这些内存被称为非分页内存

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

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

#define PAGEDCODE code_seg("PAGE")#define LOCKEDCODE code_seg()#define INITCODE code_seg("INIT")#define PAGEDDATA data_seg("PAGE")#define LOCKEDDATA data_seg()#define INITDATA data_seg("INIT")

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

#pragma PAGEDCODEVOID SomeFunction(){	PAGED_CODE();}

其中,PAGED_CODE是DDK提供的宏,它只在check版本中生效,它会检查这个函数是否运行低于DISPATCH_LEVEL的中断请求级,如果

等于或高于这个中断请求级,将产生一个断言。

#if DBG#define PAGED_CODE() \    { if (KeGetCurrentIrql() > APC_LEVEL) { \          KdPrint(( "EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql() )); \          ASSERT(FALSE); \       } \    }#else#define PAGED_CODE() NOP_FUNCTION;#endif

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

#pragma LOCKEDCODEVOID SomeFunction(){	// 做一些其他事情}

还有一种特殊情况,就是某个例程需要在初始化的时候载入内存,然后就可以从内存中卸载掉。这种情况指出现在DriverEntry情况下;

尤其是NT式的驱动,DriverEntry会很长,占据很大的空间,为了节省内存,需要及时从内存中卸载掉。代码如下:

#pragma INITCODEextern "C" NTSTATUS DriverEntry (			IN PDRIVER_OBJECT pDriverObject,			IN PUNICODE_STRING pRegistryPath	) {	// 做一些事情}


1、6    分配内核内存

Windows驱动程序使用的内存资源非常珍贵,分配内存时要尽量节约。和应用程序一样,局部变量是在存放在(Stack)空间中的,但栈空间不会像应用程序那么大,

所以驱动程序不适合递归调用或局部变量是大型结构体。如果需要大型结构体,请在堆(Heap)中申请。

堆中申请内存的函数有以下几个,原型如下:

 

NTKERNELAPIPVOIDExAllocatePool(    IN POOL_TYPE PoolType,    IN SIZE_T NumberOfBytes    );NTKERNELAPIPVOIDNTAPIExAllocatePoolWithTag(    IN POOL_TYPE PoolType,    IN SIZE_T NumberOfBytes,    IN ULONG Tag    );NTKERNELAPIPVOIDNTAPIExAllocatePoolWithQuota(    IN POOL_TYPE PoolType,    IN SIZE_T NumberOfBytes,    );NTKERNELAPIPVOIDExAllocatePoolWithQuotaTag(    IN POOL_TYPE PoolType,    IN SIZE_T NumberOfBytes,    IN ULONG Tag    );



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

typedef enum _POOL_TYPE {    NonPagedPool,    PagedPool,    NonPagedPoolMustSucceed,    DontUseThisType,    NonPagedPoolCacheAligned,    PagedPoolCacheAligned,    NonPagedPoolCacheAlignedMustS,    MaxPoolType


 

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

** 返回值分配的内存地址,一定是内核模式地址,如果返回0,则代表分配失败。

以上四个函数功能类似,函数以WithQuota结尾的代码分配时按配额分配

函数以WithTag结尾的函数,和ExtAllocatePool功能类似,唯一不同的是多了一个Tag参数,系统在要求的内存外又额外多分配了4个字节的标签。在调试时,可以找到是否有标有这个标签的内存没有被释放。

将分配的内存,进行回收的函数是ExtFreePool()和ExtFreePoolWithTag

 

2、在驱动中使用链表

在驱动程序开发中,经常使用链表这种数据结构,DDK为用户提供两种链表的数据结构,简化了对链表的操作。

链表中可以记录将整型、浮点、字符型或者程序员自定义的数据结构。链表通过指针将这些数据结构组成一个“链”,链中每个元素对应着记录的数据。

对于单向链表,每个元素中有一个Next指针指向一下元素。对于双向链表,每个元素有两个指针:BLINK指针指向前一个元素,FLINK指向向一个元素。

 

2、1  链表结构

DDK提供了标准的双向链表,双向链表可以将链表形成一个环。BLINK指针指向前一个元素,FLINK指向向一个元素。

以下是DDK提供的双向链表的数据结构。读者会奇怪这个数据只有指向前后的指针,而没有数据,这个问题在下一节介绍。

typedef struct _LIST_ENTRY {   struct _LIST_ENTRY *Flink;   struct _LIST_ENTRY *Blink;} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

 

2、2 链表初始化

 每个双向链表都是以一个链表头作为链表的第一个元素。初次使用链表需要初始化,主要将链表头FLink和BLink两个指针指向自己,

这意味着链表空。如下所示。

如何判断链表是否为空,可以判断链表头的FLink和Blink是否指向自己。DDK为程序员提供一个宏简化这种检查,这就是IsListEmpty。

#define IsListEmpty(ListHead) \    ((ListHead)->Flink == (ListHead))


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

数据结构串成一个链表。

typedef struct _WORK_QUEUE_ITEM {    LIST_ENTRY List;    PWORKER_THREAD_ROUTINE WorkerRoutine;    PVOID Parameter;} WORK_QUEUE_ITEM, *PWORK_QUEUE_ITEM;


2、3 从首部插入链表

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

在头部使用的语句是InsertHeadList,用法如下:

 InsertHeadList(&targetPortGroup->TargetPortList, &listEntry->ListEntry);

其中targetPortGroup->TargetPortList是LIST_ENTRY结构的链表头,listEntry是用户定义的数据结构,而它的子域ListEntry是包含其中的LIST_ENTRY数据结构

假设链表中已经有一个元素Entry1,如下图,现在将另外一个元素Entry2插入链表,InsertHeadList插入后的结果如下:


 

 2、4 从尾部插入链表

使用语句InsertTailList,

2、5 从链表删除

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

Entry = RemoveHeadList( &VolDo->OverflowQueue );
 RemoveTailList( &InstCtx->QueueHead );

其中VolDo->OverflowQueue是链表头,Entry是从链表删除下来的元素中的ListEntry,这里有一个问题,

就是如何从pEntry得到用户自定义的数据结构的指针,有以下两种情况。

(1)当自定义数据结构的第一个字段是LIST_ENTRY时,如:

typedef struct _WORK_QUEUE_ITEM {    LIST_ENTRY List;    PWORKER_THREAD_ROUTINE WorkerRoutine;    PVOID Parameter;} WORK_QUEUE_ITEM, *PWORK_QUEUE_ITEM;

 

此时,RemoveHeadList 返回的指针可以当做用户自定义的指针,即:

PLIST_ENTRY pEntry = RemoveHeadList(&head);PWORK_QUEUE_ITEM pMyData = (PWORK_QUEUE_ITEM)pEntry; 


(2)当自定义的数据结构第一个字段不是LIST_ENTRY时,如:

typedef struct _IRP_CONTEXT_LITE {    //  Type and size of this record (must be CDFS_NTC_IRP_CONTEXT_LITE)    NODE_TYPE_CODE NodeTypeCode;    NODE_BYTE_SIZE NodeByteSize;    //  Fcb for the file object being closed.    PFCB Fcb;    //  List entry to attach to delayed close queue.    LIST_ENTRY DelayedCloseLinks;    //  User reference count for the file object being closed.    ULONG UserReference;    //  Real device object.  This represents the physical device closest to the media.    PDEVICE_OBJECT RealDevice;} IRP_CONTEXT_LITE;



此时,RemoveHeadList 返回的指针不能当做用户的自定义的指针。

此时需要通过返回指针的地址逆向算出自定义数据的指针。一般通过RemoveHeadList返回指针在自定义数据中的偏移量,

用返回指针减去这个偏移量,就会得到用户自定义结构的指针的地址。

如下图,指针A是自定义的数据地址,指针B是自定义数据结构中的pEntry 。

为了简化这个操作,DDK为程序员提供了宏CONTAINING_RECORD,其用法如下:

 PIRP_CONTEXT_LITE NextIrpContextLite = CONTAINING_RECORD( Entry,                                                    IRP_CONTEXT_LITE,                                                    DelayedCloseLinks );

CONTAINING_RECORD的第一参数是相当于上图中的指针B,第二个参数是数据结构的名字,第三个参数是数据结构中的字段,返回相当于上图中的A。


2、6 实验

#pragma INITCODEVOID LinkListTest() {	LIST_ENTRY linkListHead;	//初始化链表	InitializeListHead(&linkListHead);	PMYDATASTRUCT pData;	ULONG i = 0;	//在链表中插入10个元素	KdPrint(("Begin insert to link list"));	for (i=0 ; i<10 ; i++)	{		pData = (PMYDATASTRUCT)			ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));		pData->number = i;		InsertHeadList(&linkListHead,&pData->ListEntry);	}	//从链表中取出,并显示	KdPrint(("Begin remove from link list\n"));	while(!IsListEmpty(&linkListHead))	{		PLIST_ENTRY pEntry = RemoveTailList(&linkListHead);		pData = CONTAINING_RECORD(pEntry,                              MYDATASTRUCT,                              ListEntry);		KdPrint(("%d\n",pData->number));		ExFreePool(pData);	} }

3 、Lookaside结构

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

3、1 频繁申请内存的弊端

如下图显示,在内存中先后申请三块内存。最开始可用的内存是连续的。当某个时刻内存块2被回收后,如果系统想分配一块略大于原先的块2的内存,这时原先的内存

2就不能被申请成功,因此频繁地申请、回收内存会导致在内存上产生大量的内存空洞。

3、2 使用Lookaside

如果驱动程序频繁地从内存中申请、回收固定大小的内存,DDK提供了一种机制来解决这个问题,就是使用Lookaside 对象

可将Lookaside对象想象成一个内存容器。在初始时,它先向Windows申请了一块比较大的内存,以后程序员每次申请内存时,不是直接向Windows申请内存,

而是向Lookaside对象申请内存。Lookaside对象会智能地避免产生内存“空洞”。

如果Lookaside对象内部的内存不够时,它会向操作系统申请更多的内存。当Lookaside对象内部有大量未使用的内存时,它会让Winodws回收一部分内存

总之,Lookaside是一个自动的内存分配容器。通过对Lookaside对象申请内存,效率高于直接向Windows申请内存。

Loodaside一般会在以下情况下使用:

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

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

如果程序员遇到上述两种情况,可以考虑使用Lookaside对象,驱动程序中的运行效率是程序员必须考虑的问题。如果驱动程序效率低,会严重影响到OS的性能。

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

NTKERNELAPIVOIDExInitializeNPagedLookasideList (    IN PNPAGED_LOOKASIDE_LIST Lookaside,    IN PALLOCATE_FUNCTION Allocate,    IN PFREE_FUNCTION Free,    IN ULONG Flags,    IN SIZE_T Size,    IN ULONG Tag,    IN USHORT Depth    );


 

NTKERNELAPIVOIDExInitializePagedLookasideList (    IN PPAGED_LOOKASIDE_LIST Lookaside,    IN PALLOCATE_FUNCTION Allocate,    IN PFREE_FUNCTION Free,    IN ULONG Flags,    IN SIZE_T Size,    IN ULONG Tag,    IN USHORT Depth    );


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

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

__inlinePVOIDExAllocateFromNPagedLookasideList(    IN PNPAGED_LOOKASIDE_LIST Lookaside    )

 

__inlinePVOIDExAllocateFromPagedLookasideList(    IN PPAGED_LOOKASIDE_LIST Lookaside    )


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

对Lookaside对象回收内存操作,有以下函数:

__inlineVOIDExFreeToNPagedLookasideList(    IN PPAGED_LOOKASIDE_LIST Lookaside,    IN PVOID Entry    )


 

 

__inlineVOIDExFreeToPagedLookasideList(    IN PPAGED_LOOKASIDE_LIST Lookaside,    IN PVOID Entry    )


 

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

NTKERNELAPIVOIDExDeleteNPagedLookasideList (    IN PPAGED_LOOKASIDE_LIST Lookaside    );

 

NTKERNELAPIVOIDExDeletePagedLookasideList (    IN PPAGED_LOOKASIDE_LIST Lookaside    );


3、3 实验

typedef struct _MYDATASTRUCT {	CHAR buffer[64];} MYDATASTRUCT, *PMYDATASTRUCT;#pragma INITCODEVOID LookasideTest() {	//初始化Lookaside对象	PAGED_LOOKASIDE_LIST pageList;	ExInitializePagedLookasideList(&pageList,NULL,NULL,0,sizeof(MYDATASTRUCT),'1234',0);#define ARRAY_NUMBER 50	PMYDATASTRUCT MyObjectArray[ARRAY_NUMBER];	//模拟频繁申请内存	for (int i=0;i<ARRAY_NUMBER;i++)	{		MyObjectArray[i] = (PMYDATASTRUCT)ExAllocateFromPagedLookasideList(&pageList);	}	//模拟频繁回收内存	for (i=0;i<ARRAY_NUMBER;i++)	{		ExFreeToPagedLookasideList(&pageList,MyObjectArray[i]);		MyObjectArray[i] = NULL;	}	ExDeletePagedLookasideList(&pageList);	//删除Lookaside对象}


4、运行时函数

4、1 内存间复制(非重叠)

在驱动程序开发中,经常会用到内存的复制。例如,将需要显示的内容,从缓冲区复制到显卡内存中。

DDK为程序员提供以下函数。

NTSYSAPIVOIDNTAPIRtlCopyMemory (   VOID UNALIGNED *Destination,   CONST VOID UNALIGNED *Source,   SIZE_T Length   );


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

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

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

和RtlCopyMemory功能类似的函数有RtlCopyBytes,这两个函数的参数全部一样,功能也完全一样,只是在不同的平台下有不同的实现。

在Inter 32位的CPU平台下,这两个函数其实是两个宏。

#define RtlCopyBytes RtlCopyMemory

另外Windows XP DDK又添加了一个新的函数RtlCopyMemory32,这个函数不是依靠memcpy实现的,因为是针对32位搬移,速度做了优化

NTSYSAPIVOIDNTAPIRtlCopyMemory32 (   VOID UNALIGNED *Destination,   CONST VOID UNALIGNED *Source,   ULONG Length   );

4、2 内存间复制(可重叠)

用RtlCopyMemory可以复制内存,但其内部没有考虑内存重叠的情况。如下图所示,有三段等长的内存,ABCD分别代表三段内存的起始地址和终止地址。

如果需要将A到C段的内存复制到B到D这段上,这时B到C的内存是重叠的部分。

RtlCopyMemory函数的内部实现是依靠memcpy函数实现的。依据C99定义,memcpy没有考虑重叠部分,因此它不能保证重叠部分是否被复制。

为了保证重叠部分也能被正确复制,C99规定memmove函数完成这个任务,这个函数对两个内存内容是否重叠进行了判断,这种判断牺牲了速度

如果程序员能确保复制的内存没重叠,请选择使用memcpy 函数。

DDK用宏对memmove进行了封装,名称变为RtlMoveMemory。

NTSYSAPIVOIDNTAPIRtlMoveMemory (   VOID UNALIGNED *Destination,   CONST VOID UNALIGNED *Source,   SIZE_T Length   );
4、3 填充内存

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

NTSYSAPIVOIDNTAPIRtlFillMemory (   VOID UNALIGNED *Destination,   SIZE_T Length,   UCHAR Fill   );


Fill: 需要填充的字节

在驱动程序开发中,还经常对某段内存填零,DDK提供的宏是RtlZeroBytes和RtlZeroMemory。

NTSYSAPIVOIDNTAPIRtlZeroMemory (   VOID UNALIGNED *Destination,   SIZE_T Length   );
4、4 内存比较

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

NTSYSAPISIZE_TNTAPIRtlCompareMemory (    const VOID *Source1,    const VOID *Source2,    SIZE_T Length    );

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

Source2:比较的第二个内存地址;

Length : 比较的长度;

4、5 关于运行时函数的注意事项

DDK提供的标准的运行时函数名都是RtlXXX形式的,其中,大部分以宏的形式给出的,例如RtlCopyMemory就是一个宏,定义为:

#define RtlEqualMemory(Destination,Source,Length) (!memcmp((Destination),(Source),(Length)))#define RtlMoveMemory(Destination,Source,Length) memmove((Destination),(Source),(Length))#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))#define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))


memcpy这些函数是由ntoskrnl.exe导出的函数。

下面通过VC提供的工具Depends查看这些导出函数。

用Depends工具打开编译的DDK驱动程序,显示出以下4部信息:

(1)标号为1的窗口,可以看出驱动程序HelloDDK.sys依赖的动态链接库(ntoskrnl.exe)。一般的动态链接库都是以DLL文件存在的,而ntoskrnl.exe却是以EXE文件形式

存在的。但不管什么,它都是PE格式文件。ntoskrnl.exe同时依赖于三个动态链接库。

(2)标号为2的窗口,列出了HelloDDK.sys中使用的ntoskrnl.exe的导出函数。例如,这个驱动程序中用到了RtlCopyMemory宏,而这个宏的定义是memcopy函数,

因此在导出函数中可以找到memcopy函数。

 

 

4、6 实现

#pragma INITCODEVOID RtlTest() {	PUCHAR pBuffer = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);	//用零填充内存	RtlZeroMemory(pBuffer,BUFFER_SIZE);	PUCHAR pBuffer2 = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);	//用固定字节填充内存	RtlFillMemory(pBuffer2,BUFFER_SIZE,0xAA);	//内存拷贝	RtlCopyMemory(pBuffer,pBuffer2,BUFFER_SIZE);	//判断内存是否一致	ULONG ulRet = RtlCompareMemory(pBuffer,pBuffer2,BUFFER_SIZE);	if (ulRet==BUFFER_SIZE)	{		KdPrint(("The two blocks are same.\n"));	}}

 

5、使用C++特性分配内存

在C++语言中分配内存时,可以使用new操作符,回收内存时使用delete操作符。但是在驱动程序中,

使用new和delete操作符,将得到错误的链接提示。如:

PCHAR  p = new CHAR[1024];

 

编译时,会得到以下错误:

1>Driver.obj : error LNK2019: 无法解析的外部符号 "void * __cdecl operator new(unsigned int)" ([email protected]@Z),该符号在函数 "void __stdcall RtlTest(void)" (?RtlTest@@YGXXZ) 中被引用1>MyDriver_Check/HelloDDK.sys : fatal error LNK1120: 1 个无法解析的外部命令

如果希望在驱动程序中使用new和delete操作符,应该对new和delete操作符进行重载

new和delete操作符可以通过内核函数ExAllocatePool  和回收函数ExFreePool实现。同时还可通过new操作符指定颁布内存还是非分布内存。

以下给出示例代码:

//全局new操作符void * __cdecl operator new(size_t size,POOL_TYPE PoolType=PagedPool){	KdPrint(("global operator new\n"));	KdPrint(("Allocate size :%d\n",size));	return ExAllocatePool(PagedPool,size);}//全局delete操作符void __cdecl operator delete(void* pointer){	KdPrint(("Global delete operator\n"));	ExFreePool(pointer);}class TestClass{public:	//构造函数	TestClass()	{		KdPrint(("TestClass::TestClass()\n"));	}	//析构函数	~TestClass()	{		KdPrint(("TestClass::~TestClass()\n"));	}	//类中的new操作符	void* operator new(size_t size,POOL_TYPE PoolType=PagedPool)	{		KdPrint(("TestClass::new\n"));		KdPrint(("Allocate size :%d\n",size));		return ExAllocatePool(PoolType,size);	}	//类中的delete操作符	void operator delete(void* pointer)	{		KdPrint(("TestClass::delete\n"));		ExFreePool(pointer);	}private:	char buffer[1024];};void TestNewOperator(){	TestClass* pTestClass = new TestClass;	delete pTestClass;	pTestClass = new(NonPagedPool) TestClass;	delete pTestClass;	char *pBuffer = new(PagedPool) char[100];	delete []pBuffer;	pBuffer = new(NonPagedPool) char[100];	delete []pBuffer;}



 

  相关解决方案