Windows驱动开发技术详解第五章(Windows内存管理)

PC上共三条总线:数据总线,地址总线,控制总线
PC中的部分设备自带内存(如显卡),该自带的内存会被映射到PC的物理内存上,读写该段内存实际就是读写设备自带的内存
操作系统和硬件(CPU中的内存管理MMU单元)共同构建出虚拟内存的概念,所有程序(包括Ring0和Ring3)可以操作的都是虚拟内存,虚拟内存是对物理内存的映射

虚拟内存和物理内存

虚拟内存的分页

CPU中有一个CR0的寄存器32位,其中的一个位(PG位)标志是否可以分页
Windows启动时,将该位设置位1,即允许分页
Windows的分页大小为4K(DDK中的PAGR_SIZE宏),4GB虚拟内存会分割为1M个分页单元

这1M个分页单元,共分为三部分:
①和实际的物理内存对应(物理内存也按照4K的分页大小划分为分页单元)【非分页内存
②和映射的磁盘文件对应(交换文件),此部分分页单元被标记为dirty脏的【分页内存
③空的,什么也没有对应

Tip:当试图读取映射为磁盘文件的内存单元时,系统将触发一个异常,导致异常处理函数将该内存单元装入内存,并标记为不脏。那些不经常读写的内存页会被交换成文件
在这里插入图片描述
部分物理内存页只映射到进程1上,此时进程1对该段内存的修改不会影响进程2
部分物理内存页同时映射进程1和进程2,此时这段内存实际上就是共享内存了,修改了同时影响进程1和进程2
大部分虚拟内存没有被映射到物理内存上

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

用户模式虚拟地址:0~0x7FFFFFFF(低2GB)
内核模式虚拟地址:0x80000000~0xFFFFFFFF(高2GB)
用户态(Ring3层)程序只能访问用户模式虚拟地址
内核态(Ring0层)程序可以访问整个虚拟内存
Windows核心代码和驱动程序都运行在Ring0层,应用层程序无法访问到
Windows在进程切换时,内核模式的地址是完全相同的,即所有进程的高2GB内存都相同
在这里插入图片描述

驱动程序和进程的关系

驱动程序看作一个DLL被加载在进程的地址空间内(加载的是内核模式的地址)
驱动程序只能访问当前加载的进程的地址空间,而不能访问其他进程的地址空间
DriverEntry例程和AddDevice例程运行在System这个进程中
System进程是操作系统运行的第一个进程
当需要加载驱动时,System进程中的一个线程会将驱动加载到内核模式地址空间内,并调用DriverEntry例程
驱动程序其他的一些例程(如IRP派遣)会运行在某个进程的环境中,也只能访问到此时该进程的地址空间

//小技巧,得到当前驱动在哪个进程中运行
VOID DisplayItsProcessName()
{
	PEPROCESS pEProcess = psGetCurrentProcess();
	PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
	KdPrint(("%s\n", ProcessName));
}

PEPROCESS 结构体微软并未公开,其中0x174位置为一个字符串指针

分页内存和非分页内存

分页内存:可以被交换到文件中的虚拟内存页面
非分页内存:永远不会交换到文件的虚拟内存页面

当程序的中断请求级等于或高于DISPATCH_LEVEL时,程序只能使用非分页内存,否则蓝屏死机

