. | . | . | . | David McCracken |
Windows Device Driver Exampleupdated:2016.07.13 |
I have written relatively ordinary kernel and application level drivers for serial, parallel, network (TCP/IP), and USB (bulk, isochronous, HID) under Win 9X, NT, 2K, XP, W7, XP, WinCE, XPe, and TabletPC. This example is different. It is a multi-transport, multi-process, content-based routing, remote DMA communication driver. Some of the features of this driver are unusual. The overall design is unique but not without purpose. It addresses important application requirements.
I originally developed the driver for 9X and later modified it to support NT/2K/XP/W7. It can be compiled for either type of OS. The interface DLL operates under either model (9X/NT), automatically selecting and loading the appropriate kernel-level (VxD/SYS) components. For this presentation I have removed the conditionally compiled 9X code and moved large comment blocks from the code into the accompanying text.
The driver uses some advanced and obscure Windows kernel capabilities, such making a ring0/3 event using ObReferenceObjectByHandle and sharing a non-pageable buffer with an application by using ZwMapViewOfSection to make a memory viewport in the application’s process space. However, nearly every facility here is available in Linux under a different name. The only capability that is not available in Linux is not in the kernel part of the driver but in the application level DLL, where RxClient::getMsg calls WaitForMultipleObjects to wait for either an input message for this process or an abort. Neither System 5 nor Posix IPC mechanisms afford this capability.
Most applications that include some form of external communication embed a significant amount of knowledge of the specific transport type. For example, applications are not designed to communicate via USB or Ethernet interchangeably. The transport information is so ingrained in many applications that they have to be completely rewritten to change the transport. In a few cases this is justified because the application is inherently associated with the transport. Usually it is not justified and it certainly is not in the case of the application for which I developed this driver.
The purpose of the driver is to connect the two major parts, data processor and real-time controller, of a complex medical laboratory instrument. It is part of an instrument development system, supporting a wide range of instruments over a 20-year product line lifetime. During this time, it is expected that even a state-of-the-art transport would become obsolete. In fact, the most pressing need was for a driver to support an old proprietary HDLC-like transport. Being able to switch out the transport with minimal disruption would be extremely valuable. Further, the full product line would contain some instruments with a data processor and controller in one box, loosely connected for example by PCI or PCIe or tightly connected by real DMA or shared memory. In other instruments, the two parts could be separated by a significant distance, perhaps even requiring a WAN connection.
It is quite common for a communication protocol developed for one type of physical link to be tunneled through another, for example PCI on PCIe. However, this usually comes at a cost in increased complexity and reduced performance (compared to the native protocol of the new link). These costs would be significant if any remote communication protocol were imposed on a tightly coupled connection. The instruments in this product line produce a fast, large data stream. The top of the line instruments might use tight coupling to avoid the cost and complexity of an external link capable of handling their data stream.
For one communication driver to efficiently support the full range of expected products, it could not impose the overhead of an external link protocol on the tightly coupled instruments. The only feasible link-agnostic architecture is one in which the tightly coupled interface is the basic mechanism, with loosely coupled protocols implemented on this, transparently to all application- and most kernel-level processes. This is the definition of remote DMA.
My remote DMA implementation bypasses Windows’ standard application-kernel interface, which imposes at least one and sometimes two or three data copies. It bypasses ring transitions and callback scheduling. My design even avoids the overhead of semaphores, instead ensuring data coherency by using atomic instructions on buffer indices. It is at least ten times faster than drivers that play by the rules. However, this doesn’t mean very much if the physical link can’t take advantage of it. It is certainly overkill for the HDLC link, but it does, even in this case, reduce the communication burden on the data processing CPU.
Since the purpose of this driver is to provide communication between the data processor and controller of an instrument, the motivation for supporting multiple simultaneous applications (processes) is not obvious. It is to support debugging, testing, and monitoring.
These are complex multi-domain (software, firmware, hardware, chemistry) instruments. They contain all of the usual problems that can be eliminated by thorough corner-case testing but also a vast number of specific scenarios, which do not exist in a continuum with corner cases. A common problem is that an instrument stops working even though both the data processor and controller are functioning but refuse to talk to each other. Often, there is no underlying communication problem. The two are out of sync and can’t restore normal operation without rebooting the entire system, losing information that would help to diagnose the problem, which becomes a phantom-- the problems that everyone thinks may be real but can never be reproduced when we are prepared to diagnose them.
It would be possible to include facilities in the data process program for debugging these difficult situations without rebooting. However, these are regulated medical instruments. All code requires testing and justification. Program debugging code is nearly impossible to justify to regulators. This is a big problem but there are also small problems. The application program undergoes rigorous testing and releases are expected to have some longevity. Debugging code is much more spontaneous, often thrown together to diagnose one problem. If the problem involves control synchronization we may be able to investigate it with generic memory probing. If it involves big data, we may need unanticipated visualization tools. Irrespective of the regulatory burden, the mismatch between debugging and application code is a version control problem.
With my driver, a "frozen" instrument is not rebooted. Instead, a debug program is started. It attaches to the driver and communicates with the controller. Significantly, the controller does not see this change and does nothing to accommodate it.
This capability can also be used to monitor (even through a WAN link) without interfering with the communication during normal operation. It is also used for testing. My driver has an IoCtl API function through which an application can inject or retrieve in-band messages at the same low level as a link-specific mechanism. A test program, completely separate from the application, uses this for testing the driver. This enables a challenge-based test strategy using coordinated application and (simulated) communication attacks. This reduces random strategies with poor coverage to provable corner cases with 100% coverage.
Any attached application can send any message to the controller through my driver. During normal operation, the controller would not be sending messages that the data processor would not want. The driver routes all incoming messages by default to the one process. A second application could significantly change the message routing situation. A monitor might want a duplicate or the only copy (exclusive) of a message. A debugger might cause the controller to send messages that the normal application doesn’t understand at all or only under other circumstances. Consequently, applications tell my driver which types of messages they want to receive and whether they want to prevent any other application from receiving them. The most recent application to attach has control. It can relinquish message types at any time and automatically relinquishes all on exiting. The driver remembers previously registered recipients and automatically restores them.
// CDVROS.C #define coreVerMajor 100 #define coreVerMinor 0 extern USHORT linkVerMajor; extern USHORT linkVerMinor; #define fixedBufferPages 2 // Each page adds 4 Kbytes to main buffer. #define MEMLEN fixedBufferPages * 4096 #define SIZE_TX_BUFFER 1024 //#define SIZE_TX_BUFFER 7424 // Shrinks rx buffer to 768 (if 2 pages) for testing. #define TxBeginFromBufferEnd ( fixedBufferPages * 4096 - SIZE_TX_BUFFER ) #pragma LOCKED_DATA UCHAR *dmaBuf; // System virtual address of DMA buffer. ULONG realDmaBuf; // Physical address of DMA buffer. extern WCHAR deviceLink[]; extern WCHAR deviceName[]; UCHAR *userDmaBuf; UCHAR *userRxBegin; int appCount = 0; PDEVICE_OBJECT deviceObject = NULL; PHYSICAL_ADDRESS physAddr; // LARGE_INTEGER = struct { ULONG LowPart; LONG highPart; } PADAPTER_OBJECT dmaAdapter; ULONG nMapRegs = 1; PKINTERRUPT irqObject; #define N_USER_ADDRESSES 10 void *userAddresses[ N_USER_ADDRESSES ]; int userAddrIdx = 0; BOOL initIrq( UCHAR irq, ISR_PROC *isr, USHORT flags ) { ULONG irqVector; KAFFINITY affinity; KIRQL irql = irq; irqVector = HalGetInterruptVector( Isa, 0, irq, irq, &irql, &affinity ); if( IoConnectInterrupt( &irqObject, isr, deviceObject, // IN PVOID ServiceContext. Passed to ISR as context. 0, irqVector, irql, irql, Latched, FALSE, affinity, FALSE ) == STATUS_SUCCESS ) return TRUE; irqObject = 0; return FALSE; } BOOL grabDmaChannel( UCHAR channel ) { return TRUE; } void reportResources( USHORT portAddr, USHORT portLen ) { #define PARTIALS 3 #define SIZE_LIST \ sizeof( CM_RESOURCE_LIST ) + \ sizeof( CM_PARTIAL_RESOURCE_DESCRIPTOR ) * ( PARTIALS - 1 ) CM_PARTIAL_RESOURCE_DESCRIPTOR *partial; CM_RESOURCE_LIST *resourceList; PHYSICAL_ADDRESS pa; BOOLEAN conflict; resourceList = ExAllocatePool( PagedPool, SIZE_LIST ); if( !resourceList ) return; RtlZeroMemory( resourceList, SIZE_LIST ); resourceList->Count = 1; // One CM_FULL_RESOURCE_DESCRIPTOR in List. // Define the one CM_FULL_RESOURCE_DESCRIPTOR's general characteristics. resourceList->List[0].InterfaceType = Isa; resourceList->List[0].BusNumber = 0; // Define the CM_PARTIAL_RESOURCE_LIST resourceList->List[0].PartialResourceList.Version = 0; // (not required). resourceList->List[0].PartialResourceList.Revision = 0; resourceList->List[0].PartialResourceList.Count = PARTIALS; // Point to the first CM_PARTIAL_RESOURCE_DESCRIPTOR. partial = &resourceList->List[0].PartialResourceList.PartialDescriptors[0]; partial->Type = CmResourceTypeInterrupt; partial->ShareDisposition = CmResourceShareDriverExclusive; partial->Flags = CM_RESOURCE_INTERRUPT_LATCHED; partial->u.Interrupt.Vector = irqNumber; partial->u.Interrupt.Level = irqNumber; partial->u.Interrupt.Affinity = -1; partial++; partial->Type = CmResourceTypeDma; partial->ShareDisposition = CmResourceShareDriverExclusive; partial->Flags = 0; partial->u.Dma.Channel = dmaChannel; partial++; partial->Type = CmResourceTypePort; partial->ShareDisposition = CmResourceShareDeviceExclusive; partial->Flags = CM_RESOURCE_PORT_IO; pa.HighPart = 0; pa.LowPart = portAddr; partial->u.Port.Start = pa; partial->u.Port.Length = portLen; partial++; IoReportResourceUsage( NULL, deviceObject->DriverObject, NULL, 0, deviceObject, resourceList, SIZE_LIST, TRUE, // Override Conflict &conflict ); ExFreePool(resourceList); } void releaseResources( void ) { BOOLEAN thing; CM_RESOURCE_LIST resourceList; int idx; onDvrExit(); // Includes cancelling timers. if( irqObject != 0 ) { IoDisconnectInterrupt( irqObject ); irqObject = 0; } if( dmaBuf != 0 ) { HalFreeCommonBuffer( dmaAdapter, MEMLEN, physAddr, dmaBuf, 0 ); dmaBuf = 0; } // Release user-addressable sections. for( idx = 0 ; idx < userAddrIdx ; idx++ ) ZwUnmapViewOfSection( (HANDLE)-1, userAddresses[ idx ]); userAddrIdx = 0; // Release events if( txEvent != 0 ) { ReleaseRing30Event( txEvent ); txEvent = 0; } for( idx = 0 ; idx < nextRxRegistrant ; idx++ ) ReleaseRing30Event( share.rx[ idx ].event ); nextRxRegistrant = 0; RtlZeroMemory( &resourceList, sizeof(CM_RESOURCE_LIST)); // Comment out to keep resources registered for verification. IoReportResourceUsage( NULL, deviceObject->DriverObject, NULL, 0, deviceObject, &resourceList, sizeof(CM_RESOURCE_LIST), TRUE, &thing ); } /**************************************************************************** * Function: mapToUser * Description: Convert physical address to user virtual. * Returns: void * user virtual address pointing to the same physical * memory as the argument. NULL if failure. * Arguments: * - PHYSICAL_ADDRESS physicalAddress. * - ULONG memlen is the byte count of the memory block referenced by phsyical- * Address. *..........................................................................*/ void *mapToUser( PHYSICAL_ADDRESS physicalAddress, ULONG memlen ) { UNICODE_STRING memName; OBJECT_ATTRIBUTES objectAttributes; HANDLE hPhysMem; void *physMemSect; PHYSICAL_ADDRESS viewBase; UCHAR *virtualAddress; ULONG length; NTSTATUS status; if( userAddrIdx == N_USER_ADDRESSES ) return 0; // No more room to store address for unmapping at exit. memset( &objectAttributes, 0, sizeof( objectAttributes )); RtlInitUnicodeString( &memName, L"\\Device\\PhysicalMemory" ); InitializeObjectAttributes( &objectAttributes, &memName, OBJ_CASE_INSENSITIVE, (HANDLE)0, (PSECURITY_DESCRIPTOR)0 ); virtualAddress = 0; status = ZwOpenSection( &hPhysMem, SECTION_ALL_ACCESS, &objectAttributes ); if( status == STATUS_SUCCESS ) { // ZwOpenSection OK. status = ObReferenceObjectByHandle( hPhysMem, SECTION_ALL_ACCESS, (POBJECT_TYPE)0, KernelMode, &physMemSect, (POBJECT_HANDLE_INFORMATION)0 ); if( status == STATUS_SUCCESS ) { // ObReferenceObjectByHandle OK. viewBase = physicalAddress; length = memlen; status = ZwMapViewOfSection( hPhysMem, // IN HANDLE SectionHandle (HANDLE)-1, // IN HANDLE ProcessHandle (Any/current?). &virtualAddress, // IN OUT PVOID *BaseAddress 0L, // IN ULONG ZeroBits length, // IN ULONG CommitSize &viewBase, // IN OUT PLARGE_INTEGER SectionOffset &length, // IN OUT PULONG ViewSize ViewShare, // IN SECTION_INHERIT InheritDisposition 0, // IN ULONG AllocationType PAGE_READWRITE ); // IN ULONG Protect. if( status == STATUS_SUCCESS ) { // ZwMapViewOfSection OK. virtualAddress += physicalAddress.LowPart - viewBase.LowPart; userAddresses[ userAddrIdx++ ] = virtualAddress; // Save for unmapping. } else virtualAddress = 0; } // ObReferenceObjectByHandle OK. ZwClose( hPhysMem ); } // ZwOpenSection OK. return virtualAddress; } void getRing0Event( void **pevent, HANDLE hevent ) { ObReferenceObjectByHandle( hevent, // IN HANDLE Handle SYNCHRONIZE, // IN ACCESS_MASK DesiredAccess NULL, // IN POBJECT_TYPE ObjectType KernelMode, // IN KPROCESSOR_MODE AccessMode pevent, // OUT PVOID *Object NULL ); // OUT POBJECT_HANDLE_INFORMATION HandleInformation } void atExit( void ) { int x = 1; // Minimum function body needed by SoftIce to recognize. } typedef struct { ToDvr toDvr; FromDvr fromDvr; } DvrBuf; #define appToDvr ((ToDvr*)parms) #define dvrToApp ( &((DvrBuf*)parms)->fromDvr ) #define IoCtl ( irpStack->Parameters.DeviceIoControl.IoControlCode ) static UCHAR *userShareBuf; // Static in order to record base address for rxReg. NTSTATUS dispatch( PDEVICE_OBJECT DeviceObject, PIRP Irp ) { PDEVICE_DESCRIPTION devDes; PIO_STACK_LOCATION irpStack; UCHAR *parms; NTSTATUS ntStatus; ULONG timeout; RxReg *rap; RxReg *rap2; UINT idx; UCHAR id; short sidx; ntStatus = STATUS_SUCCESS; irpStack = IoGetCurrentIrpStackLocation (Irp); switch( irpStack->MajorFunction ) { case IRP_MJ_DEVICE_CONTROL : break; case IRP_MJ_CLOSE : if( --appCount == 0 ) releaseResources(); default: goto done; } // IRP_MJ_DEVICE_CONTROL parms = Irp->UserBuffer; switch( IoCtl ) { case CDXDVR_GETVERSION : dvrToApp->ver.linkMajor = linkVerMajor; dvrToApp->ver.linkMinor = linkVerMinor; dvrToApp->ver.coreMajor = coreVerMajor; dvrToApp->ver.coreMinor = coreVerMinor; break; case CDXDVR_FINDPORTS : if( appCount == 0 ) // CDXDVR_FINDPORTS must precede CDXDVR_INIT. findPorts( &appToDvr->fp ); break; case CDXDVR_INIT : /* This is called for every application that attaches to * the driver. If it is the first one then buffer memory is allocated and * fundamental control variables initialized. Otherwise, initialization is * skipped. In either case, the receive and transmit buffer addresses are passed * back to the DLL for application-specific pointers to shared resources. */ if( ++appCount > 1 ) goto tellAddr; ntStatus = STATUS_INSUFFICIENT_RESOURCES; dmaBuf = 0; devDes = ExAllocatePoolWithTag( PagedPool, sizeof( DEVICE_DESCRIPTION), 'pRSO' ); if( devDes == 0 ) break; RtlZeroMemory( devDes, sizeof( DEVICE_DESCRIPTION )); devDes->InterfaceType = Isa; devDes->DmaChannel = dmaChannel; devDes->MaximumLength = MEMLEN; dmaAdapter = HalGetAdapter( devDes, &nMapRegs ); ExFreePool( devDes ); if( dmaAdapter == 0 ) break; dmaBuf = HalAllocateCommonBuffer( dmaAdapter, MEMLEN, &physAddr, FALSE ); if( dmaBuf == NULL ) break; realDmaBuf = physAddr.LowPart; memset( dmaBuf, 0, MEMLEN ); // To help debugging initial comm problems. pRxAck = dmaBuf; // Use for DMA ACK/NAK for e.g. ECP. physRxAck = realDmaBuf + ( pRxAck - dmaBuf ); rxBegin = pRxAck + 4; rxRealAddr = realDmaBuf + ( rxBegin - dmaBuf ); txBegin = dmaBuf + TxBeginFromBufferEnd; /* Use end of fixed buffer for tx. */ txRealAddr = realDmaBuf + TxBeginFromBufferEnd; beyondRx = txBegin; beyondTx = dmaBuf + MEMLEN; strcpy( rxBegin + 1, "RxBegin" ); /* For test. See ancom.cpp- * openDriver. Note that rxBegin[0] will be assigned a value to indicate no * message so we can't put this string there. */ strcpy( txBegin, "TxBegin" ); memset( rxRoute, 0, sizeof( rxRoute )); memset( &share, 0, sizeof( share )); reinit(); initLink(); // Link-specific. Note that rxMsg and rxIp are valid. tellAddr: /* All applications get this because NT+ doesn't allow applications to share pointers even to shared memory. */ /* The shared memory has to be mapped to each application's own address spaece. Just being shareable is not enough. */ userDmaBuf = mapToUser( physAddr, MEMLEN ); if( userDmaBuf == NULL ) break; userRxBegin = userDmaBuf + ( rxBegin - dmaBuf ); userShareBuf = mapToUser( MmGetPhysicalAddress( &share ), sizeof( share )); if( userShareBuf == NULL ) // static userBuf records base address for rxReg. break; dvrToApp->init.pDvrStat = (DvrStat*)( userShareBuf + offsetof( Share, ds )); dvrToApp->init.rxBegin = userRxBegin; dvrToApp->init.txBegin = userDmaBuf + ( txBegin - dmaBuf ); dvrToApp->init.beyondTx = userDmaBuf + ( beyondTx - dmaBuf ); dvrToApp->init.timeoutTypes = nTimeoutTypes; break; case CDXDVR_REINIT : stopLink(); reinit(); memset( txBegin, 0, beyondTx - txBegin ); reinitLink(); // Link-specific reinitialization (may do nothing). break; case CDXDVR_REGISTERTX : /* This is called only for the first application that connects to the driver. */ getRing0Event( &txEvent, appToDvr->regTx.event.ring3 ); break; case CDXDVR_WAKETX : if( share.ds.txQcount > 0 ) sendMsg( SMC_WAKETX ); break; case CDXDVR_GETTIMEOUT : dvrToApp->gto = getTimeout( appToDvr->gto.dest, appToDvr->gto.max, appToDvr->gto.idx ); break; case CDXDVR_SETTIMEOUT : sidx = appToDvr->sto.idx; if( sidx < nTimeoutTypes ) setLinkTimeout( sidx, appToDvr->sto.timeout ); break; case CDXDVR_CLEARSTATUS : if( appToDvr->cs.clearFailMap != 0 ) failMap = 0; if( appToDvr->cs.clearStatusMap != 0 ) statusMap = 0; break; case CDXDVR_GETSTATUS : dvrToApp->gs.debugVal = debugVal; dvrToApp->gs.statusMap = statusMap; dvrToApp->gs.failMap = failMap; break; case CDXDVR_GETSTATUSNAME : getStatusName( appToDvr->gsn.dest, appToDvr->gsn.max, appToDvr->gsn.idx ); break; case CDXDVR_REGISTERRX : if( nextRxRegistrant > MAX_RX_REGISTRANT ) dvrToApp->rx.qp = 0; else { rap = share.rx + nextRxRegistrant; memset( rap, 0, sizeof( RxReg )); //rap->qc.mcount = 0; //rap->qc.xi = 0; rap->qc.id = nextRxRegistrant; getRing0Event( &rap->event, appToDvr->regRx.event.ring3 ); dvrToApp->rx.qp = (Qctl*)( userShareBuf + ((UCHAR*)&rap->qc - (UCHAR*)&share )); /* Tell registrant where to find his Qctl. */ dvrToApp->rx.rxBegin = userRxBegin; /* The first client to register will be given all rx messages * unless it explicitly unregisters for them. Subsequent clients only * get the ones in their begin-end range. This allows an application * to register for the messages that it really must receive even if * (or because) this means stealing them from a previously registered * application but if there are no previous registrants then it * automatically gets all of the messages. */ if( nextRxRegistrant == 0 ) acceptRange( rap, 0, 0xFF ); else acceptRange( rap, appToDvr->regRx.range.begin, appToDvr->regRx.range.end ); nextRxRegistrant++; if( nextRxRegistrant == 1 ) startRx(); /* Certain link drivers may not be able * to handle the analyzer starting to talk first at startup unless * there is a registered receiver. Those should set up everything in * initLink but wait until the first receiver registration before * enabling rx. This is a dummy function for others. */ } break; case CDXDVR_UNREGISTERRX : rap = share.rx + ( id = appToDvr->id ); ReleaseRing30Event( rap->event ); // Promote all registrant filter maps that were lower // than this one in the mru list. for( rap2 = share.rx ; rap2 < share.rx + nextRxRegistrant ; rap2++ ) if( rap2->mru > rap->mru ) rap2->mru--; // Promote by decreasing mru. // Remove this registrant from the receiver list. If at end then just // decrement nextRxRegistrant. Otherwise, collapse the list over him. if( id < --nextRxRegistrant ) for( idx = id ; idx < nextRxRegistrant ; idx++ ) share.rx[ idx ] = share.rx[ idx + 1 ]; // Replace all of the references to this registrant in the routing table. for( idx = 0 ; idx < 256 ; idx++ ) if( rxRoute[ idx ] == id ) rerouteRx( idx ); break; case CDXDVR_RXACCEPT : case CDXDVR_RXREJECT : if( appToDvr->msgFilter.id < nextRxRegistrant ) ( IoCtl == CDXDVR_RXACCEPT ? acceptRange : rejectRange ) ( share.rx + appToDvr->msgFilter.id, appToDvr->msgFilter.range.begin, appToDvr->msgFilter.range.end ); break; case CDXDVR_ENDRXPAUSE : // See ackMargin note in onRxMsg. /* This operation would not be called if rxState were not RX_PAUSE. * However, by checking this here, we relieve the application threads * of having to guard their ackMargin check with a MUTEX in order to * prevent the possibility of calling here twice for one pause. */ if( rxState == RX_PAUSE ) { rxState = RX_NORMAL; // In any case, end pause state. if( rxBadLen == 0 ) { /* If not currently receiving an unexpected (because of pause) message * then finish the paused transaction now. Otherwise, let rxTimeout do * it. */ if( rxDuringPause ) { sendAckNak = NAK; rxDuringPause = FALSE; } else sendAckNak = ACK; if( simRx ) simRx = 0; else txAckNak( 0, 0, 0, 0 ); rxCnt = 0xFF; // Tell ISR it's OK to accept the next input. } } break; case CDXDVR_TEST : switch( appToDvr->test.op ) { case CDXDVRTEST_RXPAUSE : rxPauseTest = TRUE; break; case CDXDVRTEST_BREAK : bomb(); break; } break; case CDXDVR_SIMRX : simulateRxMsg( (UCHAR *)appToDvr ); break; } done: Irp->IoStatus.Information = 0; Irp->IoStatus.Status = ntStatus; IoCompleteRequest( Irp, IO_NO_INCREMENT ); // IRP is gone after this. // We never have pending operation so always return the status code. return ntStatus; } VOID unload( PDRIVER_OBJECT DriverObject ) { UNICODE_STRING deviceLinkUnicodeString; RtlInitUnicodeString( &deviceLinkUnicodeString, deviceLink ); IoDeleteSymbolicLink( &deviceLinkUnicodeString ); atExit(); /* Delete the device object */ IoDeleteDevice( DriverObject->DeviceObject ); } NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { NTSTATUS ntStatus; UNICODE_STRING deviceNameUnicodeString; UNICODE_STRING deviceLinkUnicodeString; RtlInitUnicodeString( &deviceNameUnicodeString, deviceName ); /* * Create a non-EXCLUSIVE device, i.e. one that receives every IRP_MJ_CREATE * issued for this device. Microsoft incorrectly says that this means only one * thread at a time can send I/O requests. See OSR p. 281. If this were TRUE, * I/O Manager would reject multiple OpenFile calls. */ ntStatus = IoCreateDevice( DriverObject, 0, // sizeof( Extension ) &deviceNameUnicodeString, FILE_DEVICE_DVR, 0, FALSE, // Exclusive. FALSE means to accept multiple OpenFile requests. &deviceObject ); if( ntStatus >= 0 ) { /* * Create a symbolic link that Win32 apps can specify to gain access * to this driver/device */ RtlInitUnicodeString (&deviceLinkUnicodeString, deviceLink ); ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString, &deviceNameUnicodeString ); // Create dispatch points for device control, create, close. DriverObject->MajorFunction[IRP_MJ_CREATE] = dispatch; DriverObject->MajorFunction[IRP_MJ_CLOSE] = dispatch; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = dispatch; DriverObject->DriverUnload = unload; } if( ntStatus < 0 ) { if( deviceObject ) IoDeleteDevice( deviceObject ); } return ntStatus; }
I defined a generic kernel-level API through which transport-agnostic functions interface with a transport-specific module. All of the driver code for any particular communication mechanism resides in one module, which typically comprises less than 20% of the total driver code.
Transmit is relatively simple. Any application can submit a message, requesting blocking or non-blocking. If non-blocking and the output buffer is full, the driver returns a non-critical error. There is no data coherency issue or data routing. The only complexity is that the transmit process is a chain dispatcher, where the end of one transmission triggers the beginning of the next. This stops if there is nothing to transmit. After this, the driver has to restart the cycle if a new message is submitted for transmission. Some transport types also need the driver to send management messages periodically and/or a polling message for the controller to be able to send a message. For example, if the input buffer becomes full, the transmit process would stop sending data requests. If an application extracts a message, clearing the buffer, the transmit process would have to be restarted to resume polling.
The receiving process is much more complicated. Data coherency is an issue and must be guaranteed by a semaphore or by my more efficient atomic indexing of a ring buffer. Further complicating the receiver, zero data copy (remote DMA) requires the driver to map a shared non-pageable memory buffer to each registered receiver (process or thread) and all receivers have to shared this one buffer. Ring buffer theory assumes one producer and one consumer. This is uncharted territory.
Most of the transport-agnostic kernel code is contained in two modules. OnRxMsg. c contains the receiver in-band process, that is managing the ring buffer and waking up applications waiting for input that has just arrived. It is the one producer. It is tightly coupled with its consumer counterpart in the application-level DLL through which applications retrieve their messages. Cdvros.c contains the relatively simple in-band transmit process and all out-of- band control through an IoCtl interface. It registers receivers, mapping their memory to the input buffer, creating an event through which the driver can wake them, and adding them to the routing table but it does not play any direct role in the in-band receiver process.
The kernel and application level programs have to share data for two purposes.
One is for event signaling, both from application to kernel and from kernel to
application. For this, an event is created at the application level and its
handle passed through the IoCtl interface to the kernel driver. My
getRing0Event
function calls the OS function
ObReferenceObjectByHandle
to transform this into a ring0/3 event. The
event itself has no directionality. Either kernel or application can wait on or
set one of these. In practice, no event would be used bidirectionally because
such use would theoretically and practically fail.
The kernel and application also share the receiver ring buffer. The kernel
driver allocates this from non-pageable memory without reference to any
application. When an application attaches to the driver, it is given its own
viewport into this by creating a mapping in the application’s process space. My
kernel driver does this in the mapToUser
function. At the heart of
this is a call to the OS function ZwMapViewOfSection
. Unlike
signal usage, bidirectionality is essential in this use of shared memory.
Message payloads are only written in the kernel and only read in the
application but buffer management data, which both levels read and write, is
stored in the buffer itself, which is typical for a ring buffer.
Ring 0 wakes up ring 3 using ring3/0 event pairs that are created in ring 3. Setting the ring 0 event signals the corresponding ring 3 event. There is only one such event pair for the transmit process, because all apps/threads share a single txServer ring 3 driver interface. When the transmit buffer doesn’t have room for the message, TxServer.sendMsg indicates how much space it wants in txWake and goes to sleep. When the transmitter reduces txWake to at or below 0, it wakes up txServer by setting txEvent. Each application thread has its own ring3/0 event because a sleeping registered receiver thread is awakened when an input message is routed to it individually. The ring 0 driver knows when the target thread is sleeping by the fact that it is waiting for input. The thread is signalled via its own RxReg.event.
The receive process is normally triggered by input and doesn’t need to be awakened by ring 3 (application or driver shell). However, if the input buffer becomes too full to accept another full length message then the receiver sends either ACKPAUSE or nothing (depending on configuration to match the expected analyzer). In either case, when enough messages have been extracted by ring 3 code, the receive process needs to be awakened to send an ACK, telling the analyzer that it may send more messages. AckMargin tells ring 3 when this situation exists. Ring 3 code (RxClient.getMsg) invokes the ENDRXPAUSE command to wake up the receiver.
The transmitter could be awakened whenever an application has a message to send but this would require an expensive call through the OS every time. To improve performance, I have implemented a circular multi-message transmit buffer. As long as there are messages to transmit, the process regenerates itself by onAckNak’s call to sendMsg. TxAckNak also checks for waiting transmit messages, which affords only a slight improvement in a full duplex link but is included to be more consistent with the half-duplex links, for which it is practically essential. However, this does complicate things a little. If the transmitter were completely independent of the receiver then txIdle, the flag used to tell ring 3 that it must wake up the transmitter when it inserts a message, would positively reflect the status of the transmit process. However, given that txAckNak might wake up a sleeping transmit process, the state indicated by txIdle is fuzzy. Sometimes, ring 3 calls to WAKETX would duplicate the action of txAckNak. To avoid this, WAKETX calls sendMsg only after verifying that this is needed. Eliminating txAckNak’s call to sendMsg would reduce code a bit both in the call and in WAKETX while slightly reducing transmit performance. The reason for having the Rx-Tx connection is not performance but consistency with half-duplex links.
This module provides universal services, working in concert with a link- specific module. These share buffer and status data defined in cdvr.h. The link- specific driver contains largely self-retriggering input and output message handlers, with shared memory connections to ring 3. However, the link-specific modules export 4 functions to this universal module. FindPorts and initLink support initialization. TxAckNak and sendMsg provide a means for ring 3 to restart paused handlers. TxAckNak is called to send an ACK after a pause to prevent overwriting of the input buffer. SendMsg is called when ring 3 inserts a message into the empty tx buffer and we are not waiting for an ACK/NAK reply from the analyzer and we are not already waiting to send and ACK/NAK.
This function converts the given physical address to user virtual. It is derived from the NT4.0 DDK example \ddk\src\general\mapmem. Microsoft offers no real documentation for this process.
The Unicode name for the section must be exactly \Device\PhysicalMemory or ZwOpenSection will fail. The memlen argument is used to tell ZwMapViewOfSection how long to make the section to cover the physical memory block. ZwMapViewOfSection can theoretically reduce this, but that won’t happen if the size has already been verified in the process that has already reserved the block. Therefore, no checking is done here.
The atExit function is used for testing and debugger post-unload system crashes. It doesn’t do anything but establish a permanent last program point before the driver is unloaded. Timers and direct DPCs have been cancelled; IRQ, DMA, and memory resources have been returned. Any driver activity after this point is erroneous and likely to crash the OS. This is primarily intended for debugging with SoftIce. Set SoftIce breakpoints at this time to see which, if any, DPCs have not been cancelled. Use macro. SI macro text is limited so several are needed.
This is the driver’s out-of-band API. It is invoked by applications through the DeviceIoControl function. It directly returns 0 at DIOC_GETVERSION. Otherwise the return value doesn’t matter. Various indirect returns through DIOCPARAMETERS. Various areguments are passed via DIOCPARAMETERS
Most of the functions here are used for configuration. Only ENDRXPAUSE and WAKETX participate in routine communication. They are called only after the ring 0 input and output handlers are unable to complete operations and are, therefore, unable to retrigger themselves. In the case of ENDRXPAUSE, the receive buffer is nearly full and the receiver sent either ACKPAUSE or nothing; in either case, the analyzer will not send another message, resuming the input process, until we send an ACK. In the case of WAKETX, the transmit process went to sleep because it emptied the transmit buffer; when a new message is inserted, ring 3 must restart the transmit process, which retriggers itself only as long as it has messages to transmit.
The transmit and receive buffers must be at least 770 bytes (256 * 3 + 2) in order to hold three messages plus a sentinel. Downloading the application program (APUAPP) via ECP takes the same two seconds for 1K as for 2K Tx buffer. Since neither of these is near the 4K page size, rather than wasting a full page, the transmit buffer is carved out of the receive buffer. Even if only one page is allocated, the resulting 3K Rx buffer affords good performance. However, the "normal" size is two pages in order to be sure that communication doesn’t slow down during list mode data phase. The size is control by global variable fixedBufferPages to support run-time selection for experimentation.
Microsoft claims that the DeviceIoControl out and in buffers will be passed as UserBuffer and SystemBuffer. This is not true if the IOCTL’s TransferType is METHOD_NEITHER, in which case the OS passes the driver NULL for SystemBuffer. However, although there is no way for the driver to return a count via lpBytesReturned, if the caller doesn’t provide a valid pointer, the OS page faults.
ONRXMSG.C contains the onRxMsg function, the in-band receive process. It is a callback (DPC) scheduled by a transport-specific ISR upon receiving a complete message. An identical version is used for most transport types but the possibility exists that a future driver will need to provide a replacement, so this is not embedded in cdvr.c, which is, by specification, link-neutral.
This processes a known good (CRC already checked) input message. It returns ACK if there is room in the input buffer for another max length message, else ACKPAUSE. Caller decides what to do in case of ACKPAUSE. This is a function instead of being embedded in onRxMsg only so that it can be shared with the input message simulator.
This is the callback (DPC) scheduled by the ISR upon receiving a complete message. It returns nothing. In registers but not used are ebx = VM handle, which is irrelevant for ring 0 driver; edx = RefData that could be (but isn’t) passed by ISR when it called Schedule_Global_Event; ebp = address of Client_Reg_Struc, which is irrelevant to ring 0 driver. Globals are rxMsg, share.ds, rxIp, rxBadLen, rxCnt, rxState, sendAckNak.
The callback function can modify the EAX, EBX, ECX, EDX, ESI, and EDI registers. It must return with interrupts enabled (it is called with interrupt enabled) and the direction flag clear (up).
ackMargin is used only when this function determines that the input buffer doesn’t have enough remaining space to prevent the next message, if it is MAX length, from running into either the oldest unextracted message or the end of the buffer. When this is the case, ackMargin is assign the amount of space needed plus some extra, to prevent an inefficient pause per message lockstepping. If the analyzer recognizes ACKPAUSE then this is sent instead of ACK. Otherwise, nothing is sent. When application level functions release an extract message (explicitly or by calling for the next one, if ackMargin is > 0 then the length of the released message is subtracted from ackMargin and if ackMargin goes <= 0 then the RXENDPAUSE command is invoked, causing an ACK to be sent. The ACK is sent regardless of whether the ACKPAUSE was sent. The goal of ACKPAUSE is to avoid depending on the analyzer’s tx response timeout being greater than the time that it takes for the application to clear enough space for the next transmission. The analyzer can clear its last tx message on ACKPAUSE or wait for the ACK but, in any case, should not send another message until receiving the ACK.
This inserts the given message into the input buffer as if it were from the analyzer. It returns nothing directly. It changes the first message byte to 0 if the message is inserted into the buffer or to 1 if the buffer is full, as indicated by rxState = ACKPAUSE, a state that can only end by a receiver extracting a message. Returns 2 if buffer management internal error. Argument UCHAR *msg is a standard DM type message except with a prefix byte used to return the insert status to the caller in the application.
This return method is used because Win9x and NT both screw up DeviceIoControl in different ways so that the only guaranteed object is the one user buffer. The message argument could be added to an aggregated toDvr+fromDvr structure as is done for other commands but that would increase the size of the structure significantly, since this message is much longer than the longest toDvr element.
// ONRXMSG.C #define ACK_MARGIN_EXTRA 0 // or MAX_RX_PACKET_SIZE // Extra reserve space to avoid excessive rx stalling UCHAR procRxMsg( void ) { #define MAXPACK MAX_RX_PACKET_SIZE UCHAR *nip; // Next insertion point. UCHAR rxIdx; RxReg *rap; long margin; UCHAR ackNak; long xpIdx; long ipIdx; ackNak = ACK; #if 0 /* For debugging buffer management problems. Rx buffer is shrunk to 3 max.*/ rap = &share.rx[0]; if( rap->qc.mcount >= 3 ) margin = 0; // Breakpoint #endif rxIdx = rxRoute[ rxMsg[1] ]; /* Find the registered receiver for this message type.*/ if( rxIdx < nextRxRegistrant ) { rxMsg[-1] = rxIdx; // Record ID for extractor. share.ds.rxByteCnt += *rxMsg; /* Count data record length (len and CRC not counted).*/ nip = rxMsg + *rxMsg + 2; /* Point to next available input slot. Step on second CRC byte.*/ if( nip + MAXPACK >= beyondRx ) { nip[-1] = QCTL_WRAP; /* Tell extractors to wrap around * to begin. It is safe to write here without checking receiver's * extraction points because it is on the first byte of rxMsg's CRC, * which is no longer needed by anyone. */ nip = rxBegin + 1; } * Find lowest extraction point above the next insertion point. * Receivers with no unretrieved messages are not considered because * 1) they have no messages to protect and 2) the inserter (this * function) will update their extraction points when they receive * their first message so they will never look at whatever is stored * at their current extraction point. */ xpIdx = beyondRx - rxBegin; ipIdx = nip - 1 - rxBegin; for( rap = share.rx ; rap < share.rx + nextRxRegistrant ; rap++ ) { if( rap->qc.mcount > 0 && rap->qc.xi < xpIdx && rap->qc.xi >= ipIdx ) xpIdx = rap->qc.xi; } if( ipIdx <= xpIdx + rxBegin[ xpIdx + 1 ] + 1 && ( margin = ipIdx + 1 + MAXPACK - xpIdx ) > 0 ) { share.ds.ackMargin = margin + ACK_MARGIN_EXTRA; ackNak = ACKPAUSE; } rap = share.rx + rxIdx; if( ++rap->qc.mcount == 1 ) { rap->qc.xi = rxMsg - 1 - rxBegin; if( rap->qc.flags & QCTL_RXWAITING ) { rap->qc.flags &= ~QCTL_RXWAITING; /* Do this * before waking up the app thread because Win2K may give that higher * priority when message input is by simulateRxMsg. */ SetRing30Event( rap->event ); /* Wake up the app thread waiting for input. */ } } rxMsg = nip; } // Else (no receiver registered for this type) so discard the message. #if 0 // For debugging buffer management problems. rap = &share.rx[0]; if( rap->qc.mcount >= 3 && ackNak != ACKPAUSE ) margin = 0; // Breakpoint if( rxMsg + MAX_RX_PACKET_SIZE > beyondRx ) margin = 0; // Breakpoint if( rxMsg < rxBegin || rxMsg > beyondRx ) margin = 0; // Breakpoint. #endif return ackNak; } void onRxMsg( KDPC *dpc, void *context, void *arg1, void *arg2 ) { UCHAR ackNak; share.ds.rxMsgCnt++; rxTimeoutCount = 0; /* Any input (good or bad) means analyzer isn't dead. */ *rxMsg -= 2; // Remove CRC bytes from length count. if( computeCrc( NULL1, rxMsg+1, *rxMsg ) == FALSE || nextRxRegistrant == 0 ) ackNak = NAK; else ackNak = procRxMsg(); rxIp = rxMsg; /* Next input message destination. Repeat if NAK, copy msg, or no registered rx.*/ rxBadLen = 0; #ifdef HAS_TESTRXPAUSE if( rxPauseTest ) { rxPauseTest = FALSE; if( share.ds.ackMargin <= 0 ) /* It might already be entering pause state from last message. */ { share.ds.ackMargin = 10; /* Small margin to induce the next extractor to clear it. */ ackNak = ACKPAUSE; } } #endif if( ackNak == ACKPAUSE ) { rxState = RX_PAUSE; #ifdef ANALYZER_IGNORES_RX_PAUSE sendAckNak = ACK; /* This only serves to block tx when using delayed ACK without ACKPAUSE. */ DPC_RETURN; // <-- exit without calling txAckNak. #endif /* Leave rxCnt = 0 to tell ISR to dump any further input until we * have room. */ } else rxCnt = 0xFF; // New message indicator. sendAckNak = ackNak; /* NAK, ACK when not full, ACKPAUSE if full and analyzer accepts ACKPAUSE */ txAckNak( 0, 0, 0, 0 ); } void simulateRxMsg( UCHAR *msg ) { if( rxState == RX_PAUSE ) *msg = 1; // Full, can't insert. else if( rxMsg < rxBegin || rxMsg + msg[1] + 1 > beyondRx ) *msg = 2; // Buffer management error. else { *msg = 0; // OK memcpy( rxMsg, msg + 1, msg[1] + 1 ); if( procRxMsg() == ACKPAUSE ) { rxState = RX_PAUSE; simRx = TRUE; // Tell ENDRXPAUSE not to send ACK. } } }
// ANCOM.CPP #pragma data_seg ("shared") /*.................. DLL-based Shared TxServer Resources ...................*/ #define TxRdyEventName "CdxTxRdy" HANDLE txRdyEvent = 0; ULONG txInsCnt = 0; /* Number of messages inserted into the transmit queue. * This provides an ID for each message. When pTxMsgCnt >= ID, the analyzer has * received the message or else it was discarded. */ int txbIdx = 0; /* Tx buffer insertion byte index. */ DvrApi txDvr = 0; char fullDriverName[ 20 ] = ""; // e.g. \\.\CDXHSL.VXD or SYS char deviceNames[][12] = { "\\\\.\\CDXHSL", "\\\\.\\CDXECP", "\\\\.\\CDXUSB" }; #define DRIVER_NAME_OFFSET 4 #define DEFAULT_LINK LINK_ECP DllExport short linkSelection = -1; /* Sometimes, the linker warns that these shared strings are relocatable and * may not work at run-time; sometimes it doesn't even with no code change. * This warning might be related to the issue of pointers not being shareable * but it wasn't issued for explicityly declared pointers that did fail at * run-time when different apps attached the driver. Anyway, this table doesn't * seem to have a run-time problem. */ DllExport char *quitMessages[] = { "Self terminated", // QM_SELF "Terminated on master request", // QM_MASTER "Wait Failure at getMsg", // QM_GETFAIL "Aborted by request at getMsg", // QM_GET "Wait Failure at sendMsg", // QM_SENDFAIL "Aborted by request at sendMsg", // QM_SEND "Attempted transmit using uninitialized driver", // QM_TXUNINIT "Attempt to construct RxClient from uninitialized driver", // QM_RXUNINIT }; DllExport char ancomDriverName[ 40 ] = ""; DllExport short ancomTimeoutTypes = 0; char driverLongName[] = "CDNext Analyzer Comm Driver"; HANDLE openDriverMutex = CreateMutex( NULL, FALSE, NULL ); bool isOpen = false; int appCount = 0; #pragma data_seg () // End of items shared by apps. DllExport DvrStat *dvrStat = 0; short *pTxQcount = 0; // Pointer to member of dvrStat needed for inline asm. UCHAR *rxBegin = 0; UCHAR *txBegin = 0; UCHAR *beyondTxBuffer = 0; HANDLE driverHandle = 0; // One per app. BOOL WINAPI DllMain( HINSTANCE hDllInst, DWORD fdwReason, LPVOID lpvReserved ) { return TRUE; } DvrApi::DvrApi( int toForceDllSegment ) { driverHandle = 0; } FILE *openErrFile( void ) { static char buf[] = "runtime0.err"; FILE *fp; int cnt; for( cnt = 1 ; cnt < 10 ; cnt++ ) if(( fp = fopen( buf, "r" )) == 0 ) return fopen( buf, "wb" ); else buf[ 7 ] = cnt + '0'; return 0; } HANDLE createRing33Event( HANDLE *ring3Event, char *name ) { return *ring3Event = CreateEvent( NULL, FALSE, FALSE, name ); } DllExport void DvrApi::command( UINT cmd ) { DWORD dummy; // Only NT actually needs this. DeviceIoControl( driverHandle, cmd, 0, 0, &toDvr, sizeof( toDvr ), &dummy, 0 ); void simulateRx( UCHAR *msg ) { DWORD dummy; // Only NT actually needs this. DeviceIoControl( driverHandle, CDXDVR_SIMRX, 0, 0, msg, msg[1] + 2, &dummy, 0 ); } int loadDriver( char *cmd ) { static char appName[] = "Ancom"; DWORD err; PROCESS_INFORMATION pif; STARTUPINFO startInfo = { sizeof( STARTUPINFO ), 0 }; startInfo.dwFlags = STARTF_USESHOWWINDOW; startInfo.wShowWindow = SW_SHOWMINIMIZED; //Sleep( 1000 ); if( CreateProcess( 0, // name of executable module. Do not use this. It screws up everything. cmd, // e.g. "insys cdxhsl d:\\nt40\\master\\cdxhsl.sys" 0, // SD 0, // SD FALSE, // handle inheritance option. CREATE_NEW_CONSOLE, // creation flags 0, // new environment block 0, // current directory name &startInfo, // startup information &pif ) == 0 ) // 0 = failure { err = GetLastError(); MessageBox( 0, "Unable to execute insys.exe", appName, MB_OK ); return -1; } else if( WaitForSingleObject( pif.hProcess, 10000 ) != WAIT_OBJECT_0 ) { TerminateProcess( pif.hProcess, 100 ); CloseHandle( pif.hProcess ); MessageBox( 0, "Insys appears hung", appName, MB_OK ); return -2; } else { //Sleep( 10 ); GetExitCodeProcess( pif.hProcess, &err ); if( err != 0 ) MessageBox( 0, cmd, "Driver load/unload failure", MB_OK ); //else // MessageBox( 0, "Driver load/unload success", appName, MB_OK ); return err; } } void closeDriver( void ) { char removeCmd[ 100 ]; if( driverHandle != 0 && linkSelection != -1 && --appCount == 0 ) { CloseHandle( driverHandle ); } } short openDriver( DvrApi *pDvr, short linkSel, HWND hwnd, char *pathexe, USHORT show, FindPorts *fp ) { char loadCmd[ 200 ]; char *cp; if( WaitForSingleObject( openDriverMutex, 500 ) != WAIT_OBJECT_0 ) return -1; if( linkSelection != -1 && linkSel != linkSelection ) { if( show & SHOW_LINKMATCH ) MessageBox( hwnd, "The selected link doesn't match the link already running, which will be used instead.", driverLongName, MB_OK | MB_ICONERROR | MB_SYSTEMMODAL ); linkSel = linkSelection; } if( linkSel == -1 ) linkSel = DEFAULT_LINK; strcpy( fullDriverName, deviceNames[ linkSel ]); strcat( fullDriverName, os9x ? ".VXD" : ".SYS" ); if( linkSelection == -1 ) { sprintf( loadCmd, "insys %s %s", deviceNames[ linkSel ] + DRIVER_NAME_OFFSET, pathexe ); for( cp = loadCmd + strlen( loadCmd ) ; *cp != '\\' && *cp != ':' ; --cp ) ; strcpy( cp + 1, fullDriverName + DRIVER_NAME_OFFSET ); // e.g. "insys cdxhsl d:\nt40\master\cdxhsl.sys" loadDriver( loadCmd ); // OK even if already loaded. } driverHandle = CreateFile( deviceNames[ linkSel ], GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); // Incompatible with Win95 arguments. if( driverHandle == INVALID_HANDLE_VALUE ) { if( show & SHOW_LINKFAIL ) MessageBox( hwnd, fullDriverName + DRIVER_NAME_OFFSET, "Unable to open driver", MB_RETRYCANCEL | MB_ICONERROR | MB_SYSTEMMODAL ); ReleaseMutex( openDriverMutex ); return -1; } else linkSelection = linkSel; if( fp == NULL ) { pDvr->toDvr.fp.irq = ~0; pDvr->toDvr.fp.dma = ~0; pDvr->toDvr.fp.io1 = ~0; pDvr->toDvr.fp.io2 = ~0; } else { pDvr->toDvr.fp.irq = fp->irq; pDvr->toDvr.fp.dma = fp->dma; pDvr->toDvr.fp.io1 = fp->io1; pDvr->toDvr.fp.io2 = fp->io2; } pDvr->command( CDXDVR_FINDPORTS ); pDvr->command( CDXDVR_INIT ); dvrStat = pDvr->fromDvr.init.pDvrStat; pTxQcount = &dvrStat->txQcount; // Direct pointer needed for inline asm in sendMsg. ancomTimeoutTypes = pDvr->fromDvr.init.timeoutTypes; rxBegin = pDvr->fromDvr.init.rxBegin; txBegin = pDvr->fromDvr.init.txBegin; beyondTxBuffer = pDvr->fromDvr.init.beyondTx; /* To verify sharing of DMA buffer, at CDXDVR_INIT, the string "RxBegin" was * inserted at rxBegin + 1 (rxBegin[0] is used for message indicator code). * Verify that we see that here in the application. */ cp = (char *)( rxBegin + 1 ); pDvr->command( CDXDVR_GETVERSION ); wsprintf( ancomDriverName, "%s L%d.%d-C%d.%d", fullDriverName + DRIVER_NAME_OFFSET, pDvr->fromDvr.ver.linkMajor, pDvr->fromDvr.ver.linkMinor, pDvr->fromDvr.ver.coreMajor, pDvr->fromDvr.ver.coreMinor); if( show & SHOW_LINK ) MessageBox( hwnd, ancomDriverName, driverLongName, MB_OK | MB_SYSTEMMODAL ); appCount++; ReleaseMutex( openDriverMutex ); return linkSelection; } DllExport void TxServer::init( DvrApi *dvr, HANDLE abortEvent ) { if( txMutex != 0 ) return; // This app's TxServer has already been initialized. The app // program should know better but this prevents problems during development. if( driverHandle == 0 ) throw ThrowAbort( QM_TXUNINIT ); txMutex = CreateMutex( NULL, // Default security for 95/98. NT and 2000 require SYNCHRONIZE (?). FALSE, // No initial owner, i.e. immediately signalled for the first grabber. "CdxMasterAnalyzerTxInsert" ); events[ EVENT_ABORT ] = abortEvent; if( txRdyEvent == 0 ) { // Only the first app to attach the driver does this. txDvr = *dvr; txDvr.toDvr.regTx.event.ring3 = createRing33Event( &txRdyEvent, TxRdyEventName ); if( txRdyEvent != 0 ) { txDvr.command( CDXDVR_REGISTERTX ); txbIdx = 0; events[ EVENT_TX ] = txRdyEvent; } } else { events[ EVENT_TX ] = CreateEvent( NULL, FALSE, FALSE, TxRdyEventName ); } } DllExport void TxServer::reinit( void ) { txInsCnt = 0; txbIdx = 0; } DllExport ULONG TxServer::sendMsg( UCHAR *msg, DWORD wait ) { #define TXQOFF offsetof(DvrStat,txQcount) bool overrun; UCHAR *xp; int len; DWORD useWait; UCHAR *txIp; if( !isInitialized()) throw ThrowAbort( QM_TXUNINIT ); if( WaitForSingleObject( txMutex, wait ) == WAIT_TIMEOUT ) { ReleaseMutex( txMutex ); return 0; /* This doesn't need an exception because the timeout is * infinite for any asynchronous caller. A synchronous caller passes a smaller * timeout and will report error if the return is 0. */ } len = *msg + 3; overrun = false; txIp = txBegin + txbIdx; if( txIp + len >= beyondTxBuffer ) { /* The message is too long for the space remaining between txIp and the end * of the buffer. Zero the byte at txIp (if it isn't beyond the buffer) to * tell extractor that this is the end of messages in the buffer, and wrap * around to the beginning of the buffer. */ if( txIp < beyondTxBuffer ) *txIp = 0; if( txIp < txBegin + dvrStat->txExtract ) overrun = true; txIp = txBegin; txbIdx = 0; } if( overrun || // txIp has wrapped but would have overrun txExtract. *pTxQcount > 0 && txIp <= ( xp = txBegin + dvrStat->txExtract ) && txIp + len >= xp ) { dvrStat->txWake = ( txIp + len ) - txBegin; // Note txWake is not a pointer. while( *pTxQcount > 0 ) { switch( WaitForMultipleObjects( 2, events, FALSE, useWait = *pTxQcount > 1 ? wait : 10 )) // See "Wait for space" note. { case WAIT_FAILED : ReleaseMutex( txMutex ); throw ThrowAbort( QM_SENDFAIL ); case WAIT_OBJECT_0 + EVENT_ABORT : ReleaseMutex( txMutex ); throw ThrowAbort( QM_SEND ); // Equivalent to return a value. case WAIT_TIMEOUT : if( useWait == 10 ) continue; ReleaseMutex( txMutex ); return 0; // Try the caller's timeout only once. default : // WAIT_OBJECT_0 + EVENT_TX or TIMEOUT and buffer empty. break; } break; } } memcpy( txIp, msg, len - 2 ); txbIdx += len; /* No need to also advance txIp because it isn't used again in this invocation. */ AtomicInc16At( pTxQcount ); if( dvrStat->txIdle ) txDvr.command( CDXDVR_WAKETX ); // Tx selector/driver needs wake up. ReleaseMutex( txMutex ); return ++txInsCnt; } DllExport bool TxServer::isReceived( ULONG msgId ) { if( !isInitialized()) return true; return msgId <= dvrStat->txMsgCnt; } RxClient::RxClient( DvrApi *dvr, HANDLE abortEvent, UCHAR begin, UCHAR end ) { if( driverHandle == 0 ) throw ThrowAbort( QM_RXUNINIT ); dvr->toDvr.regRx.event.ring3 = createRing33Event( &events[ EVENT_RX ], 0 ); if( events[ EVENT_RX ] != 0 ) { events[ EVENT_ABORT ] = abortEvent; dvr->toDvr.regRx.range.begin = begin; dvr->toDvr.regRx.range.end = end; dvr->command( CDXDVR_REGISTERRX ); qp = dvr->fromDvr.rx.qp; rxBegin = dvr->fromDvr.rx.rxBegin; // Repeat OK-- just be sure it's done once. } RxClient::dvr = dvr; } DllExport void RxClient::unregister( void ) { if( qp == 0 ) return; // Uninitialized RxClient. if( events[ EVENT_RX ] != 0 ) { dvr->toDvr.id = qp->id; dvr->command( CDXDVR_UNREGISTERRX ); events[ EVENT_RX ] = 0; } } DllExport void RxClient::setRxRange( UCHAR begin, UCHAR end, UINT acceptReject ) { if( this != 0 ) { dvr->toDvr.msgFilter.id = qp->id; dvr->toDvr.msgFilter.range.begin = begin; dvr->toDvr.msgFilter.range.end = end; dvr->command( acceptReject ); } } DllExport UCHAR *RxClient::getMsg( UCHAR *prev, short mode ) { USHORT len; USHORT *pmcount; USHORT *flags; UCHAR *xp; xp = rxBegin + qp->xi; if( prev == xp + 1 ) { // Discard the previous message by advancing the extraction point. xp += ( len = xp[1] + 2 ); qp->xi = xp - rxBegin; pmcount = &qp->mcount; AtomicDec16At( pmcount ); if( dvrStat->ackMargin > 0 && ( dvrStat->ackMargin -= len ) <= 0 ) dvr->command( CDXDVR_ENDRXPAUSE ); } if( mode == RCGM_ONLY_DISCARD ) return 0; if( qp->mcount == 0 ) { if( mode == RCGM_NO_WAIT ) return 0; flags = &qp->flags; AtomicOr16At( flags, QCTL_RXWAITING ); if( qp->mcount == 0 ) { switch( WaitForMultipleObjects( 2, events, FALSE, INFINITE )) { case WAIT_FAILED: throw ThrowAbort( QM_GETFAIL ); case WAIT_OBJECT_0 + EVENT_ABORT : throw ThrowAbort( QM_GET ); } } else { AtomicAnd16At( flags, ~QCTL_RXWAITING ); ResetEvent( events[ EVENT_RX ]); } xp = rxBegin + qp->xi; } /* Skip over messages not for this receiver. This isn't needed if we had to * wait for a message, because the inserter (ring 0 driver-onRxMsg) updates the * registered receiver's xp if its mcount is 0. Note reload of xp from xi. */ else { for( xp = rxBegin + qp->xi ; *xp != qp->id ; ) { if( *xp == QCTL_WRAP ) xp = rxBegin; else xp += xp[1] + 2; } qp->xi = xp - rxBegin; } return xp + 1; } void RxClient::testMultiple( int msgCount ) { while( qp->mcount < msgCount ) ; } void ComThreadApi::kill( HANDLE abort ) { int cnt; if( isActive()) { mailbox = MB_KILL; SetEvent( abort ); for( cnt = 0 ; ; cnt++ ) { if( cnt >= 10 ) break; if( mailbox != MB_DEAD ) Sleep( 100 ); else break; } } ResetEvent( abort ); }
Applications have no direct contact with the kernel level driver. They link to a DLL, whose API provides all services. This DLL is not simply a convenience. It is tightly coupled with the kernel level driver, essentially creating a hybrid kernel-application driver.
The driver provides content-based routing of messages to multiple threads and processes, which can connect and disconnect at will. If one application has already opened communication via a particular transport and another application connects requesting a different transport, the connection is made with the transport already in effect. Applications don’t know or care which transport is actually being used but they can suggest which one they would prefer and provide configuration parameters for it. This avoids the necessity of a separate program just for opening and configuring the communication system.
The DLL is shared by processes (applications) as well as by threads. Some of
the module’s static data needs to be declared a special way or else it will be
duplicated for each process, causing the driver to fail. The declaration
#pragma data_seg ("shared")
works with the link option
-SECTION:shared,rws
to do this but, additionally, every item must be initialized
or it will be put in the wrong place. "shared" is an arbitrary name. All app
programs share every initialized item in this section.
The shared data section in any module is closed by
#pragma data_seg ()
. Any data declared
after this is process-specific, i.e. duplicated for each app. This is also the
case for uninitialized items defined in the "shared" section but to avoid
confusion only truly shared (i.e. initialized) items are defined in the
"shared" section.
This was originally developed under 9x, which allowed the driverHandle and
communication buffer pointers to be shared by all apps. Under NT+ the handle
can’t be shared under any circumstances and the pointers can only be shared by
multiple instances of one app. MS warns not to share pointers in a DLL but
doesn’t address this special case of pointers to memory that the driver
allocates and explicitly makes shared. However, considering that the driver has
to map the shared memory for each application
(ZwMapViewOfSection
) it is not surprising that the apps can
share these pointers.
Although this is a small part of the code, it represents much of the core of the application level component of the driver. In particular the RxClient and related classes and functions support the multi-client remote DMA capability, which is unique and complex.
Some of the functions here throw C++ exceptions, which requires that they be exported as mangled names. Consequently, ancom.dll must be rebuilt if the application compiler changes, as there is no standard decorating syntax even between revisions of the same compiler.
This is a structured wrapper around the standard Windows interface openFile and deviceIoControl. DvrApi provides an interface to the ring 0 driver. Each instance uses object data for passing data to and receiving data back from the ring 0 driver, and class functions do not use critical sections. Consequently, the DvrApi is not thread-safe although the ring 0 driver itself is for most functions. The simplest way to avoid thread conflict is to give each thread its own DvrApi by cloning the main thread’s. However, threads may share a DvrApi if their operations are synchronized to not simultaneously need to call into the ring0 driver. For example, if a thread uses the DvrApi only to initialize objects or in response to an error which automatically prevents other threads from using the DvrApi. The main thread’s DvrApi is like this and can safely be shared with one thread.
The main thread creates a local DvrApi and passes a reference to this to each thread that it spawns. This is done instead of a global because threads may spawn threads using their own local DvrApi as the source.
This creates a registered receiver for incoming messages. The constructor calls into the ring 0 driver to register the RxClient to receive messages, by default all msgTypes. The destructor calls the driver to unregister the client, at which time any msgTypes that it filtered away from previous registrants are given back to them. This only restores their accept filters; any individual messages that it received while registered are gone. RxClient objects should be created within try blocks to ensure that their destructors are called in case of exceptions.
The range of msgTypes to be directed to the owner of this RxClient is initially set by the optional begin and end constructor arguments. If not stated, they are 0 and 255, registering to receive all types. At any time msgTypes can be added or deleted from the filter by calling acceptRange and rejectRange. Each call supports only one pair of range arguments so multiple calls are needed to register or de-register uncontiguous msgTypes. When a message type is removed from this RxClient’s filter, it reverts to the previously registered RxClient. With some restrictions, as explained in the ring 0 driver module, this process can peel back the filters to the first registrant.
The unregister function unregisters the RxClient with the ring 0 driver without invoking the destructor. This is used only for testing the driver; an unregisterd RxClient is of no value. Note that the destructor just invokes the unregister function.
Because HANDLE txRdyEvent is shared, the first process to connect to the driver should be the only one required to create it. The DLL should be able to simply assign this value to each subsequent process’ txServer.events[EVENT_TX]. However, when this is done none of the subsequent processes is connected to the original handle (more importantly to its ring0 mate). To make the connection, the event must be named and each process must "create" it and store it own unique handle. Consequently, this global handle serves only as a flag telling the first process to perform some one-time initialization procedures.
This opens the next error file for recording status and data related to a run- time error under investigation. Only 10 files are allowed, ranging in name from runtime0.err to runtime9.err. This function will not overwrite existing ones but instead try to find the next one in sequence that doesn’t already exist. If it can’t find one, then it returns NULL. The best policy is to delete *.err before starting a test run.
I originally wrote this for 9x and later adapted it to support but 9x and NT/ XP. A notable difference is in how the kernel driver sets an event to wake a sleeping application. In 9x, the driver creates the event but in NT/XP the application creates an event and passes its handle to the kernel driver to make the ring 0 counterpart. To simplify this presentation I have removed 9x code. The createRing33Event function simply creates the event. It purpose is to match the prototype of the more complex createRing30Event needed for 9x. The char *name argument provides a name for events that will be shared by processes. This name is assigned to the ring3 event. If name is NULL, processes will not be able to share the event.
This is wrapper over DeviceIoControl. It returns nothing directly but return values from the kernel driver can be read in the DvrApi.fromDvr structure. The UINT cmd argument identifies the command passed to the driver. The caller copies any additional arguments into DvrApi.toDvr before invoking this method.
This inserts a message into the driver’s input buffer for processing as if it were from the analyzer, including routing and waking up the receiver registered for this message type. The message can be passed between applications. This passes through the IoCtl interface and goes directly to the simulateRxMsg function in onRxMsg.c. It has no direct return. The first byte of the msg argument is changed to 0 if the message was inserted or 1 if it couldn’t be because the buffer is full. The UCHAR *msg is a standard message with an extra leading byte used by the driver to return the insertion status.
9x automatically loads a kernel level driver and removes it when the last application that references it closes. NT/XP loads and unloads drivers only by calling specific functions. To make the DLL run under either OS without recompiling, these functions spawn a separate exe to perform these operations. Otherwise, this DLL would not compile for 9X even though it can run under either OS.
Opens a driver on behalf of an application. All applications must call this in order to attach to the driver. A call to the ring 0 driver init function is made on behalf of each application but the driver is actually initialized (e.g. buffer memory allocated) only at the first call (i.e. the first app to attach the driver).
It returns the selected driver index, which is the same as requested by linkSel argument unless the driver is already open or -1 if the driver can’t be opened. The arguments are:
OpenDriver uses a mutex to prevent two applications from colliding. The cheap WaitForSingleObject function is used instead of the more expensive WaitForMultipleObjects, which would afford the master thread a means to abort, because this is likely to be called from the master thread anyway, so there would be no means to abort other than timeout, which is included in the wait call.
This initializes a TxServer. All applications call this to connect to the TxServer but the server initialization call into the ring 0 driver is only done for the first one. It returns nothing. DvrApi *dvr argument points to an interface that will be copied to a private instance. The caller can do anything that it wants with DvrApi upon return. HANDLE abortEvent argument provides a process-specific abort signal.
TxServers are not initialized by a constructor because they mostly comprise a shared core instance. This core includes a driver interface (DvrApi) and a multi-process MUTEX, both of which must be provided by the first application to initialize a TxServer. Subsequent apps also provide these because they don’t know that they aren’t the first, but the shared elements are not reinitialized. Each application (process) has its own TxServer object, but this comprises only the shared MUTEX (each process has its own process-relative copy of this) and a two-element array of event handles for waiting on transmit event or app main thread abort. The events[TX] handle is the same for all TxServer instances but each has its own array for faster usage in sendMsg.
The TxServer constructor only sets txMutex = 0, for use as an unitialized object indicator. The init function assigns txMutex a non-0 value only after first confirming that the driver is initialized. By inspecting txMutex, class functions determine directly that TxServer is initialized and indirectly that the driver is initialized.
This just discards all messages currently in the transmit queue.
This sends the given message to the analyzer by putting it into the transmit queue and waking up the transmit ring 0 driver if necessary. If the queue is full, the thread will be put to sleep until the message can be put. It returns ULONG insert message number, which is the message’s ID. This can subsequently be compared to the sent message (transmitted and ACK’d or discarded after three tries) count to determine when the analyzer has received the message. TxServer. isReceived provides this capability. This may also throw a ThrowAbort if the app aborts while the thread is waiting to put its message into the transmit queue.
The UCHAR *msg argument is a Pascal-type string, with the first byte indicating the length of the remainder of the string, i.e. it doesn’t count itself. This does not need to provide space for the checksum/CRC. The DWORD wait optional argument specifying the number of msec to wait for the transmit queue to have room for this message. The default is INFINITE (defined in winbase.h as 0xFFFFFFFF).
To avoid overrunning the extractor, we will go to sleep, waiting for the ISR (onRxAckNak) to remove enough messages to provide the amount of space needed. With the exception of atomic increment of txQcount, this function executes with no coherency guarding relative to the ISR. It is possible for the ISR to remove the blocking message between the time that sendMsg detects the overrun and the time that the wait is registered. If more than one message remains and the buffer is at least as big as two MAX messages (this requirement is related to wrap, not timing) then there will be another message to trigger onRxAckNak. However, if only one message remains, onRxAckNak may finish as the wait is being registered, hanging the system without an independent timeout. To reduce unnecessary delays yet avoid turning this into essentially a polled process, the txQcount is checked at several points as follows:
Overrun and end of buffer detection are independent but interact. If the insertion point would cause the message to go over the end of the buffer and the extraction point is above the insertion then there is also an overrun, obviously. What isn’t so obvious (at least in coding) is that wakeup point in this case is just the beginning of the buffer plus the length of the message, regardless of where the extraction point is relative to either the unwrapped insertion point or the end of the buffer. To avoid excessive duplication of tests (i.e. overrun within buffer end and vice versa) an overrun flag is used between the buffer end detector and the overrun detector. Without this, we couldn’t wrap txIp before the overrun detector but then its calculation of wakeup point as txIp + len would be wrong (in fact it would lie beyond the buffer).
When this was originally developed under 9x, txIp was shared by all applications. This also worked under NT+ for multiple instances of the same app but failed for multiple apps. Unlike the other pointers to share memory, such as rxBegin and txBegin, txIp is written as well as read at this level. Consequently, unlike them, txIp cannot simply be made an application instance variable. Instead, a shareable index, txbIdx, is defined. The automatic pointer, txIp, is initialized here by adding txbIdx to the app-specific txBegin. txIp is safe to use for the remainder of this function because txMutex protects txIp and txbIdx from changes by other threads (and apps) and the low- level driver doesn’t even see either of them. We just have to be sure to change txbIdx and txIp together here (or update txbIdx when leaving).
This determines whether the given message, identified by sequence number, has been transmitted and acknowledged. This is used primarily at shutdown to be sure that target debug points are removed. It returns true if the message has been transmitted and ack’d or if the communication link has never been initialized. The rationale for the latter is that this function is called to determined whether to take remedial action, such as warning or retry, which can’t serve any purpose if the link has not been established. Such action is necessary and useful only if the link has been established and the message has not be received. The ULONG msgId argument is the transmitted message number, which was returned by TxServer::sendMsg (the application must save this number in order to know what to ask for when it calls isReceived).
dvrStat->txMsgCnt is the transmit message count exported by the driver. This counts all transmitted messages that are no longer pending, whether due to ACK or abandonment (after 3 retries). Therefore, even if there is no response from the receiver, we may perceive the message has having been received, which is the ideal behavior in the context of isReceived for the same reason that uninitialized is equivalent to the message having been received, i.e. nothing can or should be done about this or any previous messages.
This throws a ThrowAbort class in case of uninitialized driver reference. It takes the following arguments:
By verifying here that the driver has been initialized, we avoid having to do it repeatedly in class functions, particularly getMsg. DvrApi’s constructor doesn’t open/initialize the driver, so we depend on the application to do this. The check here prevents system crash.
This sets a given range of rx message types to be accepted for the client, i.e. routed to it, or rejected. Rejecting a range that this client is registered to receive causes the previous registered receiver to regain the range. It returns nothing. UCHAR arguments begin and end idendentify an inclusive range of message types. The UINT acceptReject argument is CDXDVR_RXACCEPT or CDXDVR_RXREJECT, telling whether the client wants to begin receiving or stop receiving the specified range.
This is the application level complement (consumer) to the kernel driver function procRxMsg (producer). It discards the previously retrieved message (if any) and gets the next one. If one isn’t available then go to sleep until one is available or the communication interface is shut down (normally by the user interface thread). This returns a pointer to the retrieved message. This is NULL only if the caller has requested only to discard the previous message. It takes the following arguments:
To discard the previously released message, getMsg decrements mcount and advances xp, which still points to the message because the queue inserter (ring 0 driver: onRxMsg) doesn’t change a registered receiver’s xp when its mcount is not 0. Note that mcount lags one cycle behind the message reported out to the caller. One the first call, mcount and xp are not changed. On the second call, the previous message is discarded by decrementing mcount and incrementing xp. This ensures that mcount != 0 effectively acts as a mutex to prevent driver/ application conflict over xp even when the caller retrieves the last available message. Originally, xp was shared by the driver and app and was not cached. However, to support NT, it was necessary to change to an index, xi. The local xp generated from xi can become invalid at any time that mcount is 0. Consequently, the xp calculated before decrementing mcount must not be used afterward unless mcount didn’t go to 0. However, just checking for mcount > 0 is not safe because the driver could increment it after the app has decremented it. It is easier and safer to simply reload xp in both the subsequent mcount = 0 and not 0 code branches.
If message count is 0 and mode is to wait for the next input, we need to tell the low level driver (procRxMsg in onrxmsg.c) that we are waiting so that it knows to signal us. To avoid excessive signalling we don’t want to set QCTL_RXWAITING before determining that there are no messages, but setting the flag after testing mcount leaves a time period during which a message might be received and procRxMsg doesn’t know that we have already determined that we need to wait (because mcount is 0). Our setting of QCTL_RXWAITING won’t do any good in this case, because procRxMsg will have already received the first message. To avoid this, we set the flag and then retest mcount. If it is still 0 then we know that we will be signalled at the first message. Otherwise, the first message arrived between the time of our first and second tests. In this case, the event may or may not be signalled. Either way, we can safely reset the event without waiting for it. We also have to reset QCTL_RXWAITING in case procRxMsg executed only between the first test of mcount and the setting of the flag, in which case, procRxMsg doesn’t signal the event or clear the flag.
If a registrant’s mcount is 0 when a message arrives for it, onRxMsg updates xp point to the new message. This particularly speeds up infrequent message receivers because it allows them to skip over intervening messages. Modifying xp in the interrupt as well as here could require a ring3/0 mutex but this is avoided by changing xp here only when mcount is not 0. This is inherent in the message skipper toward the end and is made to happen in the discarder by decrementing mcount only after modifying xp.
If ackMargin > 0 it means that the raw input buffer is nearly full and, to prevent a possible overrun on the next input, the driver has paused (either sending ACKPAUSE or nothing). The driver assigns to ackMargin the amount of space that it wants freed up before allowing additional input from the analyzer. Since ackMargin (the actual counter-- not the pointer to it) is shared by all users of the driver, it is possible for two threads/apps to intersect at the ackMargin check and response in getMsg, causing two calls to the driver to send the delayed ACK. This could be prevented with a multi- process MUTEX but it is cheaper to use no guarding and let the driver discard all but the first call. This would be a more expensive solution if it happened often, but it will occur very infrequently. Using a guard in the driver doesn’t prevent the possibility of ackMargin being decreased by more than one thread but this is irrelevant, as it is a long and all decisions regarding it consider only two states, > 0 and <= 0.
This waits for msgCount to reach a requested level. It is used only for testing the access functions’ ability to handle multiple messages and buffer wrap-around.
Aborts this communication thread. It returns nothing. The HANDLE abort argument is the thread’s abort event.
If a thread contains both receive and transmit operations then it can only be reliably aborted by signalling both rxAbort and txAbort. Unlike normal communication threads, it may also not always sleep when it has nothing to do, in which case it would also require MB_KILL to be sure of aborting.
At the end, kill invokes ResetEvent( abort ). The abort event was created as auto-reset. However, the thread may not be waiting and may instead abort on the mailbox MB_KILL message, in which case the event is never reset because the thread is not released. The fact that the event is not reset if the thread is already going may be an anomoly of Windows but, in any case, simply resetting here solves the problem and doesn’t hurt anything.