现代操作系统一般都有分页机制,其控制方式是通过Cr0控制寄存器中的PG位,如果PG位置位则表示分页机制开启,这种机制在还未进入保护模式时就已经确定了。在32位的CPU下,假设是4KB为一页,则4GB内存可以被分成2^20个页,并且通过页表来映射到各个进程或者磁盘上去。同一页可以映射到多个进程的虚拟内存空间中。
内核态中可以分配分页内存以及非分页的内存, 分页内存是CPU开启分页机制时才存在,如果没有开启分页机制,所有内存都是非分页的。
分页机制作用主要是为了给进程一个4GB(32Bits)虚拟内存空间。操作系统通过页表来查询对应的页是否映射入进程的地址空间,如果该页被映射到了磁盘以文件存储,那么页表就会标记该页为脏,并触发一个异常,但只要将其读入内存后便可使用。
由于CPU执行代码可能在进程间切换,可能导致用户空间在进程切换后同一地址读取到的数据完全不同。因为他们所映射的内存页即物理地址可能已经不一样,只是虚拟地址还是一样罢了。进程也可能因为空间不够将一些页映射到磁盘上存储。当IRP的请求级别高于PASSIV_LEVEL到达DISPATCH_LEVEL时,只能使用非分页内存,这可能会导致蓝屏,但使用非分页的空间是常驻在内存中,可以确保安全
关于内核态的堆内存可以通过下面的WDK API进行分配。
ExAllocatePoolExAllocatePoolWithTagExAllocatePoolWithQuotaExAllocatePoolWithQuotaTag上面四个API第一个参数的类型都是POOL_TYPE, 可以是以下:
NonPagedPoolPagedPoolNonPagedPoolMustSucceedDontUseThisTypeNonPagedPoolCacheAlignedPagedPoolCahceAlignedNonPagedPoolCahceAlignedMustS第二个参数是一个SIZE_T类型用于指定请求分配的内存大小,如果API带Tag则带一个ULONG类型的Tag参数
ExFreePoolExFreePoolWithTag上面函数用于释放堆空间
如果不知道内存是否可读写可以使用下面这两个探测API:
ProbeForReadProbeForWrite参数:
Address: 被检查的内存地址
Length: 被检查的内存长度,字节单位
Alignment: 以多少字节对齐
#include <ntddk.h> VOID HeapMemTest() { CHAR *pBuf = ExAllocatePool(PagedPool, 100); // 进行分页内存分配 RtlZeroMemory(pBuf, 100); strcpy(pBuf, "Hello, world!"); KdPrint(("%s\n", pBuf)); ExFreePool(pBuf); // 释放操作 pBuf = NULL; pBuf = ExAllocatePoolWithTag(NonPagedPool, 100, 13); // 带一个13标记的非分页内存 strcpy(pBuf, "Hello, world!"); KdPrint(("%s\n", pBuf)); ExFreePoolWithTag(pBuf, 13); // 释放操作, 指定要释放的内存Tag /* This routine is called by highest-level drivers that allocate memory to satisfy a request in the context of the process that originally made the I/O request. Lower-level drivers call ExAllocatePoolWithTag instead. 与之前的函数区别在于带Quota的是为了满足最高IRP请求级别的驱动内存分配请求,一般用于用户层的IO操作,而其他的驱动可以使用上面的API */ pBuf = ExAllocatePoolWithQuota(PagedPoolCacheAligned, 100); strcpy(pBuf, "Hello, world!"); KdPrint(("%s\n", pBuf)); ExFreePool(pBuf); // 释放操作 pBuf = ExAllocatePoolWithQuotaTag(PagedPoolCacheAligned, 100, 15); // 区别在于多了个Tag strcpy(pBuf, "Hello, world!"); KdPrint(("%s\n", pBuf)); ExFreePoolWithTag(pBuf, 15); // 释放操作 } NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { pDriverObject->DriverUnload = Unload; KdPrint(("加载驱动!\n")); HeapMemTest(); return(STATUS_SUCCESS); }
WDK中提供了一个双向循环链表,以下是它的基本API以及用法:
InitializeListHeadInsertHeadListInsertTailListRemoveHeadListRemoveTailListIsListEmpty #include <ntddk.h> typedef struct _MYDATA { ULONG a; ULONG b; LIST_ENTRY listEntry; } MYDATA, *PMYDATA; VOID LinkListTest() { LIST_ENTRY listEntryHead; PMYDATA p; InitializeListHead(&listEntryHead); for (int i = 0; i < 20; ++i) { p = (PMYDATA)ExAllocatePool(PagedPool, sizeof(MYDATA)); p->a = i * 20; p->b = i * 30; InsertHeadList(&listEntryHead, &(p->listEntry)); // 头插法 } while (!IsListEmpty(&listEntryHead)) { PLIST_ENTRY pElem = RemoveHeadList(&listEntryHead); // 由于LIST_ENTRY在MYDATA结构体中不是第一个元素,需要通过LIST_ENTRY的地址算出MYDATA地址 p = CONTAINING_RECORD(pElem, MYDATA, listEntry); KdPrint(("%ld %ld\n", p->a, p->b)); ExFreePool(p); } } NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { pDriverObject->DriverUnload = Unload; KdPrint(("加载驱动!\n")); LinkListTest(); return(STATUS_SUCCESS); }
由于在微软编译器没提供new和delete,所以可以使用C++的操作符重载进行自己定义
这是Windows内部的异常处理机制。分为try-except块和try-finally块,可以嵌套但是不能有一个try对应多个except或finally的情况,也不能出现一个try同时跟着finally和except块。
except中可以放入:
EXCEPTION_EXECUTE_HANDLER(1) 如果try中遇到异常,丢到except中处理EXCEPTION_CONTINUE_SEARCH(0) 如果try中遇到异常,丢到上一级try块处理而不执行except内容EXCEPTION_CONTINUE_EXECUTION(-1) 如果try中遇到异常,继续从遇到异常的代码开始执行try-finally结构中无论try中遇到什么,都会执行finally内的语句。
VOID TryAndExceptFinally() { PCHAR p = NULL; __try { ProbeForWrite(p, 100, 4); // 以4字节对齐方式探测写入100字节 } __except (EXCEPTION_EXECUTE_HANDLER) { KdPrint(("写异常1!\n")); } __try { __try { ProbeForWrite(p, 100, 4); // 以4字节对齐方式探测写入100字节 } __except (EXCEPTION_CONTINUE_SEARCH) { KdPrint(("写异常2!\n")); } } __except (EXCEPTION_EXECUTE_HANDLER) { KdPrint(("EXCEPTION_CONTINUE_SEARCH导致回卷!\n")); } }(完)
