将事件跟踪添加到内核模式驱动程序

本部分介绍如何使用 Windows 事件跟踪 (ETW) 内核模式 API 将事件跟踪添加到内核模式驱动程序。 ETW 内核模式 API 是在 Windows Vista 中引入的,在早期操作系统中不受支持。 如果你的驱动程序需要支持 Windows 2000 及更高版本中的跟踪功能,请使用 WPP 软件跟踪或 WMI 事件跟踪

提示

若要查看演示如何使用 Windows 驱动程序工具包 (WDK) 和 Visual Studio 实现 ETW 的示例代码,请参阅 Eventdrv 示例

本节内容:

工作流 - 将事件跟踪添加到 Kernel-Mode 驱动程序

1. 确定要引发的事件的类型以及发布位置

2.创建定义提供程序、事件和通道的检测清单

3. 使用消息编译器 (Mc.exe) 编译检测清单

4. 添加生成的代码以引发 (发布) 事件 (注册、注销和写入事件)

5.生成驱动程序

6.安装清单

7.测试驱动程序以验证 ETW 支持

工作流 - 将事件跟踪添加到 Kernel-Mode 驱动程序

显示将事件跟踪添加到内核模式驱动程序的过程的流程图。

1. 确定要引发的事件的类型以及发布位置

在开始编码之前,必须确定希望驱动程序通过 Windows (ETW) 事件跟踪记录的事件类型。 例如,你可能想要记录有助于你在分发驱动程序后诊断问题的事件,或者记录在开发驱动程序时对你有帮助的事件。有关信息,请参阅 Windows 事件日志参考

事件类型由通道标识。 频道是针对特定受众的管理员、操作、分析或调试类型的命名事件流,类似于电视频道。 通道将事件从事件提供程序传递到事件日志和事件使用者。 有关信息,请参阅 定义通道

在开发过程中,你很可能对跟踪有助于调试代码的事件感兴趣。 此同一通道可用于生产代码,以帮助解决部署驱动程序后可能出现的问题。 你可能还希望跟踪可用于度量性能的事件;这些事件可帮助 IT 专业人员微调服务器性能,并有助于识别网络瓶颈。

2.创建定义提供程序、事件和通道的检测清单

