协议与网卡的绑定
协议与网卡之间的绑定和之前章节中设备对象之间的绑定不同
一般来说协议和网卡的绑定不是一对一的,而是一对多的,同一个协议是会在同一台主机的所有网卡生效。当然一张网卡也可以绑定不同的多个协议,但是实际上这是没有任何意义的,因为一般来说一个数据包只会被一个协议处理。
这一节我们的主要学习对象是以下两个回调函数
protocolChar.BindAdapterHandler = NdisProtBindAdapter;
protocolChar.UnbindAdapterHandler = NdisProtUnbindAdapter;
这表示协议的绑定与解绑过程
NdisProtBindAdapter的实现主要工作有
1.打开上下文的分配和初始化(通俗易懂的理解就是将打开上下文理解为与绑定相关的一些信息,没有这些信息程序会出错)
2.读取配置(寒江将这部分阉割了)
3.将这个打开上下文保存到全局链表,并调用ndisprotCreateBinding完成绑定
以下是《寒江独钓》中的协议驱动绑定代码
函数原型
VOID
NdisProtBindAdapter(
OUT PNDIS_STATUS pStatus,
IN NDIS_HANDLE BindContext,
IN PNDIS_STRING pDeviceName,
IN PVOID SystemSpecific1,
IN PVOID SystemSpecific2
);
/*++
Routine Description:
Protocol Bind Handler entry point called when NDIS wants us
to bind to an adapter. We go ahead and set up a binding.
An OPEN_CONTEXT structure is allocated to keep state about
this binding.
Arguments:
pStatus - place to return bind status
BindContext - handle to use with NdisCompleteBindAdapter
DeviceName - adapter to bind to
SystemSpecific1 - used to access protocol-specific registry
key for this binding
SystemSpecific2 - unused
Return Value:
No--*/
然后是一些变量的定义
PNDISPROT_OPEN_CONTEXT pOpenContext;//打开上下文结构
NDIS_STATUS Status, ConfigStatus;//状态码
NDIS_HANDLE ConfigHandle;//句柄
UNREFERENCED_PARAMETER(BindContext);//没用到
UNREFERENCED_PARAMETER(SystemSpecific2);//没用到
do
{
//
// Allocate our context for this open.
//
// 分配空间给每个打开上下文。所谓打开上下文就是每次绑定,
// 用户分配的一片空间,用来保存这次绑定相关的信息。这里
// 用宏NPROT_ALLOC_MEM分配内存是为了调试的方便。实际上本
// 质是用NdisAllocateMemoryWithTag分配空间。读者如果用
// ExAllocatePoolWithTag代替也是可行的。只是要注意必须是
// Nonpaged空间。
NPROT_ALLOC_MEM(pOpenContext, sizeof(NDISPROT_OPEN_CONTEXT));
if (pOpenContext == NULL)
{
Status = NDIS_STATUS_RESOURCES;
break;
}
// 内存清0。同样用宏。实际上用的NdisZeroMemory。
NPROT_ZERO_MEM(pOpenContext, sizeof(NDISPROT_OPEN_CONTEXT));
// 给这个空间写一个特征数据便于识别判错。
NPROT_SET_SIGNATURE(pOpenContext, oc);
// 初始化几个用到的数据成员。锁、读队列、写对队列、包队列
// 电源打开事件
NPROT_INIT_LOCK(&pOpenContext->Lock);
NPROT_INIT_LIST_HEAD(&pOpenContext->PendedReads);
NPROT_INIT_LIST_HEAD(&pOpenContext->PendedWrites);
NPROT_INIT_LIST_HEAD(&pOpenContext->RecvPktQueue);
NPROT_INIT_EVENT(&pOpenContext->PoweredUpEvent);
//
// Start off by assuming that the device below is powered up.
//
// 认为开始的时候电源是打开的。
NPROT_SIGNAL_EVENT(&pOpenContext->PoweredUpEvent);
//
// Determine the platform we are running on.
//
// 下面开始检测我们运行在什么平台。首先假定是Win9x.
// 但是为了去掉多余的部分,实际上我已经去掉了对Win9x
// 的支持。所以下面这一段已经没有意义了。但是下面的
// 代码依然有参考价值。实际上是在读取注册表的配置。
//pOpenContext->bRunningOnWin9x = TRUE;
//NdisOpenProtocolConfiguration(
// &ConfigStatus,
// &ConfigHandle,
// (PNDIS_STRING)SystemSpecific1);
//
//if (ConfigStatus == NDIS_STATUS_SUCCESS)
//{
// PNDIS_CONFIGURATION_PARAMETER pParameter;
// NDIS_STRING VersionKey = NDIS_STRING_CONST("Environment");
// NdisReadConfiguration(
// &ConfigStatus,
// &pParameter,
// ConfigHandle,
// &VersionKey,
// NdisParameterInteger);
//
// if ((ConfigStatus == NDIS_STATUS_SUCCESS) &&
// ((pParameter->ParameterType == NdisParameterInteger) ||
// (pParameter->ParameterType == NdisParameterHexInteger)))
// {
// pOpenContext->bRunningOnWin9x =
// (pParameter->ParameterData.IntegerData == NdisEnvironmentWindows);
// }
// NdisCloseConfiguration(ConfigHandle);
//}
//给打开上下文增加一个引用计数
NPROT_REF_OPEN(pOpenContext);
//
// Add it to the global list.
//
// 因为打开上下文已经被分配好。所以这里将这个打开上下文
// 保存到全局链表里以便日后检索。注意这个操作要加锁。实
// 际上这里用的就是读者前面学过的自旋锁。
NPROT_ACQUIRE_LOCK(&Globals.GlobalLock);
NPROT_INSERT_TAIL_LIST(&Globals.OpenList,
&pOpenContext->Link);
NPROT_RELEASE_LOCK(&Globals.GlobalLock);
// 正式的绑定过程。
Status = ndisprotCreateBinding(
pOpenContext,
(PUCHAR)pDeviceName->Buffer,
pDeviceName->Length);
if (Status != NDIS_STATUS_SUCCESS)
{
break;
}
}
while (FALSE);
其实这个函数的内容主要是处理上下文,然后调用ndisprotCreateBinding完成绑定。
绑定网卡的主要是在ndisprotCreateBinding这个函数中实现的,而完成一个绑定只需要调用一个NdisOpenAdapter API,这个API就将一个协议绑定到一个网卡上。
这个API的原型如下:
VOID NdisOpenAdapter(PNDIS_STATUSStatus,
PNDIS_STATUSOpenErrorStatus,
PNDIS_HANDLENdisBindingHandle,
PUINTSelectedMediumIndex,
PNDIS_MEDIUMMediumArray,
UINTMediumArraySize,
NDIS_HANDLENdisProtocolHandle,
NDIS_HANDLEProtocolBindingContext,
PNDIS_STRINGAdapterName,
UINTOpenOptions,
PSTRINGAddressingInformation);
关于这个API的介绍可以参考MSDN
ndisprotCreateBinding中调用这个API如下:
NdisOpenAdapter(
&Status,
&OpenErrorCode,
&pOpenContext->BindingHandle,
&SelectedMediumIndex,
&MediumArray[0],
sizeof(MediumArray) / sizeof(NDIS_MEDIUM),
Globals.NdisProtocolHandle,
(NDIS_HANDLE)pOpenContext,
&pOpenContext->DeviceName,
0,
NULL);
解决绑定竞争
ndisprotCreateBinding主要工作
1.设法防止多线程竞争
2.分配和初始化这次绑定的相关资源
3.获得网卡的一些参数
我们知道防止多线程竞争的主要方式是线程同步,而在内核中线程同步用的最多的就是自旋锁。为什么在内核中自旋锁用的最多?可以参考下面的链接
https://blog.csdn.net/daaikuaichuan/article/details/82950711
使用自旋锁处理线程同步,采用以下方式:
// 获得锁。为什么这里要用锁?这是因为只有对空闲的打开上下文
NPROT_ACQUIRE_LOCK(&pOpenContext->Lock);
..............//添加要保护的代码
// 释放锁。
NPROT_RELEASE_LOCK(&pOpenContext->Lock);
其中NPROT_ACQUIRE_LOCK和NPROT_RELEASE_LOCK
分别对应NdisAcquireSpinLock(_pLock)、 NdisReleaseSpinLock(_pLock)这两个API
分配接收和发送的包池与缓冲池
包池是一组预先已经分配好的”包描述符“,缓冲池是一组已经分配好的”包缓冲区描述符“
为什么要有包池和缓冲池的概念呢?这是因为在NDIS中每一个以太网包是用一个包描述符来描述,并且包内容用包缓冲区描述符来描述的
在发送和接收包的时候,包不是立即接收和发送的,它们存放在缓冲区中排队等待接收和发送,那么这个时候我们可以自己创建两个包池
来容纳发送和接收的包,这样就没有必要多次分配包描述符和包缓冲区描述符。
// 分配包池。用来做发送缓冲区,容纳将要发送出去的包。
//这个函数的作用其实就是分配堆内存
NdisAllocatePacketPoolEx(
&Status,
&pOpenContext->SendPacketPool,
MIN_SEND_PACKET_POOL_SIZE,
MAX_SEND_PACKET_POOL_SIZE - MIN_SEND_PACKET_POOL_SIZE,
sizeof(NPROT_SEND_PACKET_RSVD));
//若不成功就直接返回
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc"
" send packet pool: %x\n", Status));
break;
}
// 分配包池,用来容纳接收包
NdisAllocatePacketPoolEx(
&Status,
&pOpenContext->RecvPacketPool,
MIN_RECV_PACKET_POOL_SIZE,
MAX_RECV_PACKET_POOL_SIZE - MIN_RECV_PACKET_POOL_SIZE,
sizeof(NPROT_RECV_PACKET_RSVD));
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: failed to alloc"
" recv packet pool: %x\n", Status));
break;
}
OID请求的发送和请求完成回调
OIDs是NDIS Object Identifiers的简称,使用这个东东的主要目的是我们需要获得显卡的MAC地址以及最大帧长等MAC层、物理层相关的信息,这些信息对于发送包至关重要。
// 获得下面网卡的Mac地址
Status = ndisprotDoRequest(
pOpenContext,
NdisRequestQueryInformation,
OID_802_3_CURRENT_ADDRESS,
&pOpenContext->CurrentAddress[0],
NPROT_MAC_ADDR_LEN,
&BytesProcessed
);
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: qry current address failed: %x\n",
Status));
break;
}
// 获得网卡选项
Status = ndisprotDoRequest(
pOpenContext,
NdisRequestQueryInformation,
OID_GEN_MAC_OPTIONS,
&pOpenContext->MacOptions,
sizeof(pOpenContext->MacOptions),
&BytesProcessed
);
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: qry MAC options failed: %x\n",
Status));
break;
}
// 获得最大帧长
Status = ndisprotDoRequest(
pOpenContext,
NdisRequestQueryInformation,
OID_GEN_MAXIMUM_FRAME_SIZE,
&pOpenContext->MaxFrameSize,
sizeof(pOpenContext->MaxFrameSize),
&BytesProcessed
);
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: qry max frame failed: %x\n",
Status));
break;
}
// 获得下层连接状态。
Status = ndisprotDoRequest(
pOpenContext,
NdisRequestQueryInformation,
OID_GEN_MEDIA_CONNECT_STATUS,
&GenericUlong,
sizeof(GenericUlong),
&BytesProcessed
);
if (Status != NDIS_STATUS_SUCCESS)
{
DEBUGP(DL_WARN, ("CreateBinding: qry media connect status failed: %x\n",
Status));
break;
}
ndisprotDoRequest这个函数是协议驱动中作者自己封装的一个函数,底层架构其实是调用的NdisRequest这个函数
VOID NdisRequest(
_Out_ PNDIS_STATUS Status,
_In_ NDIS_HANDLE NdisBindingHandle,
_In_ PNDIS_REQUEST NdisRequest
);
所有的OID请求都通过它来发送,返回的NTSTATUS如果是未决,则请求完成时会调用xxxComplete函数
NDIS_STATUS
ndisprotDoRequest(
IN PNDISPROT_OPEN_CONTEXT pOpenContext,
IN NDIS_REQUEST_TYPE RequestType,
IN NDIS_OID Oid,
IN PVOID InformationBuffer,
IN ULONG InformationBufferLength,
OUT PULONG pBytesProcessed
)
{
NDISPROT_REQUEST ReqContext;
PNDIS_REQUEST pNdisRequest = &ReqContext.Request;
NDIS_STATUS Status;
// 初始化一个事件。这个事件会在请求完成函数中被设置,
// 以便通知请求完成了。
NPROT_INIT_EVENT(&ReqContext.ReqEvent);
// 请求的类型。如果只是查询信息,只要用NdisRequestQueryInformation
// 就可以了。
pNdisRequest->RequestType = RequestType;
// 根据不同的请求类型,填写OID和输入输出缓冲区。
switch (RequestType)
{
case NdisRequestQueryInformation:
pNdisRequest->DATA.QUERY_INFORMATION.Oid = Oid;
pNdisRequest->DATA.QUERY_INFORMATION.InformationBuffer =
InformationBuffer;
pNdisRequest->DATA.QUERY_INFORMATION.InformationBufferLength =
InformationBufferLength;
break;
case NdisRequestSetInformation:
pNdisRequest->DATA.SET_INFORMATION.Oid = Oid;
pNdisRequest->DATA.SET_INFORMATION.InformationBuffer =
InformationBuffer;
pNdisRequest->DATA.SET_INFORMATION.InformationBufferLength =
InformationBufferLength;
break;
default:
NPROT_ASSERT(FALSE);
break;
}
// 发送请求
NdisRequest(&Status,
pOpenContext->BindingHandle,
pNdisRequest);
// 如果是未决,则等待事件。这个事件会在完成函数中设置。
if (Status == NDIS_STATUS_PENDING)
{
NPROT_WAIT_EVENT(&ReqContext.ReqEvent, 0);
Status = ReqContext.Status;
}
// 如果成功了的话...
if (Status == NDIS_STATUS_SUCCESS)
{
// 获得结果的长度。这个结果的长度是实际需要的长度。可
// 能比我们实际提供的长度要长。
*pBytesProcessed = (RequestType == NdisRequestQueryInformation)?
pNdisRequest->DATA.QUERY_INFORMATION.BytesWritten:
pNdisRequest->DATA.SET_INFORMATION.BytesRead;
// 如果结果长度比实际上我们提供的缓冲区要长,那么就简
// 单的设置为输入参数中缓冲区的最大长度。
if (*pBytesProcessed > InformationBufferLength)
{
*pBytesProcessed = InformationBufferLength;
}
}
return (Status);
}
完成函数
VOID
NdisProtRequestComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_REQUEST pNdisRequest,
IN NDIS_STATUS Status
)
{
PNDISPROT_OPEN_CONTEXT pOpenContext;
PNDISPROT_REQUEST pReqContext;
// 这两句话起验证的作用,确保输入参数ProtocolBindingContext
// 是合法的。但是对后面的处理没影响。
pOpenContext = (PNDISPROT_OPEN_CONTEXT)ProtocolBindingContext;
NPROT_STRUCT_ASSERT(pOpenContext, oc);
// 从pNdisRequest中得到请求上下文
pReqContext = CONTAINING_RECORD(pNdisRequest, NDISPROT_REQUEST, Request);
// 保存结果状态
pReqContext->Status = Status;
// 设置事件
NPROT_SIGNAL_EVENT(&pReqContext->ReqEvent);
}