Windows驅動開發技術詳解__Windows內存管理

 

 作爲開發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提供的例程時,可以指定某個例程和某個全局變量是載入分頁內存還是非分頁內存,需要做如下定義:

#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 INITCODE
VOID SomeFunction()
{
      PAGED_CODE();
      //do something
}

如果要讓程序加載到非分頁內存,需要在函數的實現中加入如下代碼:

#pragma LOCKEDCODE
VOID SomeFunction()
{
      //do something
}

還有一種特殊情況,就是某個例程初始化的時候載入內存,然後就可以從內存中卸載掉,例如DriverEntry

#pragma INITCODE
extern "C" NTSTATUS DriverEntry(
                           IN PDRIVER_OBJECT pDriverObject,
			IN PUNICODE_STRING pRegistryPath
						     )
{

}


 

6.內存的分配:

Windows驅動程序使用的內存資源非常珍貴,局部變量是在棧(stack)空間中,但是驅動程序的棧空間不會像應用程序那麼大,所以不適合進行遞歸調用或者局部變量是大型的結構體,否則請在堆(Heap)中申請。

堆中申請內存的函數有以下幾個:

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

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

PVOID 
ExAllocatePoolWithQuota( 
		IN POOL_TYPE PoolType,
		IN SIZE_T NumberOfBytes 
		);

PVOID 
ExAllocatePoolWithQuotaTag(
		IN POOL_TYPE PoolType,
		IN SIZE_T NumberOfBytes,
		IN ULONG Tag 
		);


其中有些重要的參數:

PoolType: 是個枚舉變量,如果此值爲NonPagedPool,則分配非分頁內存。如果次值爲PagedPool,則分配內存分頁內存。

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

以上四個函數功能類似,函數以WithQuota結尾的代表分配的時候按額分配。以WithTag結尾的函數和ExAllocatePool功能類似,唯一不同的是多了一個Tag參數,

系統要求的內存外又額外地多分配4個字節的標籤。在調試的時候,可以找出是否有標有這個標籤的內存沒有被釋放。

 

將分配的內存,進行回收的函數原型如下:

VOID
ExFreePool( 
	IN PVOID P
		 );

NTKERNELAPI
VOID
ExFreePoolWithTag(
	IN PVOID P,
	IN ULONG Tag
  );


參數P就是要釋放的內存。

 

在內存中使用鏈表:

鏈表中可以記錄整形,浮點型,字符型或者程序員自定義的數據結構。對於單鏈表,元素中有一個Next指針指向下一個元素。對於雙鏈表,每個元素有兩個指針:BLINK指向前一個元素,FLINK指向下一個元素。

 

1.鏈表的結構:

DDK提供了標準的雙向鏈表。雙向鏈表可以將鏈表形成一個環。以下是DDK提供的雙向鏈表的數據結構:

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


這個結構體只有指針沒有數據。

 

2.鏈表的初始化:

每個雙向鏈表都是以鏈表頭作爲鏈表的第一個元素。初次使用鏈表頭需要進行初始化,主要將鏈表頭的Flink和Blink兩個指針都指向自己。初始化鏈表頭用InitiallizeListHead宏實現。判斷鏈表頭是否爲空,DDK提供了一個宏簡化這種檢查,這就是IsListEmpty。

IsListEmpty(&head);


程序員需要自己定義鏈表中每個元素的數據類型,並將LIST_ENTRY結構作爲自定義結構的一個子域。例如:

typedef struct _MYDATASTRUCT
{
	LIST_ENTRY ListEntry;
	ULONG x;
	ULONG y;
}MYDATASTRUCT,*PMYDATASTRCUT;


3.插入鏈表:

對鏈表的插入有兩種方式,一種是在鏈表的頭部插入,一種是在鏈表的尾部插入。

在頭部插入鏈表使用語句InsertHeadList,用法如下:

InsertHeadList(&head,&mydata->ListEntry);


在尾部插入鏈表使用語句InsertTailList,用法如下:

InsertTailList(&head,&mydata->ListEntry);

 

4.鏈表的刪除:

和插入鏈表一樣,刪除鏈表也有兩種方法。一種從頭部刪除,一種從尾部刪除。分別對應RemoveHeadList和RemoveTailList函數。其使用方法如下:

PLIST_ENTRY = RemoveHeadList(&head);

PLIST_ENTRY = RemoveTailList(&head);


 

下面代碼完整演示向鏈表進行插入,刪除等操作,其主要代碼如下:

typedef struct _MYDATASTRUCT
{
	ULONG number;
	LIST_ENTRY ListEntry;
}MYDATASTRUCT,*PMYDATASTRCUT;

