47、Windows驱动程序模型笔记(五),内存管理
内存管理
1)内核模式与用户模式地址
图示 地址空间中用户模式部分和内核模式部分
每个用户模式进程都有自己的地址上下文,它把用户模式的虚拟地址映射成一组唯一的物理页帧。这意味着,当Windows NT调度器把控制从一个进程的当前线程切换到另一个进程的某个线程时,与进程相对应的虚拟地址空间也被更换。线程切换的一个步骤就是改变处理器当前使用的页表,以便它能引用新线程的进程上下文。
在编写驱动程序时我们要遵守下面原则:
决不(或几乎从不)直接引用用户模式的内存地址,无论何时我们需要访问计算机内存,都要使用内核模式的虚拟地址。
2)页大小
在虚拟内存系统中,操作系统以固定大小的页帧组织物理内存和交换文件。在WDM驱动程序中,常量PAGE_SIZE指出页的大小。在某些Windows NT计算机中,一页有4096字节;在另一些计算机中,一页有8192字节。有一个相关常量PAGE_SHIFT,你可以从下面语句中看出它的值:
PAGE_SIZE == 1 << PAGE_SHIFT
下面预处理宏可以简化页大小的使用:
•ROUND_TO_PAGES 把指定值舍入为下一个页边界。例如,在4KB页的计算机上,ROUND_TO_PAGES(1)的结果为4096,ROUND_TO_PAGES(4097)的结果为8192。
•BYTES_TO_PAGES 得出给定的字节量需要多少页来保存。例如,BYTES_TO_PAGES(42)在所有平台上都等于1,而BYTES_TO_PAGES(5000)在4KB页的平台上为2,在8KB页的平台上为1。
•BYTE_OFFSET 返回虚拟地址的字节偏移部分。例如,在4KB页的计算机上,BYTE_OFFSET(0x12345678)的结果为0x678。
•PAGE_ALIGN 把虚拟地址舍向上一个页边界。例如,在4KB页的计算机上,PAGE_ALIGN(0x12345678)的结果为0x12345000。
•ADDRESS_AND_SIZE_TO_SPAN_PAGES 返回从指定虚拟地址开始的指定字节数所跨过的页数。例如,在4KB的计算机上, ADDRESS_AND_SIZE_TO_SPAN_PAGES(0x12345FFF,2)的结果为2,因为这两个字节跨过了页边界。
3)Windows NT把内核模式地址空间分成分页内存池和非分页内存池。(用户模式地址空间总是分页的) 必须驻留的代码和数据放在非分页池;不必常驻的代码和数据放在分页池中。
执行在高于或等于DISPATCH_LEVEL级的代码不可以引发页故障。
#ifdef ALLOC_DATA_PRAGMA
#pragma data_seg("PAGE")
#endif
data_seg编译指示使所有在其后声明的静态数据变量进入分页池。这个编译指示与alloc_text完全不同。一个分页段可以从#pragma data_seg("PAGE")出现的地方开始到#pragma data_seg()出现的地方结束。而Alloc_text仅应用于单个函数。
为了检测编译器是否是Microsoft的编译器,可以测试预定义宏_MSC_VER是否存在。
掉电期间是释放锁定内存页的最佳时期。
服务函数 | 描述 |
MmLockPagableCodeSection | 锁定含有给定地址的代码段 |
MmLockPagableDataSection | 锁定含有给定地址的数据段 |
MmLockPagableSectionByHandle | 用MmLockPagableCodeSection返回的句柄锁定代码段(仅用于Windows 2000) |
MmPageEntireDriver | 解锁所有属于某驱动程序的页 |
MmResetDriverPaging | 恢复整个驱动程序的编译时分页属性 |
MmUnlockPagableImageSection | 为一个锁定代码段或数据段解锁 |
服务函数或宏 | 描述 |
PushEntryList | 向链表顶加入元素 |
PopEntryList | 删除最上面的元素 |
4)把C/C++编程规范引入驱动编程中
如RemoveHeadList是一个宏,如果if语句中不用{},则出问题。
5)图示显示了 lookaside链表的概念。假设有一个可以在水池中直上直下平衡的玻璃杯子。这个杯子就代表lookaside链表对象。当初始化该对象时,你告诉系 统需要多大的内存块(杯中的水滴)。在早期版本的Windows NT中,你还要指出杯子的容量,但现在的操作系统可以自动适应。为了满足一个内存分配请求,系统首先尝试着从链表中取出(删除)一块内存(从杯子中取出一 滴水)。如果连一块内存也没有,系统就从外面内存池中取。相反,释放内存时,系统首先尝试着放到链表上(向杯子中加入一滴水)。但是,如果链表满了,那么 这个内存块就返回到外界的内存池中(杯子中的水溢出到水池)。
图示. Lookaside链表
4、字符串
String Manipulation
http://msdn.microsoft.com/en-us/library/ee479680.aspx
Buffer Manipulation
http://msdn.microsoft.com/en-us/library/ee479504.aspx
表示 P89串处理函数
操作 | ANSI串函数 | Unicode串函数 |
Length | strlen | wcslen |
Concatenate | strcat, strncat | wcscat, wcsncat, RtlAppendUnicodeStringToString, RtlAppendUnicodeToString |
Copy | strcpy, strncpy, RtlCopyString | wcscpy, wcsncpy, RtlCopyUnicodeString |
Reverse | _strrev | _wcsrev |
Compare | strcmp, strncmp, _stricmp, _strnicmp, RtlCompareString, RtlEqualString | wcscmp, wcsncmp, _wcsicmp, _wcsnicmp, RtlCompareUnicodeString, RtlEqualUnicodeString, RtlPrefixUnicodeString |
Initialize | _strset, _strnset, RtlInitAnsiString, RtlInitString | _wcsnset, RtlInitUnicodeString |
Search | strchr, strrchr, strspn, strstr | wcschr, wcsrchr, wcsspn, wcsstr |
Upper/lowercase | _strlwr, _strupr, RtlUpperString | _wcslwr, _wcsupr, RtlUpcaseUnicodeString |
Character | isdigit, islower, isprint, isspace, isupper, isxdigit, tolower, toupper, RtlUpperChar | towlower, towupper, RtlUpcaseUnicodeChar |
Format | sprintf, vsprintf, _snprintf, _vsnprintf | swprintf, _snwprintf |
String conversion | atoi, atol, _itoa | _itow, RtlIntegerToUnicodeString, RtlUnicodeStringToInteger |
Type conversion | RtlAnsiStringToUnicodeS ize, RtlAnsiStringToUnicodeS tring | RtlUnicodeStringToAnsiString |
Memory release | RtlFreeAnsiString | RtlFreeUnicodeString |
1)处理blob数据的服务函数
表示. 处理blob数据的服务函数
服务函数或宏 | 描述 |
memchr | 在blob中寻找一个字节 |
memcpy, RtlCopyBytes, RtlCopyMemory | 复制字节,不允许重叠 |
memmove, RtlMoveMemory | 复制字节,允许重叠 |
memset, RtlFillBytes, RtlFillMemory | 用给定的值填充blob |
memcmp, RtlCompareMemory, RtlEqualMemory | 比较两个blob |
memset, RtlZeroBytes, RtlZeroMemory | blob清零 |
•内存的“copy”和“move”操作之间的区别在于可否容忍源和目的相重叠。move操作不管源和目的是否重叠。而copy操作在源和目的有任何重叠时不工作。
•“byte” 操作和“memory”操作的区别是操作的间隔尺寸。byte操作保证按字节为单位执行。而memory操作可以在内部使用更大的块,所有这些块的和等于 指定的字节数。这个区别会根据平台的不同而改变,在32位Intel计算机上,byte操作实际上是对应memory操作的宏。但在Alpha平台 上,RtlCopyBytes与RtlCopyMemory是完全不同的函数。
5、注册表
服务函数 | 描述 |
IoOpenDeviceRegistryKey | 打开PDO专用键 |
IoOpenDeviceInterfaceRegistryKey | 打开与注册设备接口相连的键 |
RtlDeleteRegistryValue | 删除一个注册表值 |
RtlQueryRegistryValues | 从注册表中读取多个值 |
RtlWriteRegistryValue | 向注册表写一个值 |
ZwClose | 关闭注册表键句柄 |
ZwCreateKey | 创建一个注册表键 |
ZwDeleteKey | 删除一个注册表键 |
ZwEnumerateKey | 枚举子键 |
ZwEnumerateValueKey | 枚举某注册表键中的值 |
ZwFlushKey | 把注册表更改提交到磁盘 |
ZwOpenKey | 打开一个注册表键 |
ZwQueryKey | 取关于某注册表键的信息 |
ZwQueryValueKey | 取某个注册表键中的值 |
ZwSetValueKey | 置某个注册表键中的值 |
表示. 注册表访问函数
6、浮点数
在Intel处理器上,浮点协处理器还可以执行MMX指令。在历史上,驱动程序在执行浮点运算上有两个问题。对于没有浮点协处理器的计算机,操作系统将用 软件仿真一个,但是仿真的浮点协处理器会消耗很大的CPU处理能力,并且需要一个处理器异常来捕捉浮点指令。在内核模式中处理异常,尤其是在提升的 IRQL级上,是困难的。另外,在有浮点协处理器的计算机上,由于CPU结构上的原因,当线程上下文切换时,需要一个耗时的操作来保存和恢复浮点协处理器 的状态。所以,通常的做法是禁止在内核模式驱动程序中使用浮点运算。
Microsoft建议,除非必要,应避免在内核模式驱动程序中使用浮点运算。
关于如何使用及更多信息,可以参见《Windows驱动程序模型设计》,P99