作爲開發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內核函數)