[Win驱动3] Windows内核态的堆管理, LookAside,链表以及运行时函数

    科技2026-02-25  16

    内存机制简述


    现代操作系统一般都有分页机制,其控制方式是通过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提供的链表


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

     

    LookAside结构及其内存分配


    #include <ntddk.h> #define ARRAY_NUMBER 50 typedef struct _MYDATA { ULONG a; ULONG b; } MYDATA, *PMYDATA; typedef struct _MYDATA1 { CHAR strM[30]; ULONG ulM; } MYDATA1, *PMYDATA1; VOID Unload(IN PDRIVER_OBJECT pDriverObject) { KdPrint(("卸载驱动!\n")); } VOID LookAsidePagedMemoTest() { PAGED_LOOKASIDE_LIST pageList; // 该对象会申请分页内存 ExInitializePagedLookasideList(&pageList, NULL, NULL, 0, sizeof(MYDATA), '1234', 0); PMYDATA arr[ARRAY_NUMBER]; for (int i = 0; i < ARRAY_NUMBER; ++i) { arr[i] = (PMYDATA)ExAllocateFromPagedLookasideList(&pageList); arr[i]->a = 20 * i; arr[i]->b = 30 * i; } for (int i = 0; i < ARRAY_NUMBER; ++i) { KdPrint(("a: %ld, b: %ld\n", arr[i]->a, arr[i]->b)); ExFreeToPagedLookasideList(&pageList, arr[i]); arr[i] = NULL; } ExDeletePagedLookasideList(&pageList); } VOID LookAsideNPagedMemoTest() { NPAGED_LOOKASIDE_LIST nPageList; // 该对象会申请非分页内存 ExInitializeNPagedLookasideList(&nPageList, NULL, NULL, 0, sizeof(MYDATA1), '2345', 0); PMYDATA1 arr[20] = {NULL}; for (int i = 0; i < 20; ++i) { arr[i] = ExAllocateFromNPagedLookasideList(&nPageList); // 从非分页内存中申请内存到Lookaside链中 RtlZeroMemory((arr[i]->strM), 30); strcpy(arr[i]->strM, "这是字符串"); arr[i]->ulM = 300 * i / 5; } for (int i = 0; i < 20; ++i) { KdPrint(("字符串: %s 数字: %ld\n", arr[i]->strM, arr[i]->ulM)); ExFreeToNPagedLookasideList(&nPageList, arr[i]); arr[i] = NULL; } ExDeleteNPagedLookasideList(&nPageList); } NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { pDriverObject->DriverUnload = Unload; KdPrint(("加载驱动!\n")); // LookAsidePagedMemoTest(); LookAsideNPagedMemoTest(); return(STATUS_SUCCESS); }

     

    运行时函数


    RtlCopyMemory(不可进行重叠复制): memcpy的封装RtlMoveMemory(这个API用于复制而不是移动内存, 与RtlCopyMemory的区别是,它可重叠): memmove的封装RtlFillMemory(VOID *dst, SIZE_T Len, UCHAR Fill): 注意顺序,把Len个Fill填入dst指向的内存地址, memset的封装RtlZeroMemory: memset的封装RtlEqualMemory: 内存比较RtlCompareMemory: 同上 #include <ntddk.h> VOID RtlFunctionTest() { CHAR pBuf[1024] = "Hello, world"; CHAR pNewBub[1024] = {0}; KdPrint(("原字符串: %s\n", pBuf)); RtlZeroMemory(pBuf, sizeof(pBuf)); KdPrint(("经过RtlZeroMemory后的字符串: %s\n", pBuf)); RtlFillMemory(pBuf, sizeof(pBuf), 'a'); KdPrint(("经过RtlFillMemory后的字符串: %s\n", pBuf)); RtlCopyMemory(pNewBub, pBuf, sizeof(pNewBub)); KdPrint(("经过RtlCopyMemory后的新字符串: %s\n", pNewBub)); SIZE_T t = RtlCompareMemory(pBuf, pNewBub, sizeof(pBuf)); KdPrint(("RtlCompareMemory相同字符串进行比较的结果: %d\n", t)); t = RtlEqualMemory(pBuf, pNewBub, sizeof(pBuf)); KdPrint(("RtlEqualMemory相同字符串进行比较的结果: %d\n", t)); RtlMoveMemory(pBuf, "Hello, world!", sizeof("Hello, world!")); KdPrint(("RtlMoveMemory后的新字符串: %s\n", pBuf)); } NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { pDriverObject->DriverUnload = Unload; KdPrint(("加载驱动!\n")); RtlFunctionTest(); return(STATUS_SUCCESS); }

    在驱动中使用new和delete


    由于在微软编译器没提供new和delete,所以可以使用C++的操作符重载进行自己定义

    SEH结构化异常处理


    这是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")); } }

    (完)

     

     

     

     

     

     

     

     

     

     

     

     

    Processed: 0.015, SQL: 9