Windows驱动开发(2) - Windows内存管理
1、内存管理概念
1.1 物理内存
32位的CPU的寻址能力为4GB(2^32)个字节。用户最多可以使用4GB的真实物理内存。PC中的很多设备都提供了自己的设备内存,这部分的内存会映射到PC的物理内存上。
1.2 虚拟内存
Windows的所有程序(ring0,ring3),可以操作的都是虚拟内存。CPU中寄存器CR0一个位PG位来告诉系统是否分页的。1为允许分页。DDK中宏PAGE_SIZE记录着分页大小,一般为4K,4GB的虚拟内存会被分割成1M个单元。
图 物理内存的映射
1.3 用户模式地址和内核模式地址
4G虚拟地址中,低2G(00x7FFFFFFFF)为用户模式,高2G(0x800000000xFFFFFFFF)为内核模式。Windows规定用户态程序只能访问用户模式地址,而内核态程序可以访问整个4G虚拟地址。进程切换时,所有进程的内核地址映射完全一致,进程切换时改变,只是改变用户模式地址的映射。
图 用户模式和内核模式
1.4 驱动与进程的关系
Windwos驱动程序里的不同例程运行在不同的进程中。
打印当前进程的进程名:
void DisplayItsProcessName()
{
//得到当前进程
PEPROCESS pEProcess = PsGetCurrentProcess();
//得到当前进程名称
PTSTR = ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
KdPrint(("%s\n", ProcessName));
}
1.5 分页内存与非分页内存
可以交换到文件中的虚拟内存页面称为分页内存,否则称为非分页内存。当程序的中断请求级大于等于DISPATCH_LEVEL时,程序只能使用非分页内存。
指定某个例程和某个全局变量是载入分页内存还是非分页内存,需要做如下定义
#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")
将PAGEDCODE 等放在函数前来表现是可否可以分页等情况。
#pragma PAGEDCODE
VOID SomeFunction()
{
PAGED_CODE();
//其他代码
}
PAGED_CODE()是DDK提供的宏,它只在check版本中生效,来检查运行是否低于DISPATCH_LEVEL的中断请求级,如果不低于则产生断言。
1.6 分配内核内存
Windows驱动程序使用的内存资源非常珍贵,栈空间也不像应用程序那么大,所以在定义大型结构体是应在堆中申请。
堆中申请内存的函数有一下几个:
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:PoolType是个枚举变量,如果此值为NonPagedPool,则分配非分页内存。如果此值为PagedPool,则分配内存为分页内存。
- NonPagedPool:指定要求分配非分页内存。
- PagedPool:指定要求分配分页内存。
- NonPagedPoolMustSucceed:指定要求分配非分页内存,必须成功。
- DontUseThisType:未指定。
- NonPagedPoolCacheAligned:指定要求分配非分页内存,而且必须内存对齐。
- PagedPoolCacheAligned:指定要求分配分页内存,而且必须内存对齐。
- NonPagedPoolCacheAlignedMustS:指定要求分配非分页内存,而且必须内存对齐,且必须成功。
- NumberOfBytes:分配内存的大小,最好是4的倍数。
- Tag:系统额外分配4个字节的标签。
- 返回值:返回分配的内存地址,一定是内核模式地址,如果返回0,则代表分配失败。
VOID ExFreePool(
_In_ PVOID P
);
VOID ExFreePoolWithTag(
_In_ PVOID P,
_In_ ULONG Tag
);
将分配的内存回收。
- p:要释放的地址。
- Tag:标签。
2、在驱动中使用链表
2.1 链表结构
双向链表有两个指针,BLINK指向前一个元素,FLINK指向下一个元素。
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
2.2 链表初始化
初始化链表头用InitializeListHead
宏实现。
检查链表是否为空使用IsListEmpty(&head);
自定义链表
typedef struct _MYDATASTRUCT{
LIST_ENTRY ListEntry;
//自定义的数据
//......
}
2.3 插入链表
(1)首部插入链表
InsertHeadList(&head, &mydata->ListEntry);
(2)尾部插入链表
InsertTailList(&head, &mydata->ListEntry);
2.4 从链接删除
(1)首部删除链表
PLIST_ENTRY pEntry = RemoveHeadList(&head);
(2)尾部删除链表
PLIST_ENTRY pEntry = RemoveTailList(&head);
其中head是链表头,pEntry是从链表删除下的元素中的ListEntry。
当LIST_ENTRY是自定义数据结构的第一个字段时,pEntry可以当做自定义数据的地址。
3、Lookaside结构
如果驱动程序频繁地从内存中申请回收固定大小的内存,可以使用Lookaside对象。可以将Lookaside对象想象成一个自动的内存分配容器。避免产生内存空洞。
3.1 初始化:
VOID ExInitializeNPagedLookasideList(
_Out_ PNPAGED_LOOKASIDE_LIST Lookaside,
_In_opt_ PALLOCATE_FUNCTION Allocate,
_In_opt_ PFREE_FUNCTION Free,
_In_ ULONG Flags,
_In_ SIZE_T Size,
_In_ ULONG Tag,
_In_ USHORT Depth
);
VOID ExInitializePagedLookasideList(
_Out_ PPAGED_LOOKASIDE_LIST Lookaside,
_In_opt_ PALLOCATE_FUNCTION Allocate,
_In_opt_ PFREE_FUNCTION Free,
_In_ ULONG Flags,
_In_ SIZE_T Size,
_In_ ULONG Tag,
_In_ USHORT Depth
);
3.2 初始完内存后,可以申请内存了:
PVOID ExAllocateFromNPagedLookasideList(
_Inout_ PNPAGED_LOOKASIDE_LIST Lookaside
);
PVOID ExAllocateFromPagedLookasideList(
_Inout_ PPAGED_LOOKASIDE_LIST Lookaside
);
3.3 回收内存
VOID ExFreeToNPagedLookasideList(
_Inout_ PNPAGED_LOOKASIDE_LIST Lookaside,
_In_ PVOID Entry
);
VOID ExFreeToPagedLookasideList(
_Inout_ PPAGED_LOOKASIDE_LIST Lookaside,
_In_ PVOID Entry
);
3.4 删除Lookaside对象
VOID ExDeleteNPagedLookasideList(
_Inout_ PNPAGED_LOOKASIDE_LIST Lookaside
);
VOID ExDeletePagedLookasideList(
_Inout_ PPAGED_LOOKASIDE_LIST Lookaside
);
4、运行时函数
由编译器提供,不同操作系统实现不同,但是接口一样,如malloc。
4.1 内存间复制(非重叠)
VOID RtlCopyMemory(
_Out_ VOID UNALIGNED *Destination,
_In_ const VOID UNALIGNED *Source,
_In_ SIZE_T Length
);
- Destination:要复制内存的目的地址。
- Source:要复制内存的源地址。
- Length:要复制内存的长度,单位是字节。
4.2 可重叠复制
VOID RtlMoveMemory(
_Out_ VOID UNALIGNED *Destination,
_In_ const VOID UNALIGNED *Source,
_In_ SIZE_T Length
);
- Destination:要复制内存的目的地址。
- Source:要复制内存的源地址。
- Length:要复制内存的长度,单位是字节。
4.3 填充内存
VOID RtlFillMemory(
_Out_ VOID UNALIGNED *Destination,
_In_ SIZE_T Length,
_In_ UCHAR Fill
);
- Destination:目的地址。
- Length:长度。
- Fill:需要填充的字节。
VOID RtlZeroMemory(
_Out_ VOID UNALIGNED *Destination,
_In_ SIZE_T Length
);
- Destination:目的地址。
- Length:长度。
4.4 内存比较
SIZE_T RtlCompareMemory(
_In_ const VOID *Source1,
_In_ const VOID *Source2,
_In_ SIZE_T Length
);
- Source1:比较的第一个内存地址。
- Source2:比较的第二个内存地址。
- Length:比较的长度,单位为字节。
- 返回值:相等的字节数。
DDK提供的运行时函数都是RtlXX形式。
5、使用C++特性分配内存
驱动开发不能直接使用new和delete。因为MS编译器没有提供内核模式下的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;
}
6、其他
6.1 NTSTATUS含义
图 NTSTATUS含义
常用NTSTATUS状态返回值
- STATUS_SUCCESS:函数执行成功
- STATUS_UNSUCCESSFUL:函数执行不成功
- STATUS_NOT_IMPLEMENTED:函数未被实现
- STATUS_INVALID_INFO_CLASS:输入参数是无效的类别
- STATUS_INFO_LENGTH_MISMATCH:输入参数长度不匹配
- STATUS_ACCESS_VIOLATION:不允许访问
- STATUS_IN_PAGE_ERROR:发生页故障
- STATUS_INVALID_HANDLE:输入是无效的句柄
- STATUS_INVALID_PARAMETER:输入是无效的参数
- STATUS_NO_SUCH_DEVICE:指定设备不存在
- STATUS_NO_SUCH_FILE:指定文件比存在
- STATUS_INVALID_DEVICE_REQUEST:无效的设备请求
- STATUS_END_OF_FILE:文件已到结尾
- STATUS_INVALID_SYSTEM_SERVICE:无效的系统调用
- STATUS_ACCESS_DENIED:访问被拒绝
- STATUS_BUFFER_TOO_SMALL:是蠕动缓冲区过小
- STATUS_OBJECT_TYPE_MISMATCH:是蠕动对象类型不匹配
- STATUS_OBJECT_NAME_INVALID:输入的对象名无效
- STATUS_OBJECT_NAME_NOT_FOUND:输入的对象没用找到
- STATUS_PORT_DISCONNECTED:需要的端口没用被连接
- STATUS_OBJECT_PATH_INVALID:输入的对象路径无效
6.2 检查内存可用性
VOID ProbeForRead(
_In_ PVOID Address,
_In_ SIZE_T Length,
_In_ ULONG Alignment
);
- Address:需要被检查的内存地址
- Length:需要被检查的内存长度,单位是字节
- Alignment:描述该段内存是以多少字节对齐的
VOID ProbeForWrite(
_Inout_ PVOID Address,
_In_ SIZE_T Length,
_In_ ULONG Alignment
);
- Address:需要被检查的内存地址
- Length:需要被检查的内存长度,单位是字节
- Alignment:描述该段内存是以多少字节对齐的
6.3 结构化异常处理
异常概念类似于中断
A)try-except块
try-except-statement :
__try
{
compound-statement
}
__except ( expression )
{
compound-statement
}
expression有三种情况:
- EXCEPTION_EXECUTE_HANDLER:数值为1,进入到__except进行错误处理,处理完后不再回到__try{}块中,转而继续执行。
- EXCEPTION_CONTINUE_SEARCH:数值为0,不适用__except块中的异常处理,转而向上一层回卷。
- EXCEPTION_CONTINUE_EXECUTION:数值为-1,重复先前错误指令,很少用到。
例子如下:
#pragma INITCODE
VOID ProbeTest()
{
PVOID badPointer = NULL;
KdPrint(("Enter ProbeTest\n"));
__try
{
KdPrint(("Enter __try block\n"));
//判断空指针是否可读,显然会导致异常
ProbeForWrite(badPointer,100,4);
//由于在上面引发异常,所以以后语句不会被执行!
KdPrint(("Leave __try block\n"));
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
KdPrint(("Catch the exception\n"));
KdPrint(("The program will keep going\n"));
}
//该语句会被执行
KdPrint(("Leave ProbeTest\n"));
}
其它引发异常函数:
- ExRaiseAccessViolation:触发STATUS_ACCESS_VIOLATION异常
- ExRaiseDatatypeMisalignment:触发STATUS_DATATYPE_MISALIGNMENT异常
- ExRaiseStatus:用指定状态代码触发异常
B)try-finally 块
try-finally-statement :
__try compound-statement
__finally compound-statement
强迫函数有退出前执行一段代码。常用来一些资源的回收工作。
#pragma INITCODE
NTSTATUS TryFinallyTest()
{
NTSTATUS status = STATUS_SUCCESS;
__try
{
//做一些事情
return STATUS_SUCCESS;
}
__finally
{
KdPrint(("Enter finallly block\n"));
return status;
}
}
6.4 防止“侧效”错误**
也就是多行宏在if等语句中表达的意思出现了不同,在if,while,for等语句中,无论是否只有一句话,都不能省略{}。
6.5 ASSERT断言
NTSTATUS Foo(PCHAR* str)
{
ASSERT(srt!=NULL);
}