Driver post-Developement Tech

编译时检查

使用 /Wall及/WX参数编译

/Wall:打开所有的警告

/WX:将警告视为错误

但是DDK本身的头文件中还包括警告信息,可使用关闭警告的功能来克服这个问题,这些警告大多都是由微软对标准语言的扩展引起的,不影响可靠性,我们可以在源文件中关闭这些警告:

MSC_WARNING_LEVEL=/Wall /W4 /WX /wd4115 /wd4127 /wd4200 /wd4201 /wd4214 /wd4255 /wd4514 /wd4619

/wd4668 /wd4820

更好的方法是仅在引起这些警告的MS包含文件部分关闭这些警告,如下:

Source文件中为:

MSC_WARNING_LEVEL=/Wall /WX

使用如下语句包围引起警告的MS包含文件:

#pragma warning (disable: 4115 4127 4200 4201 4214 4255 4619 4668 4820)

...

#pragma warning (default: 4115 4200 4214 4255 4619 4668 4820)

注意:不同DDK版本出现的警告不同

使用C++编译器

在驱动开发中使用C++会带来一些问题,但使用C++编译器将有一些额外的好处,因为C++编译器会做更多的检查,使用C开始同时使用C++编译器编译的方法如下:

使用/TP编译开关,这个开关让编译器将.c文件作为.cpp文件来编译,这种技术通常在check版本中使用,在source文件中需要加入如下代码:

!if !$(FREEBUILD)

USER_C_FLAGS= /TP

!endif

同时,需要对DriverEntry进行标记:

#ifdef __cplusplus

extern "C"

#endif

NTSTATUS DriverEntry(

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath);

如果我们使用了WPP,还需要标记WPP的包含文件。

PREfast

PREfast按照显示规则对代码进行静态分析(DDK自带)

定义DEPRECATE_DDK_FUNCTIONS

在source 文件的C_DEFINES定义DEPRECATE_DDK_FUNCTIONS,将使编译器在编译是检查我们的代码是否使用了DDK已经过时的例程。但是 DEPRECATE_DDK_FUNCTIONS并不能检查出所有的过时例程,唯一的办法是检查DDK文档。在驱动中使用 MmGetSystemRoutineAddress例程可以用来判断我们使用的例程是否还被支持。

使用编译时处理

C_ASSERT

C_ASSERT是一个编译时断言

条件测试

使用#error MSG可以产生一个比C_ASSERT可读性更好的编译时错误:

#define BLK_SHIFT 9

#define BLK_SIZE 512 // Must be 1 << BLK_SHIFT

#if !(BLK_SIZE = (1 << BLK_SHIFT))

#error !(BLK_SIZE == (1 << BLK_SHIFT))

#endif

编译32位&64位驱动版本

编译64位版本可发现如下的问题:

指针问题

使用32位值存储指针、数据结构因为包含指针而改变大小

内联汇编

在驱动中不应该使用内联汇编

编译器差异

64位结构上的代码兼容性

ChkINF

安装前使用ChkINF检查INF文件(DDK自带)

调试及运行时检查

1、使用所有的检查选项调试

2、逐个关闭检查选项调试

3、关闭所有调试选项,在目标机运行,使用性能检测软件监视

内核调试器

KD:命令行调试器

WinDbg:带有图形前端的调试器

Check版Windows

使用Check版Window调试

驱动验证(Driver Verifier)

主动测试工具(Windows自带),同常设置如下检查:

  • 自动检查(必选)
  • 特殊内存检查
  • IRQL检查
  • 内存池跟踪
  • I/O验证
  • DMA验证
  • 死锁检测

其中有一项低资源模拟测试将在驱动中注入一些资源请求失败错误,这项检查应该在排除了其它错误后,最后来测试

调用验证(CUV)

使用调用验证需要更改source文件并重新编译驱动:

VERIFIER_DDK_EXTENSIONS=1

CUV支持如下类型的检查:

  • 初始化检查:使用一个内核类型前对其进行验证
  • 一致性检查:检查内核例程调用的一致性
  • 分页内存检查:检查不正确的分页内存使用
  • IRP堆栈检查:验证IRP堆栈的使用

使用CUV支持编译后的驱动在启动后将向调试器报告错误信息

缓冲池标记

缓冲池标记可以在分配缓冲区前加上4个字符的标记,Windows2000及XP仅在check版本上打开了缓冲池标记,free版本可使用gflags /r +ptg命令打开,2003所有的版本都打开了缓冲池标记。可以通过3种途径来查看这些缓冲池标记:

  • 调试器
  • Poolmon(Windows自带)
  • Pooltag(DDK自带)

