本次实现一个应用与驱动通信的功能,具体信息如下:
(1)应用自定义两个字符串缓冲区用以发送和接收数据;
(2)驱动生成一个控制设备,自定义一个链表结构用以保存从应用接收到的数据;
(3)应用向驱动发送一个字符串,驱动接收到字符串并将其存入链表中;
(4)应用请求接收驱动中的字符串,驱动从链表中取出一个节点,将数据传给应用,应用将其打印出来。
因为应用层的代码比较简单,先从应用层开始讲起,还有一个好处就是应用层的要求明确了,相应的内核驱动代码的编写也就明确了。另外这里要先说明一下编程环境,我的是VS2013+WDK8.1+VirtualBox 4.3.12,环境搭建请自行入坑查找,如果有闲心后面会出一篇学习笔记。
应用编程框架使用win32控制台程序框架,操作步骤如下:
(1)打开控制设备;
(2)进行设备控制请求;
(3)关闭设备。
#include "stdafx.h"
#include <windows.h>
/**
**自定义控制码,FILE_DEVICE_UNKNOWN表示不是硬件驱动
**可以使用0x800~0xfff内的值做控制码的一个参数
**METHOD_BUFFERED使用缓冲方式
**最后一个参数表示读或写请求
**/
#define SEND_STR_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_WRITE_DATA)
#define RECV_STR_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x802,METHOD_BUFFERED,FILE_READ_DATA)
#define STR_MAX_LEN 512
//一个内存清零初始化函数
void initoutbuf(UCHAR outputbuf[STR_MAX_LEN])
{
memset(outputbuf, 0, sizeof(char) * STR_MAX_LEN);
return;
}
//入口函数
int _tmain(int argc, _TCHAR* argv[])
{
/**
**第一个参数是以“\\\\.\\”开始的设备符号链接名,这个名字和驱动中建立符号链接时使用的名字一致
**其他参数可以直接照搬
**/
HANDLE hDevice = CreateFile(L"\\\\.\\LINK_NAME", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
if (hDevice == INVALID_HANDLE_VALUE)
;//获取驱动句柄失败,作相应处理
else
;//获取驱动句柄成功
DWORD ret_len = 0;//存放设备返回信息实际长度的值
char *msg = "This is a message from app.\n";
UCHAR outputbuf[STR_MAX_LEN];//接收设备返回数据的缓冲区
initoutbuf(outputbuf);//初始化缓冲区
/**
**发送字符串到设备,第一个参数表示打开设备返回的句柄
**第二个参数为自定义的控制码
**接下来的四个参数分别为输入缓冲区首地址,输入缓冲区大小,输出缓冲区首地址,输出缓冲区大小
**剩下的三个参数照搬
**/
if (DeviceIoControl(hDevice, SEND_STR_CODE, msg, strlen(msg) + 1, NULL, 0, &ret_len, 0))
;//成功将字符串传入设备,并保存在链表中
else
;//操作失败,相应处理
/**
**这里需要注意一点,因为outputbuf是一个公用的输出缓冲区,所以每次使用前应该将其中的旧数据清除
**否则上一次的数据可能会被此次的操作接收到
**/
initoutbuf(outputbuf);
if (DeviceIoControl(hDevice, RECV_STR_CODE, NULL, 0, outputbuf, STR_MAX_LEN, &ret_len, 0))
;//成功取出数据
else
;//取出数据失败,相应处理
CloseHandle(hDevice);
return 0;
}
自此一个简单的测试应用程序就完成了。接下来是控制设备的生成,简要步骤如下:
(1)创建控制设备,建立符号链接;
(2)设置分发函数和卸载函数;
(3)(分发函数中)获取主功能号,获取控制码;
(4)(分发函数中)根据主功能号和控制码进行相应的操作;
(5)(分发函数中)返回信息的处理;
(6)(卸载函数中)释放分配的内存;
(7)(卸载函数中)删除符号链接;
(8)(卸载函数中)删除设备。
具体代码如下:
#include <ntddk.h>
//定义控制码
#define SEND_STR_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_WRITE_DATA)
#define RECV_STR_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x802,METHOD_BUFFERED,FILE_READ_DATA)
#define STR_MAX_LEN 512//定义链表缓冲区最大长度
#define LINK_NAME L"\\??\\LINK_NAME" //符号链接名,以\\??\\开头
#define DEV_NAME L"\\Device\\DEVICE_NAME" //设备名,以\\Device\\开头
//定义一个双向链表节点结构
typedef struct {
LIST_ENTRY list_entry;
char buf[STR_MAX_LEN];
}STR_NODE, *PSTR_NODE;
//定义全局变量
PDEVICE_OBJECT pc_do;//控制设备指针
LIST_ENTRY head_list;//链表头
//分发函数
NTSTATUS Dispatch(IN PDEVICE_OBJECT pdev, IN PIRP pirp)
{
PSTR_NODE pstr_node = NULL;//字符串结构指针
NTSTATUS status = STATUS_INVALID_PARAMETER;
ULONG ret_len=0;
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(pirp);//得到当前栈指针
ULONG mf = irpsp->MajorFunction;//存放主功能号
ULONG ctlcode;//存放控制码
if (pdev != pc_do)
return status;
switch (mf)
{
case IRP_MJ_CREATE:
status = STATUS_SUCCESS;
break;
case IRP_MJ_CLOSE:
status = STATUS_SUCCESS;
break;
case IRP_MJ_DEVICE_CONTROL:
ctlcode = irpsp->Parameters.DeviceIoControl.IoControlCode;//获取控制码
ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;//获取输入缓冲区长度
ULONG outlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;//获取输出缓冲区长度
PVOID buffer = pirp->AssociatedIrp.SystemBuffer;//获取缓冲区指针
switch (ctlcode)
{
case SEND_STR_CODE:
/**
**在进行相应操作前,做安全性检查
**/
if (buffer == NULL)
{
break;
}
if (inlen>STR_MAX_LEN)
{
break;
}
if (strnlen((char*)buffer, inlen) == inlen)//如果输入的字符串实际长度大于inlen(实际接收的字符串不以空结束),或输入的字符串是'\0',拒绝操作
{
break;
}
//为输入的字符串分配内存
pstr_node = (PSTR_NODE)ExAllocatePoolWithTag(NonPagedPool, sizeof(STR_NODE), 'tag2');
if (pstr_node == NULL)
{
break;
}
memset(pstr_node->buf, 0, sizeof(char)*STR_MAX_LEN);
strcpy(pstr_node->buf, (char*)buffer);
//插入链表
InsertHeadList(&head_list, &pstr_node->list_entry);
if (IsListEmpty(&head_list))
break;
else status = STATUS_SUCCESS;
break;
case RECV_STR_CODE:
if (buffer == NULL)
{
break;
}
if (outlen<STR_MAX_LEN)
{
break;
}
if (IsListEmpty(&head_list))
{
break;
}
//从链表中取出一个节点
PLIST_ENTRY plist_entry = RemoveTailList(&head_list);
if (plist_entry == NULL)
{
break;
}
else
{
pstr_node = CONTAINING_RECORD(plist_entry, STR_NODE, list_entry);
strcpy((char*)buffer, pstr_node->buf);
ret_len = strnlen(buffer,STR_MAX_LEN);
ExFreePool(pstr_node);//释放分配的内存
status = STATUS_SUCCESS;
break;
}
default:
break;
}
break;
default:
break;
}
pirp->IoStatus.Status = status;
pirp->IoStatus.Information = ret_len;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
return status;
}
//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pdriver)
{
PSTR_NODE pstr_node;//链表结构指针
UNICODE_STRING SybName;//符号链接名
RtlInitUnicodeString(&SybName, LINK_NAME);
//释放分配的内存
while (TRUE)
{
pstr_node = (PSTR_NODE)RemoveHeadList(&head_list);
if (pstr_node != NULL)
ExFreePool(pstr_node);
else break;
}
//删除符号链接
IoDeleteSymbolicLink(&SybName);
//删除设备
IoDeleteDevice(pc_do);
return;
}
//入口函数
#pragma code_seg("INIT")
NTSTATUS DriverEntry(PDRIVER_OBJECT pdriver, PUNICODE_STRING preg_path)
{
NTSTATUS status;
UNICODE_STRING devname;//存放控制设备名
UNICODE_STRING linkname;//存放符号链接名
RtlInitUnicodeString(&devname, DEV_NAME);//设备名
RtlInitUnicodeString(&linkname, LINK_NAME);//符号链接名
InitializeListHead(&head_list);//初始化链表头
//创建设备
status = IoCreateDevice(pdriver, 0, &devname, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pc_do);
if (!NT_SUCCESS(status))
{
return status;
}
//建立符号链接
IoDeleteSymbolicLink(&linkname);
status = IoCreateSymbolicLink(&linkname, &devname);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pc_do);
return status;
}
pdriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = Dispatch;
pdriver->MajorFunction[IRP_MJ_CREATE] = Dispatch;
pdriver->MajorFunction[IRP_MJ_CLOSE] = Dispatch;
//卸载函数设置
pdriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
驱动中的链表操作注意事项见另一篇学习笔记LIST_ENTRY,驱动代码编写框架为KMDF,虽然本代码使用的函数都是WDM框架下的,我也试着在WDM框架下编写,后来发现WDM中的inf文件并不是自动生成的,需要自己填写关键数据,而里面的内容我看上去觉得有点复杂,所以最后使用的KMDF框架,它不但支持WDM框架下的函数,inf文件也是自动生成的,敲完代码直接生成项目就可以了。
采用缓冲方式进行通讯需要注意缓冲区是公用的,所以在每次数据传输时都要注意缓冲区的内容不应有上一次传输的数据残留,我的办法就是每次都将缓冲区的内容清零。
应用层的测试程序比较简单,本次我随便写了一个,并没有实测,不保证完全正确。驱动程序是我调试后的,win8 debug 正常运行,因为是直接把代码拷贝进来的,注释不很多,如有不明白的地方可以评论提出,如果我会的话,会做详细解释。
最后说明一点,虚拟机上最好运行release版本的应用层测试程序,如果是debug版本的话,可能会运行失败。