检测清单是一个 XML 文件,提供提供程序将引发的事件的正式说明。 检测清单标识事件提供程序,指定最多八个) (一个或多个通道,并描述事件和事件使用的模板。 此外,检测清单允许字符串本地化,因此你可以本地化跟踪消息。 事件系统和事件使用者可以使用清单中提供的结构化 XML 数据来执行查询和分析。

有关检测清单的信息,请参阅 (Windows) 编写检测清单 (Windows) 的 EventManifest 架构 将 Windows 事件日志 (Windows)

以下检测清单显示了使用名称“Sample Driver”的事件提供程序。请注意,此名称不必与驱动程序二进制文件的名称相同。 清单还指定提供程序的 GUID 以及消息和资源文件的路径。 消息和资源文件使 ETW 知道在何处找到解码和报告事件所需的资源。 这些路径指向驱动程序 (.sys) 文件的位置。 驱动程序必须安装在目标计算机上的指定目录中。

该示例使用命名通道 System,这是管理员类型的事件的通道。此通道在 Winmeta.xml 文件中定义,该文件随 Windows 驱动程序工具包 (WDK) 在%WindowsSdkDir%\include\um 目录中提供。 系统通道可保护到在系统服务帐户下运行的应用程序。 清单包括事件模板,这些模板描述发布事件时随事件提供的数据类型及其静态和动态内容。 此示例清单定义了三个事件: StartEventSampleEventAUnloadEvent

除了通道,还可以将事件与级别和关键字相关联。 关键字和级别提供了一种启用事件的方法,并提供在发布事件时筛选事件的机制。 关键字可用于将逻辑上相关的事件组合在一起。 级别可用于指示事件的严重性或详细程度,例如严重、错误、警告或信息性。 Winmeta.xml 文件包含事件属性的预定义值。

为事件有效负载 (事件消息和数据) 创建模板时,必须指定输入和输出类型。 Windows ) 的 InputType 复杂类型 (的“备注”部分介绍了支持的类型。

<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<instrumentationManifest
    xmlns="http://schemas.microsoft.com/win/2004/08/events"
    xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://schemas.microsoft.com/win/2004/08/events eventman.xsd"
    >
  <instrumentation>
    <events>
      <provider
          guid="{b5a0bda9-50fe-4d0e-a83d-bae3f58c94d6}"
          messageFileName="%SystemDrive%\ETWDriverSample\Eventdrv.sys"
          name="Sample Driver"
          resourceFileName="%SystemDrive%\ETWDriverSample\Eventdrv.sys"
          symbol="DriverControlGuid"
          >
        <channels>
          <importChannel
              chid="SYSTEM"
              name="System"
              />
        </channels>
        <templates>
          <template tid="tid_load_template">
            <data
                inType="win:UInt16"
                name="DeviceNameLength"
                outType="xs:unsignedShort"
                />
            <data
                inType="win:UnicodeString"
                name="name"
                outType="xs:string"
                />
            <data
                inType="win:UInt32"
                name="Status"
                outType="xs:unsignedInt"
                />
          </template>
          <template tid="tid_unload_template">
            <data
                inType="win:Pointer"
                name="DeviceObjPtr"
                outType="win:HexInt64"
                />
          </template>
        </templates>
        <events>
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.StartEvent.EventMessage)"
              opcode="win:Start"
              symbol="StartEvent"
              template="tid_load_template"
              value="1"
              />
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.SampleEventA.EventMessage)"
              opcode="win:Info"
              symbol="SampleEventA"
              value="2"
              />
          <event
              channel="SYSTEM"
              level="win:Informational"
              message="$(string.UnloadEvent.EventMessage)"
              opcode="win:Stop"
              symbol="UnloadEvent"
              template="tid_unload_template"
              value="3"
              />
        </events>
      </provider>
    </events>
  </instrumentation>
  <localization xmlns="http://schemas.microsoft.com/win/2004/08/events">
    <resources culture="en-US">
      <stringTable>
        <string
            id="StartEvent.EventMessage"
            value="Driver Loaded"
            />
        <string
            id="SampleEventA.EventMessage"
            value="IRP A Occurred"
            />
        <string
            id="UnloadEvent.EventMessage"
            value="Driver Unloaded"
            />
      </stringTable>
    </resources>
  </localization>
</instrumentationManifest>

3. 使用消息编译器 (Mc.exe) 编译检测清单

在编译源代码之前,必须运行 消息编译器 (Mc.exe) 。 消息编译器包含在 Windows 驱动程序工具包 (WDK) 中。 消息编译器创建一个头文件,其中包含事件提供程序、事件属性、通道和事件的定义。 你必须在源代码中包括此头文件。 消息编译器还会将生成的资源编译器脚本 (*.rc) ,生成的 .bin 文件 (资源编译器脚本包含的二进制资源) 。

可以通过多种方式将此步骤包含在生成过程中:

  • 将消息编译器任务添加到驱动程序项目文件 (,如 Eventdrv 示例) 所示。

  • 使用 Visual Studio 添加检测清单并配置消息编译器属性。

将消息编译器任务添加到项目文件 有关如何在生成过程中包括消息编译器的示例,请查看 Eventdrv 示例的项目文件。 在 Eventdrv.vcxproj 文件中,有一个 <调用消息编译器的 MessageCompile> 节。 消息编译器使用清单文件 (evntdrv.xml) 作为输入来生成头文件 evntdrvEvents.h。 本部分还指定生成的 RC 文件的路径,并启用内核模式日志记录宏。 可以复制此部分并将其添加到驱动程序项目文件 (.vcxproj) 。


    <MessageCompile Include="evntdrv.xml">
      <GenerateKernelModeLoggingMacros>true</GenerateKernelModeLoggingMacros>
      <HeaderFilePath>.\$(IntDir)</HeaderFilePath>
      <GeneratedHeaderPath>true</GeneratedHeaderPath>
      <WinmetaPath>"$(SDK_INC_PATH)\winmeta.xml"</WinmetaPath>
      <RCFilePath>.\$(IntDir)</RCFilePath>
      <GeneratedRCAndMessagesPath>true</GeneratedRCAndMessagesPath>
      <GeneratedFilesBaseName>evntdrvEvents</GeneratedFilesBaseName>
      <UseBaseNameOfInput>true</UseBaseNameOfInput>
    </MessageCompile>

