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

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

提示

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

本部分内容:

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

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

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

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

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

5.生成驱动程序

6.安装清单

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<?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. 将显示消息编译器属性。 在 “常规 ”设置下,设置以下选项,然后选择“ 应用”。

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

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

默认情况下,消息编译器使用输入文件的基名称作为生成的文件的基名称。 若要指定基名称,请设置 生成的文件基名称 (-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 im drivermanifest

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

Wevtutil.exe im evntdrv.xml

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

wevtutil.exe um drivermanifest

例如,若要卸载 Eventdrv 示例清单:

Wevtutil.exe um evntdrv.xml

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

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