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的真实的物理内存。(所以在一般的XP电脑中只能使用4G的内存条,而在64位的电脑中却可以用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 PAGEDCODE
VOID 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 LOCKEDCODE
VOID SomeFunction()
{
	// 做一些其他事情
}

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

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

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

1、6    分配内核内存

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

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

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

 

NTKERNELAPI
PVOID
ExAllocatePool(
    IN POOL_TYPE PoolType,
    IN SIZE_T NumberOfBytes
    );

NTKERNELAPI
PVOID
NTAPI
ExAllocatePoolWithTag(
    IN POOL_TYPE PoolType,
    IN SIZE_T NumberOfBytes,
    IN ULONG Tag
    );

NTKERNELAPI
PVOID
NTAPI
ExAllocatePoolWithQuota(
    IN POOL_TYPE PoolType,
    IN SIZE_T NumberOfBytes,
    );
NTKERNELAPI
PVOID
ExAllocatePoolWithQuotaTag(
    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 INITCODE
VOID 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对象,有以下两个函数可用:

NTKERNELAPI
VOID
ExInitializeNPagedLookasideList (
    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
    );


 

NTKERNELAPI
VOID
ExInitializePagedLookasideList (
    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对象后,可以进行申请内存操作了,有以下两个函数:

__inline
PVOID
ExAllocateFromNPagedLookasideList(
    IN PNPAGED_LOOKASIDE_LIST Lookaside
    )

 

__inline
PVOID
ExAllocateFromPagedLookasideList(
    IN PPAGED_LOOKASIDE_LIST Lookaside
    )


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

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

__inline
VOID
ExFreeToNPagedLookasideList(
    IN PPAGED_LOOKASIDE_LIST Lookaside,
    IN PVOID Entry
    )


 

 

__inline
VOID
ExFreeToPagedLookasideList(
    IN PPAGED_LOOKASIDE_LIST Lookaside,
    IN PVOID Entry
    )


 

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

NTKERNELAPI
VOID
ExDeleteNPagedLookasideList (
    IN PPAGED_LOOKASIDE_LIST Lookaside
    );

 

NTKERNELAPI
VOID
ExDeletePagedLookasideList (
    IN PPAGED_LOOKASIDE_LIST Lookaside
    );

3、3 实验
typedef struct _MYDATASTRUCT 
{
	CHAR buffer[64];
} MYDATASTRUCT, *PMYDATASTRUCT;

#pragma INITCODE
VOID 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为程序员提供以下函数:

NTSYSAPI
VOID
NTAPI
RtlCopyMemory (
   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位搬移,速度做了优化

NTSYSAPI
VOID
NTAPI
RtlCopyMemory32 (
   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。

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

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

NTSYSAPI
VOID
NTAPI
RtlFillMemory (
   VOID UNALIGNED *Destination,
   SIZE_T Length,
   UCHAR Fill
   );


Fill: 需要填充的字节

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

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

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

NTSYSAPI
SIZE_T
NTAPI
RtlCompareMemory (
    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 INITCODE
VOID 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)" (??2@YAPAXI@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;
}



 

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值