协议驱动框架上层与应用层交互,下层与小端口驱动绑定(可能有中间层),主要功能是处理应用层请求发包和接收以太网包的作用。
(1)定义需要用到的变量
(2)生成控制设备:针对不同操作系统调用IoCreateDeviceSecure或IoCreateDevice函数;
生成该设备的符号链接:为了应用层调用该设备对象备用,通过IoCreateSymbolicLink这个API调用;
设置控制设备对象的Flags:主要是设置其为直接IO通信方式,应该是为了方便快速;
初始化锁和链表:初始化锁是为了保护全局变量、同步;链表是为了保存多个打开上下文OpenContext以便日后检索。
(3)在DriverEntry中填写协议特征(实际上就是协议的回调函数列表)
协议特征是一个结构体NDIS_PROTOCOL_CHARACTERISTICS,包含了协议驱动的一系列特征(其实就是回调函数),对应绑定的网卡不同的信息,Windows会采用不同的回调函数处理。
系统会对每个实际存在的网卡实例,调用本协议驱动在协议特征集合中提供的一个回调函数。
发生各种事件时(比如网卡接收到一个新的数据包),特征中的某个函数也会被调用,协议的开发者实现这些函数,就可以在其中决定如何处理接收到的数据包了。
当应用层试图发出一个以太网包时,可以打开这个协议并发出请求(socket或者其他设备接口)
(4)在DriverEntry中使用内核API函数NdisRegisterProtocolDriver,把自己注册成协议驱动
(5)DriverEntry收尾工作,收尾无非就是如果status状态不为success就释放掉上文创建的资源;如果成功则返回status结束DriverEntry。
一般来说协议和网卡的绑定不是一对一的,通常来说,同一个协议是会在同一台主机的所有网卡生效。绑定主要是对应的下面这个回调函数。
protocolChar.BindAdapterHandler = NdisProtBindAdapter;
NdisProtBindAdapter的实现主要工作有
1.打开上下文的分配和初始化(通俗易懂的理解就是将打开上下文理解为与绑定相关的一些信息,没有这些信息程序会出错)
(1)首先是为打开上下文分配内存空间以及清零,代码自己设置了宏NPROT_ALLOC_MEM、NPROT_ZERO_MEM,其实就是AllocatePoolWithTag和NdisZeroMemory这两个个API;
(2)初始化打开上下文结构,包括几个用到的数据成员。锁、读队列、写对队列、包队列等
(3)给打开上下文增加一个引用计数,在释放回调函数的时候对应减去这个引用计数
(4)读取配置(寒江将这部分阉割了)
(5)正式的绑定过程是调用的ndisprotCreateBinding这个函数,并将这个打开上下文保存到全局链表。
(6)绑定网卡的主要是在ndisprotCreateBinding这个函数中实现的,而完成一个绑定只需要调用一个NdisOpenAdapter API,这个API就将一个协议绑定到一个网卡上。
2. ndisprotCreateBinding主要工作
(1.设法防止多线程竞争
使用自旋锁,主要是在内核中一次操作很快,所以自旋锁用的比较多,其中NPROT_ACQUIRE_LOCK和NPROT_RELEASE_LOCK分别对应NdisAcquireSpinLock(_pLock)、 NdisReleaseSpinLock(_pLock)这两个API
(2.分配和初始化这次绑定的相关资源
包池是一组预先已经分配好的“包描述符”,缓冲池是一组已经分配好的“包缓冲区描述符”
为什么要有包池和缓冲池的概念呢?这是因为在NDIS中每一个以太网包是用一个包描述符来描述,并且包内容用包缓冲区描述符来描述的
在发送和接收包的时候,包不是立即接收和发送的,它们存放在缓冲区中排队等待接收和发送,那么这个时候我们可以自己创建两个包池来容纳发送和接收的包,这样就没有必要多次分配包描述符和包缓冲区描述符。
(3.获得网卡的一些参数
OIDs是NDIS Object Identifiers的简称,使用这个东东的主要目的是我们需要获得显卡的MAC地址以及最大帧长等MAC层、物理层相关的信息,这些信息对于发送包至关重要。
获取参数是调用ndisprotDoRequest这个函数。ndisprotDoRequest这个函数是协议驱动中作者自己封装的一个函数,底层架构其实是调用的NdisRequest这个函数,所有的OID请求都通过它来发送,返回的NTSTATUS如果是未决,则请求完成时会调用xxxComplete函数处理,然后等待xxxComplete函数处理完得到结果。
当网卡拔出时,协议驱动会解除与这块网卡的绑定,解除绑定调用的内核API是NdisCloseAdatper。关键点是解决在解绑的同时防止再向网卡发送请求,避免冲突
主要用到的是R3程序APIReadFile、WriteFile、DeviceIoControl
主要步骤是:
1 使用CreateFile打开协议驱动对象的CDO控制设备对象,得到一个句柄。
2 使用DeviceIoControl来进行R0和R3程序的通信
3 使用WriteFile来发送数据包、使用ReadFile来接收包
4 使用CloseFile来关闭句柄,操作结束
内核相应的创建、发包收包请求是IRP_MJ_CREATE、 IRP_MJ_READ、IRP_MJ_WRITE,驱动通过调用对应的分发函数处理对应的请求。
第二点就是接收队列的设置,一个数据包传输完成会先将其保存到“包池”(缓冲区中)。
和协议驱动如出一辙。但是源代码是用的WDF框架编写的
1 首先进入DriverEntry检查版本号(可选),调用NdisGetVersion()API
2. WDF_NO_EVENT_CALLBACK初始化驱动标志
WDF_DRIVER_CONFIG_INIT(&config, WDF_NO_EVENT_CALLBACK);
3. 设置WdfDriverInitNoDispatchOverride表示框架不能拦截IO直接发给驱动的Irps
config.DriverInitFlags |= WdfDriverInitNoDispatchOverride;
4. 创建WDFDriver对象
调用WdfDriverCreate这个API
5. 初始化一个包装句柄(Wrapper Handler)
初始化包装句柄。这个句柄是注册小端口必须的。但是对小端口驱动的开发者而言,除了调用一些NDIS函数需要提供这个句柄之外,并没有什么实质的意义
6 填写小端口特征、注册小端口 其中因为要保护全局变量 所以需要有一个锁 另外需要有一个链表储存需要的信息 所以要初始化它们
注册小端口使用NdisMRegisterMiniport,这个API需要包装句柄与小端口特征。
初始化全局变量。这些全局变量是在整个驱动中使用的
NdisAllocateSpinLock(&GlobalData.Lock)和
NdisInitializeListHead(&GlobalData.AdapterList);
到这里小端口DriverEntry就结束了,逻辑很简单和协议驱动完全一致
(1)打开协议驱动设备对象
一般的IO目标只能打开本驱动生成的设备,这种IO目标被称为本地IO目标,如果要打开其他驱动生成的设备,必须使用远程IO目标
远程IO目标使用WDF的内核API函数WdfIoTargetCreate来生成,用WdfIoTargetOpen来绑定
(2)给IO目标发送DeviceIoControl请求
IO目标和设备对象不同,不能使用Zw系列函数来发送请求
所以要给IO目标发送同步的控制请求需要用WdfIoTargetSendIoCtlSynchronously这个API函数
(3)打开ndisprot接口并完成配置设备
ndisprot是一个协议驱动,这个协议驱动绑定了所有的网卡,本质上来说协议驱动绑定了所有的小端口驱动的每个实例,所以ndisprot也会和ndisedge绑定
现在ndisedge要操纵ndisprot进行收发数据包
步骤
1打开ndisprot的控制设备,这使用生成IO目标,打开IO目标来完成;
2给ndisprot的控制设备发送功能码为IOCTL_NDISPROT_BIND_WAIT的控制请求来等待绑定完成;
3不断发送IOCTL_NDISPROT_QUERY_BINDING来查询它的每个绑定,直到找到第一个名字与ndisedge生成的实例不同的绑定,或者遍历完都没有找到返回错误;
4找到一个就发送IOCTL_NDISPROT_OPEN_DEVICE来指定使用这个绑定发送\接收数据包;
这一系列的操作在NICOpenNdisProtocolInterface中完成,这个函数最终是被MPInitialize调用。
(1)使用ndisprot发送包
主要是编写小端口驱动的发包接口,发送控制块TCB,遍历包组找到不是自己的另外的驱动对象填写TCB
(2)使用ndisprot接收包
提交数据包的内核API,从接收控制块(RCB)提交包,对ndisprot读请求的完成函数,读请求的发送,用于读包的WDF工作任务,ndisedge读工作任务的生成与入列。
中间层驱动的DriverEntry和前面两种没有什么区别,并且其中的套路API基本一样,所以就不赘述了
DriverEntry入口函数步骤如下
1 初始化包装句柄
2 注册小端口特征
3 注册协议特征
4 关联两个接口
通过调用NdisIMAssociateMiniport将协议驱动包装句柄和小端口驱动包装句柄传入这个函数将NDIS协议和小端口联系起来