使用标记进行对象引用跟踪

内核对象 是 Windows 内核在系统内存中实现的基元数据对象。 它们表示实体,例如设备、驱动程序、文件、注册表项、事件、信号灯、进程和线程。

大多数内核对象不是永久性的。 为了防止 Windows 在内核模式驱动程序使用非持久性内核对象时删除该对象,驱动程序获取对对象的计数引用。 当驱动程序不再需要 对象时,驱动程序会释放其对 对象的引用。

如果驱动程序未释放其对对象的所有引用,则对象的引用计数永远不会达到零,并且对象管理器永远不会删除它。 在操作系统重启之前,无法重复使用泄漏的资源。

如果 下的驱动程序引用 对象,则会发生另一种类型的引用错误。 在这种情况下,驱动程序释放对 对象的引用比驱动程序实际保留的要多。 此错误可能导致对象管理器过早删除对象,而其他客户端仍在尝试访问该对象。

内核对象的泄漏和引用不足可能是难以追踪的 bug。 例如,进程对象或设备对象可能有数万个引用。 在这些情况下,可能很难识别对象引用 bug 的来源。

在 Windows 7 及更高版本的 Windows 中,你可以为对象引用提供标记,使这些 bug 更易于查找。 以下例程将标记与获取和释放对内核对象的引用相关联:

ObDereferenceObjectDeferDeleteWithTag

ObDereferenceObjectWithTag

ObReferenceObjectByHandleWithTag

ObReferenceObjectByPointerWithTag

ObReferenceObjectWithTag

例如,在 Windows 7 及更高版本的 Windows 中可用的 ObReferenceObjectWithTagObDereferenceObjectWithTagObReferenceObjectObDereferenceObject 例程的增强版本,这些例程在 Windows 2000 及更高版本的 Windows 中可用。 通过这些增强例程,可以提供四字节的自定义标记值作为输入参数。 可以使用 Windows 调试工具 检查包含每个调用 的标记值 的对象引用跟踪ObReferenceObjectObDereferenceObject 不允许调用方指定自定义标记,但在 Windows 7 及更高版本的 Windows 中,这些例程会将标记值“Dflt”) 的默认标记添加到跟踪 (。 因此,对 ObReferenceObjectObDereferenceObject 的 调用与调用 ObReferenceObjectWithTagObDereferenceObjectWithTag 的效果相同,该调用指定标记值为“Dflt”。 (在程序中,此标记值显示为0x746c6644或“tlfD”。)

若要跟踪潜在的对象泄漏或引用不足,请在驱动程序中标识一组关联的 ObReferenceObjectXxxWithTagObDereferenceObjectXxxWithTag 调用,这些调用会递增和递减特定对象的引用计数。 选择一个通用标记值 (例如,“Lky8”) 用于此集中的所有调用。 驱动程序使用完 对象后,递减次数应与增量数完全匹配。 如果这些数字不匹配,则驱动程序存在对象引用 bug。 调试器可以比较每个标记值的增量和递减数,并告知它们是否不匹配。 借助此功能,可以快速查明引用计数不匹配的源。

若要在 Windows 调试工具中查看对象引用跟踪,请使用 !obtrace 内核模式调试器扩展。 如果启用对象引用跟踪,则可以使用 !obtrace 扩展来显示对象引用标记。 默认情况下,对象引用跟踪处于关闭状态。 使用 全局标志编辑器 (Gflags) 启用对象引用跟踪。 有关 Gflag 的详细信息,请参阅 配置对象引用跟踪

!obtrace 扩展的输出包含“Tag”列,如以下示例所示:

0: kd> !obtrace 0x8a226130
Object: 8a226130
 Image: leakyapp.exe
Sequence   (+/-)   Tag    Stack
--------   -----   ----   --------------------------------------------
      36    +1     Dflt      nt!ObCreateObject+1c4
                             nt!NtCreateEvent+93
                             nt!KiFastCallEntry+12a

      37    +1     Dflt      nt!ObpCreateHandle+1c1
                             nt!ObInsertObjectEx+d8
                             nt!ObInsertObject+1e
                             nt!NtCreateEvent+ba
                             nt!KiFastCallEntry+12a

      38    -1     Dflt      nt!ObfDereferenceObjectWithTag+22
                             nt!ObInsertObject+1e
                             nt!NtCreateEvent+ba
                             nt!KiFastCallEntry+12a

      39    +1     Lky8      nt!ObReferenceObjectByHandleWithTag+254
                             leakydrv!LeakyCtlDeviceControl+6c
                             nt!IofCallDriver+63
                             nt!IopSynchronousServiceTail+1f8
                             nt!IopXxxControlFile+6aa
                             nt!NtDeviceIoControlFile+2a
                             nt!KiFastCallEntry+12a

      3a    -1     Dflt      nt!ObfDereferenceObjectWithTag+22
                             nt!ObpCloseHandle+7f
                             nt!NtClose+4e
                             nt!KiFastCallEntry+12a
 
--------   -----   ----   --------------------------------------------
References: 3, Dereferences 2
Tag: Lky8 References: 1 Dereferences: 0 Over reference by: 1

此示例中的最后一行指示与“Lky8”标记关联的引用和取消引用计数不匹配,并且此不匹配的结果是一个 (即泄漏) 。

如果结果为引用不足,则 !obtrace 输出的最后一行可能如下所示:

Tag: Lky8 References: 1 Dereferences: 2 Under reference by: 1

默认情况下,操作系统通过在释放对象后删除对象的对象引用跟踪来节省内存。 在跟踪引用不足时,即使在系统释放对象后,仍可在内存中保留跟踪。 为此,Gflags 工具提供了一个“永久”选项,该选项在计算机关闭并重新启动时将跟踪保留在内存中。

Windows XP 引入了对象引用跟踪。 由于最初跟踪不包括标记,因此开发人员必须使用不太方便的技术来识别对象引用 bug。 调试器可以跟踪开发人员按对象类型选择的对象组的引用。 开发人员识别对象引用和取消引用的各种源的唯一方法是比较其调用堆栈。 尽管前面的 !obtrace 示例仅包含五个堆栈,但某些类型的对象(例如进程 (EPROCESS) 对象)可能会被引用和取消引用数千次。 由于要检查数千个堆栈,如果不使用标记,可能很难识别对象泄漏或引用不足的来源。