总版主
|
阅读:5538回复:14
楚狂人的 DriverNetworks开发网络驱动教材(0-7课)
楚狂人的 DriverNetworks开发网络驱动教材(0-7课)
简要说明: 这是一本您可以免费得到和自由传播的Ndis网络驱动开发的教材。以循序渐进的方式,通过大量的简单的示例代码介绍如何使用DriverrNetworks开发网络驱动。这本书由楚狂人搜集撰写。可以自由传阅修改。仅仅用于交流与学习。一部分来源于DriverNetworks帮助的翻译。一部分是楚狂人本人本人的工作经验介绍。若将用本书的内容用于赢利,您必须准备自己应付各种版权导致的问题。 这本教材曾经在这里发表过。这里是经过整理,并且更完整的新版本。 自我介绍: 1.从事驱动开发工作,曾经完成ipsec vpn客户端,网络文件系统,防火墙内核和一些协议驱动,usb客户驱动。 2.主要擅长vxd,Ndis,WDM驱动,文件系统驱动,USB客户驱动等。 3.喜爱开发调试工具:VC,DriverStudio,DriverNetworks,Softice. 4.此方面的问题欢迎与我联系.同时接受兼职或者全职的驱动开发工作.联系方式QQ16191935. 第O课 预备知识与概述 虽然为入门教材,本教材不讲述c++语法、VC的使用、网络基础知识等知识。为此,您至少应该预备以上的三种基础知识。 NDIS网络驱动的名词解释与用户您可以很方便的在网上尤其是本论坛找到。 本书主要介绍用DriverNetworks开发Ndis网络驱动的知识。 在实际建立一个驱动之前,您应该安装开发环境。VC和DDK是必须的,理论上98DDK也可以进行WDM设备的开发。我建议学习本教材的时候您使用windows2000作为开发环境,并安装windows 2000DDK. 2000DDK可以在网络上免费下载。此外建议您安装DriverStudio2_7或者更高的版本。并建议您按VC->DDK->DriverStudio2-7的顺序来安装。如果您先安装了DriverStudio,然后再安装VC,有一些配置需要手工设置。我曾为此重新安装DriverStudio. DriverStudio的安装过程极其简单。但Softice需要相对专门的配置。注意在安装的时候要求您的windows2000用户拥有足够的权限(您最好使用Administrator),否则可能导致的问题包括从安装失败到windows无法启动等不能一一详述。如果您不需要调试工具softice,仅仅安装开发包倒是非常的安全。 只使用DDK配置开发环境需要很恼火的工作。但是使用DriverStudio几乎避免了所有的麻烦。安装后vc中出现一个新的工具栏,选择DriverNetworks向导即可快速的生成一个工程。 在生成框架后按普通的方法编译,DriverStudio常常会弹出对话框,提示说有某些库还没有编译。此时切勿取消,点对话框上的“启动VC”按纽,出来的新工程来一个批构件全部编译。以后就不会再有此问题。否则编译的时候出现无数的连接错误。 点了向导后,输入工程名。然后选择微端口驱动、中间层驱动或者协议驱动。微端口驱动是实际网卡驱动。协议驱动的特点是只能得到包和发送包,而不能阻止其他协议得到包。中间层驱动是一种过滤驱动,在小端口驱动和协议驱动中间,可以得到所有的数据包并决定它们的命运。 现在我们选择中间层驱动,下去选择Filter而不是Mux(先挑简单的下手),我只对以太网包有兴趣,因此我选择Medium Type 802_3. 网络设置控制面板需要我们实现一个叫Notify Object的东西。但是我已经决定由我自己的程序来控制,不关控制面板的事情,所以我不选择这个。 继续往下,可以决定在注册表中保存的参数。我对此无兴趣,直接下一步,生成了工程。 工程可以直接便宜,如果出现了连接错误或者其他任何错误请严格按上边的步骤来操作。编译结束了应该生成一个sys文件。 工程目录下有两个inf文件。这两个文件必须与sys在一起才能正常安装驱动。点开控制面板网络添加服务。选择没有带MP的那个inf文件。安装。如果一切顺利,打开DriverMonitor,您能看到输出信息,您的中间层驱动已经开始工作了。 现在回到我们前面所叙述的,您应该看到一些类:其中最重要的是MyProjectAdapter,"MyProject"是您的工程名字。 下面会从基础知识开始介绍。您可以使用刚刚建立的框架来测试下边的简单代码。TRACE()宏在DriverNetworks环境下可以非常方便的输出信息。请使用DriverMoniter或者DbgView来观看。 第一课 管理NDIS Packets NDIS Packet(包描述符)是最基本的NDIS数据类型(NDIS_PACKET结构),被多种网络驱动用于描述临近的两个网络接口之间传输的数据。NDIS_PACKET是比NDIS_BUFFER更高层的抽象。NDIS_BUFFER描述NDIS_PACKET所使用的内存空间。在Windows NT中,就是是MDL(内存描述符号链)。NDIS_PACKET描述了在层之间收到或者发出的数据包的内容。这些内容保存在一个NDIS_PACKET所拥有的NDIS_BUFFER链中。 DriverNetworks通过KNdisPackets类来使用NDIS packets。KNdisPacket是PNDIS_PACKET的c++外包类,而且有与PNDIS_PACKET同样的运行效率。对于类型转换的支持使KNdisPacket可以被直接用于所有以PNDIS_PACKET为参数的函数中。 NDIS Packets总是从NDIS packet pool(包描述符号池,下面简称包池)中分配的。在DriverNetworks中,packet pool由KNdisPacketPool类描述。如果你的驱动管理自己的包池,它总是包括一个KNdisPacketPool对象作为adapter类(这个类以后再说)的数据成员。并在Adapter类的Initialize中初始化它,然后可以在其他地方分配或者释放你的包描述符。 当一个Ndis pakcet通过NDIS在一个驱动到另一个驱动之间传递的时候,该描述符的所有权可以临时的转移到后一个驱动。为了区分这些Ndis pakcet的所在环境,NDIS_PACKET结构提供一些特别的区域,名保留域,来在这些包描述符中保存上下文信息。DriverNetworks提供KNdisPacketWithContext与KNdisPacketListWithContext来管理这些区域。 为解决NDIS中间层驱动执行包管理计划的困难,DriverNetworks中间层驱动往往使用了包描述符中的一些保留域。为了在框架代码和用户代码之间正确的共享这些区域,DriverNetworks提供了KNdisFilterPacketPool类,这个类提供了一个安全的机制来在中间层驱动使用自己的包池。 下面是分配包池的例子。 //MyAdapter类也就是我的驱动的主要部分,由DriverNetWorks的向导生成,注意成员函数实现写在类声明里了,别被这个给迷惑 class MyAdapter : public KNdisMiniAdapter { ... KNdisPacketPool m_Pool; public: NDIS_STATUS Initialize(KNdisMedium& Medium, IN KNdisConfig& Config) { ... m_Pool.Initialize(8); //初始化包池(在其中初始化8个包描述符) ASSERT(m_Pool.IsValid()); ... } void SomeMethod() { //这里在某个成员函数中分配一个包描述符 KNdisPacket pkt = m_Pool.Allocate(); if(pkt.IsValid()) { ... //如果想使用就使用 } void AnotherMethod(KNdisPacket& pkt) { //在这里将包描述符号释放还给包池 m_Pool.Free(pkt); } } 使用非常简单,应该注意的是包描述符只是一个描述符,并不包含真实的数据包数据。只是使你可以找到并管理数据包。 DriverNetworks有另一个类KNdisPacketList,可以用于管理Ndis Packet链表。KNdisPacketList是NDIS_PACKET的双向链表。注意KNdisPacketList并不是线程安全的,这对与标准的NDIS4微端口驱动来说足够了。不连续的NDIS5微端口驱动可能必须使用KNdisInterLockedPacketList代替之,这个类使用一个自旋锁保证几个线程对链表的操作不会互相干扰。 KNdisPacketList类一般用于执行先进先出式的包处理过程。下面是例子。 class MyAdapter : public KNdisMiniAdapter { ... KNdisPacketList m_Queue; public: void Process(PNDIS_PACKET pkt) { if(/*如果想立刻处理*/) { //处理过程 } else {//如果不想处理,暂时加入队列中 m_Queue.InsertTail(pkt); } } void ProcessLater() { //在这里处理 KNdisPacket pkt = m_Pool.Remove(); if(pkt.IsValid()) { //处理 } else {} //说明队列是空的? } } NDIS_PACKET提供MiniportReserved[]和ProtocolReserved[]这样特殊的保留区域,主要用于保存可能这些包的不同的上下文(或者说执行环境)信息。举个例子,一个协议驱动收到应用程序请求并生成了一个包,协议可能要储存有一个IRP的指针。NDIS要求微端口使用MiniportReserved[]而协议驱动使用ProtocolReserved[]。中间层驱动则更要严格的注意,当前是Miniport呢还是Protocol在使用哪一个域。 DriverNetworks提供了类模板KNdisPacketWithContext,这使你可以随意处理保留区域而不必担心类型。KNdisPacketWithContext当然来自KNdisPacket,而驱动开发者必须可以自己定义保留区域中的数据结构,然后使用GetContext()方法来返回一个指针访问保留区域。 KNdisPacketWithContext一般用于KNdisPacketListWithContext中。后者是KPacketList的容器,并且提供了根据用户定义数据结构很方便的访问保留区域的方法。 KNdisPacketWithContext和KNdisPacketListWithContext这两个模板都通过两个参数生成类,一个上下文类型,也就是用户定义的保留区域数据结构。另一个是一个bool变量,表示描述符号是用于微端口的还是协议的。 下面是例子 class MyPacketDevice : public KDevice { //上下文 (保存在包描述符的保留区域中) struct PacketContext { PIRP Irp; PMDL pMdl; }; typedef KNdisPacketListWithContext<PacketContext> PacketList; protoected: KNdisPacketPool m_PacketPool; PacketList m_List; } //现在看如何使用了 void Submit(KIrp I) { KNdisPacketWithContext packet = m_PacketPool.Allocate(); packet->GetContext()->Irp = I; //看见了吧,直接用用户定义的类型访问保留区域 m_List.InsertTail(packet); ... } void SubmitDone() { KNdisPacketWithContext packet = m_RcvList.RemoveHead(); KIrp I = packet.GetContext()->Irp; // ... } 同时,类KNdisFilterPacketPool提供了安全的方法来在中间层驱动中使用私有的包池。值得注意,强烈推荐使用KNdisFilterPacketPool代替KNdisPool,如果你要在中间层驱动中使用私有的包池的话。 使用步骤如下: 1.定义你的保留区域数据类型T struct MyContext {PVOID data;} 2.定义你的包池 typedef kNdisFilterPacketPool<MyContext,true> CTxPool; typedef KNdisFilterPacketPool<MyContext,false> CRxPool; 3.分配包并使用保留区域 KNdisPacket p = m_CTxPool.Allocate(); CTxPool::GetContext(p)->data = ... 第二课 管理Ndis Buffers,访问注册表 NDIS_BUFFER是另一个基本的数据结构,几乎被所有的网络驱动用于描述在系统内存中分配的内存快。在Windows NT中,NDIS_BUFFER就在NT内核中常用的MDL(内存描述符链)。 DriverNetworks将NDIS_BUFFER包装成KNdisBuffer类。这个类可以直接用于任何以PNDIS_BUFFER为参数的函数中。 NDIS_BUFFER总是从一个NDIS buffer pool(缓冲描述符池,下面简称缓冲池,别和真的缓冲池混淆)中分配的。在DriverNetWorks中,缓冲池相关的类是KNdisBufferPool,如果你的驱动使用自己的缓冲池,一般得在你的Adapter类中包含一个KNdisBufferPool成员,并且在adapter的Initialize中初始化。 下面是使用 KNdisBuffer类的例子。 class MyAdapter : public KNdisMiniAdapter { ... KNdisBufferPool m_Pool; public: NDIS_STATUS Initialize((KNdisMedium &Medium, IN KNdisconfig& Config)) { m_Pool.Initialize(8); //初始化8个缓冲描述符的缓冲池 ASSERT(m_Pool.IsValid()); ... } void SomeMethod(PVOID Data, UINT DataSize) { KNdisBuffer buf = m_Pool.Allocate(Data,DataSize); if(buf.IsValid()){/*在这里使用buf*/} else{ //很糟糕,说明缓冲描述符用完了! } } Ndis buffer就介绍到这里,这里只提一下在什么地方用,具体怎么操作,以后再说。 Ndis驱动通过类KNdisConfig类访问注册表。DriverNetworks架构总是在驱动初始化的时候生成一个KNdisConfig对象,并完adapter类的Initialize()成员函数中传入一个引用。包括微端口、中间层驱动和协议驱动都是这种模式的。 每个NDIS驱动在注册表上都有一个子树,记载了设置参数。 对于NDIS微端口驱动,他的参数保存在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}\xxxx\Ndi\params (那串数字找起来很麻烦,但是我记记住了72两个数字,你会发现很有用!) 对于协议驱动,他的参数在: HKEY_LOCAL_MACHINE\SYSTEM|CurrentControlSet\Services\<protocol>\Parameters 这个子树会被安装脚本(你写的驱动会需要一个inf文件才能安装,就这个东西)和系统填写。 类KNdisConfig类提供了公有成员函数Read()和Write来从注册表读写一些数值,包括32位整数和Unicode字符串。 下面的例子读了一个32位的整数: ULONG uCardMode; Config.Read(KNDIS_STRING_CONST("CardMode"),&uCardMode); 这里的KNDIS_STRING_CONST宏是一个生成Unicode字符串的快速方式。 下面访问字符串 NDIS_STRING strCardName; NDIS_STATUS err = Config.Read(KNDIS_STRING_CONST("CardName"),&strCardName); if(err) { //说明“CardName”没找到! } else { //strCardName.Buffer是一个指向空字符为结束的Unicode字符缓冲区的指针。 } 前面的例子中Read()需要一个指向NDIS_STRING的指针作为第二个参数,但是参数转换使你可以直接使用一个类KNdisString的对象代替之。返回的字符串由NDIS管理,绝不能被调用者修改或者释放掉。当Config对象被释放的时候,这个字符串的空间会被挥手。一般这发生在MyAdapter::Initialize返回之后。 KNdisConfig还允许你查询一些随系统不同而变化的参数,比如: ReadNetworkAddrss()――能读一个注册表中预定义的键值NetworkAddress, IsNT()――能检查现在是运行在WindowsNT下还是Win9X下。 NdisVersion()――能检查NDIS的版本。 最后,KNdisConfig类允许你执行快速IO,有些微端口驱动利用这个功能。这个以后再说。 第三课 访问IO端口 中间层驱动和协议驱动可能对IO端口不感兴趣,但用DriverNetworks开发NDIS微端口驱动时访问io端口或者内存映射io端口,基本上有三个步骤: 1.向注册io端口或者内存地址范围。 2.访问这些端口。 3.注销你注册的东西。 首先是注册,你必须在你的Adapter类中包括KNdisIoRange类或者KNdisMemory的数据成员.例子如下: class MyAdapter : public KNdisMiniAdapter { ... KNdisIoRange m_Ports; KNdisMemoryRange m_Memory; ... } 然后你在adapter的Initialize函数中访问一个KNdisXxxResource类,从中得到你的微端口驱动所拥有的资源。(当然包括io口)。 最后你就可以初始化你的KNdisIoRange以及KNdisMemoryRange成员了。下面有例子: NDIS_STATUS MyAdapter::Initialize(KNdisMedium& Medium,IN KNdisConfig& Config) { //注意这行代码是向导生成的 ... //现在来设法获取资源 KNdisPnpResourceRequest request(Config); KNdisResource<CmResourceTypePort> Port(request); //判断获取结果是否有问题 if(!Port.IsValid()) KNDIS_RETURN_ERROR(NDIS_STATUS_NOT_ACCEPTED; //注册io端口范围 m_Ports.Initialize(); if(!m_Port.IsValid()) KNDIS_RETURN_ERROR(NDIS_STATUS_RESOURCES); ... } 就是这样,注册结束。下面看怎么访问。 DriverNetworks提供了四种方法来访问你的m_Ports 1.使用in()和out()成员函数, 它们使用一个ULONG型的偏移量作为参数访问io端口,这个偏移量从io范围的开始地址开始计算。看下面的例子: UCHAR reg = m_Ports.inb(4); if(reg|1) m_Ports.outb(4,reg|1); else m_Ports.outb(4,0); 2.使用KNdisIoRegister和KNdisMemoryRegister提供了一种访问这些io范围中一些特殊的寄存器的方法。KNdisIoRegister或者KNdisMemoryRegister的对象可以在对KNdisIoRange或者KNdisMemoryRange对象做[]操作的时候获得。而且KNdisIoRegister和KNdisMemoryRegister对象还可以当作一些基本类型使用,比如它们可以当作ULONG,USHORT,UCHAR等等来访问。看例子: KNdisIoRegister reg = m_Ports[4]; if(UCHAR(reg)|1) reg |= UCHAR(reg)|1; else reg = 0; 3.使用模板。DriverNetworks提供两个模板KNdisIoRegisterSafe和KNdisMemoryRegisterSafe作为刚刚2中提到的两个类的“safe”版本。怎么说呢,非“safe”版本就是说在编译的时候不会检查读取数据的宽度和实际硬件寄存器数据宽度的不同。比方说你试图往一个只有8位的寄存器中写一个ULONG的数据,象这样 m_Ports.outd(4,1),这编译没问题,但是可能网卡不会象你希望的那么工作。而“safe”版本则可以指定其数据宽度,你可以根据寄存器的实际宽度指定成ULONG,USHORT,UCHAR这样的类型。如果尝试读写不同的数据宽度的类型,你会得到一个编译错误。例子如下: ULONG newval = 0; KNdisIoRegisterSafe<UCHAR> reg = m_Ports[4]; if(UCHAR(reg)|1) reg|=UCHAR(reg)|1; else reg = val; //这里你将得到编译错误! 4.最后一种访问io端口的方式是所谓的直接io访问。这是基于NdisImmediateXxx系列函数的。这提供了一种方法让你的驱动可以在硬件资源已经分配,io或者内存范围已经注册之间就访问实际网卡硬件。这种情况下,系统会对每一次访问都要进行 硬件资源转换和映射,所以这种方法访问是很慢的。而且只能用在硬件初始化过程中。举个例子,一个驱动在开始必须读一个网卡上的EEPROM,来获得某种信息(比如版卡类型?),然后才能开始资源分配请求,这种情况下不能不使用这个技术。一般这种方法在即插即用环境中是不推荐的。 DriverNetworks通过KNdisConfig类来支持这种技术。(刚好上一课介绍了这个东西)。这个类有一系列的in()/out()成员函数来访问io口。例子如下: UCHAR reg = Config.inb(4); if(reg | 1) Config.outb(4,reg|1); else Config.outb(4.0); 最后是注册的io范围的注销,这很简单,一般在MyAdapter的Halt()函数中做。另一个选择是在Adapter的析构函数中做。只要这样: m_Ports.Invalidate(); m_Memory.Invalidate(); 就可以了。 第四课 关于中断 这也是只用于微端口的。涉及以下几个主题。 1.注册一个中断。 (1).首先,应该在你的工程的Characteristics.h文件中(关于这个文件,你在使用向导生成了一个微端口驱动的框架之后,自然就会看见。)声明你对中断相关的回调函数。这里你要使用一个宏KNDIS_MINPORT_HANDLER.例子如下: KNDIS_MINIPORT_HANDLER(MyAdapter,DisableInterrupt) KNDIS_MINIPORT_HANDLER(MyAdapter,EnableInterrupt) KNDIS_MINIPORT_HANDLER(MyAdapter,HandleInterrupt) KNDIS_MINIPORT_HANDLER(MyAdapter,Isr) (2).在你的adapter类中包括一个KNdisInterrupt的数据成员。 class MyAdapter:public KNdisMiniAdapter( ... KNdisInterrupt m_Interrupt; ... }; (3).你必须在adapter的Initiazlize()函数中访问一个KNdisXxxResource类来获得系统分配的中断资源。 (4).根据3中得到的信息初始化你的KNdisInterrupt成员。这个过程举例如下: NDIS_STATUS MyAdapter::Initialize(KNdisMedium& Medium, IN KNdisConfig& Config) { . . . //获得资源信息 KNdisPnpResourceRequest request(Config); KNdisResource<CmResourceTypeInterrupt> Int(request); //确保其可用 if (!Int.IsValid()) KNDIS_RETURN_ERROR (NDIS_STATUS_NOT_ACCEPTED); //注册你的中断 m_Interrupt.Initialize(this, Int, NdisInterruptLatched); if (!m_Interrupt.IsValid()) KNDIS_RETURN_ERROR (NDIS_STATUS_RESOURCES); . . . } (5).在硬件上使能你的中断。这和网卡有关。以后我们见具体的例子。 2.同步工作 任何微端口驱动函数若和其他同在DIRQL层上运行的函数共享任何资源,都必须处理同步问题。这个问题出现在中断相关的两个回调函数Isr()和DisableInterrupt()上。为了同步这些函数,驱动使用KNdisInterrupt::Synchronize()函数。这个函数将安排指定的函数运行的时候持有一个自旋锁,从来解决同步问题。例子如下: m_Interrupt.Synchronize(KNDIS_MEMBER_CALLBACK(CardSetMulticast),this); void MyAdapter::CardSetMulticast() { //我们假设这个函数运行于DIRQK,并其中不想被 //Isr()之类的调用打断工作 } 换句话说,使用了Synchronize函数之后,就不必担心CardSetMulicast这个函数会被Isr()或者是DisableInterrupt()这两个函数打断了。 但是在MyAdapter的声明中,还应该有下边的语句: class MyAdapter:public KNdisMiniAdapter( ... KNDIS_DECLARE_SYNCHROCALLBACK( MyAdapter,CardSetMulticast); void CardSetMulticast(); ... ); SYNCHROCALLBACK宏会在adapter类中增加一个静态的成员函数。而系统将会通过这个函数来控制CardSetMulticast()这个函数的运行。 3.最后是注销中断,这样: m_Interrupt.Invalidate(); 关于中断就介绍到这里。具体的使用过程,以后我们看看通过具体的微端口驱动实例就知道了。 下一课介绍如何生成wdm设备对象,以及被动和主动的与应用程序通信。 第五课 Ndis驱动与应用程序交换信息 驱动程序即使可以做界面也是非常困难的。但是往往用户可以通过一些界面来对驱动程序进行一些设置。某些网络驱动提供固定的dll接口来实现设置。这必须阅读windows的规范。我更喜欢使用自己的用户程序来设置我的网络驱动,这样比较自由。 用户程序将信息发送给驱动可以通过WriteFile,或者DeviceIoControl.这些是标准的windowsAPI函数。一个驱动程序注册了WDM设备之后,在windows2000中将被看作一个类似文件的东西。应用可以对它进行读、写和一些其他的控制。 NDIS 5 提供了一个新的函数,NdisMRegisterDevice,允许用户在Ndis驱动中注册一个设备。实际上在98下,无NDIS5的时候,你不得不用另外的代码来做这件事情。假设下面就是自己撰写的一个函数,你可以通过执行这个函数来得到一个设备,你可以在驱动初始化的时候调用这个函数。 --------------示例5.1 ---------------- NDIS_STATUS MyAdapter::CreateMyDevice() { NDIS_STATUS Status; // 如果在2000下编译,我使用NDIS5的方法.DDK表明,2000下 // 的Ndis驱动“不应”使用IoCreateDevice来创建设备,我没 // 试过会有什么后果 #if VDEV_WIN2K static PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; NdisZeroMemory(MajorFunction, sizeof(MajorFunction)); MajorFunction[IRP_MJ_CREATE] = MajorFunction[IRP_MJ_CLOSE] = MajorFunction[IRP_MJ_CLEANUP] = MajorFunction[IRP_MJ_READ] = MajorFunction[IRP_MJ_WRITE] = MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatch; Status = NdisMRegisterDevice( *KNdisMiniDriver::DriverInstance(), KNDIS_STRING_CONST("\\Device\\MyNdisDevice"), KNDIS_STRING_CONST("\\DosDevices\\MyNdisDevice"), MajorFunction, &m_pDeviceObject, &m_DeviceHandle); // 下面的方法用于win98 #else PDRIVER_OBJECT pDriverObject = KNdisMiniDriver::DriverInstance()->DriverObject(); pDriverObject->MajorFunction[IRP_MJ_CREATE] = pDriverObject->MajorFunction[IRP_MJ_CLOSE] = pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = pDriverObject->MajorFunction[IRP_MJ_READ] = pDriverObject->MajorFunction[IRP_MJ_WRITE] = pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatch; Status = IoCreateDevice( pDriverObject, 0, KNDIS_STRING_CONST("\\Device\\MyNdisDevice"), FILE_DEVICE_NETWORK, 0, FALSE, &m_pDeviceObject); if (STATUS_SUCCESS == Status) // 告诉io系统我们已经准备好了 m_pDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING; #endif return Status; } 为了记住注册的结果,并准备在驱动卸载的时候释放掉,在向导生成的MyAdapter类中,还增加了两个成员: NDIS_HANDLE m_DeviceHandle; PDEVICE_OBJECT m_pDeviceObject; 可以看到这两个成员都在上边的函数用到了。其他在卸载的时候应该这样来注销掉这个设备: --------------示例5.2 ---------------- VOID MyAdapter::Halt(VOID) { #if VDEV_WIN2K // win2000 if (m_DeviceHandle) { NdisMDeregisterDevice(m_DeviceHandle); m_DeviceHandle = NULL; } #else // win98 if (m_pDeviceObject) IoDeleteDevice(m_pDeviceObject); #endif m_pDeviceObject=NULL; } 在这里您依然可以注意到2000与98的不同之处。 此外注意到上边用到一个函数IoDispatch(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp);这个函数是另外写的,专用来处理外部应用程序到这个设备的io请求。你不能把这个函数作为一个C++类的一个普通成员(除非您标明static ),所以只好作为一个全局函数或者static成员。 在上边的注册中我们已经把所有的请求处理函数都指向这一个函数,因此我们可以专心写好这个函数来解决所有的请求问题。 注意\Device\MyNdisDevice中的MyNdisDevice,这是所谓的符号连接名。您的应用程序将通过这个名字来访问这个驱动。如果系统中本来已经有了这个名字,注册将失败。 因此,建议这个符号尽量复杂,不要重名。有更好的使用GUID的注册方式,当时访问起来也更麻烦。这里不讨论了。 对于这个设备,应用程序可以打开、读、写、发出控制命令、关闭。 --------------示例3----------------- // 打开 HANDLE Handle=CreateFile("\\\\.\\MyNdisDevice", GENERIC_WRITE|GENERIC_READ, FILE_SHARE_WRITE|FILE_SHARE_READ, NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL); if(Handle==INVALID_HANDLE_VALUE) {}// 失败了 else {}// 成功了 // 写 if(WriteFile(Handle,buf,len,&dlen,NULL)) {}// 成功了 else {}//失败了 其他的函数调用涉及CloseHandle(),DeviceIoControl(),ReadFile(),使用参数请阅读MSDN,与读写文件并无不同之处。主要要注意的是CreateFile的参数,建议您直接用上边的示例。我不太清楚每个参数的准确含义,但是我用上边的参数总是可以成功。 我不知道有没有更好的办法,我见过的驱动主动通知应用程序的办法是应用程序用一个线程来读驱动。驱动把要通知应用程序的东西让应用程序读出。这需要一个无尽循环的循环来读这个驱动。当无数据可读的时候可以阻塞。当驱动想通知应用(如丢出一笔日志)的时候,写一些东西让应用程序的ReadFile返回即可。 为了处理io请求我们现在来写那个IoDispatch.这个函数在WDM驱动中一般称为分发例程。这个函数在Passive Level运行,因此非常安全,几乎可以调用绝大部分的系统内核服务。 请结合下边的例子,可以看到最简单的是IRP_MJ_CREATE和IRP_MJ_CLOSE调用。如果返回STATUS_SUCCESS则表示成功,STATUS_UNSUCCESSFULE则表示失败。最简单的方法是只让一个进程打开自己,你可以设想一下应该怎样实现,并作为一个小练习。 这里设备采用缓冲模式,这是最为简单的一种方式,其他方式我们不讨论。为了设置为缓冲模式,我们回到示例代码5.1,在注册设备之后,加上这句: m_pDeviceObject->Flags |= DO_BUFFERED_IO; ReadFile的处理主要是输出数据。用户提供输出缓冲及其长度。你写入数据,并说明你写入的长度。在IrpStack->Parameters.Read.Length中得到输出缓冲长度。数据写入Irp->AssociatedIrp.SystemBuffer中。实际输出数据长度请写到Irp->IoStatus.Information中即可。 WriteFile的处理与ReadFile类似。不用的是Irp->AssociatedIrp.SystemBuffer成了输入缓冲,而长度在IrpStack->Parameters.Write.Length中。 DeviceIoControl的情况稍微复杂,一般先要得到一个功能码,用户程序一般要输入数据(在输入缓冲中),同时要获得输出(请你写入输出缓冲中),并指明了这些缓冲区的长度。你还必须指明你输出数据的真实长度。 功能码在IrpStack->Parameters.DeviceIoControl.IoControlCode; 缓冲模式,输入缓冲长度为IrpStack->Parameters.DeviceIoControl.InputBufferLength; 缓冲模式,输出缓冲长度为IrpStack->Parameters.DeviceIoControl.OutputBufferLength; 共用缓冲区为Irp->AssociatedIrp.SystemBuffer; 实际输出数据长度请写到Irp->IoStatus.Information中。 用下边的方法返回失败最为完整:注意指定了为参数错误。你也可以指定其他错误。请查阅ddk帮助。 Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_INVALID_PARAMETER; 用下边的方法返回成功: Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_SUCCESS; //------------示例4-------------- NTSTATUS IoDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { BOOLEAN ret = FALSE; KIrp I(Irp); PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp); switch(I.MajorFunction()) { case IRP_MJ_CREATE: // 如果有进程调用了CreateFile ret = TRUE; break; case IRP_MJ_CLOSE: // 如果有进程调用了CloseFile ret = TRUE; break; case IRP_MJ_CLEANUP: ret = TRUE; break; case IRP_MJ_READ: // 有进程调用了ReadFile() ret = FALSE; break; case IRP_MJ_WRITE: // 如果调用了WriteFile() ret = FALSE; break; case IRP_MJ_DEVICE_CONTROL: ret = FALSE; break; default: ret = FALSE; }; if(ret) { Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_SUCCESS; } else { Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp,IO_NO_INCREMENT); return STATUS_INVALID_PARAMETER; } } 好,希望我已经说明白了驱动与应用之间如何交互。如果你还不是很理解,请留意下一章的实际例子。我们将做一个简单的中间层驱动,您可以通过应用程序来配置这个驱动的一些行为(如允许所有的包通过,或是禁止所有包通过)。 第六课 一个实际的中间层驱动 第O课中我们已经建立了一个框架,现在在浏览几个重要的函数。最重要的是两个OnReceive和一个OnSend. 先看框架生成的第一个OnRecevie.注意我的工程名为My. //---------------示例6.1-------------------- NDIS_STATUS MyAdapter::OnReceive (const KNdisPacket& Original, KNdisPacket& Repackaged) { TRACE("MyAdapter::OnReceive() %u bytes\n", Original.QueryTotalLength()); HEADER* Content = (HEADER*) Original.QueryFirstBuffer(); Repackaged.CloneUp(Original); return NDIS_STATUS_SUCCESS; } 这个函数似乎比较好理解。当计算机接受到网络数据包的时候,我们的OnReceive被调用。Original是就是数据包。请结合第一课中的内容。KNdisPacket是一个网络数据包,但是其实际内容放在KNdisBuffer中。一个KNdisPacket拥有一个KNdisBuffer链。 KNdisBuffer可以直接转成任何地址来使用,也可以用KNdisBuffer::Address()来得到地址。 那么如上边的示例,Original.QueryFirstBuffer()得到数据包头。 注意,第一个缓冲区的长度至少为14个字节(其实我并不这么肯定,但是我每次都恰好至少得到了14个字节),但是不要指望第一个KNdisBuffer就直接帮你搞定后边的ip头,tcp头数据。一个链式的结构非常的不好处理,所以我建议费点力气把头的部分复制到一个连续缓冲区中。一般都处理到tcp头即可,也不过14+20+20才54个字节。拷贝起来是不费多少资源的。 假设有一个KNdisPacket Packet,你可以用下边的例子把前54个字节的数据拷贝到MyAheadBuf中。 //----------------示例6.2--------------------- UINT nBufCnt = Packet.QueryBufferCount(); KNdisBuffer Buf = Packet.QueryFirstBuffer(); if (!Buf.IsValid()) return; unsigned char MyAheadBuf[14+20+20]; unsigned long MyAheadBufLen = 0,WantLen = 14+20+20; // 拷贝足够的长度 while(WantLen > MyAheadBufLen) { if(WantLen - MyAheadBufLen > Buf.Length()) { memcpy(&MyAheadBuf[MyAheadBufLen],Buf.Address(),Buf.Length()); MyAheadBufLen += Buf.Length(); Buf = Buf.GetNext(); if(!Buf.IsValid()) break; } else { memcpy(&MyAheadBuf[MyAheadBufLen],Buf.Address(),WantLen-MyAheadBufLen); MyAheadBufLen = WantLen; } } 不幸的是并不是所有的计算机上都只调用上一个OnRecevie.另一个可能被调用的OnReceive函数如下。这可能和下层的微端口驱动的特性有关。下边这个函数HeaderBuffer是以太网包头,长度一般为14.LookAheadBuffer是以太网包头之后的部分,如果这是个ip包,那就是ip头了。我们能否驱动到我们关心的ip头和tcp头?这需要LookAheadBufferLength至少为40.我认为LookAheadBufferLength长度至少为40.因为我似乎还没有发现过少于40的情况。 如果不是,请发邮件给我MFC_Tan_Wen@163.com,非常感谢。 这导致我们可以直接用LookAheadBuffer来得到足够的信息做大多数的工作。但是并不总是这样的。比如我要对整个数据包加密,我有必要得到整个数据包。但是LookAheadBuffer可能小于PacketSize.这种情况,你必须调用TransferData()函数,并在MyAdpater::OnTransferComplete()中处理这个数据包。但是OnTransferComplete()这种情况恰好和上一种OnReceive()的情况类似,所以这里不再详述了。 //------------------示例6.3--------------------------- NDIS_STATUS MyAdapter::OnReceive( IN OUT KNdisPartialPacket& PacketToAccept, IN PVOID HeaderBuffer, IN UINT HeaderBufferSize, IN PVOID LookAheadBuffer, IN UINT LookaheadBufferSize, IN UINT PacketSize) { TRACE("MyAdapter::OnReceive() Partial %u/%u%/%u bytes\n", HeaderBufferSize, LookaheadBufferSize, PacketSize); UNREFERENCED_PARAMETER(PacketToAccept); return NDIS_STATUS_SUCCESS; } OnSend()在有数据包发出的情况下被调用,处理方法应该与第一种OnReceive的方法相同。 如果我想阻止数据包的接收或者发送,我直接return NNDIS_STATUS_NOT_ACCEPTED即可。并且我不向上或者向下复制数据包。 请定义一个全局的变量BOOLEAN Allow.并假定Allow如果为TRUE,我则不干涉所有数据包的发送接受。而如果为FALSE,我丢弃接收到的所有数据包,并阻止所有的发送包。 回到示例6.1,内容应该改为 TRACE("MyAdapter::OnReceive() %u bytes\n", Original.QueryTotalLength()); if(Allow) { HEADER* Content = (HEADER*) Original.QueryFirstBuffer(); Repackaged.CloneUp(Original); return NDIS_STATUS_SUCCESS; } else return NDIS_STATUS_NOT_ACCEPTED; 其他的两个函数请自己修改。 现在的问题是我必须用应用程序来设置这个变量。以便控制我的网络是否连通。这样的一个防火墙只有两个选项,全开或者全关,当然这是一个无实际用处的防火墙。 回到上一章节的叙述,我们在这个驱动中加入注册一个WDM设备。但是仅仅做了上边的事情无法通过编译。DriverNetworks的帮助指出,要注册WDM设备必须在向导中选中NDIS WDM选项。但是实际上只有微端口驱动才有这个选项。因此必须用我下边所说的方法: 首先包含头文件<kndisvdw.h>。然后编译预定义宏定义增加这几个:NTVERSION='WDM',NDIS_WDM=1.此时编译ok,但是连接未必能搞定。 为此建议建立一个微端口驱动,选中wdm,编译过程中会跳出来要你编译很多lib,这时全部编译之即可。 回头来,您的中间层驱动已经成功的加入了WDM设备。 现在考虑如何用WDM设备来控制开与断。我设计一个DeviceIoCtrl来做这个事情: 定义: #define IOCTL_SET_NET_OPEN_OR_CLOSE CTL_CODE(\ FILE_DEVICE_UNKNOWN,\ 0x802,\ METHOD_BUFFERED,\ FILE_ANY_ACCESS) 这个指令带一个字节的参数保存在输入缓冲中。如果为0则全断,反之全开。 然后修改IoDispatch函数,加入以下的处理: ... ULONG ControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode; ULONG InputLength = IrpStack->Parameters.DeviceIoControl.InputBufferLength; ULONG OutputLength = IrpStack->Parameters.DeviceIoControl.OutputBufferLength; switch(ControlCode) { case IOCTL_SET_NET_OPEN_OR_CLOSE: { // 主要的任务是首先检查长度是否足够 if( 1 > OutputLength) { ret = FALSE; break;; } // 得到输入参数并设置自己全开全闭 UCHAR cAllow = *((UCHAR *)Irp->AssociatedIrp.SystemBuffer); Allow = (cAllow==0)?FALSE:TRUE; DWW_RULE_S *pDst = (DWW_RULE_S *)Buffer; Irp->IoStatus.Information ret = TRUE; break; } ... } 至于应用程序如何调用DeviceIoControl,请参照上一课的例子。 |
沙发#
发布于:2004-08-24 00:07
其它方面的还有吗?
|
|
板凳#
发布于:2005-03-25 15:54
顶一下``~~
|
|
|
地板#
发布于:2005-04-27 09:17
多谢
|
|
地下室#
发布于:2007-01-03 22:53
多谢
|
|
5楼#
发布于:2007-05-31 18:23
我要学习
|
|
|
6楼#
发布于:2007-06-01 07:31
太好了,学习。
|
|
7楼#
发布于:2007-06-10 00:14
有没有DriverWork的教材阿/?有的话提供一份给我,万分感谢!!mslnjust@126.com
|
|
8楼#
发布于:2007-06-27 17:39
太好了,谢谢你,
|
|
9楼#
发布于:2007-07-11 17:14
多谢
|
|
10楼#
发布于:2007-07-29 15:56
多谢!刚学完楚狂人的文件过滤教程,受益颇多!现在要学习ndis了,多谢狂人的教材!谢谢楼主!
|
|
|
11楼#
发布于:2007-09-29 14:20
好啊
|
|
12楼#
发布于:2009-02-28 12:51
学习中,好东西
|
|
13楼#
发布于:2009-05-16 22:40
好东西~~~~
正要好好学习呢~~~~顶 |
|
14楼#
发布于:2009-05-22 21:23
楚狂人的必顶~ |
|
|