#pragma INITCODE
VOID LinkListTest()
{
	LIST_ENTRY linkListHead;
	//初始化鏈表
	InitializeListHead(&linkListHead);
    
	PMYDATASTRCUT pData;
	ULONG i = 0;
	//在鏈表中插入10個元素

    KdPrint(("Begain insert to link list"));
	for (i = 0 ; i < 10 ; i++)
	{
		pData = (PMYDATASTRCUT)
			     ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));
		pData->number = i;
		InsertHeadList(&linkListHead,&pData->ListEntry);
	}
	//從鏈表中取出,並顯示
    KdPrint(("Begain 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);
	}
}


在DebugView中打印log信息:

 

Lookaside結構:

頻繁申請和回收內存,會導致在內存上產生大量的內存“空洞”,從而導致最終無法申請內存。DDK爲程序員提供了Lookaside結構來解決這個問題。

 

1.使用Lookaside:

Lookaside一般會在一下情況使用:

(1)程序員每次申請固定大小的內存

(2)申請和回收的操作十分頻繁

使用Lookaside對象,首先要初始化Lookaside對象,有以下兩個函數可以使用:

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 
			);

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
			);


這兩個函數分別對非分頁和分頁Lookaside對象進行初始化。

在初始化完Lookaside對象後,可以進行申請內存的操作了,有以下兩個函數:

PVOID
ExAllocateFromNPagedLookasideList(
			IN PNPAGED_LOOKASIDE_LIST Lookaside 
			      );

PVOID
ExAllocateFromPagedLookasideList(
			IN PPAGED_LOOKASIDE_LIST Lookaside
			     );


這兩個函數分別是對非分頁和分頁內存的申請。

對Lookaside對象進行回收內存的操作,有以下兩個函數:

VOID
ExFreeToNPagedLookasideList( 
				IN PNPAGED_LOOKASIDE_LIST Lookaside,
				IN PVOID Entry
				);

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

這兩個函數分別是對非分頁和分頁內存的回收。

在使用完Lookaside對象後,需要刪除Lookaside對象,有以下兩個函數:

VOID
ExDeleteNPagedLookasideList(
				IN PNPAGED_LOOKASIDE_LIST Lookaside
				);

VOID
ExDeletePagedLookasideList( 
				IN PPAGED_LOOKASIDE_LIST Lookaside
				);


下面代碼完整展示Lookaside對象的使用:

#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;
	}
	//刪除Lookaside對象
	ExDeletePagedLookasideList(&pageList);
}


運行時函數:

 

1.內存間複製(非重疊)

在驅動程序開發中,經常用到內存的複製。DDK爲程序員提供了以下函數:

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

Destination:表示要複製內存的目的地址

Source:表示要複製內存的源地址

Length:表示要複製內存的長度,單位是字節

 

2.內存間的複製(可重疊)

函數原型:

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

Destination:表示要複製內存的目的地址

Source:表示要複製內存的源地址

Length:表示要複製內存的長度,單位是字節

 

3.內存填充:

驅動程序開發中,還經常用到對某段內存區域用固定字節填充。DDK爲程序員提供了函數RtlFillMemory。它在IA32平臺下也是個宏,實際是memset函數。

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


Destination:目的地址

Length:長度

Fill:需要填充的字節

 

在驅動開發中,還經常需要對某段內存填零,DDK提供的宏是RtZeroBytes和RtZeroMemory。

VOID
RtlZeroMemory(
	   IN VOID UNALIGNED *Destination, 
	   in SIZE_T Length
	   );


Destination:目的地址

Length:長度

 

4.內存比較:

驅動開發中,還會用到比較兩塊內存是否一致。該函數是RtlCompareMemory,其申明是:

ULONG
RtlEqualMemory(
      CONST VOID *Source1,
      CONST VOID *Source2
      SIZE_T Length
	   );

Source1:比較的第一個內存地址

Source2:比較的第二個內存地址
Length:比較的長度,單位爲字節

 

將這些運行時函數統一做一個實驗,代碼如下:

#define BUFFER_SIZE 1024
#pragma INITCODE
VOID RtTest()
{
	PUCHAR pBuffer = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);
	//用零填充內存
	RtlZeroMemory(pBuffer,BUFFER_SIZE);

	PUCHAR pBuffer2 = (PUCHAR)ExAllocatePool(PagedPool,BUFFER_SIZE);
	//用固定字節填充內存
	RtlFillMemory(pBuffer,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"));
	}
}


 

 

 

(下一篇:Windows內核函數)


 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章