简单的串行端口监视器






4.94/5 (35投票s)
非常简单的串口监视器。
引言
首先,请原谅我的英语,因为它不是我的母语,这也是我的第一篇文章。在这篇文章中,我想分享我所知道的关于如何监视串口的知识。请注意,这仅仅是我“所知道的”,并且我可能在驱动程序编程方面有所误解,尤其是在这篇文章中。如果您发现我错了,请告诉我,我们可以进一步讨论。
那么,什么是串口监视器呢?嗯,我相信您知道它是什么。这个串口监视器的基本思想是:创建一个系统驱动程序,然后为其添加过滤驱动程序的功能。好了,让我们开始详细介绍。
I. 系统驱动程序
正如您在源代码中看到的,这只是一个系统驱动程序(没有实际硬件),并且它实现了系统驱动程序的最小调度函数。如果您想了解系统驱动程序的要求,请查阅 MSDN。在这个驱动程序中,我简单地将发送到此驱动程序的 IRP 转发到低层驱动程序作为默认处理程序,并使用 WDK 建议的“标准 PnP 和电源调度处理”。此驱动程序还处理打开、清理、关闭、读取和控制请求,并根据 WDK(Windows 驱动程序工具包)的要求处理一些作为串口驱动程序的 IRP 请求。
II. 附加到目标设备并从目标设备分离
当客户端应用程序发送一个 I/O 控制请求以附加到目标设备时,使用一个串口名称的字符串参数执行 IOCTL_DKPORTMON_ATTACH_DEVICE
,驱动程序会执行以下操作:
- 驱动程序使用
IOCTL_DKPORTMON_ATTACH_DEVICE
请求中的字符串参数,通过IoGetDeviceObjectPointer()
获取目标设备对象的最顶层。如果成功,此例程将填充我们提供的设备对象变量的指针。 - 然后,驱动程序创建一个新的设备对象,该对象具有从
IoGetDeviceObjectPointer()
获取的设备对象特征,并且设备扩展的大小为 0。 - 之后,驱动程序从
IoGetDeviceObjectPointer()
创建的设备对象中复制标志,并添加任何“附加标志”。 - 使用
IoAttachDeviceToDeviceStack()
函数将新创建的设备对象附加到设备堆栈,然后设置初始化标志。
附加设备的详细代码(您可以在文件 DkIoExt.c 的 DkCreateAndAttachDevice()
函数中看到)。
...
RtlInitUnicodeString(&usTgtDevName,
(PCWSTR) pIrp->AssociatedIrp.SystemBuffer);
ntStat = IoGetDeviceObjectPointer(&usTgtDevName,
GENERIC_ALL,
&pFlObj,
&pTgtDevObj);
if (!NT_SUCCESS(ntStat)){
DkDbgVal("Error get device object pointer!", ntStat);
return ntStat;
}
ObDereferenceObject(pFlObj);
ntStat = IoCreateDevice(pDevEx->pDrvObj,
0,
NULL,
pTgtDevObj->Characteristics,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&pDevEx->pTgtDevObj);
if (!NT_SUCCESS(ntStat)){
DkDbgVal("Error create target device object!", ntStat);
goto EndFunc;
}
pDevEx->pTgtDevObj->Flags |= (pTgtDevObj->Flags &
(DO_BUFFERED_IO | DO_POWER_PAGABLE | DO_DIRECT_IO));
pDevEx->pTgtNextDevObj = NULL;
pDevEx->pTgtNextDevObj = IoAttachDeviceToDeviceStack(pDevEx->pTgtDevObj,
pTgtDevObj);
if (pDevEx->pTgtNextDevObj == NULL){
DkDbgVal("Error attach device to device stack!", ntStat);
ntStat = STATUS_UNSUCCESSFUL;
goto EndFunc;
}
pDevEx->pTgtDevObj->Flags &= ~DO_DEVICE_INITIALIZING;
...
当客户端应用程序发送 IOCTL_DKPORTMON_DETACH_DEVICE
请求时,驱动程序将从目标设备分离,并删除之前创建的设备对象,如下面的代码片段所示。
...
if (pDevEx->pTgtNextDevObj){
IoDetachDevice(pDevEx->pTgtNextDevObj);
pDevEx->pTgtNextDevObj = NULL;
}
if (pDevEx->pTgtDevObj){
IoDeleteDevice(pDevEx->pTgtDevObj);
pDevEx->pTgtDevObj = NULL;
}
...
III. 处理 I/O 请求
附加到目标设备后,此驱动程序不仅会接收发往此驱动程序的 I/O 请求,还会接收发往我们附加到的目标设备对象的 I/O 请求。这意味着,如果某个应用程序发送一个针对我们附加到的设备的 I/O 请求,它将首先发送到我们的驱动程序,因为我们的驱动程序位于目标设备堆栈的顶部,在这种情况下,我们的目标设备就是串口驱动程序。我们如何知道请求是“发往”我们的设备对象还是不是呢?嗯,最简单的方法是声明一个我们的设备对象的全局变量,在 DkAddDevice()
例程中创建此对象,然后在收到 I/O 请求时,执行一个简单的“if”判断,如下所示:
...
extern PDEVICE_OBJECT g_pThisDevObj;
...
NTSTATUS DkCreateClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
...
if (pDevObj != g_pThisDevObj)
return DkTgtCreateClose(pDevExt, pIrp);
...
III. A. 处理发往我们的设备对象的 I/O 请求
在进一步讨论处理请求之前,我想简单谈谈这个驱动程序中的队列。这个驱动程序使用两种类型的队列,一种用于处理 IRP(如 WDK 所建议的取消安全队列),另一种用于收集数据(简单的先进先出数据队列/FIFO 数据队列)。我们将在下一节讨论如何收集数据。
我们的驱动程序处理打开(IRP_MJ_CREATE
)、关闭(IRP_MJ_CLOSE
)、清理(IRP_MJ_CLEANUP
)、读取(IRP_MJ_READ
)和控制(IOCTL_DKPORTMON_ATTACH_DEVICE
和 IOCTL_DKPORTMON_DEATCH_DEVICE
)请求。正如您在源代码中看到的,打开、关闭和清理请求在同一个调度例程 DkCreateClose()
中处理。对于打开请求,我们只初始化我们的 FIFO 数据队列,以 STATUS_SUCCESS
完成请求,并返回 STATUS_SUCCESS
。对于清理请求,我们分离设备(如果存在,如分离函数所述),清理数据队列和取消安全队列,然后完成请求。对于关闭请求,它只是“接受”它,完成请求,并返回 STATUS_SUCCESS
。
当从客户端应用程序接收到读取请求时,我们从 FIFO 数据队列中检索数据。如果有数据,我们将其复制到“代表”用户缓冲区的系统缓冲区中,然后将其移除/销毁/删除/释放,最后以 STATUS_SUCCESS
完成请求,并包含从 FIFO 数据队列中获取的数据大小。如果 FIFO 数据队列中没有数据,我们则将 IRP 入队到取消安全队列,然后返回一个挂起状态,并指示该 IRP 已入队,稍后将由驱动程序中的另一个函数(DkTgtCompletePendedIrp()
函数)完成。这是文件 DkIoReq.c 中 DkReadWrite()
函数中的代码片段。
...
pQueDat = DkQueGet();
if (pQueDat == NULL){
IoCsqInsertIrp(&pDevExt->ioCsq, pIrp, NULL);
IoReleaseRemoveLock(&pDevExt->ioRemLock, (PVOID) pIrp);
return STATUS_PENDING;
} else {
pDat = (PDKPORT_DAT) pIrp->AssociatedIrp.SystemBuffer;
RtlCopyMemory(pDat, &pQueDat->Dat, sizeof(DKPORT_DAT));
DkQueDel(pQueDat);
IoReleaseRemoveLock(&pDevExt->ioRemLock, (PVOID) pIrp);
DkCompleteRequest(pIrp, ntStat, (ULONG_PTR) sizeof(DKPORT_DAT));
return ntStat;
}
...
对于控制请求,请参阅上面 **II. 附加到目标设备并从目标设备分离** 的子节。
III. B. 处理发往我们的目标设备对象的 I/O 请求
当 I/O 请求的目标是目标设备时,基本上就像普通的过滤驱动程序一样,将其转发到下一个较低的设备对象。但是为了监控目的,我们需要收集到设备或从设备收集数据以进行进一步分析。问题是,何时可以收集到数据?是在转发请求之前还是之后?首先,我们需要知道“数据的方向”。换句话说,我们需要知道这种请求是“GET 请求”还是“PUT 请求”。在“GET 请求”(例如,IRP_MJ_READ
、IOCTL_SERIAL_GET_BAUD_RATE
等)中,数据来自目标驱动程序,这意味着我们可以在目标驱动程序完成请求后收集数据。所以,如果我们收到这类请求,我们会为该请求设置一个完成例程。此完成例程将在较低的下一个驱动程序完成其工作后执行,这样我们就可以收集数据。正如您在源代码(文件 DkIoReq.c)中看到的,当此驱动程序收到发往目标设备的 IRP_MJ_READ
请求时,该请求由 DkTgtReadWrite()
处理,它会设置一个完成例程,然后转发到下一个对象,就像这个代码片段一样。
...
IoCopyCurrentIrpStackLocationToNext(pIrp);
IoSetCompletionRoutine(pIrp,
(PIO_COMPLETION_ROUTINE) DkTgtReadCompletion,
NULL,
TRUE,
TRUE,
TRUE);
return IoCallDriver(pDevExt->pTgtNextDevObj, pIrp);
...
而在完成例程中,我们可以通过 DkTgtCompletePendedIrp()
函数收集数据,如下所示。
...
pDevExt = (PDEVICE_EXTENSION) g_pThisDevObj->DeviceExtension;
pIrp = IoCsqRemoveNextIrp(&pDevExt->ioCsq, NULL);
if (pIrp == NULL){
DkQueAdd(szFuncName, ulFuncNameByteLen, pDat, ulDatByteLen);
} else {
pNewDat = (PDKPORT_DAT) pIrp->AssociatedIrp.SystemBuffer;
RtlFillMemory(pNewDat, sizeof(DKPORT_DAT), '\0');
pNewDat->FuncNameLen = ulFuncNameByteLen;
pNewDat->DataLen = ulDatByteLen;
if (szFuncName != NULL){
RtlCopyMemory(pNewDat->StrFuncName, szFuncName, ulFuncNameByteLen);
}
if (pDat != NULL){
RtlCopyMemory(pNewDat->Data, pDat, ulDatByteLen);
}
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = sizeof(DKPORT_DAT);
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
}
...
对于其他请求,例如发往目标驱动程序的 I/O 控制请求,使用与上面相同的方法,关键是:注意“数据的方向”。
IV. 客户端程序
客户端程序打开设备驱动程序然后开始监视。它通过在另一个线程循环中读取来捕获数据。当数据到来时,它会“触发一个事件”,该事件会调用“与之连接”的函数。基本上,它只是一个函数指针,我们为其分配一个函数。您可以查看文件 DkPortClient.h 和 DkPortClient.cpp 中的 C++ 类 CDkPortClient
。要开始监视,它向驱动程序发送 IOCTL_DKPORTMON_ATTACH_DEVICE
;要停止监视,它发送 IOCTL_DKPORTMON_DETACH_DEVICE
。
V. 缺点。
以下是我能想到的缺点:
- 当您开始监视时,目标驱动程序或串口不能被另一个应用程序使用(打开)。因此,这个串口监视器必须在任何应用程序(如超级终端)开始访问目标端口之前启动。这是因为串口驱动程序一次只能打开一次。
- 它不能保证 I/O 请求的顺序。这是什么意思?嗯,考虑这种情况:某些应用程序以重叠标志打开端口,然后异步读取端口,然后写入端口。之后,应用程序等待来自先前读取请求的传入数据,然后数据到来。在这种情况下,您将在监视器上看到的请求顺序可能如下所示:
- 此驱动程序不检查请求的状态,无论它们成功还是失败。
...
IRP_MJ_CREATE
...
IRP_MJ_WRITE
...
IRP_MJ_READ
...
如您所见,IRP_MJ_WRITE
出现在 IRP_MJ_CREATE
之后。这是因为此端口监视器不监视“IRP 状态”。它在驱动程序转发请求之前/之后收集数据,正如我们在前一节中讨论的关于收集数据的内容。
VI. 安装和使用
此驱动程序仅支持 Windows XP x86。如果您在 Windows XP 以外的其他平台上使用它,则需要根据目标平台重新编译。因此,以下过程仅适用于 Windows XP。
IV. A. 安装
驱动程序安装步骤
- 在“控制面板”中,双击“添加硬件”,然后单击“下一步”,这将显示“正在搜索硬件”对话框。
- 在“正在搜索硬件”对话框完成后,单击“是,我已经连接了硬件”单选按钮,然后单击“下一步”。
- 在“已安装的硬件”列表框中,向下滚动并选择/单击“添加新硬件设备”,然后单击“下一步”。
- 选择/单击“手动从列表中选择要安装的硬件(高级)”单选按钮,然后单击“下一步”。
- 在“常见硬件类型”列表框中,选择/单击“系统设备”,然后单击“下一步”。
- 单击“从磁盘开始”按钮,然后找到文件 DkPortMon2.inf,然后单击“下一步”。
- 再次单击“下一步”以完成驱动程序安装。
如果驱动程序安装成功,设备管理器应该如下所示:
VI. B. 使用。
只需运行 Bin 目录中的名为“DkPortMonGui.exe”的程序。首先,您需要选择要监视的端口,单击工具 - 选择端口,选择它,然后单击工具 - 开始以开始监视端口。如果您想查看驱动程序的调试消息,请使用 SysInternals 的 DbgView。
VII. 编译和链接源代码
源代码分为两部分:驱动程序源代码本身和客户端程序。安装 WDK,然后单击开始 - WDK xxx-xxxxxx-x - Build Environments - Windows XP - x86 Checked Build Environment。这将为您提供一个用于构建此源代码的命令提示符环境(x86 机器的检查构建环境/调试构建)。进入每个目录,然后键入build -cegZ 来编译和链接源代码。此方法适用于具有“调试构建”环境的 Windows XP。
已修复错误
- 2012 年 4 月 26 日,在 DkGui.cpp 文件中的 DkPortOnDataReceived() 函数中,将
*pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] && 0xF ];
更改为*pTmp++ = HexWChar[g_DkPortClient.m_Dat.Data[ul] & 0xF ];
。