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
时,程序只能使用非分页内存,否则蓝屏死机
驱动程序中的函数代码和全局变量可以指定载入分页还是非分页内存
-
先作如下定义:
#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")
-
定义函数载入分页内存:
#pragma PAGEDCODE VOID SomeFunction() { PAGED_CODE(); //do something }
PAGED_CODE()
:一个宏只在debug版本中有效,检查函数是否运行低于
DISPATCH_LEVEL`的中断请求级,等于或高于则会产生一个断言 -
定义函数载入非分页内存:
#pragma LOCKEDCODE VOID SomeFunction() { //do something }
-
特殊情况,某个例程需要在初始化时载入内存,运行完毕后从内存中卸载掉(如: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适用情况:
- 程序员每次申请固定大小的内存
- 申请和回收的操作十分频繁
使用方法:
首先初始化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
);
其他注意事项
-
C语言数据类型和DDK中的数据类型对应:
-
DDK额外提供64位的长整型(
LONGLONG
),只有无符号形式,使用时加上i64
结尾LONGLONG llValue = 100i64;
-
DDK提供了一个64位的数据结构联合体,
LARGE_INTEGER
typedef union _LARGE_INTEGER { struct { ULONG LowPart; LONG HighPart; }; struct { ULONG LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER;
-
DDK绝大部分返回值都是
NTSTATUS
类型(一个32位整数)typedef LONG NTSTATUS;
判断操作是否成功使用NT_SUCCESS
宏,而不是与0进行比较,因为只要状态码高位为0,无论其他位是否设置都表示成功常见状态码:
-
检查内存可用性
探测某段内存是否可读或可写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 );
当出现不可读写的情况,则将引发一个异常,需要使用结构化异常机制捕获它
-
结构化异常处理
__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{}和ProbeForRead
、ProbeForWrite
配合检查某段内存是否可读写
结构化异常(__try{}__finally{})__try { } __finally { }
在__try中无论运行什么代码(即使return语句或者触发异常),程序退出前,始终会执行__finally中的内容
-
断言
ASSERT(str != NULL);
一旦断言失败,将触发一个异常