生成 Eventdrv.sys 示例时,Visual Studio 会创建事件跟踪所需的文件。 它还将 evntdrv.xml 清单添加到驱动程序项目的资源文件列表中。 可以选择并按住 (或右键单击清单) 以查看消息编译器属性页。

使用 Visual Studio 添加检测清单

可以将检测清单添加到驱动程序项目,然后配置消息编译器属性以生成必要的资源和头文件。

使用 Visual Studio 将检测清单添加到项目

  1. 在解决方案资源管理器,将清单文件添加到驱动程序项目。 选择并按住 (或右键单击) 资源文件 > 添加 > 现有项 (,例如,evntdrv.xml 或 mydriver.man) 。

  2. 选择并按住 (或右键单击) 刚添加的文件,然后使用属性页将项类型更改为 MessageCompile ,然后选择“ 应用”。

  3. 将显示消息编译器属性。 在“ 常规 设置”下,设置以下选项,然后选择“ 应用”。

    常规 设置
    生成内核模式日志记录宏 是 ( 公里)
    使用输入的基名称 是 (-b)
  4. “文件选项”下,设置以下选项,然后选择“ 应用”。

    文件选项 设置
    生成包含计数器的头文件
    头文件路径 $(IntDir)
    生成 RC 和二进制消息文件路径
    RC 文件路径 $(IntDir)
    生成文件基名 $ (Filename)

默认情况下,消息编译器使用输入文件的基名称作为它生成的文件的基名称。 若要指定基名称,请设置 “生成的文件基名称 (-z) 字段。 在 Eventdr.sys 示例中,基名称设置为 evntdrvEvents ,以便它与 evntdrv.c 中包含的头文件 evntdrvEvents.h 的名称匹配。

注意

如果未在 Visual Studio 项目中包括生成的 .rc 文件,则可能会在安装清单文件时收到有关找不到资源的错误消息。

4. 添加生成的代码以引发 (发布) 事件, (注册、注销和写入事件)

