第二章 开始内核开发
在本章处理了内核驱动开发的一些启动和运行的基础需求。在该章中,有将安装必要的工具和写一个能够加载和卸载的基础驱动。
在该章中:
- 安装必要的工具
- 创建一个驱动项目
- 驱动入口与卸载例程
- 部署驱动
- 简单的追踪
安装工具
在早些年(2012年之前),开发和构建驱动的过程包括使用来自设备驱动程序工具包(DDK)的专用构建工具,开发者并没有有一个集成开发环境的经验像开发用户程序那样。这儿有一些周转办法,但是他们是不完美的也没有官方支持。幸运的是,自从Visual Studio 2012 和 Windows Driver Kit 8 之后,微软开始官方地使用Visual Studio构建驱动,不用再需要使用一个分离的编译器和构建工具。
为了能够开发驱动,下列工具必须要安装:
- 最新版本的Visual Studio 2017 or 2019,确保在安装过程中选择了C++开发工具。在写这个的时候,Visual Studio 2019已经发布了,可以被用于驱动开发。注意包括免费的社区版本,任何SKU都可以做。
- Windows 10 SDK(最好是最新的),确保在安装期间,选择Windows调试工具。
- Windows 10 Driver Kit(WDK)。应该是最新的,但是要确保你也安装了Visual Studio的工程木仔在最后的标准安装中。
- Sysinternals 工具,它们是非常重要的,可以免费下载从 http://www.sysinternals.com,点击网页左边的 Sysinternals Suite,下载Sysinternals Suite Zip,解压全部的文件夹,这些工具就准备好运行了。
【快速确保WDK模板被安装正确的方式是打开Visual Studio ,选择New Project,找到驱动项目,比如"Empty WDM Driver"】
开发驱动程序
当上述安装就绪之后,一个驱动项目就可以被创建了。在这一节,你讲选择一个“WDM Empty Driver”模板。图2-1展示了在Visual Studio 2017这个类型驱动的新项目对话框的样子。图2-2展示了在Visual Studio 2019最初是的引导。在两幅图中,这项目都被命名“Sample”。
一旦这项目被创建,这解决方案窗口展示了一个单独的名为Sample.inf文件。在这个例子中,你不需要这个文件,因此你可以删除它。
现在是时候增加源文件了,在解决方案窗口中右键Source Files,在菜单中选择 Add / New Item..选择一个C++源文件并且命名它为Sample.cpp,点击来创建它。
驱动入口与卸载例程
每一个驱动有一个名为DriverEntry的默认入口。这可以被认为驱动的main - 相当于一个用户模式程序的类main。这个函数被一个系统线程在IRPL PASSIVE_LEVEL(0)级调用(IRQLs将在第八章详细讨论)。
DriverEntry有一个前缀声明,如下:
_In_注释是源(代码)注释语言(SAL)的一部分。这些注释对编译器来说死不可见的,但是为人和静态分析工具提供有用的元数据。我们尽可能地使用它来提高代码的清晰度。
一个最小的DriverEntry例程仅仅返回一个成功状态,其如下:
这代码还没有编译,首先,你应该包含一个头文件,它包含着DirverEntry中出现的类型定义,可以使用:
现在代码有更好的编译机会了,但是仍然会失败。原因是因为编译器默认地把警告作为错误。这函数没有使用它所使用的参数。移除这个“把错误作为警告”是不推荐的,因为有些警告错误可能混淆在里面。这些警告可以通过完全移除参数名称来解决(或者注释掉它们),这是对于C++文件中是很好的。另外一个,比较经典的解决方案是使用UNREFERENCED_PARAMETER宏:
事实证明,这个宏实际上只通过将它的值写成原样来引用给定的参数,这会关闭编译器,使参数“被引用”。
现在可以很好的编译建立这个工程了,但是会引发一个链接错误。这DirverEntry函数必须使用C-linkage,而不是由C++编译的默认值。这儿有一个成功编译DirverEntry函数的最终版本:
在某些情况下,驱动可能被卸载,这时在DriverEntry函数所做的事情必须被撤销。如果失败的话会造成一次泄漏,内核将在下次重启前无法清除。在被卸载之前,驱动有一个在内存的卸载例程自动被调用。它的指针一定要使用驱动对象成员DriverUnload来设置:
这个卸载例程接受驱动对象(与DriverEntry传递的是一致的),并且返回Void。在我们例子中,就资源分配而言并没有做什么事情在DirverEntry,因此卸载例程中什么页不需要做,因此我们现在这里直接让它为空:
下面指出这个完整的驱动源文件:
部署驱动
现在我们成功地编译了 sample.sys 驱动文件,让我们在系统中安装并加载它。一般来说,你将在虚拟机中安装和加载一个驱动,避免了造成虚拟机崩溃的风险。您可以自由地这样做,或者冒一下这个最低限度驱动程序的风险。
安装一个驱动软件,就想安装一个用户模式的服务一样,需要正确地调用CreateService Api或者使用现有的工具。一个用于这个的比较知名的工具是Sc.exe,一个控制服务的Windows内置工具。我们将使用这个工具来安装和加载驱动。注意这个驱动的安装和加载是一个高权限操作,一般必须要用管理员权限。
打开一个高权限的命令行窗口并且输入下面的内容(最后一部分应该是你SYS文件所存储的路径):
注意这里没没有空白在type和=之间,在=与kernel之间存在空白;第二个参数是相同的。
如果一切进行顺利,其会输出成功。你可以打开注册表编辑器(regedit.exe)来测试安装是否成功,然后在HKLM\System\CurrentControlSet\Servuces\Sample中搜索驱动。图2-3展示了命令输入之后展示了一个注册表的快照。
为了加载驱动,我们可以再次使用Sc.exe工具,这次我们使用start选项,这调用StartService Api来加载驱动(与加载驱动相同的API)。然而,在64位操作系统中必须使用签名,因此一般情况下下面的命令行会失败:
因为在开发期间为驱动签名是非常不方便的(可能你没有一个正确的证书),一个好的选择是将系统进入测试前面模式中。在这种模式下,未前面的驱动也可以被正常运行。
使用一个高权限的命令行窗口,驱动测试可以像下面这样开启:
不幸的是,这个命令需要重启来会生效,一旦重启,这个之前的启动命令应该会成功。
【如果你没有在Windows10在安全模式下,改变测试签名模式将会失败。这个设置被安全启动所保护(也被本地内核调试器所保护)。由于IT政策和其他原因,如果你不能通过BIOS设置来进入安全启动,你最好在虚拟机中测试一下。】
如果你想在Windows10的几期上测试,这儿有另外一个你需要指定的设置。在这里,你必须在项目属性对话框中设置正确的目标OS版本,如图2-4所展示的。注意我已经选择全配置和全平台,因此当其切换配置(Debug/Release)或者平台(x86/x64/ARM/ARM64),这设置会被保存。
一旦测试签名模式开启,这个驱动被加载,你可以看到如下输出:
这意味着一切良好,这驱动被加载。为了确认,我们可以打开Process Explorer找到Sample.Sys驱动映射文件。图2-5展示了这个sample驱动映像加载进系统空间的有关细节。
我们可以使用下列的命令来卸载驱动:
在幕后,Sc.exe调用SERVICE_CONTROL_STOP值的ControlService Api 来卸载驱动,这会操作卸载例程被调用,在这次中它并没有做什么事情。你可以通过再次查看Process Explorer来确认这驱动的确被卸载;这个驱动映像应该不在这儿。
简单的追踪
我们如何确保其真正执行了DriverEntry和Unload例程?让我们为这些函数添加一些线索。驱动可以使用KdPrint宏来输出printf-style文本,其可以使用内核调试器和其他内核工具查看。KdPrint是DbgPrint内核Api的宏,其只有在Debug模式构建下才会被编译。
这面代码是使用了KdPirnt来追踪代码执行的最新版本:
注意当使用KdPrint时要使用双括号,这是必需的,因为KdPrint是一个宏,但是显然可以接受任意数量的参数,比如printf。由于宏不能接收可变数量的参数,所以使用编译器技巧来调用实际的DbgPrint函数。
当这些都完成之后,我们将再一次加载驱动然后查看信息,我们使用在第四章中的一个内核调试器,但是现在我们使用一个在Systenternals中有用的工具 - DebugView. 在运行DebugView之前,你需要做一些准备。首先,开启Windows Vista,DbgPrint输出无法除非在注册表中有一个值,你必须在 HKML\SYSTEM\CurrentControlSet\Control\SessionManager(这个键一般不存在)下添加一个名为 Debug Print Filter的键。在这个新键中,添加一个名为DEFAULT的双字的值(并不是存在于任何键的默认值),设置它的值为8(从技术上来说,这个值的第3位将被设置),图2-6显示了在RegEdit中的设置。不幸的是,你必须重启来让这个设置生效。
一旦这个设置被应用,以管理员程序运行DebugView(DebugView.exe)。在Options菜单中,确保Capture Kernel 被选择(或按Ctrl+K键)。你可以安全的取消选择Capture Win32 和 Capture Global Win32 为了各个进程中的输出的信息不会扰乱显示。
构建驱动(如果你还没有的话),现在你使用高权限的命令行窗口再一次加载驱动(sc start sample)。你应该在DebugView中看到如图2-7的输出信息。如果你卸载驱动,你应该按到另一条显示的信息,因为卸载例程被调用。(第三行来自另一个驱动,我们的sample驱动什么也没做)。