Introduction to the Transport Device Interface
|
||||||||||
IntroductionWelcome to the fifth installment of the driver development series. The title of this article is a little bit misleading. Yes, we will be writing a TDI Client for demonstration purposes however that is not the main goal of this tutorial. The main goal of this tutorial is to further explore how to handle and interact with IRPs. This tutorial will explore how to queue and handle the canceling of IRPs. The real title of this article should be "Introduction to IRP Handling" however it's not as catchy a title! Also, it's not a complete fib we will be doing this while demonstration implementing a TDI Client driver. So, I actually have to explain how that part is implemented as well. The supplied example is a very simple client/server chat program which we will be using to explore how to handle IRPs. Sockets RefresherWe will first be starting off with something that you should probably already know. If you don't know you may want to read some other articles on the subject. Even so, I have supplied this quick refresher course as well as example source of how to implement winsock. What is IP?IP or "Internet Protocol" is essentially a protocol used to send data or packets between two computers. This protocol does not need any setup and only requires that, each machine on the network have a unique "IP Address". The "IP Address" can then be used to route packets between communication end points. This protocol provides routing but it does not provide reliability. Packets sent only by IP can arrive corrupted, out of order or not at all. There are however other protocols implemented on top of IP which provide these features. The "IP" Protocol lies at the Network Layer in the OSI model. What is TCP?TCP is known as "Transmission Control Protocol" and it sits on top of the "IP" protocol. This is also commonly referred to as "TCP/IP". The "IP" layer provides the routing and the "TCP" layer reliable, sequenced uncorrupted delivery of data. To distinguish between multiple TCP transmissions on the machine they are identified by a unique TCP port number. In this manner multiple applications or even the same application can open a communications pipeline and the underlying transport will be able to correctly route the data between each end point. The "TCP" protocol lies at the Transport in the OSI model. There are other protocols which then sit on top of TCP such as FTP, HTTP, etc. These protocols sit at the "Application Layer" of the OSI model. Protocol LayeringIn some sense any part of the communications stack can be replaced by an "equivalent" protocol. If FTP for example requires reliable transport and routing, then sitting on top of any protocol which provides this would still work. In that example if an application was using "SPX" instead of "TCP/IP" it shouldn't make a difference. In that sense if "TCP" or some implementation of "TCP" sat on top of an unreliable protocol like "IPX", it should work. The reason for "some implementation" should work is because, it obviously depends on how dependent the upper protocol is on the actual implementation and inner workings of the underlying protocol they are. What are sockets?A "socket" is generally referred to as a communications end point as implemented by a "sockets" library. The "sockets" library API was generally written to be a simple way (and portable in some cases) to implement networking applications from user mode. There are a few flavors of socket APIs but in Windows we use "WINSOCK". There are aspects of Winsock which can be implemented as portable (I once implemented a winsock application that was compiled on both Unix and Windows NT with minimal conflict but of course it was a very simple program) and there are others which are not directly portable. Socket Server ApplicationThe server side of a socket connection simply accepts incoming connections. Each new connection is given a separate handle so that the server can then communicate to each client individually. The following outlines the steps used in communications. Step One: Create a SocketThe first step is to create a socket. The following code shows how to create a socket for streaming (TCP/IP). hSocket = socket(PF_INET, SOCK_STREAM, 0); if(hSocket == INVALID_SOCKET) { /* Error */ } This is then simply a handle to the network driver. You use this handle in other calls to the socket API. Step Two: Bind the SocketThe second step is to bind a socket to a TCP port and IP Address. The following code demonstrates this behavior. The socket is created in our example simply using a number, however in general you should use macros to put the port into network byte order. 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 */ } This operation binds the socket handle with the port address. You can specify the IP Address as well however using "0" simply allows the driver to bind to any IP Address (the local one). You can also specify "0" for the port address to bind to a random port. However servers generally use a fixed port number since the clients still need to find them but there are exceptions. Step Three: Listen on the SocketThis will put the socket into a listening state. The socket will be able to listen for connections after this call. The number specified is simply the back log of connections waiting to be accepted that this socket will allow. if(listen(hSocket, 5) != 0) { /* Error */ } Step Four: Accept ConnectionsThe if((hNewClient = accept(pServerInfo->hServerSocket, (struct sockaddr *)&NewClientSockAddr, &uiLength)) != INVALID_SOCKET) { The returned handle can then be used to send and receive data. Step Five: Close the SocketWhen you are done you need to close any and all handles just like anything else! closesocket(hNewClient); There is one extra detail omitted here about the Socket Client ApplicationThe client side of a sockets communications simply connects to a server and then sends/receives data. The following steps break down how to setup this communications. Step One: Create a SocketThe first step is to create a socket. The following code shows how to create a socket for streaming (TCP/IP). hSocket = socket(PF_INET, SOCK_STREAM, 0); if(hSocket == INVALID_SOCKET) { /* Error */ } This is then simply a handle to the network driver. You use this handle in other calls to the socket API. Step Two: Connect to a ServerYou need to setup the address and port of the server to connect to and they must be in network byte order. You will then call the 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 */ } Step Three: Send and Receive DataOnce you are connected, you just need to send and receive data whenever you want, using the 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 */ } Please note that these examples may refer to sending and receiving strings, however any binary data can be sent. Step Four: Close the SocketWhen you are done you need to close any and all handles just like anything else! closesocket(hSocket); There is one extra detail omitted here about the Transport Device InterfaceThe sockets primer was really to get you ready for the TDI API. The "Transport Device Interface" is a set of APIs which can be used by a driver to communicate with a Transport (Protocol) Driver such as TCP. The TCP driver would implement this API set so that your driver can communicate to it. This is a little more complex than using sockets and the documentation on MSDN can be more confusing than helpful. So we will go over all the steps needed to make a client side connection. Once you understand this, you should be able to use the API to perform other operations such as creating a server for example. The ArchitectureThe following diagram outlines the TDI/NDIS relationship. In general, TDI is a standard interface in which transport/protocol driver developers can implement in their drivers. In this manner developers that wish to use their protocol can implement a standard interface without the hassle of implementing separate interfaces for each protocol they wish to support. This does not mean that those developers are limited to only implementing TDI. They can also implement any proprietary interface that they wish on the top level of their driver. I am not an expert in NDIS, so I will leave these as simple explanations, so I hopefully won't get anything wrong! These are just "good to know" type information anyway and we don't need to understand any of these to use the TDI Client Driver. The Protocol drivers will talk to the NDIS interface API on the lower end of the driver. The job of the protocol driver is just that, to implement a protocol and talk with NDIS. The upper layer of the driver can be a proprietary interface or TDI or both. By the way, these are NOT "NDIS Clients". They do not exist. There are websites out there that have referred to these drivers as "NDIS Clients" and that's completely wrong. I once asked an NDIS expert about "NDIS Clients" and they didn't know what I was talking about!
You can find more information on the TDI and NDIS architectures on MSDN. Step One: Open a Transport AddressThe first step is to create a handle to a "Transport Address". This will require you to use The method of opening this handle is a little obscure for those who are not used to developing drivers. You have to specify the "EA" or "Extedned Attributes" which are then passed to the driver via The following code illustrates how to open a Transport Address. 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; } This is described on MSDN. Step Two: Open a Connection ContextThe second step is to open a Connection Context. This is the handle that you will actually be using in all subsequent operations to be performed on this connection. This is also done by The following code demonstrates opening up a connection context. Note that you can also specify a pointer value called a " 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; } This is described on MSDN. Step Three: Associate The Transport Address and Connection ContextYou need to associate the two handles, the transport and connection, before you can perform any operations. This is done by sending an IOCTL to the the device. If you remember before how to send an IOCTL we need to allocate an IRP, set the parameters and send it to the device. This however is simplified since the TDI header files provide macros and other functions which can do this for you. The The one thing you may notice different here than what we talked about last time is the " The following code demonstrates how to do this. 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; } This is described on MSDN. Step Four: ConnectTo create the client side of a TCP connection, we need to connect! 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; } This is described on MSDN. Step Five: Send and Receive DataTo send data you simply create a 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; } The same can be done for receive using the 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; } The code which uses this API and implements the callback are as follows: 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; } Don't get scared with the This is described on MSDN. Step Six: DisconnectThis is nothing special you just disconnect the connection by implementing the 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; } This is described on MSDN. Step Seven: Disassociate the HandlesThis is very simple, we just implement another IOCTL call as follows. 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; } This is described on MSDN. Step Eight: Close the HandlesThis function is called on both handles, the Transport and the Connection Context. 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; } This is described on MSDN. Other ResourcesThe TDI Interface will get a bit easier once you get familiar with it. One of the biggest things to get right when writing any driver is your IRP handling. TDI does seem a little bit more complex than sockets but it is a kernel interface. If you have ever investigated TDI or NDIS you have probably run into Thomas Divine. If you are looking to purchase complex TDI or NDIS examples, you can find them and other resources on the website of his company. You can also find tutorials of his on various other websites. IRP HandlingThe last article touched on some very basic concepts of IRPs and how to handle them. To keep that article simple, there are actually large gaps in what was described. So in this article we will pick up the pace and attempt to fill in as many of those gaps as we can. You should have a decent bit of exposure to driver development at this time that we should be able to do this quite easily however it will be a lot of information and not all of it is in the example code. You will need to experiment with IRP handling yourself. It is the essential part of developing a driver. Driver RequestsWhen writing a driver there are two different times that you will be exposed to IRPs. These are IRPs that are requested to your driver and IRPs that you create to request processing from other drivers. As we remember, there is a stack of drivers and each driver in the stack has their own stack location in the IRP. Each time an IRP is sent down the stack the current stack location of that IRP is advanced. When it comes to your driver you have a few choices. Forward and ForgetYou can forward the IRP to the next driver in the stack using IoMarkIrpPending(Irp);
IoCallDriver(pDeviceObject, Irp);
return STATUS_PENDING;
The second choice would be to set a completion routine. We should remember those from the code in part 4 however we used them then to simply stop the IRP from completing by returning 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; } You could again stop the processing here and if you did, you would not need to do One thing to note is, it's possible that if a completion routine isn't supplied, that the I/O Manager may be nice enough to propagate this "IoMarkIrpPending" information for you. However information is so scattered on this subject that you may not want to trust that and just make sure everything you do is correct. Forward and Post ProcessThis is what we actually did in Part 4 with a slight difference. We need to take into account the pending architecture and if the IRP returns pending from the lower level driver, we need to wait until the lower level driver completes it. Once the driver has completed it we need to wake up our original thread so that we can do processing and complete the IRP. As an optimization, we only want to set the event if pending was returned. There is no reason to add overhead of setting and waiting on events if everything is being processed synchronously! The following is a code example of this. 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; } Queue and PendYou have the option to queue the IRP and process it at a later time or on another thread. This is allowed since you own the IRP while it is at your driver stack level. You have to take into account that the IRP can be canceled. The problem is that if the IRP is canceled, you really don't want to perform any processing since the result will be thrown away. The other problem we want to solve is that, if there are active IRPs associated with a process or thread that process or thread cannot be completely terminated until all active IRPs have been completed. This is very tricky and documentation on how to do this is scarce. However we will show you how to do it here. Grab your lockThe first thing you need to do is acquire your spinlock that protects your IRP list. This will help synchronize the execution between your queuing logic and your cancel routine. There is a system cancel spinlock that can also be acquired and in some cases it needs to be if you are using certain system provided queuing mechanisms. However since the cancel spinlock is system wide, what do you think is more likely? That another processor would grab your spinlock or that it would grab the cancel spinlock? Most likely it would end up grabbing the cancel spinlock and this can be a performance hit. On a single processor machine, it obviously doesn't matter which one you use but you should attempt to implement your own spinlock. Set a Cancel RoutineYour cancel routine will also need to grab your spinlock to synchronize execution and remove IRPs from the list. Setting a cancel routine makes sure that if this IRP is canceled, then you know about it and can remove it from your IRP list. Remember, you STILL MUST COMPLETE THE IRP! There's no way around it. If an IRP is canceled it just doesn't disappear from out under your feet. If it did then while you processed the IRP, if it was canceled, you'd be in big trouble! The purpose of the cancel routine is just while it is in the queue it can be removed from the queue at any time if it's canceled without any hassle. Check Cancel FlagYou then must check the cancel flag of the IRP. If it is not canceled then you will call If it has been canceled we need to know if it called your cancel routine. You do this by setting the cancel routine to You now have two choices remember that only one location can complete the IRP. If the cancel routine was called then as long as the cancel routine doesn't complete the IRP, if it's not in your IRP list, then you can free it. If the cancel routine always completes it, then you must not complete it. If the cancel routine was not called then you obviously must complete it. No matter what happens you must remember two things. The first is that somewhere in your driver you must complete this IRP. The second thing to remember is that you must never complete it twice! When you remove an IRP from the list it's the same thing. You should always check to make sure the IRP has not been canceled. You will also set the cancel routine to 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); } Alternatively you can use something like cancel safe IRP queues. Process and CompleteThis is where you simply process the request in line and complete it. If you don't return Creating IRPsThere was an extreme brief description of how to create and send IRPs in the previous article. We will go over those steps again here in more detail. We will also learn the difference between the APIs that we can use to create IRPs. Step One: Create the IRPThere are a few APIs that can be used to create an IRP. As we already know, however there is a difference between them that we need to understand. The source in article 4 was very sloppy with IRP handling and this was simply to introduce IRPs without having to explain everything that we are explaining here. There are Asynchronous IRPs and Synchronous IRPs. If you create an IRP using If you create an IRP using Also remember before you consider creating an IRP make sure that you understand what IRQL your code will be called at. The benefit of using Step Two: Setup the IRP ParametersThis is very simple and taking the TDI example the macro Step Four: Send to the driver stackThis is very simple and we have done it over and over again. We simply use Step Five: Wait and Clean upIf the driver returned any status besides " If you created a synchronous IRP, you either let the I/O Manager handle it and you're done or you set the completion routine to return more processing in which case you do it here than call If the status returned is " If your IRP was created synchronously then the I/O Manager will set this event for you. You don't need to do anything unless you want to return the status more processing from the completion routine. Please read the section on "How Completion Works" to further understand what to do here. Non-Paged Driver CodeIf you remember in the first tutorial we learned about If you look at the code, you will notice that some of the /* #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) */ How Completion Works?The completion works in a way that each device's STACK LOCATION may have an associated completion routine. This completion routine is actually called for the driver above it not for the current driver! The current driver knows when he completes it. So when the driver does complete it the completion routine of the current stack location is read and if it exists it's called. Before it is called the current That is probably a bit confusing so you refer back up to the talk on how to "Forward and Post Process". Now if your driver created the IRP you do not have to mark the IRP as pending! You know why? Because you don't have an You will notice that example code may actually show a completion routine calling " I implemented a completion routine in our TDI Client driver. We create synchronous IRPs there however if you check out bit of debugging as follows: 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? As you can see we are at Why STATUS_PENDING?As if I haven't already confused you enough we need to talk about The other use is that a driver in the middle of the stack can change this status from Overlapped I/OThe " Other ResourcesThe following are other resources and articles on IRP Handling that you may want to refer to and read.
These are "cheat sheets" which simply show sample code on how to handle IRPs. I am skeptical on the information in Cheat Sheet 2 on the IRP Completion routines which mark the Synchronous IRPs as Pending! Remember what I talked about the IRP completion routine is called with the stack location of that device. If you allocated that IRP, it doesn't mean you are on the device stack! I have not tried the code myself, so I could be missing something in the implementation.
There are many other resources out on the web and the URLs I provided will probably be gone or moved someday! Example SourceThe example source will build six binaries as listed here. 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 The TDI Client Driver that was created can be used using a simple API set as implemented in NETLIB.LIB. I named it the " Driver ArchitectureThe architecture of the driver is very simple. It simply queues all read and write IRPs. It has a special write thread that it created in the system process. This is just to demonstrate queuing IRPs and performing Asynchronous operations. The call to write network data can return to user mode without having to wait for the data to be sent or having to copy the data. The read is the same the IRPs are queued and when the data receive callback occurs those are completed. The source is fully commented. Building the SourceFirst as always make sure that all makefiles point to the location of your DDK. The current makefiles assume the root of the same drive the source is on at /NTDDK/INC. The second is to make sure that your Visual Studio environment variables are setup using VCVARS32.BAT. I created a new make file at the root of the "network" directory which you can then use to build all directories. The first command you can use is "nmake dir". This command will fail if any of the directory already exists. What it will do is pre-create all directories needed to build the source. Sometimes the source build will fail if the directories do not already exist. 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 The second thing that you can do is "nmake" or "nmake all" to build the sources. It will go into each directory and build all 6 binaries in the correct order. 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) The last option you have is "nmake clean" which will then go into each directory and delete the object files. This will then cause that project to be rebuilt upon typing "nmake" or "nmake all". Of course you can type "nmake and "nmake clean" in any of the application directories as well however this is a convenient way to build all binaries at one time. 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 Chat ServerThe chat server is a very simple implementation. It simply accepts connections and puts these connections into a list. Any time it receives data from any client it simply broadcasts this to all other clients. Chat ClientsThere are two chat clients but they both are essentially implemented the same. The only difference is that one talks to the Winsock API and the other uses our " Chat ProtocolThe chat protocol is extremely simple. The first packet sent will be the name of the client and used to identify him to all other clients. The rest are simply broadcast as strings. There is no packet header. So the server and clients all assume that each bit of chat text sent will be read in one receive! This is extremely prone for error and was just used as an example. To beef it up you may want to consider actually creating a protocol! Bugs!There are essentially three bugs that are known in the source code. Two of them are actually things just left out of the implementation and the other is just something I saw that I didn't feel like fixing. This is example code you are lucky it compiles! Have you ever seen books where they give code that you know would not compile! Well, here at least this is working in the most simplest of cases. The bugs are there for you to fix. I figure that I'll give some guidance and you can get better acquainted with the code by fixing these bugs. I did run some of the driver verifier tests on the source to make sure there were no bluntly obvious bugs but there has not been extensive testing. Then again this isn't a commercial software. There could be other bugs, if you find any see if you can fix them. If you need some help let me know. Bug One: TDI Client Detect DisconnectThere is no implementation to detect when the client disconnects from the server. If the server is aborted while the client is connected it simply does not know and continues to attempt to send data. The return value from Bug Two: No ProtocolThere is no protocol implemented between the clients and server. A protocol should be implemented that does not rely on receiving the entire packet ever read and be more flexible! Perhaps add even a simple file transfer! Bug Three: Incorrect DisplayThere is a bug that involves two connected clients. This bug actually will occur using either client implementats, TDI or Sockets. The bug occurs when one client is about to type a message but it doesn't send it. The other client then sends 5 or so messages. The client that didn't send any message then sends his message. This message is corrupted, the name is overwritten with the data being sent. As a hint, you may want to investigate the data being sent and pay attention to the "/r/n" pairings. ConclusionThis article implemented a simple chat program that used sockets and an implementation of a TDI Client. There was also a lot of information on how to handle IRPs along with links to other locations to further your education. IRPs are the backbone of driver development and they are key to understand how to write device drivers for Windows. Please remember that there are a lot of misinformation, missing information and bad examples out there so make sure that you visit a few different sites and attempt a few techniques so that you can distinguish what is correct and what is incorrect. |