从XP以后,我们都应该使用PROTECTED_POOL标记调用ExFreePoolWithTag来释放我们分配的内存,当驱动验证的特殊内存检查开启时,缓冲池标记不能被开启

代码覆盖

代码覆盖包括行覆盖及路径覆盖,可通过如下途径来提高代码覆盖率:

  • 测试所有有效的请求
  • 测试所有不正确的输入
  • 使用驱动验证中的低资源模拟
  • 随机的注入错误

性能监测

使用KrView及KernRate可以检测驱动的CPU占用率,我们应该通过两种方法来测试驱动的负载:

  • 在整个系统上运行性能监测,即使驱动负载较大,在整个系统负载中,它应该只占一个比较小的比例
  • 在驱动上运行性能监测,分析驱动中最花时间的部分

内核日志

通过Trace View(DDK自带),我们可以记录9种类型的数据:

  • 进程创建及终止
  • 线程创建及终止
  • 文件I/O
  • 磁盘I/O
  • 映象文件加载及卸载
  • 注册表访问

安装日志

通过使能安装日志,我们可以跟踪驱动在安装时的动作,在注册表种设置如下键值:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurentVersion\Setup

为0x3000FFFF可以使能详细安装日志,在完成调试后,需要关闭这个日志以保证性能,日志文件在%systemroot%\setupapi.log

杂项工具

  • DriverQuery(Windows自带):使用这个工具可以查询系统中驱动的相关信息
  • DeviceTree:显示设备及驱动的相关数据
  • DevCon:命令行工具,提供所有设备管理的功能,部分DriberQuery、DeviceTree功能,DDK的src\setup\devcon有源码,可用于编写驱动控制工具
  • RegEdit32:注册表编辑工具

诊断

断言

通常使用的断言宏有:

ASSERT ( expression )

ASSERTMSG ( message, expression )

在Win2000以前的系统中,这两个宏不返回任何值,以后的系统中,这两个宏返回expression的值,这两个断言仅在check版本中有效,通常在如下情况下使用断言:

  • 在进入和退出非静态函数时要对静态数据成员进行检查
  • 在进行一个处理前,校验输入参数是否正确
  • 在进入和退出循环前, 校验输入参数是否正确
  • 在使用指针时,对指针及指针指向的数据进行校验;在使用全局变量前,对其进行检验

校验的方式:

  • 检查指针是否有效

除了检查指针是否为NULL外,我们还可以做进一步检查.例如检查数据的类型:

ASSERTMSG ( “Invalid IRP pointer”,

irp != NULL && irp->Type == IO_TYPE_IRP)

  • 检查获得的值是否在范围以内
  • 检查双向链表的一致性

ASSERTMSG ( “Corrupted List”,

listPtr != NULL && listPtr->Flink != NULL &&

listPtr->Flink->Blink == ListPtr )

  • 检查字符串的有效性

ASSERTMSG ( “Invalid Counted String”, string != NULL &&

string->MaxLength > string->Length &&

string != NULL ? (string->Length != 0 ) : TRUE ))

  • 检查函数执行时的IRQL
  • 避免检查的侧效

ASSERT ( --CurrentCount > 0 )

  • 注意区分断言检查和错误检查的区别

p = ExAllocatePoolWithTag ( NonPagedPool, BLK_SIZE,
BLK_TAG );
ASSERT ( p != NULL )
在分配内存时,因为系统资源不足而引起的分配失败是很正常的事情,这里不应该使用断言

调试打印

通常使用的调试打印有如下四种:

KdPrint ( format, ... );

KdPrintEx ( componentId, level, format, ... );

DbgPrint ( format, ... );

DbgPrintEx ( componentId, level, format, ... );

头两个调用仅在check版本有效,后两个在所有版本都有效, componentId 和 level用来过滤调试输出

通常应该打印输出的信息如下:

  • 打印文字而不是数字

例如用IRP_MJ_CLOSE代替0x2

  • 打印描述数据结构的信息,而不是指向数据结构的指针

例如打印出IRP包的主功能码

  • 使用标准格式打印
  • 打印调用栈
  • 更多的输出信息将有助于调试,例如文件名,行号,当前执行线程,IRQL

格式化的规则:

  • 如果要打印指针,使用%p格式符,这样可以使程序在32位及64位平台上均可运行
  • 如果打印一个计数的字符串,使用%Z (ANSI)或者%wZ (Unicode),它可以正确打印出非空结束的字符串
  • 如果需要打印Unicode值,必须确认程序运行在DISPATCH_LEVEL级别之下
  • 每个输出调用限制在512字节内

