如果驱动程序要和应用程序通信,那么要生成一个设备对象.
设备对象和分发函数构成了整个内核体系的基本框架.
设备对象可以在内核中暴露出来给应用层,应用层可以像操作文件一样操作内核驱动,所以称之为控制设备对象.
生成设备可以使用
NTSTATUS
IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,//驱动对象,可以直接从DriverEntry的参数中获得
_In_ ULONG DeviceExtensionSize,//设备扩展的大小
_In_opt_ PUNICODE_STRING DeviceName,//设备名,可以为空
_In_ DEVICE_TYPE DeviceType,//设备类型
_In_ ULONG DeviceCharacteristics,//一组设备属性
_In_ BOOLEAN Exclusive,//是否是独占设备,一般都不是独占,不过独占有时有好处,比如只允许自己的进程打开
_Outptr_result_nullonfailure_
_At_(*DeviceObject,
__drv_allocatesMem(Mem)
_When_((((_In_function_class_(DRIVER_INITIALIZE))
||(_In_function_class_(DRIVER_DISPATCH)))),
__drv_aliasesMem))
PDEVICE_OBJECT *DeviceObject//返回结果,如果函数执行成功,这里就是设备对象的指针.
);
注意这个函数生成的设备具有默认安全属性,结果是必须要有管理员权限的进程才能打开.如果要让任何用户都可以打开设备,可以用下面这个函数,但是作为商业软件而言显然不安全,这里只是为了使用方便.
NTSTATUS
WdmlibIoCreateDeviceSecure(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_In_ PCUNICODE_STRING DefaultSDDLString,//表示设备对象的安全设置
_In_opt_ LPCGUID DeviceClassGuid,//设备的GUID全球唯一标识
_Out_
_At_(*DeviceObject,
__drv_allocatesMem(Mem)
_When_((((_In_function_class_(DRIVER_INITIALIZE)) ||(_In_function_class_(DRIVER_DISPATCH)))),
__drv_aliasesMem)
_On_failure_(_Post_null_))
PDEVICE_OBJECT *DeviceObject
);
这个函数在 wdmsec.h ,参数和上面的一样,只是多个两个参数
//定义全局设备对象
PDEVICE_OBJECT gcdo = NULL;
//{06A16B65-7DA0-4A3F-9D9A-2679395D0D93}
//生成控制设备,生成符号连接 安全设置(任何用户都可以打开的,就是完全不安全的意思) 设备名称
UNICODE_STRING sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
UNICODE_STRING cdoname = RTL_CONSTANT_STRING(L"\\Device\\misaka_201701032346");
//生成一个控制设备
status = WdmlibIoCreateDeviceSecure(driver,0,&cdoname,FILE_DEVICE_UNKNOWN,FILE_DEVICE_SECURE_OPEN,FALSE,&sddl,(LPCGUID)&SLBKGUID_CLASS_MYCDO,&gcdo);
if(!NT_SUCCESS(status)){
return status;
}
应用层无法通过设备名字来打开设备对象的,所以要建立一个暴露给应用层的符号连接
符号连接就是记录一个字符串对应另一个字符串的简单结构
NTSTATUS
IoCreateSymbolicLink(
_In_ PUNICODE_STRING SymbolicLinkName,//符号连接名称
_In_ PUNICODE_STRING DeviceName//设备名称
);
一般函数都会返回成功,如果符号名称在系统存在(返回链接是windows全局存在的),则会返回失败.
所以为了防止冲突,稳妥的方法是使用 GUID 方式来访问设备.
//符号连接名称常量
#define CWK_CDO_SYB_NAME L"\\??\\misaka_201701032346"
//符号名称
UNICODE_STRING cdosyb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);
//删除符号
IoDeleteSymbolicLink(&cdosyb);
//生成符号
status = IoCreateSymbolicLink(&cdosyb,&cdoname);
if(!NT_SUCCESS(status)){
//失败,删除设备对象
IoDeleteDevice(gcdo);
return status;
}
控制设备的删除
在驱动卸载时应该删除符号连接,否则符号连接会一直存在,应用程序可能会尝试打开进行操作
操作方法是依次删除符号连接和控制设备
ASSERT(gcdo !== NULL);//<--这个可以不用,意思是假设设备对象存在,如果不存在当然就返回(错误)了.
//删除符号
IoDeleteSymbolicLink(&cdosyb);
IoDeleteDevice(gcdo);
分发函数
分发函数是一组用来处理发送给设备对象的请求的函数,由内核驱动开发者编写.
分发函数是设置在驱动对象上的,每个驱动都有自己的分发函数
windows IO管理器收到请求时,根据请求的目标来调用这个设备对象从属驱动对象对应的分发函数
不同分发函数处理不同的请求,也可以一个分发函数处理,自己区分
在这里先使用以下三种
打开 Create 访问设备对象前,需要先打开成功才能发送其他请求
关闭 Close 结束后要关闭,关闭后需要再次打开才能访问
设备控制 这个请求即可以输入(应用程序->内核)也可以输出(内核->应用程序)
标准分发函数如下:
NTSTATUS misakaDispatch(IN PDEVICE_OBJECT dev,IN PIRP irp)
dev:请求的目标对象 irp:请求数据结构指针
在 DriverEntry 中所有的分发函数都设置成一样的,MajorFunction 是函数指针数组
for(i=0;i
driver->MajorFunction[i] = misakaDispatch;
}
请求的处理
先获得栈空间,从栈空间指针中获得主功能号(每中请求都有),主功能号说明是什么请求
打开请求的主功能号是 IRP_MJ_CREATE
关闭请求的主功能号是 IRP_MJ_CLOAS
设备控制的主功能号是 IRP_MJ_DEVICE_CONTROL
请求当前栈空间使用 IoGetCurrentIrpStackLocation 获得
PIO_STACK_LOCATION
IoGetCurrentIrpStackLocation(
_In_ PIRP Irp
)
然后根据主功能号进行处理
NTSTATUS misakaDispatch(IN PDEVICE_OBJECT dev,IN PIRP irp){
//请求当前栈空间,获得主功能号
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
//...
NTSTATUS status = STATUS_SUCCESS;
//缓存长度
ULONG retlen = 0;
//判断请求是不是发给自己(因为一个驱动对象何以生成很多个设备对象?)
if(dev != gcdo){
//不处理
}else{
//判断请求的类型
if(irpsp->MajorFunction == IRP_MJ_CREATE || irpsp->MajorFunction == IRP_MJ_CLOSE){
}
if(irpsp->MajorFunction == IRP_MJ_DEVICE_CONTROL){
//首先获得功能号
//如果有输入缓冲区,必须获得输入缓冲区的指针和长度
//如果有输出缓冲区,必须获得输出缓冲区的指针和长度
//获得缓冲区,只有设备控制设置缓存方式请求才对,这个缓冲区是输入和输出共享的
PVOID buffer = irp->AssociatedIrp.SystemBuffer;
//获得输入缓冲区的长度
ULONG inlen = irpsp->Parameters.DeviceIoContorl.InputBufferLength;
//获得输出缓冲区的长度
ULONG outlen = irpsp->Parameters.DeviceIoContorl.OutputBufferLength;
//处理
switch(irpsp->Parameters.DeviceIoContorl.IoControlCode){
case 800:
if(buffer != NULL and inlen > 0 and outlen == 0){
DbgPrint((char *)buffer);
}
case 801:
default:
//未知的请求,参数错误等等
status = STATUS_INVALID_PARAMETER;
}
}
}
}
返回,在分发函数中处理
irp->IoStatus.Information = 返回的长度,要小于或等于缓冲区
irp->IoStatus.Status = status;//完成状态(一般情况)
IoCompleteRequest(irp,IO_NO_INCREMENT);//结束这个请求
return status;
应用程序的请求(实现随时发送字符串给内核驱动,或从内核驱动将内核驱动缓冲的字符读取出来)
打开设备 API CreateFile 文件的路径是符号链接的路径,在应用层中是以 \\.\ 开头的,且要转义
所以 #define CWK_DEV_SYM L"\\\\.\\misaka_201701032346"
HANDLE device = NULL;
//打开设备
device = CreateFile(CWK_DEV_SYM,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
if(device == INVALID_HANDLE_VALUE){
printf("打开驱动 failed\n");
return -1;
}else{
printf("打开驱动 successfully\n");
}
//关闭设备
CloseHandle(device);
//设备控制
#define CWK_DVC_SEND (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_WRITE_DATA)
CTL_CODE 是一个宏,SDK头文件提供,参数有4个
1.设备类型:因为生成的控制设备和任何硬件都没有关系,所以设置为 FILE_DEVICE_UNKNOWN(未知类型)
2.功能号核心数字,和其他参数"合成"功能号,0x0 - 0x7ff 已被微软保留,所以要大于 0x7ff,且不能大于 0xfff,不同的功能号根据这个数字区分
3.METHOD_BUFFERED说明收用缓存方式,输入和输出缓存会在用户和内核之间拷贝,比较简单和安全的一种方式
4.需要的权限,因为要写,设置为 FILE_WRITE_DATA
上面是发送功能号,下面定义接收功能号
#define CWK_DVC_RECV_STR (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_READ_DATA)
//发送请求过程
char *msg = {"hello misaka driver ,my name is app, this is message from"};
if(!DeviceIoControl(device,CWK_DVC_RECV_STR,msg,strlen(msg)+1,NULL,0,&东东的,0)){
//这里写失败的输出什么的
}
上面是我学习的结果,可能写的比较乱,可以直接看代码:
驱动的代码:
#include
#include
//定义全局设备对象
PDEVICE_OBJECT gcdo = NULL;
//{06A16B65-7DA0-4A3F-9D9A-2679395D0D93}
static const GUID SLBKGUID_CLASS_MYCDO = { 0x06A16B65, 0x7DA0, 0x4A3F, { 0x9D, 0x9A, 0x26, 0x79, 0x39, 0x5D, 0x0D, 0x93 } };
//生成控制设备,生成符号连接 安全设置(任何用户都可以打开的,就是完全不安全的意思) 设备名称
UNICODE_STRING sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
UNICODE_STRING cdoname = RTL_CONSTANT_STRING(L"\\Device\\misaka_201701032346");
//符号连接名称常量
#define CWK_CDO_SYB_NAME L"\\??\\misaka_201701032346"
//符号名称
UNICODE_STRING cdosyb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);
//声明函数
NTSTATUS misakaDispatch(IN PDEVICE_OBJECT dev, IN PIRP irp);
//设备控制 发送和接收
#define CWK_DVC_SEND (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_WRITE_DATA)
#define CWK_DVC_RECV_STR (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_READ_DATA)
VOID DriverUnload(PDRIVER_OBJECT driver){
//删除符号和设备对象
IoDeleteSymbolicLink(&cdosyb);
IoDeleteDevice(gcdo);
DbgPrint("misaka: uninstall driver and delete symbol and device success\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path){
//..
NTSTATUS status;
if (gcdo == NULL){
//生成一个控制设备
status = WdmlibIoCreateDeviceSecure(driver, 0, &cdoname, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &sddl, (LPCGUID)&SLBKGUID_CLASS_MYCDO, &gcdo);
if (!NT_SUCCESS(status)){
return status;
}
//删除符号
IoDeleteSymbolicLink(&cdosyb);
//生成符号
status = IoCreateSymbolicLink(&cdosyb, &cdoname);
if (!NT_SUCCESS(status)){
//失败,删除设备对象
IoDeleteDevice(gcdo);
return status;
}
}
driver->DriverUnload = DriverUnload;
//所有的分发函数都是同一个
ULONG i;
for (i = 0; i
driver->MajorFunction[i] = misakaDispatch;
}
DbgPrint("misaka: driver open success!!\r\n");
return STATUS_SUCCESS;
}
//然后根据主功能号进行处理
NTSTATUS misakaDispatch(IN PDEVICE_OBJECT dev, IN PIRP irp){
//请求当前栈空间(Irp),获得主功能号
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
//...
NTSTATUS status = STATUS_SUCCESS;
//缓存长度
ULONG retlen = 0;
//判断请求是不是发给自己(因为一个驱动对象何以生成很多个设备对象?)
if (dev != gcdo){
//不处理
DbgPrint("driver error 1\r\n");
}else{
//判断请求的类型
if (irpsp->MajorFunction == IRP_MJ_CREATE){
DbgPrint("open driver\r\n");
status = STATUS_SUCCESS;
}
if (irpsp->MajorFunction == IRP_MJ_CLOSE){
DbgPrint("close driver\r\n");
status = STATUS_SUCCESS;
}
if (irpsp->MajorFunction == IRP_MJ_DEVICE_CONTROL){
DbgPrint("control number %d\r\n", irpsp->Parameters.DeviceIoControl.IoControlCode);
//首先获得功能号
//如果有输入缓冲区,必须获得输入缓冲区的指针和长度
//如果有输出缓冲区,必须获得输出缓冲区的指针和长度
//获得缓冲区,只有设备控制设置缓存方式请求才对,这个缓冲区是输入和输出共享的
PVOID buffer = irp->AssociatedIrp.SystemBuffer;
//获得输入缓冲区的长度
ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;
//获得输出缓冲区的长度
ULONG outlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;
//处理
switch (irpsp->Parameters.DeviceIoControl.IoControlCode){
case CWK_DVC_RECV_STR:
if (buffer != NULL && inlen > 0 && outlen == 0){
DbgPrint((char *)buffer);
status = STATUS_SUCCESS;
break;
}
case 800:
default:
//未知的请求,参数错误等等
status = STATUS_INVALID_PARAMETER;
}
}//endcontrol
}//endif
//返回, 在分发函数中处理
irp->IoStatus.Information = retlen;
irp->IoStatus.Status = status;//完成状态(一般情况)
IoCompleteRequest(irp, IO_NO_INCREMENT);//结束这个请求
DbgPrint("cloase and status ? %d\r\n",status);
return status;
}
应用层的代码:
#include
#include
//符号路径
#define CWK_DEV_SYM L"\\\\.\\misaka_201701032346"
HANDLE device = NULL;
//设备控制 发送和接收
#define CWK_DVC_SEND (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_WRITE_DATA)
#define CWK_DVC_RECV_STR (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_READ_DATA)
int main(int argc, char* argv[]){
//打开设备
device = CreateFile(CWK_DEV_SYM, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
if (device == INVALID_HANDLE_VALUE){
printf("打开驱动 failed\r\n");
return -1;
}else{
printf("打开驱动 successfully\r\n");
//发送请求过程
char *msg = { "hello misaka driver ,my name is app, this is message from\r\n" };
//实际返回的字节数
DWORD dwOutput;
if (!DeviceIoControl(device, CWK_DVC_RECV_STR, msg, strlen(msg) + 1, NULL, 0, &dwOutput, 0)){
//这里写失败的输出什么的
printf("发送请求失败,%d %p %d %d\r\n", GetLastError(), device, CWK_DVC_RECV_STR, dwOutput);
}else{
printf("发送请求成功\r\n");
}
//关闭设备
CloseHandle(device);
}
return 0;
}
结果:
应用程序输出
打开驱动 successfully
发送请求成功
驱动程序输出
open driver
control number 2252804
hello misaka driver ,my name is app, this is message from <--重要
close driver
misaka: uninstall driver and delete symbol and device success
遇到了许多坑!最坑的是 switch 的 break !!!!!!! 我说为什么一直输出 87 呢!
P.S. 有些输出是测试用得,可以去掉!