今天好不容易晚上抽空写点东西,好累啊!先整这些当是笔记了
1.学会使用向导
很多人不喜欢向导,可是一个从应用开发转做驱动开发的人可能更容易接受这种方式,但在今天我不打算使用过多的向导,而是手工写驱动代码,只是使用了DriverWorks的包装类,使用MFC可以反过来使我的SDK技术长进不少,那同样的使用DriverWorks我相信同样可以使我们的DDK技术得到锻炼.
首先使用DDK Build Settings启动Visual C++ 6.0
1.1 点击DriverStudio工具栏上的Launch DriverWizard按钮,选择New Project中的"Start a New Driver Project"
1.2 向导会自动选中DDK Source/makefile和Visual Studio 6,在Project Name里输入"hello"后点下一步
1.3 project type页中选择"Kernel Mode Service"点下一步
1.4 IRP Handlers页中把所有自动勾上的请求都去掉,因为我们要手工写DriverWorks代码以加深印象.点下一步
1.5 在Additional页里把自动勾上的Generate SoftICE NMS file in custom build step去掉.点下一步
1.6 预览工程,并点击完成:
Project Summary For kruglinski
Project Name = hello
Project Location = D:\Workspace\hello
Project Type = NT
1.8 空的工程框架就会生成完毕.
此时直接编译会出错,也许是DriverStudio的一个Bug,只要把sources中的:
TARGETLIBS=$(DDK_LIB_PATH)\ntstrsafe.lib $(DDK_LIB_PATH)\csq.lib
这一行去掉就可以编译通过了,让我们编试一下,一定感觉很不错吧!
2.拨开云雾见日出
2.1 现在我们要做的是清理掉所有会让我们眼花的东东(一大堆向导生成的代码),只留下编译环境,我喜欢在Visual Studio里直接编译的感觉,很讨厌总是看到那个黑黑的Build Console.切换到FileView初图,把除了makefile和sources文件以外的其它文件全部删除,然后我们只复制hello.dsp,sources和makefile三个文件到一个单独的文件夹helloworld里.选择File-Close Workspace关闭刚向导出来的工程,再把copy出来的hello.dsp拖到Visual Studio 6中打开(这时会自动生成一个hello.dsw).
2.2 现在要正式开工了,按Ctrl+N在Files页选择C++ Source File,在File栏里输入hello.cpp
在hello.cpp里首先要#include "vdw.h"
,因为我们要用的DriverWorks包装类的头文件都由该文件包括.
这里需要注意的是在#include "vdw.h"
前应该加入一句#define VDW_MAIN因为DriverWorks的帮助文件里这样说(懒得译了,将就着看吧!!)
Symbol VDW_MAIN must be #define'd in exactly one source module of the driver, generally the module that implements the driver class. If, as is true for most drivers, there is more than one source module, do not #define VDW_MAIN in any other modules. The definition must precede #include
. The purpose of this symbol is to control the inclusion of certain source files in the DriverWorks library. These source files must be compiled in the context of the driver being built.
2.3 我们定义一个驱动对象类,它派生自KDriver,必须提供一个纯虚函数DriverEntry的实现:
class MyDriver:public KDriver
{
SAFE_DESTRUCTORS;
public:
virtual NTSTATUS DriverEntry(PUNICODE_STRING RegistryPath);
};
DECLARE_DRIVER_CLASS(MyDriver,NULL)
NTSTATUS MyDriver::DriverEntry(IN PUNICODE_STRING RegistryPath)
{
DbgPrint("Hello KMD world!");
return STATUS_SUCCESS;
}
这里还有一个DECLARE_DRIVER_CLASS(MyDriver,NULL)要注意,再copy一段帮助文档上的内容:
One module of the driver must invoke macro DECLARE_DRIVER_CLASS. This macro actually inserts a small function that the framework calls during initialization. The function creates an instance of the driver class.
现在已经可以编译成sys,用我的小工具Driver Logicl Tester可以测试一下效果,可以看到输出了一行"hello KMD World!",我想需要解释一下KDriver,KDriver是驱动对象,按照DriverWorks对象模型里的观念,一个驱动程序里至少要包括一个驱动对象,这个驱动对象可以包括一个或多个设备对象,Ring3的应用程序可以通过设备对象来于驱动程序交互.后面我会做简单的演示.
3.打开潘多拉魔盒
上面演示的东东其实本上没有多大实际的用处,当然可以在riverEntry里添加一个调用门让Ring3的应用程序有机会使用调用门进入Ring0.所以我们必须要做到可以控制驱动程序执行指定的代码完成指定的任务.
3.1 设备对象与KDevice
在DriverWorks里KDevice和KPnpDeivce做为设备对象类的抽像,因为这里想演示的是KMD而不是WDM,所以我在这里只使用KDevice,KPnpDeivce以后有空再说.
在#define VDW_MAIN后#include "vdw.h"
前加上以下几行:
#define DRIVER_FUNCTION_DEVICE_CONTROL
#define DRIVER_FUNCTION_CREATE
#define DRIVER_FUNCTION_CLOSE
#define DRIVER_FUNCTION_CLEANUP
当需要覆盖设备或是驱动对象基类的某个函数时,需要在include
之前定义这些宏.原因不要问,文档上要求这样做那就这样做咯.具体的可以查看DriverWorks源代码,这四个是设备对象要用到的,可以对应看下面的代码.
现在我们实现一个设备对象的派生类MyDevice,它将负责与上层的驱动测试工具做交互.
class MyDevice : public KDevice
{
public:
SAFE_DESTRUCTORS;
MyDevice();
~MyDevice();
DEVMEMBER_DISPATCHERS;
};
DriverWorks帮助文档里说到如果没有SAFE_DESTRUCTORS这个宏,Unload例将工作的不太正常,所以不用问那么照做就是.而DEVMEMBER_DISPATCHERS宏的功能是自动的定义所有的派遣函数,如果Create,Close,Cleanup...等.
然后分别实现前面#define的那几个派遣函数:
NTSTATUS MyDevice::Create(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::Close(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::DeviceControl(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p,CodeCode=%d\n", I,I.IoctlCode());
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::CleanUp(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
3.2 构造一个设备对象
在DriverEntry里创建这个设备对象,其实非常简单,只是调用一个new运算符函数.
NTSTATUS MyDriver::DriverEntry(PUNICODE_STRING RegistryPath)
{
NTSTATUS status = STATUS_SUCCESS;
DbgPrint(__FUNCTION__"RegistryPath:%S\n",RegistryPath->Buffer);
MyDevice* pDevice = new (
L"HelloDevice",
FILE_DEVICE_UNKNOWN,
L"HelloDevice",
0,
DO_DIRECT_IO
)
MyDevice();
if (pDevice == NULL)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
status = pDevice->ConstructorStatus();//读取设备对象构造后的状态
if (!NT_SUCCESS(status))
{
delete pDevice;
}
else
DbgPrint(__FUNCTION__"Created HelloDevice!\n");
}
return status;
}
我想需要解释一下这个new运算符,它是KDevice的重载动算符,我使用的原型是:
void* __cdecl operator new(
size_t n,
PCWSTR DeviceName,
DEVICE_TYPE Type,
PCWSTR LinkName=NULL,
ULONG Characteristics=0,
ULONG DeviceFlags=DO_BUFFERED_IO
);
这个new运算符函数的comment说到:
// This is the form of new that allocates the storage for the class instance in
// the system device object extension.
在设备对象扩展里分配并存储类实例,我看了一下它的源代码在kfilter.cpp中,它会调用一个叫__construct的函数,而__construct函数会调用IoCreateDevice创建一个驱动设备对象(注意不是DriverWorks设备对象类),并分配一个大小为n(在这里是n=sizeof(MyDevice))的设备扩展用来存储这个设备对象类(包括它的数据成员,感觉确实是很方便啊!),并调用IoCreateSymbolLink在\DosDevice\创建一个符号链接供Ring3程序使用,我想LinkName使用一个GUID字符串更合适一些.呵呵!其实不需要把代码跟到这里看,只不过了解一些底下的东东也好,但我很赞成潘爱民老师所说的,做应用开发尽可能不要去读软件开发商提供的类库源代码.只看参考手册能够用的很熟练很巧就行了.所以我只看D.j.Kruglinski的Inside VC++而从不去看什么深入浅出MFC,我的MFC照样用的很好,也可以理解那些MFC底层如何实现,其实深入浅出MFC里的东西,Inside VC++里只用了几页纸就讲的非常清楚了,呵呵!话扯远了.也许每个人学习的思想和方法不一样.
再搬出我的小工具,测试一下.可以看到我们写的几个函数都被调用到了,每按一下"IoControl"按钮,DeviceControl函数就会被调用.
4.输入与输出
4.1 简单的KMD与上层应用同步方法
我想有了前面的基础,做输出输出已经不是什么难事,可以在DeviceIoControl里做,也可以在Read和Write派遣函数里做,不过我想通常的做法是,驱动创建一个同步事件,使用DriverWorks的KEvent对象,并使用这个成员函数原型进行初使化:
VOID Initalize(
KUstring& Name,
EVENT_TYPE type
);
第一个参数可以填以\BaseNamedObjects\为前缀的事件对象名,后一个参数可以是SynchronizationEvents(同步事件)或是NotificationEvents(通知事件),然后Ring3的程序可以用CreateEvent打开这个对象,WaitForSingleObject做同步.当有数据时上层的程序会在第一时间得到通知.
一般设备对象会添加几个容器成员,像前面分析的,它们会被会配并存储在设备扩展里,当然容器要使用的堆内存会是从PagedPool或是NonPagedPool里分配出来的,一些常用的容器如KList,KLockableFifo,KInterlockedList...等等.这样,数据的排队和存储解决后,就是在派遣函数中处理它们了,或是从上层收到并存入容器,或是从容器里摘除并返回到上层.
4.2 IO控制代码
一般使用一个叫做CTL_CODE的宏来定义IO控制代码,在每个派遣函数有一个KIrp类型的参数,它也IRP请求包的包装类,KIrp::IoctlCode函数返回控制代码的引用,也就是说我们可以读也可以改写控制代码.通常在DeviceIoControl里根据不同的控制代码做不同的动作.类似于这样:
NTSTATUS MyDevice::DeviceControl(KIrp I)
{
switch (I.IoctlCode())
{
case CTRL_SETUP_HOOK:
.............break;
case CTRL_REMOVE_HOOK:
.............break;
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
...
}
而输入输出缓冲及大小分别由,KIrp::IoctlBuffer,KIrp::IoctlInputBufferSize和KIrp::IoctlOutputBufferSize来获得.这只是Buffered IO方式,还有Direct IO方式有空再说!
完整的程序代码,我更喜欢看面向对象的代码结构,呵呵!
//hello.cpp:
#define VDW_MAIN
#define DRIVER_FUNCTION_DEVICE_CONTROL
#define DRIVER_FUNCTION_CREATE
#define DRIVER_FUNCTION_CLOSE
#define DRIVER_FUNCTION_CLEANUP
#define DRIVER_FUNCTION_UNLOAD
#include "vdw.h"
class MyDriver : public KDriver
{
SAFE_DESTRUCTORS
public:
virtual NTSTATUS DriverEntry(PUNICODE_STRING RegistryPath);
virtual VOID Unload(VOID);
};
DECLARE_DRIVER_CLASS(MyDriver,NULL)
class MyDevice : public KDevice
{
public:
SAFE_DESTRUCTORS;
MyDevice();
~MyDevice();
DEVMEMBER_DISPATCHERS
};
MyDevice::~MyDevice()
{
}
MyDevice::MyDevice() :
KDevice()
{
if (!NT_SUCCESS(m_ConstructorStatus))
{
DbgPrint(__FUNCTION__": Failed to create device MyDevice");
return;
}
}
NTSTATUS MyDevice::Create(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::Close(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::DeviceControl(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p,CodeCode=%d\n", I,I.IoctlCode());
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
NTSTATUS MyDevice::CleanUp(KIrp I)
{
DbgPrint(__FUNCTION__":IRP %p\n", I);
I.Information() = 0;
I.Complete(STATUS_SUCCESS);
return STATUS_SUCCESS;
}
VOID MyDriver::Unload(VOID)
{
KDriver::Unload();
}
NTSTATUS MyDriver::DriverEntry(PUNICODE_STRING RegistryPath)
{
NTSTATUS status = STATUS_SUCCESS;
DbgPrint(__FUNCTION__"RegistryPath:%S\n",RegistryPath->Buffer);
MyDevice* pDevice = new (
L"HelloDevice",
FILE_DEVICE_UNKNOWN,
L"HelloDevice",
0,
DO_DIRECT_IO
)
MyDevice();
if (pDevice == NULL)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
status = pDevice->ConstructorStatus();
if (!NT_SUCCESS(status))
{
delete pDevice;
}
else
DbgPrint(__FUNCTION__"Created HelloDevice!\n");
}
return status;
}
到现在为止,如果你是已经可以很熟练的用DDK写驱动肯定觉得这一切会有点繁琐,但这仅仅是开始,驱动的工程越大,代码量越多时使用DriverWorks表现出的效率会越明显.可以类比像MFC与SDK!!!