WPP软件跟踪

WPP软件提供了一种低开销的日志数据机制,它创建一个二进制数据日志文件存放信息,它的显著优点是可以从驱动收集最终用户不可理解的数据。

使用WPP的最简单方法是在SOURCE文件中添加如下语句:

RUN_WPP=$(SOURCES) –km

此外,还需设置一些定义及包含文件,WPP的用法如下:

DoTraceMessage(IN TraceFlag, IN Format, ...)

我们也可以通过如下定义将WPP转变为调试打印:

#define WPP_DEBUG(args) DbgPrint args;

WPP支持一些DbgPrint没有提供的格式符宏。

由于WPP目的为低开销,所以于调试打印的规则略有不同:

  • 无需将数字转换成有意义的字符串
  • 无需输出文件及行号数据
  • ……

事件日志

系统事件日志用来报告驱动运行过程中发生的事情,主要是发生的问题,每个日志项的大小限制为256字节,使用系统事件日志主要考虑以下四点:

  • 事件日志的大小有限

系统日志的大小及覆盖规则有管理员来指定,驱动加入的每一个事件日志都应当有充分的理由,管理员并不关系驱动什么时候启动或者关闭,而仅关系驱动发生了什么问题

  • 每个错误指定一个唯一的错误代码,每个事件仅包含错误,便于管理员查询
  • 不要全部使用一样的错误信息,对每个错误使用实际的错误信息
  • 注意消息分类文件的处理

性能监测

一个好的驱动应该给用户提供性能信息,通常通过WMI来提供这些性能数据,通常我们可以提供如下数据来衡量驱动的性能:

  • IRP计数
  • 处理数据计数
  • 出错率或者出错个数
  • 安全事件计数

WMI的例子见src\wdm\wmi\wmifilt

自定义信息转储

驱动可以通过自定义信息转储来提高驱动的调试效率,在Win2000中使用KeRegisterBugCheckCallback实现自定义信息转储,在XP及以后的系统中使用KeRegisterBugCheckReasonCallback实现自定义信息转储。

版本块

使用版本信息将有利于用户报告驱动的问题,在资源描述文件中,使用VERSIONINFO定义来描述驱动的版本信息。

测试工具

测试的基本原则:

  • 易于复现
  • 易于确认测试结果
  • 使用不同的平台测试
  • 修复BUG后做回归测试

Device Path Exerciser

Device Path Exerciser(dc2.exe,DDK自带)使用不同的设备控制I/O调用来检测驱动的稳定性。这个工具的调用格式如下:

dc2 [options] /dr driver [driver …] (XP and later, support to 10 drivers)

dc2 [options] device (all, support one dirver)

日志部分

dc2.exe共产生四个日志文件:DC2.log,Diags.log,CrashN.log,Crash.log

打开设备部分

dc2.exe试图使用多种手段来打开设备进行测试。

测试部分

dc2.exe包括很多测试的内容,常用的如下:

  • 杂项测试(读/写/取消请求)
  • 零长度缓冲区测试
  • 随机测试

PNP驱动测试

PNP测试(pnpdtest.exe,DDK自带)主要包括如下部分:

  • 可移除性
  • 重新分配资源
  • 意外移除设备
  • 压力测试(包括5分钟的重新分配资源及移除测试,最后一个意外移除)

休眠及ACPI测试

使用sleeper.exe(DDK自带)测试驱动的电源管理与系统的电源管理的配合情况,使用pmte.exe(DDK自带)测试驱动的电源管理功能。

硬件兼容性测试(HCT)

HCT是一系列Windows Logo Program指定的必须通过的测试,它的主要目的是测试硬件,但有助于驱动开发,主要包括如下几类测试:

Audio

Bus Controllers

Display

Anti-Virus/File System Filter Drivers

Imaging

Input and HID

Modems

Network Devices

Storage Controllers and Devices

Streaming Media and Broadcast

Systems

Unclassified

总结

通常,驱动开发应该遵守以下规则:

  • 使用最新的DDK编译驱动
  • 在编译是发现问题

使用/Wall, C++编译,使用PREfast,使用DEPRECATE_DDK_FUNCTIONS 及C_ASSERT宏,使用ChkINF检查INF文件

  • 使用运行时检查及日志手段
  • 提供调试诊断信息来快速定位错误
  • 使用HCT及DDK中提供的工具进行测试,使用自定义的test case进行测试