Windows驱动开发(2) - Windows内存管理

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个单元。
Windows驱动开发(2) - Windows内存管理_第1张图片
图 物理内存的映射

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

4G虚拟地址中,低2G(0~0x7FFFFFFFF)为用户模式,高2G(0x80000000~0xFFFFFFFF)为内核模式。Windows规定用户态程序只能访问用户模式地址,而内核态程序可以访问整个4G虚拟地址。进程切换时,所有进程的内核地址映射完全一致,进程切换时改变,只是改变用户模式地址的映射。
Windows驱动开发(2) - Windows内存管理_第2张图片
图 用户模式和内核模式

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含义

Windows驱动开发(2) - Windows内存管理_第3张图片
图 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_CONTINUE_EXECUTION:数值为1,进入到__except进行错误处理,处理完后不再回到__try{}块中,转而继续执行。
- EXCEPTION_CONTINUE_SEARCH:数值为0,不适用__except块中的异常处理,转而向上一层回卷。
- EXCEPTION_EXECUTE_HANDLER:数值为-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);
}

你可能感兴趣的:(windows,驱动开发)