在检测清单中,你定义了事件提供程序的名称和事件描述符。 使用消息编译器编译检测清单时,消息编译器将生成一个头文件,用于描述资源,并为事件定义宏。 现在,必须将生成的代码添加到驱动程序中,以引发这些事件。

  1. 在源文件中,包括由消息编译器 (MC.exe) 生成的事件头文件。 例如,在 Eventdrv 示例中,Evntdrv.c 源文件包括上一步中生成的头文件 (evntdrvEvents.h) :

    #include "evntdrvEvents.h"  
    
  2. 添加将驱动程序注册和注销为事件提供程序的宏。 例如,在 Eventdrv 示例 (evntdrvEvents.h) 的头文件中,消息编译器根据提供程序的名称创建宏。 在清单中, Eventdrv 示例 使用名称“Sample Driver”作为提供程序的名称。 消息编译器将提供程序的名称与事件宏组合在一起,以注册提供程序,在本例中, EventRegisterSample_Driver

    //  This is the generated header file envtdrvEvents.h
    //
    //  ...
    //
    //
    // Register with ETW Vista +
    //
    #ifndef EventRegisterSample_Driver
    #define EventRegisterSample_Driver() McGenEventRegister(&DriverControlGuid, McGenControlCallbackV2, &DriverControlGuid_Context, &Sample_DriverHandle)
    #endif
    

    EventRegister<提供程序> 宏添加到 DriverEntry 函数。 在创建和初始化设备对象的代码后面添加此函数。 请注意,必须将对 EventRegister<提供程序> 函数的调用与对 EventUnregister<提供程序>的调用匹配。 可以在驱动程序的 Unload* 例程中注销驱动程序。

       // DriverEntry function
       // ...
    
    
        // Register with ETW
        //
        EventRegisterSample_Driver();
    
  3. 将生成的代码添加到驱动程序的源文件中,以编写 (引发) 清单中指定的事件。 从清单编译的头文件包含为驱动程序生成的代码。 使用头文件中定义的 EventWrite<事件> 函数将跟踪消息发布到 ETW。 例如,以下代码显示 eventdrv 示例的 evntdrvEvents.h 中定义的事件的宏。

    用于写入这些事件的宏称为: EventWriteStartEventEventWriteSampleEventAEventWriteUnloadEvent。 如这些宏的定义中所示,宏定义会自动包含 一个 EventEnabled<事件> 宏,用于检查事件是否已启用。 如果未启用事件,检查无需生成有效负载。

    
    ///
    // This is the generated header file envtdrvEvents.h
    //
    //  ...
    //
    // Enablement check macro for StartEvent
    //
    
    #define EventEnabledStartEvent() ((Sample_DriverEnableBits[0] & 0x00000001) != 0)
    
    //
    // Event Macro for StartEvent
    //
    #define EventWriteStartEvent(Activity, DeviceNameLength, name, Status)\
            EventEnabledStartEvent() ?\
            Template_hzq(Sample_DriverHandle, &StartEvent, Activity, DeviceNameLength, name, Status)\
            : STATUS_SUCCESS\
    
    //
    // Enablement check macro for SampleEventA
    //
    
    #define EventEnabledSampleEventA() ((Sample_DriverEnableBits[0] & 0x00000001) != 0)
    
    //
    // Event Macro for SampleEventA
    //
    #define EventWriteSampleEventA(Activity)\
            EventEnabledSampleEventA() ?\
            TemplateEventDescriptor(Sample_DriverHandle, &SampleEventA, Activity)\
            : STATUS_SUCCESS\
    
    //
    // Enablement check macro for UnloadEvent
    //
    
    #define EventEnabledUnloadEvent() ((Sample_DriverEnableBits[0] & 0x00000001) != 0)
    
    //
    // Event Macro for UnloadEvent
    //
    #define EventWriteUnloadEvent(Activity, DeviceObjPtr)\
            EventEnabledUnloadEvent() ?\
            Template_p(Sample_DriverHandle, &UnloadEvent, Activity, DeviceObjPtr)\
            : STATUS_SUCCESS\
    
    

    EventWrite<事件> 宏添加到要引发的事件的源代码中。 例如,以下代码片段显示了 Eventdrv 示例中DriverEntry 例程。 DriverEntry 包括用于将驱动程序注册到 ETW (EventRegisterSample_Driver) 的宏,以及用于将驱动程序事件写入 ETW (EventWriteStartEvent) 的宏。

    NTSTATUS
    DriverEntry(
        IN PDRIVER_OBJECT DriverObject,
        IN PUNICODE_STRING RegistryPath
        )
    /*++
    
    Routine Description:
    
        Installable driver initialization entry point.
        This entry point is called directly by the I/O system.
    
    Arguments:
    
        DriverObject - pointer to the driver object
    
        RegistryPath - pointer to a unicode string representing the path
            to driver-specific key in the registry
    
    Return Value:
    
       STATUS_SUCCESS if successful
       STATUS_UNSUCCESSFUL  otherwise
    
    --*/
    {
        NTSTATUS Status = STATUS_SUCCESS;
        UNICODE_STRING DeviceName;
        UNICODE_STRING LinkName;
        PDEVICE_OBJECT EventDrvDeviceObject;
        WCHAR DeviceNameString[128];
        ULONG LengthToCopy = 128 * sizeof(WCHAR);
        UNREFERENCED_PARAMETER (RegistryPath);
    
        KdPrint(("EventDrv: DriverEntry\n"));
    
        //
        // Create Dispatch Entry Points.  
        //
        DriverObject->DriverUnload = EventDrvDriverUnload;
        DriverObject->MajorFunction[ IRP_MJ_CREATE ] = EventDrvDispatchOpenClose;
        DriverObject->MajorFunction[ IRP_MJ_CLOSE ] = EventDrvDispatchOpenClose;
        DriverObject->MajorFunction[ IRP_MJ_DEVICE_CONTROL ] = EventDrvDispatchDeviceControl;
    
        RtlInitUnicodeString( &DeviceName, EventDrv_NT_DEVICE_NAME );
    
        //
        // Create the Device object
        //
        Status = IoCreateDevice(
                               DriverObject,
                               0,
                               &DeviceName,
                               FILE_DEVICE_UNKNOWN,
                               0,
                               FALSE,
                               &EventDrvDeviceObject);
    
        if (!NT_SUCCESS(Status)) {
            return Status;
        }
    
        RtlInitUnicodeString( &LinkName, EventDrv_WIN32_DEVICE_NAME );
        Status = IoCreateSymbolicLink( &LinkName, &DeviceName );
    
        if ( !NT_SUCCESS( Status )) {
            IoDeleteDevice( EventDrvDeviceObject );
            return Status;
        }
    
     //
     // Choose a buffering mechanism
     //
     EventDrvDeviceObject->Flags |= DO_BUFFERED_IO;
    
     //
     // Register with ETW
     //
     EventRegisterSample_Driver();
    
     //
     // Log an Event with :  DeviceNameLength
     //                      DeviceName
     //                      Status
     //
    
     // Copy the device name into the WCHAR local buffer in order
     // to place a NULL character at the end, since this field is
     // defined in the manifest as a NULL-terminated string
    
     if (DeviceName.Length <= 128 * sizeof(WCHAR)) {
    
         LengthToCopy = DeviceName.Length;
    
     }
    
     RtlCopyMemory(DeviceNameString,
                   DeviceName.Buffer,
                   LengthToCopy);
    
     DeviceNameString[LengthToCopy/sizeof(WCHAR)] = L'\0';
    
     EventWriteStartEvent(NULL, DeviceName.Length, DeviceNameString, Status);
    
     return STATUS_SUCCESS;
    }
    

