注意事项: 大部分widnows驱动程序都是内核驱动(Kernel Driver),所以本笔记不分”驱动程序”与”内核编程”,也不区分”内核模块”(Kernel Module)、“驱动程序”(Driver)与”内核程序”,这些词汇统一指编译出的扩展名为”.sys”的可执行文件(并非强制扩展名为.sys),也不区分”应用层”与”用户态”。
驱动分类:
笔记中如不是特殊说明,一般都为NT驱动。
打开 Visual Studio 2019 选择”创建新项目“,选择 “Empty WDM Driver”
键入工程名字”MyDriver”
点击”创建“
创建成功。
然后在菜单中找到“项目§”→“添加新项”,在弹出的对话框中选择“C++文件(.cpp)”,在下方的名称(N)中输入“First.c”,最后点击“添加”。
现在已经可以看到工程内存在一个空白的First.c文件,开发者可以往这个空白文件中添加内核代码,但在添加代码前,需要包含驱动开发的头文件ntddk.h。
内核驱动入口函数 DriverEntry
原型如下
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
参数介绍: DriverObject,表示一个驱动对象的指针,指向操作系统在内存中为该驱动分配的一个类型为PDRIVER_OBJECT数据结构,用于记录该驱动的详细信息。 RegistryPath,表示当前驱动所对应的驱动对象指针。是一个类型为UNCODE_STRING的指针,表示当前驱动所对应的注册表位置。UNICODE_STRING是内核中表示字符串的结构体,定义如下:
typedef struct _UNICODE_STRING {
USHORT Length; //表示Buffer所指向缓冲区中字符串的长度,单位为字节。
USHORT MaximumLength;//表示Buffer所执行的缓冲区的总空间大小,单位为字节。
_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;//指向一个UNICODE类型的字符串缓冲区
} UNICODE_STRING;
typedef UNICODE_STRING* PUNICODE_STRING;
注意:Buffer指向的字符串,并不要求以 ‘\0’ 作为结束,在大多数情况下,Buffer指向的字符串没有以 ‘\0’ 结尾。
RegistryPath 为 PUNICODE_STRING 类型的参数,表示的是这个驱动所对应注册表的位置,这是因为内核驱动时作为 Windows 系统服务 (Service) 存在的,Widnows系统又众多服务,如果从服务运行的环境来区分,服务分为用户态服务、内核态服务,统称为 “服务(Service)”,不同服务通过服务的名字来识别,服务的名称简称 “服务名”。
在安装操作系统后,系统会内置一系列服务,这些服务统称为系统服务,称为第三幅服务。开发者可以开发属于自己的服务,称为第三方服务。一个驱动SYS文件需要运行(加载到内核中),首先需要把这个驱动文件注册(创建)成一个服务(第三方服务),注册成功后,系统会把该服务信息写入到注册表HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Services
下,以服务的名字作为一个注册表的键名。
入口函数DriverEntry的返回值类型为NTSTATUS,定义如下
typedef _Return_type_success_(return >= 0) LONG NTSTATUS;
,由此可见返回值实际上是一个LONG类型。
Windows操作系统规定DriverEntry返回STATUS_SUCCESS表示成功,返回其他值表示失败。
内核驱动作为Windows服务运行,在执行具体代码前,驱动SYS文件首先会被映射到内核地址空间,作为内核的一个驱动模块(MODULE),接着系统对这个驱动模块执行导入表初始化、修正重定位表中对应的数据偏移等操作,最后系统会调用该驱动模块的DriverEntry入口函数,如果这个入口函数返回STATUS_SUCCESS,系统认为这个驱动初始化成功;如果这个入口函数返回除STATUS_SUCCESS以外的其他值,系统认为驱动初始化失败,系统执行一系列的清理工作,并把驱动模块从内核空间中移除,从用户态角度看,就是服务启动失败。
实例:
#include "ntddk.h"
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
if (DriverObject != NULL)
{
DbgPrint("[%ws]Driver Upload,Driver Object Address:%p", __FUNCTIONW__, DriverObject);
}
return;
}
/// @brief 入口函数
/// @param DriverObject 一个驱动对象的指针
/// @param RegistryPath 当前驱动对应的注册表位置
/// @code
/// typedef struct _UNICODE_STRING {
/// USHORT Length;
/// USHORT MaximumLength;
/// #ifdef MIDL_PASS
/// [size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT* Buffer;
/// #else // MIDL_PASS
/// _Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;
/// #endif // MIDL_PASS
/// } UNICODE_STRING;
/// typedef UNICODE_STRING* PUNICODE_STRING;
/// @encode
/// @return 返回一个NTSTATUS值,即 STATUS_SUCCESS 或适当的错误状态
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
// DbgPring函数是WDK提供的API,类似c语言的printf函数
// %ws表示打印一个以'\0'结尾的UNICODE字符串
// __FUNCTIONW__是以'\0' 结束的UNICODE字符串,表示当前函数的名字,对应格式化字符串中的%ws
DbgPrint("[%ws]Hello Kernel World\n", __FUNCTIONW__);
// 判断两个参数是否为空,不为空打印两个参数值
if (RegistryPath != NULL)
{
// RegistryPath类型为UNICODE_STRING类型,打印该类字符串需要使用%wZ格式化参数
DbgPrint("[%ws]Driver RegistryPath:%wZ\n", __FUNCTIONW__, RegistryPath);
}
if (DriverObject != NULL)
{
DbgPrint("[%ws]Driver Object Address:%p\n", __FUNCTIONW__, DriverObject);
// 服务停止回调
DriverObject->DriverUnload = DriverUnload;
}
return STATUS_SUCCESS;
}
注意:不能使用一下方式打印UNICODE_STRING类型的变量,例如不能使用下面的方式打印上面实例中的RegistryPath参数:
DbgPring("[%ws] Driver RegistryPath: %ws\n",__FUNCTIONW__,RegistryPath->Buffer);
因为UNICODE_STRING结构体内Buffer指向的字符串结尾不一定有’\0’,而对于 %ws 类型来说,会一直寻找Buffer字符串的’\0’,在这种情况下,行为是不可预料的。
DriverEntry函数除了打印一系列信息,还有一个重要的操作:
DriverObject->DriverUnload = Unload;
DriverUnload是DriverObject结构体中的一个成员,DriverObject表示当前的驱动对象,记录了当前驱动的详细信息,DriverUnload为驱动对象结构体内的一个函数指针。
驱动是作为服务方式运行的,服务可以被启动,也可以被停止,停止的实质就是系统把该驱动模块对应在内核地址空间中的代码以及数据移除。当一个内核驱动被要求停止时,DriverObject→DriverUnload指向的函数就会被系统调用,开发者可以在这个函数中执行一些清理相关的工作。
DriverUnload函数非常重要,但重要并不等于必须,DriverUnload函数是可选的,开发者可以不提供DriverUnload函数,这样做的结果是该驱动不支持停止,也就是说,只要开发者不提供DriverUnload函数,这个驱动对应的服务一旦启动后,再也无法停止。该特性被很多安全软件利用,刻意不提供DriverUnload函数,避免驱动被恶意停止。
DriverEntry函数执行一系列操作后,最后返回STATUS_SUCCESS,表示驱动初始化成功。DriverEntry函数返回除STATUS_SUCCESS以外的其他值时,表示驱动初始化失败,系统发现驱动初始化失败会移除内核地址空间的驱动代码与数据,这个操作看起来与驱动服务的停止非常类似,但是请读者注意:驱动初始化失败不会触发DriverUnload函数的调用,DriverUnload只有在驱动服务成功启动(初始化)后,被要求停止时才会触发。
点击生成,如果出现以下错误,则删除MyDriver.inf文件即可,Ctrl/Command + ; 输入MyDriver.inf查找文件,右键点中该文件,选择 “删除”。
编译成功后,读者可以在工程的文件夹目录中找到一个x64的文件夹,在x64目录下找到Debug文件夹,该文件夹下的FirstDriver.sys文件就是编译好的驱动文件,对应的还有FirstDriver.pdb文件,FirstDriver.pdb文件包含了驱动相应的调试信息,如结构体定义、函数名等,在驱动调试中非常重要。该文件夹下还有其他文件,如FirstDriver.cer,可以暂时忽略。
通过WDK自带的编译程序来进行编译。在编译前,还需要准备一个Sources文件,Sources文件实际上是一个文本文件,文件名字为Sources,该文件主要描述了需要编译的C文件列表,以及需要链接的LIB库等信息,针对本例,Sources文件的内容编写如下:
TARGETNAME=FirstDriver
TARGETTYPE=DRIVER
SOURCES=First.c
TARGETNAME表示编译出来的目标名字;TARGETTYPE表示编译出来的二进制类型是DRIVER,而不是LIB或其他;SOURCES后面是需要编译的C文件,本例中只有一个First.c,如果有多个C文件需要编译,可以写成:
SOURCES=First.c \
Second.c
在开始菜单中找到Windows Driver Kit→选择Windows 7的x64 Checked Build Environment。在WDK编译工具的命令行中,使用cd命令进入First.c文件所在的目录,然后在命令行中输入“build”命令开始编译。注意,输入的build命令不带引号。
在编译过程中会报以下错误:
这是因为老版本的WDK不支持__FUNCTIONW__标识。这里给读者一个建议,如果编写的驱动代码需要支持不同版本的WDK编译,请不要使用新版本WDK独有的特性。针对上面的问题,请读者修改代码,去掉__FUNCTIONW__以及配套的%ws,或者简单地把DbgPrint注释掉,当然,更好的做法是根据不同WDK版本进行条件编译。修改后重新编译。成功生成驱动文件。
成功编译后,在First.c的文件夹内会生成一个objfre_wxp_x86\i386文件夹,成功编译后,在First.c的文件夹内会生成一个objchk_win7_amd64文件夹,这个文件夹的命名包含了驱动编译的版本信息,其中chk表示Debug版本,Win7表示使用的是Windows 7版本WDK编译环境,amd64表示64位驱动程序,在objchk_win7_amd64\amd64\文件夹下,生成了FirstDriver.sys文件以及FirstDriver.pdb文件。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/184449.html原文链接:https://javaforall.cn