驱动程序开发第五部分:传输设备接口入门






4.92/5 (71投票s)
2005 年 3 月 29 日
34分钟阅读

325649

6205
TDI 客户端驱动程序简介及更多 IRP 处理。
引言
欢迎来到驱动程序开发系列的第五部分。本文的标题有些误导。是的,我们将编写一个 TDI 客户端作为演示,但这并不是本教程的主要目标。本教程的主要目标是进一步探讨如何处理和交互 IRP。本教程将探讨如何排队和处理 IRP 的取消。本文的真正标题应该是“IRP 处理简介”,但它不够吸引人!另外,这也不是完全撒谎,我们将在演示实现 TDI 客户端驱动程序的过程中完成这些。所以,我实际上也必须解释这部分是如何实现的。提供的示例是一个非常简单的客户端/服务器聊天程序,我们将使用它来探索如何处理 IRP。
套接字复习
我们首先从你应该已经知道的内容开始。如果你不知道,你可能想阅读一些关于该主题的其他文章。即便如此,我还是提供了一个快速复习课程以及如何实现 winsock 的示例源代码。
什么是 IP?
IP 或“互联网协议”本质上是一种用于在两台计算机之间发送数据或数据包的协议。此协议不需要任何设置,只需要网络上的每台计算机都有一个唯一的“IP 地址”。然后可以使用“IP 地址”来路由通信端点之间的数据包。此协议提供路由,但不提供可靠性。仅通过 IP 发送的数据包可能会损坏、乱序或根本不到达。但是,在 IP 之上实现了其他协议来提供这些功能。“IP”协议位于 OSI 模型中的网络层。
什么是 TCP?
TCP 被称为“传输控制协议”,它位于“IP”协议之上。这通常也称为“TCP/IP”。“IP”层提供路由,“TCP”层提供可靠、有序、无损的数据传输。为了区分计算机上的多个 TCP 传输,它们通过唯一的 TCP 端口号进行标识。这样,多个应用程序甚至同一个应用程序都可以打开通信管道,底层传输将能够正确地在每个端点之间路由数据。“TCP”协议位于 OSI 模型中的传输层。然后有其他协议在此之上,例如 FTP、HTTP 等。这些协议位于 OSI 模型中的“应用层”。
协议分层
在某种意义上,通信栈的任何部分都可以被“等效”协议替换。例如,如果 FTP 需要可靠的传输和路由,那么位于提供此功能的任何协议之上仍然可以工作。在该示例中,如果应用程序使用的是“SPX”而不是“TCP/IP”,则应该没有区别。从这个意义上说,如果“TCP”或“TCP”的某种实现位于“IPX”等不可靠协议之上,它应该可以工作。“某些实现”可以工作的原因是,这显然取决于上层协议对底层协议的实际实现和内部工作原理的依赖程度。
什么是套接字?
“套接字”通常被称为由“套接字”库实现的通信端点。“套接字”库 API 通常被编写成一种简单(在某些情况下是可移植的)的方式,用于从用户模式实现网络应用程序。套接字 API 有几种不同的形式,但在 Windows 中我们使用“WINSOCK”。Winsock 的某些方面可以实现为可移植的(我曾经实现过一个 winsock 应用程序,它可以在 Unix 和 Windows NT 上以最小的冲突进行编译,当然它是一个非常简单的程序),而另一些方面则不能直接移植。
套接字服务器应用程序
套接字连接的服务器端仅接受传入连接。每个新连接都会获得一个单独的句柄,以便服务器可以单独与每个客户端通信。以下概述了通信所用的步骤。
第一步:创建套接字
第一步是创建一个套接字。以下代码显示了如何为流(TCP/IP)创建套接字。
hSocket = socket(PF_INET, SOCK_STREAM, 0); if(hSocket == INVALID_SOCKET) { /* Error */ }
这只是一个网络驱动程序的句柄。您可以在其他套接字 API 调用中使用此句柄。
第二步:绑定套接字
第二步是将套接字绑定到 TCP 端口和 IP 地址。以下代码演示了此行为。在我们的示例中,套接字仅使用一个数字创建,但通常您应该使用宏将端口放入网络字节顺序。
SockAddr.sin_family = PF_INET; SockAddr.sin_port = htons(4000); /* Must be in NETWORK BYTE ORDER */ /* * BIND the Socket to a Port */ uiErrorStatus = bind(hSocket, (struct sockaddr *)&SockAddr, sizeof(SOCKADDR_IN)); if(uiErrorStatus == INVALID_SOCKET) { /* Error */ }
此操作将套接字句柄与端口地址绑定。您也可以指定 IP 地址,但使用“0”只是允许驱动程序绑定到任何 IP 地址(本地地址)。您还可以为端口地址指定“0”以绑定到随机端口。但是,服务器通常使用固定的端口号,因为客户端仍然需要找到它们,但也有例外。
第三步:监听套接字
这将使套接字进入监听状态。调用此函数后,套接字将能够监听连接。指定的数字只是此套接字允许的等待接受的连接的积压。
if(listen(hSocket, 5) != 0) { /* Error */ }
第四步:接受连接
accept
API 将为每个传入连接提供一个新的句柄。以下是使用 accept 的代码示例。
if((hNewClient = accept(pServerInfo->hServerSocket, (struct sockaddr *)&NewClientSockAddr, &uiLength)) != INVALID_SOCKET) {
返回的句柄随后可用于发送和接收数据。
第五步:关闭套接字
完成后,您需要关闭所有句柄,就像其他任何事情一样!
closesocket(hNewClient);
这里省略了一个额外的细节,关于使用 select
API 来获取连接到来和数据可用的通知。这只是一个复习,有关更多详细信息,您应该查阅套接字教程或 MSDN 等 API 参考。
套接字客户端应用程序
套接字通信的客户端侧只需连接到服务器,然后发送/接收数据。以下步骤分解了如何设置此通信。
第一步:创建套接字
第一步是创建一个套接字。以下代码显示了如何为流(TCP/IP)创建套接字。
hSocket = socket(PF_INET, SOCK_STREAM, 0); if(hSocket == INVALID_SOCKET) { /* Error */ }
这只是一个网络驱动程序的句柄。您可以在其他套接字 API 调用中使用此句柄。
第二步:连接到服务器
您需要设置要连接的服务器的地址和端口,并且它们必须是网络字节顺序。然后,您将调用 connect
API 来建立客户端和服务器之间的连接。
pClientConnectInfo->SockHostAddress.sin_family = PF_INET; pClientConnectInfo->SockHostAddress.sin_port = htons(4000); /* Network Byte Order! */ printf("Enter Host IP Address like: 127.0.0.1\n"); fgets(szHostName, 100, stdin); pClientConnectInfo->SockHostAddress.sin_addr.s_addr = inet_addr(szHostName); /* Network Byte Order! */ iRetVal = connect(hSocket, (LPSOCKADDR)&pClientConnectInfo->SockHostAddress, sizeof(SOCKADDR_IN)); if(iRetVal == INVALID_SOCKET) { /* Error */ }
第三步:发送和接收数据
一旦连接,您就可以使用 recv
和 send
API 随时发送和接收数据。
iRetVal = send(hSocket, szBuffer, strlen(szBuffer), 0); if(iRetVal == SOCKET_ERROR) { /* Error */ } ... iRetVal = recv(hSocket, szBuffer, 1000, 0); if(iRetVal == 0 || iRetVal == SOCKET_ERROR) { /* Error */ }
请注意,这些示例可能指的是发送和接收字符串,但是可以发送任何二进制数据。
第四步:关闭套接字
完成后,您需要关闭所有句柄,就像其他任何事情一样!
closesocket(hSocket);
这里省略了一个额外的细节,关于使用 select
API 来获取数据可用的通知。这只是一个复习,套接字的大部分细节都被省略了,因此有关更多详细信息,您应该查阅套接字教程或 MSDN 等 API 参考。
传输设备接口
套接字入门教程实际上是为了让您为 TDI API 做好准备。“传输设备接口”是一组 API,驱动程序可以使用这些 API 与传输(协议)驱动程序(如 TCP)进行通信。TCP 驱动程序将实现此 API 集,以便您的驱动程序可以与其通信。这比使用套接字要复杂一些,MSDN 上的文档可能比有用的更令人困惑。所以我们将回顾建立客户端连接所需的所有步骤。一旦您理解了这一点,您应该能够使用该 API 来执行其他操作,例如创建服务器。
架构
下图概述了 TDI/NDIS 关系。通常,TDI 是一个标准的接口,传输/协议驱动程序开发人员可以在其驱动程序中实现。这样,希望使用其协议的开发人员无需麻烦为他们希望支持的每种协议实现单独的接口。这并不意味着这些开发人员仅限于实现 TDI。他们还可以在其驱动程序的顶层实现任何专有接口。我不是 NDIS 专家,所以我将简单解释一下,希望不会出错!这些只是“要知道”类型的信息,我们无需理解其中任何一个即可使用 TDI 客户端驱动程序。
协议驱动程序将与驱动程序底层的 NDIS 接口 API 通信。协议驱动程序的任务就是实现协议并与 NDIS 通信。驱动程序的上层可以是专有接口、TDI 或两者兼有。顺便说一下,这些不是“NDIS 客户端”。它们不存在。有一些网站将这些驱动程序称为“NDIS 客户端”,这是完全错误的。我曾经问过一位 NDIS 专家关于“NDIS 客户端”的事情,他不知道我在说什么!
- NDIS 协议驱动程序
下一层是中间层驱动程序。这些驱动程序可以进行数据转换、数据包调度或过滤。
- NDIS 中间层驱动程序
最后一层是 NDIS 微端口驱动程序。它实际上与物理 NIC 设备通信。
- NDIS 微端口驱动程序
您可以在 MSDN 上找到有关 TDI 和 NDIS 体系结构的更多信息。
第一步:打开传输地址
第一步是创建一个“传输地址”的句柄。这将需要您使用 ZwCreateFile
来创建一个指向“传输地址”实例的句柄。“传输地址”是本地机器的 IP 地址。这不是远程机器!允许您绑定到特定 IP 地址的原因是,当多台 IP 地址与本地计算机关联时,例如当安装了多个 NIC 时。您也可以简单地指定“0.0.0.0”来获取任何随机 NIC。
打开此句柄的方法对于不习惯开发驱动程序的来说有点晦涩。您必须指定“EA”或“扩展属性”,然后通过 IRP_MJ_CREATE
传递给驱动程序!是的,除了在 DOS 设备名称的末尾添加内容(我们上篇文章中已这样做)之外,还可以将参数传递到打开操作中。此时您还可以指定本地端口。如果您正在创建服务器,那么现在是指定端口的时候。由于我们只实现客户端连接,因此我们不关心端口,所以端口保留为 0。
以下代码演示了如何打开传输地址。
NTSTATUS TdiFuncs_OpenTransportAddress(PHANDLE pTdiHandle, PFILE_OBJECT *pFileObject) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; UNICODE_STRING usTdiDriverNameString; OBJECT_ATTRIBUTES oaTdiDriverNameAttributes; IO_STATUS_BLOCK IoStatusBlock; char DataBlob[sizeof(FILE_FULL_EA_INFORMATION) + TDI_TRANSPORT_ADDRESS_LENGTH + 300] = {0}; PFILE_FULL_EA_INFORMATION pExtendedAttributesInformation = (PFILE_FULL_EA_INFORMATION)&DataBlob; UINT dwEASize = 0; PTRANSPORT_ADDRESS pTransportAddress = NULL; PTDI_ADDRESS_IP pTdiAddressIp = NULL; /* * Initialize the name of the device to be opened. ZwCreateFile takes an * OBJECT_ATTRIBUTES structure as the name of the device to open. * This is then a two step process. * * 1 - Create a UNICODE_STRING data structure from a unicode string. * 2 - Create a OBJECT_ATTRIBUTES data structure from a UNICODE_STRING. * */ RtlInitUnicodeString(&usTdiDriverNameString, L"\\Device\\Tcp"); InitializeObjectAttributes(&oaTdiDriverNameAttributes, &usTdiDriverNameString, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); /* * The second step is to initialize the Extended Attributes data structure. * * EaName = TdiTransportAddress, 0, TRANSPORT_ADDRESS * EaNameLength = Length of TdiTransportAddress * EaValueLength = Length of TRANSPORT_ADDRESS */ RtlCopyMemory(&pExtendedAttributesInformation->EaName, TdiTransportAddress, TDI_TRANSPORT_ADDRESS_LENGTH); pExtendedAttributesInformation->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH; pExtendedAttributesInformation->EaValueLength = TDI_TRANSPORT_ADDRESS_LENGTH + sizeof(TRANSPORT_ADDRESS) + sizeof(TDI_ADDRESS_IP); pTransportAddress = (PTRANSPORT_ADDRESS)(&pExtendedAttributesInformation->EaName + TDI_TRANSPORT_ADDRESS_LENGTH + 1); /* * The number of transport addresses */ pTransportAddress->TAAddressCount = 1; /* * This next piece will essentially describe what * the transport being opened is. * AddressType = Type of transport * AddressLength = Length of the address * Address = A data structure that is essentially * related to the chosen AddressType. */ pTransportAddress->Address[0].AddressType = TDI_ADDRESS_TYPE_IP; pTransportAddress->Address[0].AddressLength = sizeof(TDI_ADDRESS_IP); pTdiAddressIp = (TDI_ADDRESS_IP *)&pTransportAddress->Address[0].Address; /* * The TDI_ADDRESS_IP data structure is essentially simmilar to * the usermode sockets data structure. * sin_port * sin_zero * in_addr * *NOTE: This is the _LOCAL ADDRESS OF THE CURRENT MACHINE_ Just as with * sockets, if you don't care what port you bind this connection to t * hen just use "0". If you also only have one network card interface, * there's no reason to set the IP. "0.0.0.0" will simply use the * current machine's IP. If you have multiple NIC's or a reason to * specify the local IP address then you must set TDI_ADDRESS_IP * to that IP. If you are creating a server side component you may * want to specify the port, however usually to connectto another * server you really don't care what port the client is opening. */ RtlZeroMemory(pTdiAddressIp, sizeof(TDI_ADDRESS_IP)); dwEASize = sizeof(DataBlob); NtStatus = ZwCreateFile(pTdiHandle, FILE_READ_EA | FILE_WRITE_EA, &oaTdiDriverNameAttributes, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, pExtendedAttributesInformation, dwEASize); if(NT_SUCCESS(NtStatus)) { NtStatus = ObReferenceObjectByHandle(*pTdiHandle, GENERIC_READ | GENERIC_WRITE, NULL, KernelMode, (PVOID *)pFileObject, NULL); if(!NT_SUCCESS(NtStatus)) { ZwClose(*pTdiHandle); } } return NtStatus; }
这在 MSDN 上有介绍。
第二步:打开连接上下文
第二步是打开连接上下文。这是您将在后续所有操作中实际使用的句柄。这也由 ZwCreateFile
完成,并且也在同一个设备“\Device\Tcp”上执行。该设备实际上允许您打开三种不同的句柄:传输句柄、连接上下文和控制句柄。一个常见的错误是认为句柄打开成功了,但实际上是打开了错误的句柄!这是因为它们使用“扩展属性”来确定正在打开哪个句柄。显然,如果驱动程序不识别 EA 值,它将简单地打开默认句柄类型“Control”!这在创建的描述中已有 MSDN 中进行了记录。
以下代码演示了如何打开连接上下文。请注意,您还可以指定一个称为“CONNECTION_CONTEXT
”的指针值,它只是一个指向用户定义数据的指针。稍后您可能会注意到,某些事件回调会将此指针返回给您。这本质上是您可以使用的上下文值。
NTSTATUS TdiFuncs_OpenConnection(PHANDLE pTdiHandle, PFILE_OBJECT *pFileObject) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; UNICODE_STRING usTdiDriverNameString; OBJECT_ATTRIBUTES oaTdiDriverNameAttributes; IO_STATUS_BLOCK IoStatusBlock; char DataBlob[sizeof(FILE_FULL_EA_INFORMATION) + TDI_CONNECTION_CONTEXT_LENGTH + 300] = {0}; PFILE_FULL_EA_INFORMATION pExtendedAttributesInformation = (PFILE_FULL_EA_INFORMATION)&DataBlob; UINT dwEASize = 0; /* * Initialize the name of the device to be opened. ZwCreateFile * takes an OBJECT_ATTRIBUTES structure as the name of the device * to open. This is then a two step process. * * 1 - Create a UNICODE_STRING data structure from a unicode string. * 2 - Create a OBJECT_ATTRIBUTES data structure from a UNICODE_STRING. * */ RtlInitUnicodeString(&usTdiDriverNameString, L"\\Device\\Tcp"); InitializeObjectAttributes(&oaTdiDriverNameAttributes, &usTdiDriverNameString, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); /* * The second step is to initialize the Extended Attributes data structure. * * EaName = TdiConnectionContext, 0, Your User Defined Context Data * (Actually a pointer to it) * EaNameLength = Length of TdiConnectionContext * EaValueLength = Entire Length */ RtlCopyMemory(&pExtendedAttributesInformation->EaName, TdiConnectionContext, TDI_CONNECTION_CONTEXT_LENGTH); pExtendedAttributesInformation->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH; pExtendedAttributesInformation->EaValueLength = TDI_CONNECTION_CONTEXT_LENGTH; /* Must be at least TDI_CONNECTION_CONTEXT_LENGTH */ dwEASize = sizeof(DataBlob); NtStatus = ZwCreateFile(pTdiHandle, FILE_READ_EA | FILE_WRITE_EA, &oaTdiDriverNameAttributes, &IoStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, 0, pExtendedAttributesInformation, dwEASize); if(NT_SUCCESS(NtStatus)) { NtStatus = ObReferenceObjectByHandle(*pTdiHandle, GENERIC_READ | GENERIC_WRITE, NULL, KernelMode, (PVOID *)pFileObject, NULL); if(!NT_SUCCESS(NtStatus)) { ZwClose(*pTdiHandle); } } return NtStatus; }
这在 MSDN 上有介绍。
第三步:关联传输地址和连接上下文
在执行任何操作之前,您需要关联两个句柄:传输句柄和连接句柄。这是通过向设备发送 IOCTL 来完成的。如果您还记得如何发送 IOCTL,我们需要分配一个 IRP,设置参数,然后将其发送到设备。但是,这已经简化了,因为 TDI 头文件提供了宏和其他函数来完成这项工作。TdiBuildInternalDeviceControlIrp
实际上是调用 IoBuildDeviceIoControlRequest
的宏。此宏的一些参数实际上被忽略,但仅用于注释(例如提供的 IOCTL!)。此 API 很简单,我们在下面将其用于演示目的;但是,使用其他创建 IRP 的机制(例如稍后将介绍的 IoAllocateIrp
)也有优势。我们将使用的其他宏只是为下一个底层驱动程序设置 IO_STACK_LOCATION
的参数。
您可能在这里注意到的一个与上次讨论不同的地方是“STATUS_PENDING
”。这将在本教程稍后讨论。
以下代码演示了如何执行此操作。
NTSTATUS TdiFuncs_AssociateTransportAndConnection(HANDLE hTransportAddress, PFILE_OBJECT pfoConnection) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to send these * requests to the TDI Driver. */ pTdiDevice = IoGetRelatedDeviceObject(pfoConnection); /* * Step 1: Build the IRP. TDI defines several macros and functions * that can quickly create IRP's, etc. for variuos purposes. * While this can be done manually it's easiest to use the macros. * * http://msdn.microsoft.com/library/en-us/network/hh/network/ * 34bldmac_f430860a-9ae2-4379-bffc-6b0a81092e7c.xml.asp?frame=true */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_ASSOCIATE_ADDRESS, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Add the correct parameters into the IRP. */ TdiBuildAssociateAddress(pIrp, pTdiDevice, pfoConnection, NULL, NULL, hTransportAddress); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means that the IRP * will not be completed synchronously and the driver has queued the * IRP for later processing. This is fine but we do not want * to return this thread, we are a synchronous call so we want * to wait until it has completed. The EVENT that we provided will * be set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } } return NtStatus; }
这在 MSDN 上有介绍。
第四步:连接
要创建 TCP 连接的客户端,我们需要连接!
NTSTATUS TdiFuncs_Connect(PFILE_OBJECT pfoConnection, UINT uiAddress, USHORT uiPort) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; TDI_CONNECTION_INFORMATION RequestConnectionInfo = {0}; TDI_CONNECTION_INFORMATION ReturnConnectionInfo = {0}; LARGE_INTEGER TimeOut = {0}; UINT NumberOfSeconds = 60*3; char cBuffer[256] = {0}; PTRANSPORT_ADDRESS pTransportAddress =(PTRANSPORT_ADDRESS)&cBuffer; PTDI_ADDRESS_IP pTdiAddressIp; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to send these * requests to the TDI Driver. */ pTdiDevice = IoGetRelatedDeviceObject(pfoConnection); /* * Step 1: Build the IRP. TDI defines several macros and functions * that can quickly create IRP's, etc. for variuos purposes. * While this can be done manually it's easiest to use the macros. * * http://msdn.microsoft.com/library/en-us/network/hh/network/ * 34bldmac_f430860a-9ae2-4379-bffc-6b0a81092e7c.xml.asp?frame=true */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_CONNECT, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Add the correct parameters into the IRP. */ /* * Time out value */ TimeOut.QuadPart = 10000000L; TimeOut.QuadPart *= NumberOfSeconds; TimeOut.QuadPart = -(TimeOut.QuadPart); /* * Initialize the RequestConnectionInfo which specifies * the address of the REMOTE computer */ RequestConnectionInfo.RemoteAddress = (PVOID)pTransportAddress; RequestConnectionInfo.RemoteAddressLength = sizeof(PTRANSPORT_ADDRESS) + sizeof(TDI_ADDRESS_IP); /* * The number of transport addresses */ pTransportAddress->TAAddressCount = 1; /* * This next piece will essentially describe what the * transport being opened is. * AddressType = Type of transport * AddressLength = Length of the address * Address = A data structure that is essentially * related to the chosen AddressType. */ pTransportAddress->Address[0].AddressType = TDI_ADDRESS_TYPE_IP; pTransportAddress->Address[0].AddressLength = sizeof(TDI_ADDRESS_IP); pTdiAddressIp = (TDI_ADDRESS_IP *)&pTransportAddress->Address[0].Address; /* * The TDI_ADDRESS_IP data structure is essentially simmilar * to the usermode sockets data structure. * sin_port * sin_zero * in_addr */ /* * Remember, these must be in NETWORK BYTE ORDER (Big Endian) */ /* Example: 1494 = 0x05D6 (Little Endian) or 0xD605 (Big Endian)*/ pTdiAddressIp->sin_port = uiPort; /* Example: 10.60.2.159 = 0A.3C.02.9F (Little Endian) or 9F.02.3C.0A (Big Endian) */ pTdiAddressIp->in_addr = uiAddress; TdiBuildConnect(pIrp, pTdiDevice, pfoConnection, NULL, NULL, &TimeOut, &RequestConnectionInfo, &ReturnConnectionInfo); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means * that the IRP will not be completed synchronously * and the driver has queued the IRP for later processing. * This is fine but we do not want to return this thread, * we are a synchronous call so we want to wait until * it has completed. The EVENT that we provided will be * set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } } return NtStatus; }
这在 MSDN 上有介绍。
第五步:发送和接收数据
要发送数据,您只需创建一个 TDI_SEND IOCTL
并将其传递给传输设备。以下代码实现了发送。
NTSTATUS TdiFuncs_Send(PFILE_OBJECT pfoConnection, PVOID pData, UINT uiSendLength, UINT *pDataSent) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; PMDL pSendMdl; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to * send these requests to the TDI Driver. */ pTdiDevice = IoGetRelatedDeviceObject(pfoConnection); *pDataSent = 0; /* * The send requires an MDL which is what you may remember from DIRECT_IO. * However, instead of using an MDL we need to create one. */ pSendMdl = IoAllocateMdl((PCHAR )pData, uiSendLength, FALSE, FALSE, NULL); if(pSendMdl) { __try { MmProbeAndLockPages(pSendMdl, KernelMode, IoModifyAccess); } __except (EXCEPTION_EXECUTE_HANDLER) { IoFreeMdl(pSendMdl); pSendMdl = NULL; }; if(pSendMdl) { /* * Step 1: Build the IRP. TDI defines several macros and functions * that can quickly create IRP's, etc. for variuos purposes. * While this can be done manually it's easiest to use * the macros. */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_SEND, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Add the correct parameters into the IRP. */ TdiBuildSend(pIrp, pTdiDevice, pfoConnection, NULL, NULL, pSendMdl, 0, uiSendLength); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means that the * IRP will not be completed synchronously and the driver has * queued the IRP for later processing. This is fine but we do * not want to return this not want to return this not want to * return this to wait until it has completed. The EVENT * that we providedwill be set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); } NtStatus = IoStatusBlock.Status; *pDataSent = (UINT)IoStatusBlock.Information; /* * I/O Manager will free the MDL * if(pSendMdl) { MmUnlockPages(pSendMdl); IoFreeMdl(pSendMdl); } */ } } } return NtStatus; }
使用 TDI_RECIEVE
也可以做到接收,但我们的实现没有使用它。如果您注意到,您实际上可以创建通知回调来告诉您何时有数据或其他事件。这就是我们所做的,我实现的用于创建任何事件处理程序的 API 包装器如下。
NTSTATUS TdiFuncs_SetEventHandler(PFILE_OBJECT pfoTdiFileObject, LONG InEventType, PVOID InEventHandler, PVOID InEventContext) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; LARGE_INTEGER TimeOut = {0}; UINT NumberOfSeconds = 60*3; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to send these * requests to the TDI Driver. */ pTdiDevice = IoGetRelatedDeviceObject(pfoTdiFileObject); /* * Step 1: Build the IRP. TDI defines several macros and functions * that can quickly create IRP's, etc. for variuos purposes. * While this can be done manually it's easiest to use the macros. * */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_SET_EVENT_HANDLER, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Set the IRP Parameters */ TdiBuildSetEventHandler(pIrp, pTdiDevice, pfoTdiFileObject, NULL, NULL, InEventType, InEventHandler, InEventContext); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means that * the IRP will not be completed synchronously and the driver has * queued the IRP for later processing. This is fine but we do not * want to return this thread, we are a synchronous call so we want * to wait until it has completed. The EVENT that we provided * will be set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } } return NtStatus; }
使用此 API 并实现回调的代码如下。
NtStatus = TdiFuncs_SetEventHandler( pTdiExampleContext->TdiHandle.pfoTransport, TDI_EVENT_RECEIVE, TdiExample_ClientEventReceive, (PVOID)pTdiExampleContext); ... NTSTATUS TdiExample_ClientEventReceive(PVOID TdiEventContext, CONNECTION_CONTEXT ConnectionContext, ULONG ReceiveFlags, ULONG BytesIndicated, ULONG BytesAvailable, ULONG *BytesTaken, PVOID Tsdu, PIRP *IoRequestPacket) { NTSTATUS NtStatus = STATUS_SUCCESS; UINT uiDataRead = 0; PTDI_EXAMPLE_CONTEXT pTdiExampleContext = (PTDI_EXAMPLE_CONTEXT)TdiEventContext; PIRP pIrp; DbgPrint("TdiExample_ClientEventReceive 0x%0x, %i, %i\n", ReceiveFlags, BytesIndicated, BytesAvailable); *BytesTaken = BytesAvailable; /* * This implementation is extremely simple. We do not queue * data if we do not have an IRP to put it there. We also * assume we always get the full data packet sent every recieve. * These are Bells and Whistles that can easily be added to * any implementation but would help to make the implementation * more complex and harder to follow the underlying idea. Since * those essentially are common-sense add ons they are ignored and * the general implementation of how to Queue IRP's and * recieve data are implemented. * */ pIrp = HandleIrp_RemoveNextIrp(pTdiExampleContext->pReadIrpListHead); if(pIrp) { PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(pIrp); uiDataRead = BytesAvailable > pIoStackLocation->Parameters.Read.Length ? pIoStackLocation->Parameters.Read.Length : BytesAvailable; pIrp->Tail.Overlay.DriverContext[0] = NULL; RtlCopyMemory(pIrp->AssociatedIrp.SystemBuffer, Tsdu, uiDataRead); pIrp->IoStatus.Status = NtStatus; pIrp->IoStatus.Information = uiDataRead; IoCompleteRequest(pIrp, IO_NETWORK_INCREMENT); } /* * The I/O Request can be used to recieve the rest of the data. * We are not using it in this example however and will actually * be assuming that we always get all the data. * */ *IoRequestPacket = NULL; return NtStatus; }
不要被 HandleIrp_RemoveNextIrp
吓到。我们将在本文后面详细介绍如何排队 IRP 请求。
这在 MSDN 上有介绍。
第六步:断开连接
这没什么特别的,您只需实现 TDI_DISCONNECT IOCTL
来断开连接。
NTSTATUS TdiFuncs_Disconnect(PFILE_OBJECT pfoConnection) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; TDI_CONNECTION_INFORMATION ReturnConnectionInfo = {0}; LARGE_INTEGER TimeOut = {0}; UINT NumberOfSeconds = 60*3; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to send * these requests to the TDI Driver. */ pTdiDevice = IoGetRelatedDeviceObject(pfoConnection); /* * Step 1: Build the IRP. TDI defines several macros and functions * that can quickly create IRP's, etc. for variuos purposes. * While this can be done manually it's easiest to use the macros. * */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_DISCONNECT, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Add the correct parameters into the IRP. */ /* * Time out value */ TimeOut.QuadPart = 10000000L; TimeOut.QuadPart *= NumberOfSeconds; TimeOut.QuadPart = -(TimeOut.QuadPart); TdiBuildDisconnect(pIrp, pTdiDevice, pfoConnection, NULL, NULL, &TimeOut, TDI_DISCONNECT_ABORT, NULL, &ReturnConnectionInfo); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means that the * IRP will not be completed synchronously and the driver has * queued the IRP for later processing. This is fine but we do * not want to return this thread, we are a synchronous call so * we want to wait until it has completed. The EVENT that * we provided will be set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } } return NtStatus; }
这在 MSDN 上有介绍。
第七步:取消关联句柄
这很简单,我们只需实现另一个 IOCTL 调用,如下所示。
NTSTATUS TdiFuncs_DisAssociateTransportAndConnection(PFILE_OBJECT pfoConnection) { NTSTATUS NtStatus = STATUS_INSUFFICIENT_RESOURCES; PIRP pIrp; IO_STATUS_BLOCK IoStatusBlock = {0}; PDEVICE_OBJECT pTdiDevice; TDI_COMPLETION_CONTEXT TdiCompletionContext; KeInitializeEvent(&TdiCompletionContext.kCompleteEvent, NotificationEvent, FALSE); /* * The TDI Device Object is required to send these requests to the TDI Driver. * */ pTdiDevice = IoGetRelatedDeviceObject(pfoConnection); /* * Step 1: Build the IRP. TDI defines several macros and * functions that can quickly create IRP's, etc. for * variuos purposes. While this can be done manually * it's easiest to use the macros. * */ pIrp = TdiBuildInternalDeviceControlIrp(TDI_DISASSOCIATE_ADDRESS, pTdiDevice, pfoConnection, &TdiCompletionContext.kCompleteEvent, &IoStatusBlock); if(pIrp) { /* * Step 2: Add the correct parameters into the IRP. */ TdiBuildDisassociateAddress(pIrp, pTdiDevice, pfoConnection, NULL, NULL); NtStatus = IoCallDriver(pTdiDevice, pIrp); /* * If the status returned is STATUS_PENDING this means that the * IRP will not be completed synchronously and the driver has * queued the IRP for later processing. This is fine but we * do not want to return this thread, we are a synchronous call * so we want to wait until it has completed. The EVENT that we * provided will be set when the IRP completes. */ if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&TdiCompletionContext.kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } } return NtStatus; }
这在 MSDN 上有介绍。
第八步:关闭句柄
此函数用于两个句柄:传输句柄和连接上下文句柄。
NTSTATUS TdiFuncs_CloseTdiOpenHandle(HANDLE hTdiHandle, PFILE_OBJECT pfoTdiFileObject) { NTSTATUS NtStatus = STATUS_SUCCESS; /* * De-Reference the FILE_OBJECT and Close The Handle */ ObDereferenceObject(pfoTdiFileObject); ZwClose(hTdiHandle); return NtStatus; }
这在 MSDN 上有介绍。
其他资源
一旦您熟悉了 TDI 接口,它就会变得更容易一些。编写任何驱动程序时,最大的事情之一就是正确处理 IRP。TDI 似乎比套接字复杂一些,但它是一个内核接口。
如果您曾经研究过 TDI 或 NDIS,您可能已经遇到过 Thomas Divine。如果您正在寻找购买复杂的 TDI 或 NDIS 示例,您可以在他公司的网站上找到它们以及其他资源。公司。您还可以在各种其他 网站上找到他的教程。
IRP 处理
上一篇文章讨论了一些非常基本的 IRP 概念以及如何处理它们。为了保持文章的简洁性,实际上对所描述的内容存在很大的空白。因此,在本文中,我们将加快步伐,并尝试填补尽可能多的空白。此时您应该已经对驱动程序开发有相当多的了解,我们应该能够相对轻松地完成此任务,但它将包含大量信息,并且并非所有信息都包含在示例代码中。您需要自己尝试 IRP 处理。它是开发驱动程序的基本部分。
驱动程序请求
编写驱动程序时,您会遇到 IRP 的两种情况:您的驱动程序请求的 IRP,以及您创建以请求其他驱动程序处理的 IRP。如我们所知,驱动程序有一个堆栈,堆栈中的每个驱动程序在 IRP 中都有自己的堆栈位置。每次将 IRP 发送到堆栈中时,该 IRP 的当前堆栈位置都会前进。当涉及到您的驱动程序时,您有几种选择。
转发并忘记
您可以使用 IoCallDriver
将 IRP 转发给堆栈中的下一个驱动程序。这就是我们在其他驱动程序教程中所做的。我们转发了 IRP,然后就忘了它。但有一个问题,我们没有考虑 STATUS_PENDING
。STATUS_PENDING
是一种实现异步操作的方法。底层驱动程序通知调用者它们尚未完成 IRP。它们也可能在一个单独的线程上完成此 IRP。规则是,如果您返回 STATUS_PENDING
,您还必须在返回之前调用 IoMarkIrpPending
。但这现在成了一个问题,如果您已经将 IRP 转发给下一个驱动程序。调用之后您就不能再触碰它了!所以您基本上有两种选择。
IoMarkIrpPending(Irp);
IoCallDriver(pDeviceObject, Irp);
return STATUS_PENDING;
第二个选择是设置一个完成例程。我们应该从第四部分的 कोड 中记住它们,但当时我们只是通过返回 STATUS_SUCCESS
而不是 STATUS_MORE_PROCESSING_REQUIRED
来阻止 IRP 完成。
IoSetCompletionRoutine(Irp, CompletionRoutine, NULL, TRUE, TRUE, TRUE); return IoCallDriver(pDeviceObject, Irp); ... NTSTATUS CompletionRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { if(Irp->PendingReturned) { IoMarkIrpPending(Irp); } return STATUS_SUCCESS; }
您又可以停止这里的处理,如果您这样做了,您就不需要做 IoMarkIrpPending
。这里存在循环逻辑,如果您调用 IoMarkIrpPending
,那么您必须从驱动程序返回 STATUS_PENDING
。如果您从驱动程序返回 STATUS_PENDING
,那么您必须调用 IoMarkIrpPending
。请记住,如果您停止完成处理,则意味着您必须完成它!我们在第四部分完成了这一点。
需要注意的一点是,如果未提供完成例程,I/O 管理器可能会好心地为您传播此“IoMarkIrpPending”信息。但是关于此主题的信息非常分散,您可能不想依赖它,只需确保您所做的一切都是正确的。
转发并事后处理
这就是我们在第四部分所做的,但略有不同。我们需要考虑待处理架构,如果 IRP 从底层驱动程序返回待处理,我们需要等到底层驱动程序完成它。驱动程序完成后,我们需要唤醒我们原来的线程,以便我们可以进行处理并完成 IRP。作为一种优化,我们只在返回待处理时设置事件。如果没有必要,就没有理由增加设置和等待事件的开销,如果一切都在同步处理!以下是代码示例。
IoSetCompletionRoutine(Irp, CompletionRoutine, &kCompleteEvent, TRUE, TRUE, TRUE); NtStatus = IoCallDriver(pDeviceObject, Irp); if(NtStatus == STATUS_PENDING) { KeWaitForSingleObject(&kCompleteEvent, Executive, KernelMode, FALSE, NULL); /* * Find the Status of the completed IRP */ NtStatus = IoStatusBlock.Status; } /* * Do Post Processing */ IoCompleteRequest(pIrp, IO_NO_INCREMENT); return NtStatus; ... NTSTATUS CompletionRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context) { if(Irp->PendingReturned) { KeSetEvent(Context, IO_NO_INCREMENT, FALSE); } return STATUS_MORE_PROCESSING_REQUIRED; }
排队并待处理
您可以选择排队 IRP 并在稍后或在另一个线程上处理它。这是允许的,因为当 IRP 在您的驱动程序堆栈级别时,您拥有它。您必须考虑到 IRP 可能会被取消。问题是,如果 IRP 被取消,您真的不想执行任何处理,因为结果将被丢弃。我们要解决的另一个问题是,如果存在与进程或线程关联的活动 IRP,则在所有活动 IRP 完成之前,该进程或线程无法完全终止。这非常棘手,关于如何做到这一点的信息很少。但是,我们将在这里向您展示如何做到。
获取您的锁
您需要做的第一件事是获取保护您 IRP 列表的自旋锁。这将有助于同步您的排队逻辑和取消例程之间的执行。有一个系统取消自旋锁也可以获取,在某些情况下,如果您使用某些系统提供的排队机制,则需要它。但是,由于取消自旋锁是全局的,您认为哪种可能性更大?另一个处理器会获取您的自旋锁,还是会获取取消自旋锁?最有可能的是,它将最终获取取消自旋锁,这可能会影响性能。在单处理器机器上,使用哪个显然无关紧要,但您应该尝试实现自己的自旋锁。
设置取消例程
您的取消例程也将需要获取您的自旋锁来同步执行并从列表中删除 IRP。设置取消例程可确保如果取消此 IRP,您将知晓并可以将其从 IRP 列表中删除。请记住,您仍然必须完成 IRP!没有办法绕过它。如果取消了 IRP,它也不会从您手中消失。如果它消失了,您在处理 IRP 时,如果被取消了,您将陷入大麻烦!取消例程的目的只是当它在队列中时,可以随时从队列中删除它,而不会有任何麻烦。
检查取消标志
然后您必须检查 IRP 的取消标志。如果未取消,则调用 IoMarkIrpPending
并将 IRP 排队到您的链接列表或其他结构中。然后您必须确保从驱动程序返回 STATUS_PENDING
。
如果已被取消,我们需要知道它是否调用了您的取消例程。您通过将取消例程设置为 NULL
来实现这一点。如果返回值是 NULL
,则您的取消例程已被调用。如果返回值不是 NULL
,则未调用取消例程。这仅仅意味着在您设置取消例程之前它就被取消了。
您现在有两个选择,请记住只有一个位置可以完成 IRP。如果调用了取消例程,只要取消例程不完成 IRP,如果它不在您的 IRP 列表中,您就可以释放它。如果取消例程总是完成它,那么您就不能完成它。如果未调用取消例程,那么您显然必须完成它。无论发生什么,您都必须记住两件事。第一件事是,在您的驱动程序的某个地方,您必须完成此 IRP。第二件事是要记住,您绝不能完成两次!
当您从列表中删除 IRP 时,情况也是如此。您应该始终检查以确保 IRP 未被取消。在删除 IRP 进行处理之前,您还将取消例程设置为 NULL
。这样,即使现在被取消了,您也不在乎,结果将被丢弃。现在最好的方法是查看代码。
Irp->Tail.Overlay.DriverContext[0] = (PVOID)pTdiExampleContext->pWriteIrpListHead; NtStatus = HandleIrp_AddIrp(pTdiExampleContext->pWriteIrpListHead, Irp, TdiExample_CancelRoutine, TdiExample_IrpCleanUp, NULL); if(NT_SUCCESS(NtStatus)) { KeSetEvent(&pTdiExampleContext->kWriteIrpReady, IO_NO_INCREMENT, FALSE); NtStatus = STATUS_PENDING; } ... /********************************************************************** * * HandleIrp_AddIrp * * This function adds an IRP to the IRP List. * **********************************************************************/ NTSTATUS HandleIrp_AddIrp(PIRPLISTHEAD pIrpListHead, PIRP pIrp, PDRIVER_CANCEL pDriverCancelRoutine, PFNCLEANUPIRP pfnCleanUpIrp, PVOID pContext) { NTSTATUS NtStatus = STATUS_UNSUCCESSFUL; KIRQL kOldIrql; PDRIVER_CANCEL pCancelRoutine; PIRPLIST pIrpList; pIrpList = (PIRPLIST)KMem_AllocateNonPagedMemory(sizeof(IRPLIST), pIrpListHead->ulPoolTag); if(pIrpList) { DbgPrint("HandleIrp_AddIrp Allocate Memory = 0x%0x \r\n", pIrpList); pIrpList->pContext = pContext; pIrpList->pfnCleanUpIrp = pfnCleanUpIrp; pIrpList->pIrp = pIrp; pIrpList->pfnCancelRoutine = pDriverCancelRoutine; /* * The first thing we need to to is acquire our spin lock. * * The reason for this is a few things. * * 1. All access to this list is synchronized, the obvious reason * 2. This will synchronize adding this IRP to the * list with the cancel routine. */ KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); /* * We will now attempt to set the cancel routine which will be called * when (if) the IRP is ever canceled. This allows us to remove an IRP * from the queue that is no longer valid. * * A potential misconception is that if the IRP is canceled it is no * longer valid. This is not true the IRP does not self-destruct. * The IRP is valid as long as it has not been completed. Once it * has been completed this is when it is no longer valid (while we * own it). So, while we own the IRP we need to complete it at some * point. The reason for setting a cancel routine is to realize * that the IRP has been canceled and complete it immediately and * get rid of it. We don't want to do processing for an IRP that * has been canceled as the result will just be thrown away. * * So, if we remove an IRP from this list for processing and * it's canceled the only problem is that we did processing on it. * We complete it at the end and there's no problem. * * There is a problem however if your code is written in a way * that allows your cancel routine to complete the IRP unconditionally. * This is fine as long as you have some type of synchronization * since you DO NOT WANT TO COMPLETE AN IRP TWICE!!!!!! */ IoSetCancelRoutine(pIrp, pIrpList->pfnCancelRoutine); /* * We have set our cancel routine. Now, check if the IRP has * already been canceled. * We must set the cancel routine before checking this to ensure * that once we queue the IRP it will definately be called if the * IRP is ever canceled. */ if(pIrp->Cancel) { /* * If the IRP has been canceled we can then check if our * cancel routine has been called. */ pCancelRoutine = IoSetCancelRoutine(pIrp, NULL); /* * if pCancelRoutine == * NULL then our cancel routine has been called. * if pCancelRoutine != * NULL then our cancel routine has not been called. * * The I/O Manager will set the cancel routine to NULL * before calling the cancel routine. * We have a decision to make here, we need to write the code * in a way that we only complete and clean up the IRP once. * We either allow the cancel routine to do it or we do it here. * Now, we will already have to clean up the IRP here if the * pCancelRoutine != NULL. * * The solution we are going with here is that we will only clean * up IRP's in the cancel routine if the are in the list. * So, we will not add any IRP to the list if it has * already been canceled once we get to this location. * */ KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); /* * We are going to allow the clean up function to complete the IRP. */ pfnCleanUpIrp(pIrp, pContext); DbgPrint("HandleIrp_AddIrp Complete Free Memory = 0x%0x \r\n", pIrpList); KMem_FreeNonPagedMemory(pIrpList); } else { /* * The IRP has not been canceled, so we can simply queue it! */ pIrpList->pNextIrp = NULL; IoMarkIrpPending(pIrp); if(pIrpListHead->pListBack) { pIrpListHead->pListBack->pNextIrp = pIrpList; pIrpListHead->pListBack = pIrpList; } else { pIrpListHead->pListFront = pIrpListHead->pListBack = pIrpList; } KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); NtStatus = STATUS_SUCCESS; } } else { /* * We are going to allow the clean up function to complete the IRP. */ pfnCleanUpIrp(pIrp, pContext); } return NtStatus; } /********************************************************************** * * HandleIrp_RemoveNextIrp * * This function removes the next valid IRP. * **********************************************************************/ PIRP HandleIrp_RemoveNextIrp(PIRPLISTHEAD pIrpListHead) { PIRP pIrp = NULL; KIRQL kOldIrql; PDRIVER_CANCEL pCancelRoutine; PIRPLIST pIrpListCurrent; KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); pIrpListCurrent = pIrpListHead->pListFront; while(pIrpListCurrent && pIrp == NULL) { /* * To remove an IRP from the Queue we first want to * reset the cancel routine. */ pCancelRoutine = IoSetCancelRoutine(pIrpListCurrent->pIrp, NULL); /* * The next phase is to determine if this IRP has been canceled */ if(pIrpListCurrent->pIrp->Cancel) { /* * We have been canceled so we need to determine if our * cancel routine has already been called. pCancelRoutine * will be NULL if our cancel routine has been called. * If will not be NULL if our cancel routine has not been * called. However, we don't care in either case and we * will simply complete the IRP here since we have to implement at * least that case anyway. * * Remove the IRP from the list. */ pIrpListHead->pListFront = pIrpListCurrent->pNextIrp; if(pIrpListHead->pListFront == NULL) { pIrpListHead->pListBack = NULL; } KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); pIrpListCurrent->pfnCleanUpIrp(pIrpListCurrent->pIrp, pIrpListCurrent->pContext); DbgPrint("HandleIrp_RemoveNextIrp Complete Free Memory = 0x%0x \r\n", pIrpListCurrent); KMem_FreeNonPagedMemory(pIrpListCurrent); pIrpListCurrent = NULL; KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); pIrpListCurrent = pIrpListHead->pListFront; } else { pIrpListHead->pListFront = pIrpListCurrent->pNextIrp; if(pIrpListHead->pListFront == NULL) { pIrpListHead->pListBack = NULL; } pIrp = pIrpListCurrent->pIrp; KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); DbgPrint("HandleIrp_RemoveNextIrp Complete Free Memory = 0x%0x \r\n", pIrpListCurrent); KMem_FreeNonPagedMemory(pIrpListCurrent); pIrpListCurrent = NULL; KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); } } KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); return pIrp; } /********************************************************************** * * HandleIrp_PerformCancel * * This function removes the specified IRP from the list. * **********************************************************************/ NTSTATUS HandleIrp_PerformCancel(PIRPLISTHEAD pIrpListHead, PIRP pIrp) { NTSTATUS NtStatus = STATUS_UNSUCCESSFUL; KIRQL kOldIrql; PIRPLIST pIrpListCurrent, pIrpListPrevious; KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); pIrpListPrevious = NULL; pIrpListCurrent = pIrpListHead->pListFront; while(pIrpListCurrent && NtStatus == STATUS_UNSUCCESSFUL) { if(pIrpListCurrent->pIrp == pIrp) { if(pIrpListPrevious) { pIrpListPrevious->pNextIrp = pIrpListCurrent->pNextIrp; } if(pIrpListHead->pListFront == pIrpListCurrent) { pIrpListHead->pListFront = pIrpListCurrent->pNextIrp; } if(pIrpListHead->pListBack == pIrpListCurrent) { pIrpListHead->pListBack = pIrpListPrevious; } KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); NtStatus = STATUS_SUCCESS; /* * We are going to allow the clean up function to complete the IRP. */ pIrpListCurrent->pfnCleanUpIrp(pIrpListCurrent->pIrp, pIrpListCurrent->pContext); DbgPrint("HandleIrp_PerformCancel Complete Free Memory = 0x%0x \r\n", pIrpListCurrent); KMem_FreeNonPagedMemory(pIrpListCurrent); pIrpListCurrent = NULL; KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql); } else { pIrpListPrevious = pIrpListCurrent; pIrpListCurrent = pIrpListCurrent->pNextIrp; } } KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql); return NtStatus; } /********************************************************************** * * TdiExample_CancelRoutine * * This function is called if the IRP is ever canceled * * CancelIo() from user mode, IoCancelIrp() from the Kernel * **********************************************************************/ VOID TdiExample_CancelRoutine(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { PIRPLISTHEAD pIrpListHead = NULL; /* * We must release the cancel spin lock */ IoReleaseCancelSpinLock(pIrp->CancelIrql); DbgPrint("TdiExample_CancelRoutine Called IRP = 0x%0x \r\n", pIrp); /* * We stored the IRPLISTHEAD context in our DriverContext on the IRP * before adding it to the queue so it should not be NULL here. */ pIrpListHead = (PIRPLISTHEAD)pIrp->Tail.Overlay.DriverContext[0]; pIrp->Tail.Overlay.DriverContext[0] = NULL; /* * We can then just throw the IRP to the PerformCancel * routine since it will find it in the queue, remove it and * then call our clean up routine. Our clean up routine * will then complete the IRP. If this does not occur then * our completion of the IRP will occur in another context * since it is not in the list. */ HandleIrp_PerformCancel(pIrpListHead, pIrp); } /********************************************************************** * * TdiExample_IrpCleanUp * * This function is called to clean up the IRP if it is ever * canceled after we have given it to the queueing routines. * **********************************************************************/ VOID TdiExample_IrpCleanUp(PIRP pIrp, PVOID pContext) { pIrp->IoStatus.Status = STATUS_CANCELLED; pIrp->IoStatus.Information = 0; pIrp->Tail.Overlay.DriverContext[0] = NULL; DbgPrint("TdiExample_IrpCleanUp Called IRP = 0x%0x \r\n", pIrp); IoCompleteRequest(pIrp, IO_NO_INCREMENT); }
或者,您可以使用类似 取消安全 IRP 队列 的内容。
处理并完成
这是您直接处理请求并完成它的地方。如果您不返回 STATUS_PENDING
,那么您就没问题了。这就是我们在大多数教程中对所有驱动程序请求所做的事情。我们处理它们,然后当我们完成时。我们只需调用 IoCompleteRequest
,这是一个强制性调用。
创建 IRP
上一篇文章对如何创建和发送 IRP 进行了非常简要的描述。我们将在这里再次详细介绍这些步骤。我们还将了解我们可以使用的 API 创建 IRP 之间的区别。
第一步:创建 IRP
有几个 API 可以用来创建 IRP。正如我们已经知道的,它们之间存在差异,我们需要理解。第四篇文章中的源代码在 IRP 处理方面非常粗糙,这仅仅是为了介绍 IRP 而不必解释我们在这里解释的所有内容。
有异步 IRP 和同步 IRP。如果您使用 IoAllocateIrp
或 IoBuildAsynchronousFsdRequest
创建 IRP,则您已创建了异步 IRP。这意味着您应该设置一个完成例程,并且当 IRP 完成时,您需要调用 IoFreeIrp
。您需要控制这些 IRP,并且必须妥善处理它们。
如果您使用 IoBuildDeviceIoControlRequest
或 IoBuildSynchronousFsdRequest
创建 IRP,那么您已创建了同步 IRP。请记住,TdiBuildInternalDeviceControlIrp
是一个宏,并且会创建一个同步 IRP。这些 IRP 由 I/O 管理器拥有和管理!不要释放它们!这是我在互联网上看到的常见错误,他们会在失败时调用 IoFreeIrp
!这些 IRP 必须使用 IoCompleteRequest
完成。如果您将此 IRP 传递给 IoCallDriver
,则无需完成它,因为下面的驱动程序会为您完成。但是,如果您使用完成例程拦截了 IRP,那么在完成后您需要调用 IoCompleteRequest
。
另外,请记住,在考虑创建 IRP 之前,请确保您了解您的代码将在什么 IRQL 下被调用。使用 IoAllocateIrp
的好处是它可以在 DISPATCH_LEVEL
下使用,而 IoBuildDeviceIoControlRequest
不能。
第二步:设置 IRP 参数
这非常简单,以 TDI 示例为例,宏 TdiBuildSend
向我们展示了如何做到这一点。我们使用 IoGetNextIrpStackLocation
并设置参数。我们还将 MDL 和我们需要的任何其他属性设置在 IRP 本身。
第四步:发送到驱动程序堆栈
这非常简单,我们已经做了很多次了。我们只需使用 IoCallDriver
将 IRP 发送到堆栈。
第五步:等待并清理
如果驱动程序返回的状态不是“STATUS_PENDING
”,则您已完成。如果您异步创建了 IRP,那么您要么在完成例程中释放了 IRP,要么在这里将其设置为需要进一步处理,然后现在进行处理并使用 IoAllocateIrp
释放它。
如果您创建了同步 IRP,那么您要么让 I/O 管理器处理它,您就完成了;要么您将完成例程设置为返回更多处理,在这种情况下,您在此处进行处理,然后调用 IoCompleteRequest
。
如果返回的状态是“STATUS_PENDING
”,那么您现在有几个选择。您可以根据 IRP 在此处等待,或者您可以离开并异步完成它。这取决于您的体系结构。如果您异步创建了 IRP,那么您设置的完成例程必须检查 IRP 是否设置为“Pending”,然后设置您的事件。这样,您就不会在没有必要的情况下浪费处理。这也解释了为什么您不会等待事件,除非返回了 STATUS_PENDING
!如果所有调用都等待事件,无论如何,那将是多么缓慢!
如果您的 IRP 是同步创建的,那么 I/O 管理器会为您设置该事件。除非您想从完成例程返回更多处理,否则您无需做任何事情。请阅读“完成工作原理”部分以进一步了解此处的操作。
不可分页驱动程序代码
如果您还记得在第一个教程中,我们学习了 #pragma
以及将驱动程序代码放入不同节的能力。有一个 INIT 节是可丢弃的,还有一个 PAGE 节将内存放入可分页代码区域。那获取自旋锁的代码怎么办?当代码必须不可分页时,我们该怎么办?我们根本不指定 #pragma
!已加载驱动程序的默认状态是位于不可分页内存中,我们实际上是通过 #pragma 将其强制放入可分页内存中,因为在没有必要时我们不希望系统耗尽物理内存。
如果您查看代码,您会注意到一些 #pragma
被注释掉了。这些是需要不可分页的函数,因为它们使用自旋锁并在 > APC_LEVEL
下运行。我注释掉它们而不是不写它们的原因是,我不想让您认为我忘记了它们并添加它们!我想表明我决定将它们排除在外!
/* #pragma alloc_text(PAGE, HandleIrp_FreeIrpListWithCleanUp) */ /* #pragma alloc_text(PAGE, HandleIrp_AddIrp) */ /* #pragma alloc_text(PAGE, HandleIrp_RemoveNextIrp) */ #pragma alloc_text(PAGE, HandleIrp_CreateIrpList) #pragma alloc_text(PAGE, HandleIrp_FreeIrpList) /* #pragma alloc_text(PAGE, HandleIrp_PerformCancel) */
完成工作原理?
完成的工作方式是,每个设备的 STACK LOCATION 可能有一个相关的完成例程。此完成例程实际上是为上面的驱动程序调用的,而不是为当前驱动程序调用的!当前驱动程序知道何时完成它。因此,当驱动程序完成它时,会读取当前堆栈位置的完成例程,如果存在,则会调用它。在调用之前,当前 IO_STACK_LOCATION
会移动到指向前一个驱动程序的旧位置!这很重要,我们稍后会看到。如果该驱动程序没有完成它,它必须通过调用“IoMarkIrpPending
”来将待处理状态传播到上层,就像我们之前提到的那样。这是因为如果驱动程序返回 STATUS_PENDING
,它必须将 IRP 标记为待处理。如果它返回的状态与底层驱动程序不同,则无需将 IRP 标记为待处理。也许它拦截了 STATUS_PENDING
并等待完成。然后它可以停止 IRP 的完成,然后重新完成它,同时返回一个不同于 STATUS_PENDING
的状态。
这可能有些令人困惑,所以您请参考上面关于“转发并事后处理”的讨论。现在,如果您的驱动程序创建了 IRP,您不必标记 IRP 为待处理!您知道为什么吗?因为您没有 IO_STACK_LOCATION
!您不在设备的堆栈上!如果您这样做,您实际上将开始损坏内存,因为 IoMarkIrpPending
实际上是在 IO_STACK_LOCATION
中设置位!另一个奇怪的是设备对象是 NULL
。这很可能是因为我们的堆栈位置不存在!我们没有关联的设备对象,因为我们不是这个设备堆栈的一部分。这是有效的。顺便说一下,堆栈编号可能会超过 I/O 管理器和请求发起者的堆栈数量。尝试实际使用这些堆栈位置是不行的!
您可能会注意到,示例代码实际上会显示一个调用“IoMarkIrpPending
”的完成例程,即使它创建了 IRP!这不应该是这样的。实际上,如果您查看真实代码,如果创建了一个同步 IRP,完成例程通常不存在,或者仅用于返回更多处理的状态。
我在我们的 TDI 客户端驱动程序中实现了一个完成例程。我们在那里创建同步 IRP;但是,如果您查看调试部分,如下所示。
kd> kb ChildEBP RetAddr Args to Child fac8ba90 804e4433 00000000 80d0c9b8 00000000 netdrv!TdiFuncs_CompleteIrp [.\tdifuncs.c @ 829] fac8bac0 fbb20c54 80d1d678 80d0c9b8 00000000 nt!IopfCompleteRequest+0xa0 fac8bad8 fbb2bd9b 80d0c9b8 00000000 00000000 tcpip!TCPDataRequestComplete+0xa4 fac8bb00 fbb2bd38 80d0c9b8 80d0ca28 80d1d678 tcpip!TCPDisassociateAddress+0x4b fac8bb14 804e0e0d 80d1d678 80d0c9b8 c000009a tcpip!TCPDispatchInternalDeviceControl+0x9b fac8bb24 fc785d65 ffaaa3b0 80db4774 00000000 nt!IofCallDriver+0x3f fac8bb50 fc785707 ff9cdc20 80db4774 fc786099 netdrv!TdiFuncs_DisAssociateTransportAndConnection+0x94 [.\tdifuncs.c @ 772] fac8bb5c fc786099 80db4774 ffaaa340 ff7d1d98 netdrv!TdiFuncs_FreeHandles+0xd [.\tdifuncs.c @ 112] fac8bb74 804e0e0d 80d33df0 ffaaa340 ffaaa350 netdrv!TdiExample_CleanUp+0x6e [.\functions.c @ 459] fac8bb84 80578ce9 00000000 80cda980 00000000 nt!IofCallDriver+0x3f fac8bbbc 8057337c 00cda998 00000000 80cda980 nt!IopDeleteFile+0x138 fac8bbd8 804e4499 80cda998 00000000 000007dc nt!ObpRemoveObjectRoutine+0xde fac8bbf4 8057681a ffb3e6d0 000007dc e1116fb8 nt!ObfDereferenceObject+0x4b fac8bc0c 80591749 e176a118 80cda998 000007dc nt!ObpCloseHandleTableEntry+0x137 fac8bc24 80591558 e1116fb8 000007dc fac8bc60 nt!ObpCloseHandleProcedure+0x1b fac8bc40 805916f5 e176a118 8059172e fac8bc60 nt!ExSweepHandleTable+0x26 fac8bc68 8057cfbe ffb3e601 ff7eada0 c000013a nt!ObKillProcess+0x64 fac8bcf0 80590e70 c000013a ffa25c98 804ee93d nt!PspExitThread+0x5d9 fac8bcfc 804ee93d ffa25c98 fac8bd48 fac8bd3c nt!PsExitSpecialApc+0x19 fac8bd4c 804e7af7 00000001 00000000 fac8bd64 nt!KiDeliverApc+0x1c3 kd> dds esp fac8ba94 804e4433 nt!IopfCompleteRequest+0xa0 fac8ba98 00000000 ; This is the PDEVICE_OBJECT, it's NULL!! fac8ba9c 80d0c9b8 ; This is IRP fac8baa0 00000000 ; This is our context (NULL) kd> !irp 80d0c9b8 Irp is active with 1 stacks 2 is current (= 0x80d0ca4c) No Mdl Thread ff7eada0: Irp is completed. Pending has been returned cmd flg cl Device File Completion-Context [ f, 0] 0 0 80d1d678 00000000 fc786579-00000000 \Driver\Tcpip netdrv!TdiFuncs_CompleteIrp Args: 00000000 00000000 00000000 00000000 If there's only 1 stack how can it be on 2?
正如您所看到的,我们位于 IO_STACK_LOCATION #2
,它不存在。因此,IRP 最初从一个不存在的高 IO_STACK_LOCATION
开始。如果您还记得,我们需要调用 IoGetNextIrpStackLocation
来设置参数!这意味着如果我们在这里调用 IoMarkIrpPending
,我们将有效地访问我们不应该访问的内存,因为 IoMarkIrpPending
实际上会在 IO_STACK_LOCATION
中设置位!另一个奇怪的是设备对象是 NULL
。这很可能是因为我们的堆栈位置不存在!我们没有关联的设备对象,因为我们不是这个设备堆栈的一部分。这是有效的。顺便说一下,堆栈编号可能会超过 I/O 管理器和请求发起者的堆栈数量。尝试实际使用这些堆栈位置是不行的!
为什么是 STATUS_PENDING?
如果我还没有让您足够困惑,我们需要谈谈 STATUS_PENDING
和 IoMarkIrpPending
。有什么用?用处在于我们可以异步处理 IRP,而上层驱动程序和 I/O 管理器需要知道!第一部分 STATUS_PENDING
是作为优化返回的。所以如果我们想等待,我们只为异步操作这样做。第二部分是 IoMarkIrpPending
实际上是将“PendingReturned
”状态传播到 IRP 上。这样我们可以进行优化,所以我们不必总是调用 KeSetEvent
,而只在返回 STATUS_PENDING
的情况下才这样做!
另一个用途是,堆栈中间的驱动程序可以将此状态从 STATUS_PENDING
更改为 STATUS_SUCCESS
,而无需将整个待处理状态传播到驱动程序堆栈的顶部。这样,优化就发挥了作用,我们不必进行异步操作所涉及的许多额外处理。请记住,IRP 有两个代码路径:向上堆栈返回的值和可能在不同线程上发生的完成路径。因此,您可以看到它们为什么需要同步并沿着这两个路径传播此状态。
重叠 I/O
“STATUS_PENDING
”架构本质上是实现重叠 I/O 的方式。仅仅因为本文中的示例源代码使用了 ReadFileEx
和 WriteFileEx
,并不意味着 ReadFile
和 WriteFile
就不能在此处工作。它们也可以工作。如果您查看 CreateFile
API,我添加了一个标志来启用重叠 I/O。如果您删除此标志,I/O 管理器实际上将阻塞在 STATUS_PENDING
上,而不是返回到应用程序。它将等待一个事件,直到 I/O 完成。这本质上就是为什么用户模式应用程序使用异步 I/O。尝试这些不同的方法!
其他资源
以下是有关 IRP 处理的其他资源和文章,您可能想参考和阅读。
这些是“速查表”,它们只是显示如何处理 IRP 的示例代码。我对 Cheat Sheet 2 中关于 IRP 完成例程将同步 IRP 标记为 Pending 的信息持怀疑态度!请记住我关于 IRP 完成例程在设备堆栈位置被调用的说法。如果您分配了 IRP,这并不意味着您在设备堆栈上!我还没有亲自尝试过代码,所以我可能遗漏了实现中的一些内容。
网上还有许多其他资源,我提供的 URL 可能会在将来失效或移动!
示例源代码
示例源代码将构建以下六个二进制文件。
CHATCLIENT.EXE - Winsock Chat Client
CHATCLIENTNET.EXE - Lightbulb Chat Client
CHATSERVER.EXE - Winsock Chat Server
DRVLOAD.EXE - Example TDI Client Driver Loader
NETDRV.SYS - Example TDI Client Driver
NETLIB.LIB - Lightbulb Library
创建的 TDI 客户端驱动程序可以使用 NETLIB.LIB 中实现的一组简单的 API。我将其命名为“LightBulb
”API 集,以“Sockets”为灵感。实际上有两个客户端,一个使用 Winsock,另一个仅出于示例目的使用 Lightbulbs
。
驱动程序架构
驱动程序的架构非常简单。它只是排队所有读写 IRP。它有一个特殊的可写线程,该线程在系统进程中创建。这只是为了演示排队 IRP 和执行异步操作。写入网络数据的调用可以在不等待数据发送或复制数据的情况下返回用户模式。读取也是如此,IRP 被排队,当数据接收回调发生时,这些 IRP 就被完成了。源代码有完整的注释。
构建源代码
首先,一如既往,请确保所有 makefile 都指向您的 DDK 位置。当前的 makefile 假定与源文件在同一驱动器的根目录为 \NTDDK\INC。其次,请确保使用 VCVARS32.BAT 设置了 Visual Studio 环境变量。
我在“network”目录的根目录下创建了一个新的 makefile,然后您可以使用它来构建所有目录。您可以使用第一个命令“nmake dir”。如果任何目录已存在,此命令将失败。它将预先创建构建源代码所需的所有目录。有时,如果目录尚不存在,源构建会失败。
C:\Programming\development\DEBUG\private\src\drivers\network>nmake dir
Microsoft (R) Program Maintenance Utility Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.
mkdir ..\..\..\..\bin
您可以做的第二件事是“nmake”或“nmake all”来构建源。它将进入每个目录并按正确的顺序构建所有 6 个二进制文件。
C:\Programming\development\DEBUG\private\src\drivers\network>nmake
Microsoft (R) Program Maintenance Utility Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.
cd chatclient
nmake
Microsoft (R) Program Maintenance Utility Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.
cl /nologo /MD /W3 /Oxs /Gz /Zi /I "..\..\..\..\inc" /D "WIN32" /D "_W
INDOWS" /Fr.\obj\i386\\ /Fo.\obj\i386\\ /Fd.\obj\i386\\ /c .\client.c
client.c
link.exe /LIBPATH:..\..\..\..\lib /DEBUG /PDB:..\..\..\..\..\bin\SYMBOL
S\chatclient.PDB /SUBSYSTEM:CONSOLE /nologo kernel32.lib Advapi32.lib WS2_32.
LIB /out:..\..\..\..\..\bin\chatclient.exe .\obj\i386\client.obj kernel32.lib A
dvapi32.lib WS2_32.LIB
rebase.exe -b 0x00400000 -x ..\..\..\..\..\bin\SYMBOLS -a ..\..\..\..\..
\bin\chatclient
REBASE: chatclient - unable to split symbols (2)
您拥有的最后一个选项是“nmake clean”,它将进入每个目录并删除对象文件。然后,键入“nmake”或“nmake all”时,该项目将重新生成。当然,您也可以在任何应用程序目录中键入“nmake”和“nmake clean”,但这是一种方便的一次性构建所有二进制文件的方法。
C:\Programming\development\DEBUG\private\src\drivers\network>nmake clean
Microsoft (R) Program Maintenance Utility Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.
cd chatclient
nmake clean
Microsoft (R) Program Maintenance Utility Version 6.00.8168.0
Copyright (C) Microsoft Corp 1988-1998. All rights reserved.
Deleted file - C:\Programming\development\DEBUG\private\src\drivers\network\chat
client\obj\i386\client.obj
Deleted file - C:\Programming\development\DEBUG\private\src\drivers\network\chat
client\obj\i386\client.sbr
Deleted file - C:\Programming\development\DEBUG\private\src\drivers\network\chat
client\obj\i386\vc60.pdb
聊天服务器
聊天服务器是一个非常简单的实现。它仅接受连接并将这些连接放入列表中。无论何时从任何客户端接收到数据,它都会将其广播给所有其他客户端。
聊天客户端
有两个聊天客户端,但它们本质上都实现了相同的目的。唯一区别是,一个与 Winsock API 通信,另一个使用我们的“Lighbulb
”API。这些客户端仅打印任何传入数据,并发送用户输入的任何数据。它们是控制台应用程序,因此用户输入任何内容时,传入的输出都会被阻止,直到您完成输入。
聊天协议
聊天协议非常简单。发送的第一个数据包将是客户端的名称,用于将其标识给所有其他客户端。其余的只是作为字符串广播。没有数据包头。因此,服务器和客户端都假定每条聊天文本都会在一次接收中被读取!这极易出错,仅用作示例。为了改进它,您可能需要考虑实际创建一个协议!
Bug!
源代码中已知存在三个 Bug。其中两个实际上是实现中遗漏的内容,另一个是我看到的东西,我懒得修复。这是示例代码,您很幸运它能编译!您是否曾见过书中给出的代码,您知道它无法编译!嗯,至少在这里,它在最简单的情况下是有效的。Bug 就在那里等着您修复。我认为我会提供一些指导,您可以通过修复这些 Bug 来更好地熟悉代码。我确实对源代码运行了一些驱动程序验证器测试,以确保没有明显的 Bug,但没有进行广泛的测试。再说,这不是商业软件。可能还有其他 Bug,如果您发现任何 Bug,请尝试修复它们。如果您需要帮助,请告诉我。
Bug 一:TDI 客户端检测断开连接
没有实现来检测客户端何时与服务器断开连接。如果服务器在客户端连接时被中止,它根本不知道,并且会继续尝试发送数据。TDI_SEND
的返回值被忽略,也没有其他注册事件来通知断开连接。实现根本不存在。这是您的工作。您必须实现一种方法来检测连接何时断开。有多种实现可以做到这一点。
Bug 二:无协议
客户端和服务器之间没有实现协议。应实现一个不依赖于接收整个数据包且更灵活的协议!也许甚至可以添加简单的文件传输!
Bug 三:显示不正确
存在一个与两个连接的客户端相关的 Bug。此 Bug 实际上在使用任何客户端实现(TDI 或 Sockets)时都会发生。当一个客户端即将键入消息但未发送时,会发生此 Bug。然后另一个客户端发送大约 5 条消息。未发送任何消息的客户端然后发送其消息。此消息已损坏,名称被发送的数据覆盖。作为提示,您可能需要研究发送的数据并注意“\r\n”配对。
结论
本文实现了一个简单的聊天程序,该程序使用了套接字和 TDI 客户端的实现。还有大量关于如何处理 IRP 的信息,以及指向进一步学习的链接。IRP 是驱动程序开发的支柱,理解它们是编写 Windows 设备驱动程序到关键。请记住,网上有很多错误信息、缺失信息和不良示例,所以请确保您访问几个不同的网站并尝试几种技术,以便您能够区分什么是正确的,什么是不正确的。