将所有 EventWrite<事件> 宏添加到要引发的事件的源代码中。 可以检查 Eventdrv 示例 ,了解如何为驱动程序源代码中的事件调用另外两个宏。

  1. 使用生成的头文件中的 EventUnregister< 提供程序宏将驱动程序注销为事件提供程序>

    将此函数调用置于驱动程序卸载例程中。 调用 EventUnregister<提供程序> 宏后,不应进行跟踪调用。 卸载进程时,未能注销事件提供程序可能会导致错误,因为与进程关联的任何回调函数不再有效。

        // DriverUnload function
        // ...
        //
    
        //  Unregister the driver as an ETW provider
        //
        EventUnregisterSample_Driver();
    

5.生成驱动程序

如果已将检测清单添加到项目并配置了消息编译器 (MC.exe) 属性,则可以使用 Visual Studio 和 MSBuild 生成驱动程序项目或解决方案。

  1. 在 Visual Studio 中打开驱动程序解决方案。

  2. 通过选择“生成解决方案”,从“ 生成”菜单生成示例。 有关生成解决方案的详细信息,请参阅 生成驱动程序

6. 安装清单

必须在目标系统上安装清单,以便事件使用者 (例如,事件日志) 可以找到包含事件元数据的二进制文件的位置。 如果在步骤 3 中将消息编译器任务添加到驱动程序项目,则会编译检测清单,并在生成驱动程序时生成资源文件。 使用 Windows 事件命令行实用工具 (Wevtutil.exe) 安装清单。 安装清单的语法如下所示:

wevtutil.exe imdrivermanifest

例如,若要安装 Evntdrv.sys 示例驱动程序的清单,请使用提升的权限打开命令提示符窗口, (以管理员身份运行) 导航到 evntdrv.xml 文件所在的目录并输入以下命令:

Wevtutil.exe im evntdrv.xml

跟踪会话完成后,使用以下语法卸载清单。

wevtutil.exe umdrivermanifest

例如,若要卸载 Eventdrv 示例的清单,请执行以下操作:

Wevtutil.exe um evntdrv.xml

7. 测试驱动程序以验证 ETW 支持

安装驱动程序。 练习驱动程序以生成跟踪活动。 在事件查看器中查看结果。 还可以运行 Tracelog,然后运行 Tracerpt(一种用于处理事件跟踪日志的工具)来控制、收集和查看事件跟踪日志。