使用带有未公开参数的NtQuerySystemInformation函数时遇到的问题

那天的早晨开始于“如果”破裂的事实。 这个表达式曾经由我的一位同事创造,他演示了调试器在单步执行代码时如何进入if块,而if被检查的条件恰好是错误的。 当时的问题证明是微不足道的-他使用了发行版优化的版本,在这种情况下,当然不可能相信逐步调试。 但是,“如果失败了”这一表述已经扎根,并从那时起就一直用于表示一种情况,其中一些根本性的东西停止工作,甚至使人难以置信。

因此,那天NtQuerySystemInformation函数发生了故障-Windows OS最重要的函数之一,该函数返回有关进程,线程,系统描述符等的信息。 关于使用此功能的好处,我曾经写过这篇文章 。 但是事实证明,即使是系统的这种基石有时也会失败。

那么发生了什么。

在相当长的时间内(几年),我们使用对带有SystemHandleInformation参数的NtQuerySystemInformation函数的调用来获取有关系统中所有描述符的信息。 是的,此参数正式引用了未记录的参数,但是,如果您开始寻找有关如何在当前所有正在运行的Windows OS应用程序中列出所有描述符的信息,则NtQuerySystemInformation + SystemHandleInformation组合将是最常提出的选项。 它确实适用于从Windows NT开始的所有操作系统。

为什么需要在所有进程中搜索描述符? 好吧,由于各种原因。 诸如Process Hacker之类的实用工具仅出于参考目的将其显示。 有一些程序可以执行此操作,以便搜索当前被某人阻止的资源(例如文件)。 例如,您可以在外部进程中找到一个互斥锁,该互斥锁仅用于运行该程序的一个副本,然后将其关闭并允许启动该应用程序的两个实例。 或列出描述符以复制它们以组织沙箱。 通常,有许多任务。

在这里,我将不对描述符枚举进行完整的描述,而只是说,它与一般示例类似,如下所示

while ((status = NtQuerySystemInformation( SystemHandleInformation, handleInfo, handleInfoSize, NULL )) == STATUS_INFO_LENGTH_MISMATCH) handleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH. if (!NT_SUCCESS(status)) { printf("NtQuerySystemInformation failed!\n"); return 1; } for (i = 0; i < handleInfo->HandleCount; i++) { ... } 

但是随后我启动​​了应用程序-突然发现我需要的描述符(而且我肯定知道它存在!)不在NtQuerySystemInformation()函数返回的列表中。 就这样,他们来了-“如果破产了”。

尝试在办公室的其他计算机上重现该问题。 在某些情况下,大多数情况下都可以复制-并非如此。 我们正试图了解其复制品与一切都良好的产品有何不同。 Windows版本到处都是相同的,更新,程序的构建–一切都是相同的。 突然,有人注意到所有再现问题的笔记本电脑都是同一型号。 硬件不兼容? 但是为什么现在突然就可以使用呢?此外,办公室里还有其他相同型号的笔记本电脑也可以使用。 甚至比较了设备驱动程序的版本-一切似乎都是相同的。 但是,所有内容都可以在某些笔记本电脑上使用,而不能在其他笔记本电脑上使用。

拉动头部的头发持续了大约半天,直到我不小心注意到了两件事:

  1. 由于某些原因,进程PID在我的计算机上通常是三位,四位或五位数字,现在已变成六位。 奇怪的是,看到的是780936类型的PID。我以前没有注意到这些。 而且,正在运行的进程总数足够多(多达一百个)。
  2. “ CPU”选项卡上的任务管理器显示了系统中描述符的总数-庞大,超过80万。

对于正常的应用程序,打开一百或两个描述符是正常的。 好吧,一千。 在活跃使用的情况下,chrome可以打开大约2000个,Visual Studio可以打开3000个大型项目,但是谁打开了80万个? 幸运的是,前面提到的Process Hacker允许您显示每个进程的描述符数量,甚至可以按使用的描述符数量对进程列表进行排序。

我们看到了什么? 我们看到如下图所示:



我必须说,我刚刚制作了上面的屏幕截图,所以进程列表中的第一个“共有”大约20,000个描述符。 然后,当我第一次看到问题时,那里大约有65万,我们的英雄是谁? 宾果! 这是过程SynTPEnhService.exe。

然后整个谜团在我脑海中浮现。 SynTPEnhService.exe是Synaptics触摸板驱动程序的一部分。 它仅安装在我们办公室中特定型号的笔记本电脑上,在该笔记本电脑上出现了问题。 简短的观察表明,此进程每5秒会启动子进程SynTPEnh.exe,该进程将在1-2秒后关闭。 同时,父进程继续保留子进程的描述符,这导致描述符泄漏。 每5秒一次。 这是每天17,280个描述符。 将计算机打开一周,现在您有十万多个挂起描述符。 我的个人计算机没有重启超过一个月,因此,新进程的PID超过了50万。 这也解释了为什么该问题是在我们办公室的某些笔记本电脑上重现的,而在其他笔记本电脑上却没有发生:我的一些同事每天都重新启动PC,而像我这样的人却把它们打开了一夜。

顺便说一句,在这个地方,我记得我已经读过有关Synaptics触摸板驱动程序的某种问题。 经过一番挖掘,我发现了这篇由布鲁斯·道森(Bruce Dawson)撰写的文章 (他的文章的许多译本是在不同的时间在哈布雷(Habré)上发表的,但不是这个特定的译本)。 他在那里描述了由于SynTPEnh.exe进程的这种无休止的重启而引起的内存泄漏问题,但是却没有提及句柄泄漏问题,因此我的发现仍然与此不同。

解决问题


那么,触摸板驱动程序会“吞噬”成千上万个描述符-那又如何呢? 而且在Windows NT时代写回的函数NtQuerySystemInformation(SystemHandleInformation,...)具有(并且具有)相当有限的内部缓冲区。 我在任何地方都找不到确切的大小指示,但是显然,它不是为一百万个描述符设计的。 结果,该函数“尽可能多地”返回它们,这意味着它们之中可能有也可能不是所需的。

怎么办 正如动画系列《里克与莫蒂》中的里克所说:“发明隐形传态时,你会立即发现一件不愉快的事情:你是宇宙中最后一个发明它的人。” 事实证明,Microsoft在20年前使用SystemHandleInformation参数调用NtQuerySystemInformation时就意识到了缓冲区有限的问题,因此,从WindowsXP开始,他们添加了NtQuerySystemInformation函数另一个(也是未记录的)参数SystemExtendedHandleInformation。 当您调用NtQuerySystemInformation(SystemExtendedHandleInformation,...)时,无论有多少描述符,系统中的所有描述符都将返回给您。 嗯,或者确切地说,我不确定这一点,也许对此参数有一些限制,但是可以肯定的是,他可以在一个状态中返回800,000个描述符。

在网上可以找到使用SystemExtendedHandleInformation的示例,例如this 。 通常,那里的一切都相似,仅使用其他结构,仅此而已。

这是一个关于使用Widnows操作系统的未记录参数的说明性故事,这可能非常有用,但需要仔细测试并为非标准问题做好准备。

Source: https://habr.com/ru/post/zh-CN433906/


All Articles