驱动程序中的函数代码和全局变量可以指定载入分页还是非分页内存

  1. 先作如下定义:

    #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")
    
  2. 定义函数载入分页内存:

    #pragma PAGEDCODE
    VOID SomeFunction()
    {
    	PAGED_CODE();
    	//do something
    }
    

    PAGED_CODE():一个宏只在debug版本中有效,检查函数是否运行低于DISPATCH_LEVEL`的中断请求级,等于或高于则会产生一个断言

  3. 定义函数载入非分页内存:

    #pragma LOCKEDCODE
    VOID SomeFunction()
    {
    	//do something
    }
    
  4. 特殊情况,某个例程需要在初始化时载入内存,运行完毕后从内存中卸载掉(如:DriverEntry,节省内存)

    #pragma INITCODE
    extern "C" NTSTATUS DriverEntry(
    	IN PDRIVER_OBJECT pDriverObject,
    	IN PUNICODE_STRING RegistryPath)
    {
    	//do something
    }
    
内核中的内存分配

内核中的内存非常珍贵,应尽量节约
局部变量和应用程序一样在栈中,但栈没有应用程序的栈空间那么大
驱动程序中不适合使用递归或大型结构体作为局部变量
大型结构体需要在堆中申请

堆中申请内存函数:

PVOID ExAllocatePool(				
	IN POOL_TYPE PoolType,
	IN SIZE_T NumberOfBytes
);

PVOID ExAllocatePoolWithTag(			//且额外分配4个字节的标签
	IN POOL_TYPE PoolType,
	IN SIZE_T NumberOfBytes,
	IN ULONG Tag
);

PVOID ExAllocatePoolWithQuota(			//按配额分配
	IN POOL_TYPE PoolType,
	IN SIZE_T NumberOfBytes
);

PVOID ExAllocatePoolWithQuotaTag(		//按配额分配,且额外分配4个字节的标签
	IN POOL_TYPE PoolType,
	IN SIZE_T NumberOfBytes,
	IN ULONG TAG
);

PoolTag:枚举型变量

枚举型变量类型含义
NonPagedPool指定分配非分页内存
PagedPool指定分配分页内存
NonPagedPoolMustd指定分配非分页内存,且必须成功
DontUseThisType未指定
NonPagedPoolCacheAligned指定分配非分页内存,且必须内存对齐
PagedPoolCacheAligned指定分配分页内存,且必须内存对齐
NonPagedPoolCacheAlignedMustS指定分配非分页内存,且必须内存对齐,且必须成功

NumberOfBytes:分配内存的大小,注意最好是4的倍数
返回值:分配得到的内存地址(内核模式地址),如果为0则分配失败

带有Tag的分配函数,可用于调试时,检测内存泄漏

堆内存释放函数:

VOID ExFreePool(
	IN PVOID P
);

NTKERNELAPI VOID ExFreePoolWithTag(
	IN PVOID P,
	IN ULONG Tag
)

P:要释放的堆内存地址

驱动中使用链表

DDK中提供了单向链表和双向链表两种链表结构,并简化了链表操作
单项链表:Next指针指向下一个元素
双向链表:BLINK指针指向前一个元素,FLINK指针指向下一个元素
在这里插入图片描述
双向链表定义:

typedef struct _LIST_ENTRY {
	struct _LIST_ENTRY *Flink;
	struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;

自定义链表中每个元素的数据类型:

typedef struct _MYDATASTRUCT {
	//LIST_ENTRY必须作为_MYDATASTRUCT结构体的一部分
	LIST_ENTRY ListEntry;
	//下面是自己定义的数据
	ULONG X;
	ULONG y;
} MYDATASTRUCT, *PMYDATASTRUCT;

常用链表操作:

InitializeListHead:一个宏,用于初始化双向链表,将Flink和Blink都指向自己
IsListEmpty:一个宏,检查双向链表是否为空,即检查Flink和Blink是否都指向自己
InsertHeadList:从链表头部插入一个元素
InsertTailList:从链表尾部插入一个元素
RemoveHeadList:从链表头部删除一个元素
RemoveTailList:从链表尾部删除一个元素
建议无论自定义数据结构的第一个字段是否为LIST_ENTRY,最好都是用CONTAINING_RECORD宏来得到自定义数据的指针

Lookaside结构

Lookaside结构是为了解决频繁申请内存和回收内存造成的大量内存空洞
Lookaside:一个内存容器,初始化时向Windows申请一段比较大的内存。以后程序员每次申请内存时,不是直接向Windows申请内存,而是向Lookaside申请内存。Lookaside会智能的避免生产内存空洞。Lookaside内部空间不够时会自动向Windows申请更多内存,内部空间有大量未使用内存时,会自动让Windows回收一部分内存。

Lookaside适用情况:

  1. 程序员每次申请固定大小的内存
  2. 申请和回收的操作十分频繁

使用方法:
首先初始化Lookaside:

//对非分页的Lookaside进行初始化
void ExInitializeNPagedLookasideList(
  [out]          PNPAGED_LOOKASIDE_LIST Lookaside,
  [in, optional] PALLOCATE_FUNCTION     Allocate,
  [in, optional] PFREE_FUNCTION         Free,
  [in]           ULONG                  Flags,
  [in]           SIZE_T                 Size,
  [in]           ULONG                  Tag,
  [in]           USHORT                 Depth
);

//对分页的Lookaside进行初始化
void ExInitializePagedLookasideList(
  [out]          PPAGED_LOOKASIDE_LIST Lookaside,
  [in, optional] PALLOCATE_FUNCTION    Allocate,
  [in, optional] PFREE_FUNCTION        Free,
  [in]           ULONG                 Flags,
  [in]           SIZE_T                Size,
  [in]           ULONG                 Tag,
  [in]           USHORT                Depth
);

初始化完毕后,可以对Lookaside对象进行申请内存的操作

//向非分页的Lookaside进行申请内存
PVOID ExAllocateFromNPagedLookasideList(
  [in, out] PNPAGED_LOOKASIDE_LIST Lookaside
);

//对分页的Lookaside进行申请内存
PVOID ExAllocateFromLookasideListEx(
  [in, out] PLOOKASIDE_LIST_EX Lookaside
);

内存使用完毕后,可以将从Lookaside申请到的内存进行回收给Lookaside

//回收非分页内存
void ExFreeToNPagedLookasideList(
  [in, out] PNPAGED_LOOKASIDE_LIST Lookaside,
  [in]      PVOID                  Entry
);

//回收分页内存
void ExFreeToPagedLookasideList(
  [in, out] PPAGED_LOOKASIDE_LIST Lookaside,
  [in]      PVOID                 Entry
);

删除Lookaside对象

//删除非分页内存Lookaside对象
void ExDeleteNPagedLookasideList(
  [in, out] PNPAGED_LOOKASIDE_LIST Lookaside
);

//删除分页内存Lookaside对象
void ExDeletePagedLookasideList(
  [in, out] PPAGED_LOOKASIDE_LIST Lookaside
);

运行时函数

标准运行时函数都以Rtl开头
绝大部分运行时函数都是宏,内部实际调用的函数都由ntoskrnl.exe导出

内存复制(非重叠,未考虑重叠情况):

void RtlCopyMemory(
   void*       Destination,
   const void* Source,
   size_t      Length
);

内存复制(可重叠,考虑到重叠情况,牺牲了性能):

void RtlMoveMemory(
   void*       Destination,
   const void* Source,
   size_t      Length
);

填充内存:

void RtlFillMemory(
   void*  Destination,
   size_t Length
   int    Fill
);

清空某段内存(全部置0):

void RtlZeroMemory(
   void*  Destination,
   size_t Length
);

内存比较:

//获取两块内存相等的字节数
NTSYSAPI SIZE_T RtlCompareMemory(
  [in] const VOID *Source1,
  [in] const VOID *Source2,
  [in] SIZE_T     Length
);

//直接判断两块内存是否完全一致,不完全一致返回0,完全一致返回非0
BOOL WINAPI
RtlEqualMemory(
   void*  Destination,
   void*  Source,
   size_t Length
);

其他注意事项

  1. C语言数据类型和DDK中的数据类型对应:
    在这里插入图片描述

  2. DDK额外提供64位的长整型(LONGLONG),只有无符号形式,使用时加上i64结尾

    LONGLONG llValue = 100i64;

  3. DDK提供了一个64位的数据结构联合体,LARGE_INTEGER

    typedef union _LARGE_INTEGER {
    	struct {
    		ULONG LowPart;
    		LONG HighPart;
    	};
    	struct {
    		ULONG LowPart;
    		LONG HighPart;
    	} u;
    	LONGLONG QuadPart;
    } LARGE_INTEGER;
    
  4. DDK绝大部分返回值都是NTSTATUS类型(一个32位整数)

    typedef LONG NTSTATUS;

    在这里插入图片描述
    判断操作是否成功使用NT_SUCCESS宏,而不是与0进行比较,因为只要状态码高位为0,无论其他位是否设置都表示成功

    常见状态码:
    在这里插入图片描述

  5. 检查内存可用性
    探测某段内存是否可读或可写

    void ProbeForRead(
      [in] const volatile VOID *Address,
      [in] SIZE_T              Length,
      [in] ULONG               Alignment
    );
    
    void ProbeForWrite(
      [in, out] volatile VOID *Address,
      [in]      SIZE_T        Length,
      [in]      ULONG         Alignment
    );
    

    当出现不可读写的情况,则将引发一个异常,需要使用结构化异常机制捕获它

  6. 结构化异常处理

    __try
    {
    
    }
    __except(filter_value)
    {
    
    }
    

    被__try{}包裹的块中,如果出现异常,会根据filter_value的值,判断是否需要在__except{}中处理
    filter_value有三种可能:
    EXCEPTION_EXECUTE_HANDLER:该数值为1,进入__except进行错误处理,处理完不再回到__try中,转而继续执行
    EXCEPTION_CONTINUE_SEARCH:该数值为0,不使用__except块中的异常处理,而是向上一层查找异常处理函数,进行处理
    EXCEPTION_CONTINUE_EXECUTION:该数值为-1,重复之前的错误指令,很少用到

    自行触发异常:
    在这里插入图片描述
    举例:使用__try{}__catch{}和ProbeForReadProbeForWrite配合检查某段内存是否可读写
    在这里插入图片描述
    结构化异常(__try{}__finally{})

    __try
    {
    
    }
    __finally
    {
    
    }
    

    在__try中无论运行什么代码(即使return语句或者触发异常),程序退出前,始终会执行__finally中的内容

  7. 断言

    ASSERT(str != NULL);

    一旦断言失败,将触发一个异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值