驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

上一篇内容里已经讲到了如何搭建双机通信,这也是为内核态下的驱动程序调试做准备。众所周知,KMDF驱动程序主要分为两个部分的代码,一个是包含DriverEntry、DeviceAdd和一些对于触发事件的回调函数,下面我会统一称为驱动程序;另一部分则是从用户态上发出一些IO指令请求,需要驱动程序来做出应答,通常使用的是CreteFile或者IoDeviceControl这些函数,这部分我叫它应用程序。

无论是驱动程序还是应用程序都是一种代码,所以也可以像我们写C或者Python代码那样进行调试,这也是直接观察到程序运行正确与否的最佳手段。驱动程序的调试稍微要麻烦一点,下面就来仔细介绍一下。

(1) 首先驱动程序是运行在内核态下的,一般的打印语句print是显示不出来的,这里我使用的是KdPrintEx语句,将鼠标放到VS的KdPrintEx()上就会看到 #define KdPrintEx() DbgPrintEx,所以这个语句实际上也是DbgPrintEx的宏定义。

同时内核态下的打印语句也是需要优先级值的,为了保证打印出来,可以直接设置该语句为:KdPrintEx((DPFLTR_IHVAUDIO_ID, DPFLTR_ERROR_LEVEL, “打印字样”)) 。如果还是不能看到打印信息,需要在注册表里重新设一个值, HKEY_LOCAL_MACHINE\SYSTEM\
CurrentControlSet\COntrol\Session Manager\Debug Print Filter
设置值为 “DEFALULT” = dword:0000000F

(2) 在Debugging 通信之前,先得确保驱动程序的代码没有错误,编译通过,可以在工具栏里选择“生成”,下拉框里选择 “生成‘项目名’”,如果下面输出框内没有报错,并且在项目的目录下可以找到生成的.exe文件,如下图所示,则说明编译通过,可执行的应用程序也被生成出来。
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

(3) 在工具栏里选择Debugging Tools for Windows - Kernel Debugger,前面的解决方案配置选择Debug,解决方案平台选择x64(根据你的操作系统位数来选择),如下图所示:
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

点击之后等待主机和目标机的通信,在这个过程中主机会将和驱动程序安装相关的inf和sys文件拷贝到目标机,但是并不会自动安装,尤其是使用了真实设备,需要在目标机手动给设备安装上驱动程序。当通信完成之后,出现下图字样,则表示通信已经成功建立起来了。
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

(4) 对于驱动程序的调试主要借鉴了网上各路大神的博客,使用了主机加断点,目标机安装驱动程序命中断点的模式。当通信成功后,选择工具栏的 调试->全部中断,出现下图字样就可以加断点了,这时候会发现目标机卡住不动,说明目标机进入了断点模式。
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

这里我们主要在IoDevicecontrol函数入口前和函数内加了两个断点,加好之后在Debugger Window窗口的底部 ?:kd>栏输入命令g,使驱动程序运行起来,如下图所示,这时候又会发现目标机恢复正常了。
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

(5) 驱动程序主要是由入口函数DriverEntry和一系列触发事件的回调函数组成,所以在进行上述操作后,驱动程序应该只运行到了入口函数,后续的DeviceAdd和资源分配等回调函数没有被触发到。这里我们将对使用到的真实设备安装上驱动程序,以此来触发驱动程序里的DeviceAdd和资源分配等回调函数。没有安装驱动程序的真实设备(我们使用的是一个PCIe接口的FPGA板卡)接上目标机后,在设备管理器里会读出来,但是显示没有安装驱动,如下图所示:
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

这里我们选择更新驱动程序,并且是手动的方式,找到之前建立通信过程中主机拷贝配置文件到目标机的文件夹,一般在C盘的DriverTest->Drivers文件里,选择.sys文件进行安装。当安装上的那一刻,就会发现目标机这个操作会触发主机VS的驱动程序,并命中断点。

我们点击VS里的继续,让驱动程序运行到第一个断点处(这个时候目标机已经进入了断点模式,卡住不动),就可以发现之前设置在DeviceAdd等函数内的打印语句全部出现在了Debugger Window的窗口之中,如下图所示:
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

这是因为,给板卡安装驱动程序这个过程,实际上就是触发了DeviceAdd等函数,如果提前在这些函数内写好了打印语句,就会把这些信息打印出来,这种操作也间接证明了这个过程中哪些函数被执行了。

(6) 之后点击继续运行,会发现目标机又恢复了,出现下图情况,驱动程序已被装好。
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用
这是因为,前面的DeviceAdd等函数已经触发完了,回调函数也已经做出了应答。驱动程序进入了等待触发状态,等待下个事情的产生来触发某个回调函数。

(7) 接下来,我们需要调试的是应用程序对于驱动程序的IO请求,所以需要两者来配合使用。这个过程原理是在目标机运行应用程序,发出IO请求,触发驱动程序里的IoDeviceControl函数,从而实现对于驱动程序读写数据的功能。

先把之前在主机内编译产生的exe文件拷贝到目标机,接着在目标机运行CMD命令提示符,进入到exe文件的目录,执行exe文件,由此触发IoDeviceControl函数,如下图所示:
驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

可以看到,断点已经触发,目标机重新进入断点模式,在这个函数内的打印信息也可以显示出来了,表明执行exe这个过程成功触发了IoDevceControl函数,并作出了相关应答。如果想要一步一步调试的话,也可以使用逐语句或者逐过程更仔细地调试。

这时候在CMD里就可以看到应用程序内的打印信息也显示出来了,如下图所示:驱动程序(2) 驱动程序在内核态下的调试以及应用程序的配合使用

这里我主要做的测试是应用程序先输入一个数字,然后驱动程序返回该数字的大写,也就是charSample例子,可以看到确实返回了大写数字(手动忽略后面的乱码 Q_Q 后面我会再改进代码的),这个函数也就执行成功了。

到此,驱动程序的调试就完成了!下一篇将一些操作过程中出现的问题作出说明和解决方案!