Windows10 用户模式事件追踪(ETW)

😄

1 ETW介绍

ETW(Event Tracing for Windows)利用事件机制(Event)能够跟踪和记录由用户模式应用程序和内核模式驱动程序引发的事件。ETW 使用内核中实现的缓冲区和日志记录机制,提供对用户模式应用程序和内核模式设备驱动程序引发的事件的跟踪机制

通过 ETW 获取的信息非常丰富,通过 ETW 通常可以获取到的信息有:

  1. 文件类信息,包括文件创建、删除、读写等信息。
  2. 注册表信息,包括注册表的创建、删除、读写等信息。
  3. 进程线程信息,包括进程创建退出、线程创建退出、模块加载等。
  4. 网络信息,TCP、UDP 协议的发送,接收 IP 地址以及数据长度等。
  5. CPU 的使用情况、内存使用情况以及发生事件时的堆栈信息等。

关于事件 Event:

  • Windows Vista 之前,可以使用事件跟踪(ETW)或事件日志来记录事件。
  • Windows Vista 及其之后,ETW 统一了以上两者,统一使用事件追踪 ETW。在 ETW 中,Windows 10 引入了 TraceLogging 的 ETW。

说明:ETW 事件追踪跟踪应用程序/驱动程序,并不像是调试器附件进程那样,ETW 是非侵入式的进行监控。

2 ETW 框架

ETW 架构如下:

1.png

ETW 包含三个组件:事件控制器(Controllers)、事件提供器(Providers)、事件消耗器(Consumers)。另外还包括会话(Sessions)。

  • 事件控制器(Controllers):用于启动和停止事件跟踪会话并启用提供程序。
  • 事件提供器(Providers):用来产生事件。
  • 事件消耗器(Consumers):用来处理事件、消费事件。
  • 会话(Sessions):记录了一个或多个 Providers 输出的事件,其主要用来管理和刷新事件的缓存。

Controllers

  • 定义日志文件的大小和位置。
  • 设置日志是输出到文件还是实时解析。
  • 启动和停止 Session。
  • 启用 Providers。

Providers

Providers 用来提供(产生)事件。当一个 Provider 注册后,可以通过 Controller 控制它是否输出事件。

Providers 程序共分为四类:

Providers 类型 系统 特性
MOF (经典) 提供程序 Windows 2000,Windows XP 1、使用 RegisterTraceGuids 和 TraceEvent 函数注册和写入事件。
2、使用 MOF 类定义事件,以便使用者知道如何使用这些事件。
3、一次只能启用一个跟踪会话。
WPP 提供程序 Windows 2000,Windows XP 1、使用 RegisterTraceGuids 和 TraceEvent 函数注册和写入事件。
2、将关联的 TMF (编译为二进制文件的 .pdb) ,其中包含从预处理器扫描源代码中的 WPP 检测中推断出的解码信息。
3、一次只能启用一个跟踪会话。
基于清单(manifest-based)的提供程序 Vista 新增 1、使用 EventRegister 和 EventWrite 注册和写入事件。
2、 使用清单定义事件,以便使用者知道如何使用这些事件。
3、最多可以同时启用 8 个跟踪会话。
TraceLogging 提供程序 Windows 10 新增 1、使用 TraceLoggingRegister 和 TraceLoggingWrite 注册和写入事件。
2、使用自描述事件,使事件本身包含使用它们所需的所有信息。
3、最多可以同时启用 8 个跟踪会话。

Consumers

  • 可以同时接收多个 Session 的事件(系统按时间顺序传递事件)。
  • 可以接收处理存储在日志文件中的事件,或是实时处理从 Session 获取的事件。

Sessions

  • 由 Controllers 定义,Session 记录了一个或多个 Providers 输出的事件,其主要用来管理和刷新事件的缓存。
  • 系统同时最多支持 64 个 Session(Windows 2000 仅支持 32 个事件跟踪会话),这些 Session 中有两个 Session 比较特殊,由操作系统直接定义,可直接使用,分别是:
    1. 全局记录器会话(Global Logger Session),它用来记录操作系统早期启动过程中的事件,例如设备驱动相关的事件。
    2. NT 内核记录器会话(NT Kernel Logger Session),它用来记录操作系统生成的预定义系统事件,例如磁盘 IO 或页面错误事件。

ETW 处理流程

3.png

3 应用层 ETW

本文介绍应用层的 ETW,下一篇文章再介绍内核层的 ETW。

下面参考 使用事件跟踪—MSDN,以下主题介绍如何使用 ETW API 进行事件跟踪。

主题 说明
控制事件跟踪会话 介绍如何管理事件跟踪会话。
提供事件 介绍如何注册和检测事件跟踪提供程序。
使用事件 介绍如何实现用于从跟踪日志文件或实时使用和处理事件的回调函数。
Windows软件跟踪预处理器 提供一种有效的机制,用于记录和使用在应用程序或驱动程序执行过程中发生的事件。

4 Providers 提供事件

Providers 程序共分为四类:

Providers 类型 系统 特性
MOF (经典) 提供程序 Windows 2000,Windows XP 1、使用 RegisterTraceGuids 和 TraceEvent 函数注册和写入事件。
2、使用 MOF 类定义事件,以便使用者知道如何使用这些事件。
3、一次只能启用一个跟踪会话。
WPP 提供程序 Windows 2000,Windows XP 1、使用 RegisterTraceGuids 和 TraceEvent 函数注册和写入事件。
2、将关联的 TMF (编译为二进制文件的 .pdb) ,其中包含从预处理器扫描源代码中的 WPP 检测中推断出的解码信息。
3、一次只能启用一个跟踪会话。
基于清单(manifest-based)的提供程序 Vista 新增 1、使用 EventRegister 和 EventWrite 注册和写入事件。
2、 使用清单定义事件,以便使用者知道如何使用这些事件。
3、最多可以同时启用 8 个跟踪会话。
TraceLogging 提供程序 Windows 10 新增 1、使用 TraceLoggingRegister 和 TraceLoggingWrite 注册和写入事件。
2、使用自描述事件,使事件本身包含使用它们所需的所有信息。
3、最多可以同时启用 8 个跟踪会话。

如何将事件写入跟踪会话取决于提供程序的类型。

本节以基于清单的事件进行介绍 Providers。

4.1 注册/注销 Providers

每个进程理论上最多可以注册 1024 个提供程序 GUID;但是,应将每个进程注册的提供程序数限制为一两个。

ETW 通过 GUID 来标识系统中的 ETW 提供器。

提供器的使用顺序:注册提供器——将事件写入会话——卸载提供器。

本节以基于清单的事件进行介绍 Providers。

若要注册基于清单的提供程序,请调用 EventRegister 函数。 该函数注册提供程序的 GUID,并在控制器启用或禁用提供程序时标识 ETW 调用的可选回调。

EventRegister 函数:注册 ETW 事件提供程序,创建可用于写入 ETW 事件的句柄。

1
2
3
4
5
6
ULONG EVNTAPI EventRegister(
[in] LPCGUID ProviderId,
[in, optional] PENABLECALLBACK EnableCallback,
[in, optional] PVOID CallbackContext,
[out] PREGHANDLE RegHandle
);
  • ProviderId:标识提供程序的 GUID,唯一值。
  • EnableCallback:当跟踪会话启用或禁用此提供程序时,ETW 将调用的可选 EnableCallback 。 如果需要任何回调,请使用 NULL 。
  • CallbackContext:回调函数的参数。
  • RegHandle:输出值,提供程序的句柄,方便 EventWrite、 EventProviderEnabled 和 EventRegister 函数使用。

返回值:如果成功 , 则返回 ERROR_SUCCESS。

EventUnregister 函数:取消注册(即注销) ETW 事件提供程序。如果 DLL 注册事件提供程序,然后卸载而不注销事件提供程序,则进程可能会崩溃。

1
2
3
ULONG EVNTAPI EventUnregister(
[in] REGHANDLE RegHandle // EventRegister 返回的事件提供程序注册句柄
);

返回值:如果成功 , 则返回 ERROR_SUCCESS。

4.2 提供者回调函数

ETW 事件提供程序定义 EnableCallback 函数(可选)以接收配置更改通知。 回调函数调用时机:当跟踪会话启用或禁用此提供程序时,ETW 将调用的可选 EnableCallback 。

PENABLECALLBACK 类型定义指向此回调函数的指针。 EnableCallback 是应用程序定义函数名称的占位符。

1
2
3
4
5
6
7
8
9
10
11
12
PENABLECALLBACK Penablecallback;

void Penablecallback(
[in] LPCGUID SourceId, // 提供者 GUID
[in] ULONG IsEnabled,
[in] UCHAR Level,
[in] ULONGLONG MatchAnyKeyword,
ULONGLONG MatchAllKeyword,
[in, optional] PEVENT_FILTER_DESCRIPTOR FilterData,
[in, optional] PVOID CallbackContext
)
{...}
  • IsEnabled:指示与此通知对应的 ControlCode。
    • EVENT_CONTROL_CODE_DISABLE_PROVIDER(0),禁用提供者。
    • EVENT_CONTROL_CODE_ENABLE_PROVIDER(1),启用提供者。
    • EVENT_CONTROL_CODE_CAPTURE_STATE(2),会话请求提供程序记录其状态信息。 提供程序通常通过编写包含提供程序状态的事件来响应。
  • Level:该值指定提供程序应写入的事件的详细程度。
  • FilterData:指向具有事件提供程序筛选器数据的 EVENT_FILTER_DESCRIPTOR 的指针。每个会话只能指定一个筛选器。 筛选器数据仅在回调中有效。

4.3 写入事件

基于清单的提供程序调用 EventWrite 或 EventWriteString 函数以将事件写入会话。

  • 如果事件数据是字符串,或者未为提供程序定义清单,并且事件数据是单个字符串,请调用 EventWriteString 函数以写入事件。
  • 对于包含数值或复杂数据类型的事件数据,请调用 EventWrite 函数来记录事件。
1
2
3
4
5
6
ULONG EVNTAPI EventWrite(
[in] REGHANDLE RegHandle, // 提供程序的注册句柄
[in] PEVENT_DESCRIPTOR EventDescriptor, // 用来描述事件的属性(元数据)
[in] ULONG UserDataCount, // 参数 4 的个数
[in, optional] PEVENT_DATA_DESCRIPTOR UserData // 该结构用来描述事件内容
);

返回值:

  • 成功,ERROR_SUCCESS。
  • 失败,返回错误码。

下面介绍非常重要的 EVENT_DESCRIPTOR、EVENT_DATA_DESCRIPTOR 结构。

  • EVENT_DESCRIPTOR:主要用来描述事件相关的属性(元数据)。
  • EVENT_DATA_DESCRIPTOR:主要用来描述事件包含的内容。

EVENT_DESCRIPTOR:

1
2
3
4
5
6
7
8
9
typedef struct _EVENT_DESCRIPTOR {
USHORT Id; // 标志基于清单的事件(使用16个数字)
UCHAR Version; // 指定基于清单的事件的版本(8个数字)
UCHAR Channel; // 启用特殊事件处理的 8 位数字
UCHAR Level; // 描述事件的严重性或重要性的 8 位数字
UCHAR Opcode; // 用于标记具有特殊语义的事件的 8 位数字
USHORT Task; // 用于批注事件或相关事件组的 16 位数字
ULONGLONG Keyword; // 一个 64 位掩码,用于指示一组事件类别中的事件成员身份
} EVENT_DESCRIPTOR, *PEVENT_DESCRIPTOR;

EVENT_DATA_DESCRIPTOR:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _EVENT_DATA_DESCRIPTOR {
ULONGLONG Ptr; // 指向数据的指针
ULONG Size; // 数据的大小(以字节为单位)
union {
ULONG Reserved;
struct {
UCHAR Type; // 在事件中使用此数据,表示时间类型
UCHAR Reserved1; // 未使用, 设置为 0
USHORT Reserved2; // 未使用, 设置为 0
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} EVENT_DATA_DESCRIPTOR, *PEVENT_DATA_DESCRIPTOR;

EVENT_DATA_DESCRIPTOR 的用法:

一般会在程序中定义一个 EVENT_DATA_DESCRIPTOR[MAXLENTH] 数组,EVENT_DATA_DESCRIPTOR[i].Ptr 指针指向与该结构相关的数据。一般可以直接手动给 EVENT_DATA_DESCRIPTOR 结构赋值,但是通常使用 EventDataDescCreate 函数来对该结构进行初始化。

用法参考编写基于清单的事件。

5 Controllers 控制事件跟踪会话

控制事件跟踪会话由 Controllers 来完成,主要分为会话配置和启动。

5.1 配置会话属性

2.png

会话属性 EVENT_TRACE_PROPERTIES 配置时,要注意:

  • WNODE_HEADER 指定:
    • BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(KERNEL_LOGGER_NAME)。
    • Guid:
      • SystemTraceControlGuid,NT 内核记录器会话。
      • SystemTraceControlGuid 或 GlobalLoggerGuid,系统记录器。
    • ClientContext:
      • 1,查询性能计数器 (QPC) 。
      • 2,系统时间。
      • 3,CPU 周期计数器。
    • Flags:WNODE_FLAG_TRACED_GUID。
  • LogFileMode(指定日志输出的模式,输出到日志文件、缓冲区、实时输出):请参阅 日志记录模式常量。
  • EnableFlags:指示跟踪哪种类型的事件。EnableFlags 仅适用于系统记录器(以下属性都必须设置):
    • LogFileMode 包含 EVENT_TRACE_SYSTEM_LOGGER_MODE。
    • 会话名称 = KERNEL_LOGGER_NAME(已弃用 - 不应用于新代码,因为它与其他还尝试使用这些 GUID 的组件冲突)
    • WNODE_HEADER.Guid = SystemTraceControlGuid
    • WNODE_HEADER.Guid = GlobalLoggerGuid(已弃用 - 不应用于新代码,因为它与其他还尝试使用这些 GUID 的组件冲突)。
  • LoggerNameOffset:偏移量,会话名称的字符存储的位置的偏移(相对于 EVENT_TRACE_PROPERTIES 结构)。
    • 通常取值:sizeof(EVENT_TRACE_PROPERTIES)
  • LogFileNameOffset:偏移量,日志文件名称的字符存储的位置的偏移(相对于EVENT_TRACE_PROPERTIES结构)。
    • 日志文件名限制为 1024 个字符,LogFileMode = EVENT_TRACE_REAL_TIME_MODE 时,LogFileNameOffset = 0
    • 通常取值:sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME)

5.2 启用会话

StartTrace 函数注册并启动事件跟踪会话。

指定会话的属性后,调用 StartTrace 函数以启动会话。 如果函数成功, SessionHandle 参数将包含会话句柄, 而 LoggerNameOffset 属性将包含会话名称的偏移量。

1
2
3
4
5
ULONG WMIAPI StartTraceW(
[out] PTRACEHANDLE TraceHandle,
[in] LPCWSTR InstanceName,
[in, out] PEVENT_TRACE_PROPERTIES Properties
);

参数说明:

  • TraceHandle:接收会话的句柄,以便随后与 ControlTrace 等 API 一起使用。

  • InstanceName:包含事件跟踪会话名称的以 Null 结尾的字符串。 会话名称限制为 1024 个字符,不区分大小写,并且必须是唯一的。

    此函数将 InstanceName 指定的会话名称复制到 EVENT_TRACE_PROPERTIES.LoggerNameOffset 成员指向的偏移量的内存中。

  • Properties:指向指定会话行为的 EVENT_TRACE_PROPERTIES 结构的指针。 以下是要设置的结构的关键成员:

    • Wnode.BufferSize
    • Wnode.Guid
    • Wnode.ClientContext
    • Wnode.Flags
    • LogFileMode
    • LogFileNameOffset
    • LoggerNameOffset

返回值:

当函数 StartTraceW 开启会话之后,EVENT_TRACE_PROPERTIES 中的 OUT 成员就被赋值了。

不使用此函数启动全局记录器会话 (已弃用) 。 有关启动全局记录器会话的详细信息,请参阅 配置和启动全局记录器会话。

若要在收集事件后停止跟踪会话,请调用 ControlTrace 函数,并将 EVENT_TRACE_CONTROL_STOP 作为控制代码传递。

若要指定要停止的会话,可以将从之前调用 StartTrace 函数获取的事件跟踪会话句柄或之前启动的会话的名称传递。 在停止会话之前,请务必禁用所有提供程序。 如果在首次禁用提供程序之前停止会话,ETW 将禁用提供程序,并尝试调用提供程序的控制回调函数。 如果启动会话的应用程序在未禁用提供程序或调用 ControlTrace 函数的情况下结束,则提供程序将保持启用状态。

5.3 启用 Providers

  • MOF经典的Providers:使用函数 EnableTrace 。
  • 基于清单的 Providers:使用函数 EnableTraceEx 。
  • 会话筛选的Providers:调用函数 EnableTraceEx2。

实例:

5.4 停止会话

ControlTrace 函数刷新、查询、更新或停止指定的事件跟踪会话。

若要停止事件跟踪会话,请调用 ControlTrace 函数并将 ControlCode 参数设置为 EVENT_TRACE_CONTROL_STOP。

1
2
3
4
5
6
ULONG WMIAPI ControlTraceW(
[in] TRACEHANDLE TraceHandle,
[in] LPCWSTR InstanceName,
[in, out] PEVENT_TRACE_PROPERTIES Properties,
[in] ULONG ControlCode
);

参数说明:

  • TraceHandle:事件跟踪会话的句柄,或 0。如果 InstanceName 为 NULL,则必须指定非零 TraceHandle。 如果 InstanceName 不为 NULL,ETW 将忽略句柄。

  • InstanceName:事件跟踪会话的名称,或 NULL。 如果 TraceHandle 为 0,则必须指定 InstanceName。

    若要指定 NT 内核记录器会话,请将 InstanceName 设置为 KERNEL_LOGGER_NAME。

  • Properties:指向初始化 EVENT_TRACE_PROPERTIES 结构的指针。在设置任何字段之前,此结构应为零。

    该参数中传递给 ControlTraceW 函数前成员的设置,取决于参数 ControlCode。

    1. 如果 ControlCode 指定EVENT_TRACE_CONTROL_STOP、EVENT_TRACE_CONTROL_QUERY或EVENT_TRACE_CONTROL_FLUSH,则只需设置EVENT_TRACE_PROPERTIES结构的 Wnode.BufferSize、Wnode.Guid、LoggerNameOffset 和 LogFileNameOffset 成员。
    2. 如果 ControlCode 在输入中指定 EVENT_TRACE_CONTROL_UPDATE,则成员必须为要更新的属性指定新值。 在输出中, 属性 包含事件跟踪会话的属性和统计信息。 可以更新以下属性:EnableFlags、FlushTimer、LogFileNameOffset、LogFileMode、MaximumBuffers。
  • ControlCode:请求的控制函数。 可以指定以下值之一:

    • EVENT_TRACE_CONTROL_FLUSH:刷新会话的活动缓冲区。

      这可与内存中会话一起使用, (从 EVENT_TRACE_BUFFERING_MODE 标志开始的会话) ,以便将数据从跟踪写入文件。

      通常不需要刷新基于文件的会话或实时会话,因为 ETW 会在缓冲区满 (时自动刷新缓冲区,即当它没有下一个事件空间) 、跟踪会话的 FlushTimer 过期或跟踪会话关闭时。

      Windows 2000: 不支持此值。

    • EVENT_TRACE_CONTROL_QUERY:检索会话属性和统计信息。

    • EVENT_TRACE_CONTROL_STOP:停止会话。 会话句柄不再有效。

    • EVENT_TRACE_CONTROL_UPDATE:汇报会话属性。

    • EVENT_TRACE_CONTROL_INCREMENT_FILE:如果会话具有 EVENT_TRACE_FILE_MODE_NEWFILE,请更新会话以立即切换到下一个文件,而不是等待前一个文件填充。 从Windows 10 2018 年 10 月更新开始支持。

    • EVENT_TRACE_CONTROL_CONVERT_TO_REALTIME:将文件模式会话更改为实时会话 (启用实时传送,并禁用将事件写入 ETL 文件) 。 从 2020 年 10 月更新Windows 10开始支持。

注意:DllMain 中刷新缓冲区或停止 (跟踪会话可能会导致死锁) 是不安全的。

返回值:

5.4 系统记录器(会话)

参考 StartTraceW 函数。下面所说的记录器(Logger)实际上就是会话。

要使记录器成为系统记录器并从 SystemTraceProvider 或其他 系统提供程序接收事件,以下任一项都必须为 true:

  • Properties.LogFileMode = EVENT_TRACE_SYSTEM_LOGGER_MODE。
  • 属性成员 Wnode.Guid 设置为 SystemTraceControlGuid 或 GlobalLoggerGuid (已弃用 - 不应用于新代码,因为它与其他还尝试使用这些 GUID 的组件冲突) 。
  • InstanceName 设置为 KERNEL_LOGGER_NAME (弃用 - 不应用于新代码,因为它与其他组件冲突,这些组件也会尝试使用此名称) 。

系统记录器必须设置EVENT_TRACE_PROPERTIES结构的 EnableFlags 成员,以指示跟踪中应包含哪些 SystemTraceProvider 事件。

由于系统记录器接收特殊内核事件,因此它们受到其他限制:

  • 同一系统上的系统记录器不能超过 8 个。
  • 无法在 Windows Server 容器中创建系统记录器。
  • 系统记录器不能使用 EVENT_TRACE_USE_PAGED_MEMORY 标志。
  • 在 Windows 10 版本 1703 之前,任何系统记录器都可以同时使用 2 个不同的时钟类型。 例如,如果一个活动系统记录器使用“CPU 周期计数器”时钟类型,而另一个活动系统记录器使用的是“查询性能计数器”时钟类型,则任何使用“系统时间”时钟类型启动系统记录器的尝试都将失败,因为它需要激活第三个时钟类型。 由于此限制,Microsoft 强烈建议系统记录器不使用“系统时间”时钟类型。
  • 从 Windows 10 版本 1703 开始,已删除时钟类型限制。 所有三种时钟类型现在都可以由系统记录器同时使用。

若要指定 NT 内核记录器会话 (已弃用) ,请将 InstanceName 设置为 KERNEL_LOGGER_NAME,并将属性的 Wnode.Guid成员设置为 SystemTraceControlGuid。 如果未将 GUID 指定为 SystemTraceControlGuid,ETW 将重写 GUID 值并将其设置为 SystemTraceControlGuid。

Windows 2000: 若要启动内核会话,会话名称必须 KERNEL_LOGGER_NAME ,GUID 必须是 SystemTraceControlGuid。

5.5 配置和启用各类会话

6 Consumers 消费事件

ETW 消耗器在接收 ETW 消息前,必须使用 OpenTrace 打开一个 etl 文件(文件模式) 或实时的 ETW 会话(实时模式),同时注册自己用于接收消息的回调函数。

Consumers 可以从日志文件或实时处理事件,处理模式由 EVENT_TRACE_LOGFILEA.LogFileName/LoggerName 来指定。

Consumers 消费事件大致过程:

  1. OpenTrace 打开会话,获得会话句柄 TRACEHANDLE;
  2. ProcessTrace 从会话将事件传递给消费者;
  3. 使用CloseTrace 函数关闭使用 OpenTrace 打开的跟踪处理会话。
  4. EventRecordCallback/EventCallback/BufferCallback 接收事件。

3.png

注意:ProcessTrace 函数会让线程线程阻塞,直到它传递所有事件、BufferCallback 函数返回 FALSE 或调用 CloseTrace。 此外,如果使用者实时消耗事件, 则 ProcessTrace 函数会在控制器停止跟踪会话后返回。 (请注意,函数返回前几秒可能会有延迟。)

6.1 OpenTraceW 打开会话

OpenTrace 函数打开指定 ETW 跟踪会话,调用成功则会返回该会话句柄。OpenTrace 函数可以处理一个或多个日志文件中的事件,但只能处理一个实时会话中的事件。

PEVENT_TRACE_LOGFILEW 指定要打开的会话,以及指定 Consumers 处理事件的回调函数。

1
2
3
ETW_APP_DECLSPEC_DEPRECATED TRACEHANDLE WMIAPI OpenTraceW(
[in, out] PEVENT_TRACE_LOGFILEW Logfile // IN、OUT 说明该结构有输入成员也有输出成员
);

EVENT_TRACE_LOGFILE 结构存储有关跟踪数据源的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef struct _EVENT_TRACE_LOGFILEW {
LPWSTR LogFileName; // 如果使用文件模式,则指向 ETL 文件完整路径名称(包括后缀);实时会话为 NULL
// 调用 OpenTrace 时,使用事件的用户必须具有读取文件的权限
LPWSTR LoggerName; // 如果使用实时模式,则指向 ETW 控制器定义的会话名称
// LogFileName、LoggerName 只能二选一
LONGLONG CurrentTime; //【OUT 输出值】,当前时间
ULONG BuffersRead; //【OUT 输出值】,已读缓冲区个数
union {
ULONG LogFileMode; // 保留,请勿使用
ULONG ProcessTraceMode; // 用于处理事件的模式。PROCESS_TRACE_MODE_EVENT_RECORD、PROCESS_TRACE_MODE_REAL_TIME
} DUMMYUNIONNAME;
EVENT_TRACE CurrentEvent; //【OUT 输出值】,最后一个已处理事件的 EVENT_TRACE 结构
TRACE_LOGFILE_HEADER LogfileHeader; //【OUT 输出值】,ETL 文件信息
PEVENT_TRACE_BUFFER_CALLBACKW BufferCallback; // 回调函数用来接收 ETW递送缓冲区中事件的统计信息
// ETW 在传递每个缓冲区的事件后调用此函数
ULONG BufferSize; //【OUT 输出值】,每个缓冲区的大小(以字节为单位)
ULONG Filled; //【OUT 输出值】,缓冲区中包含的有效信息字节数
ULONG EventsLost; // 未使用
union {
PEVENT_CALLBACK EventCallback; // ProcessTraceMode != PROCESS_TRACE_MODE_EVENT_RECORD 时才使用该回调函数
// 不应在新代码中使用此回调,应使用 EventRecordCallback
PEVENT_RECORD_CALLBACK EventRecordCallback; // 仅当 ProcessTraceMode == PROCESS_TRACE_MODE_EVENT_RECORD 时才使用该回调函数
} DUMMYUNIONNAME2;
ULONG IsKernelTrace; //【OUT 输出值】,如果为 TRUE 则为 NT 内核记录器(NT Kernel Logger)
PVOID Context; // 如果使用者使用 EventRecordCallback 来使用事件,
// ETW 会将 EVENT_RECORD 结构的 UserContext 成员设置为此值(将该值传给回调函数)
} EVENT_TRACE_LOGFILEW, *PEVENT_TRACE_LOGFILEW;

注意:

  1. 如果 LoggerName != NULL,则必须 LogFileName == NULL,且需要指定 ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME。
  2. 如果使用建议的 EventRecordCallback 回调函数,则 EVENT_TRACE_LOGFILEW.Context 指向传递给该回调函数的参数,且 ProcessTraceMode 必须要包含 PROCESS_TRACE_MODE_EVENT_RECORD。
  3. ProcessTrace 将为每个事件,调用事件回调函数。

6.2 ProcessTrace 传递事件

ProcessTrace 函数将事件从一个或多个 ETW 会话传递给 Consumer。

在成功调用 ProcessTrace 后,如果在指定的 ETW 会话中有输出事件,那么系统便会调用消耗器注册的回调函数来处理事件。

1
2
3
4
5
6
7
ETW_APP_DECLSPEC_DEPRECATED ULONG WMIAPI ProcessTrace(
[in] PTRACEHANDLE HandleArray,// 要处理事件的会话句柄数组。数组最多可以包含 64 个文件处理会话句柄,也可以包含一个实时处理会话的句柄
// 数组不能同时包含文件处理会话句柄和实时处理会话句柄
[in] ULONG HandleCount,// HandleArray 数组元素的个数
[in] LPFILETIME StartTime, // 指向可选 FILETIME 结构的指针,该结构指定要接收事件的开始时间段
[in] LPFILETIME EndTime // 可选,该结构指定要接收事件的结束时间段
);

返回值:如果函数成功,则返回值为 ERROR_SUCCESS。

注意:ProcessTrace 函数会让线程线程阻塞,直到它传递所有事件、BufferCallback 函数返回 FALSE 或调用 CloseTrace。 此外,如果使用者实时消耗事件, 则 ProcessTrace 函数会在控制器停止跟踪会话后返回。 (请注意,函数返回前几秒可能会有延迟。)

所以可以在调用该函数之后直接使用 CloseTrace 函数:

1
2
3
4
5
6
...   
// 开始处理事件,该函数为阻塞函数,在这里不断调用事件处理函数处理事件,直到session关闭
ProcessTrace(&handle, 1, 0, 0);

CloseTrace(handle);
return 0;

注意:

  1. ProcessTrace 函数通过调用 Consumer 的 BufferCallback、EventCallback 和 EventRecordCallback 回调函数从会话传递事件。
  2. ProcessTrace 函数尝试根据事件的时间戳的顺序传递事件。 在某些情况下, ProcessTrace 可能会按顺序传递事件。

7 两个特殊会话

  1. 全局记录器会话(Global Logger Session),它用来记录操作系统早期启动过程中的事件,例如设备驱动相关的事件。
  2. NT 内核记录器会话(NT Kernel Logger Session),它用来记录操作系统生成的预定义系统事件,例如磁盘 IO 或页面错误事件。

ETW 应用:

8 ETW 总结

事件可以存储在日志文件、循环缓冲区、或者实时消费。

四种不同的提供事件的方法:

4.png

  1. 控制器、提供者、消费者等个组件相关 API 所在位置如下。

    6.png

  2. 创建全局会话时,调用 Sechost!StartTrace->Sechost!EtwpStartLogger->ntdll!NtTraceControl 进入内核后创建一个WMI_LOGGER_CONTEXT 结构来描述会话,同时会生成一个会话 GUID。所有的会话是一个数组,数组每个元素成员都是WMI_LOGGER_CONTEXT 结构。

    会话 Session 对应 WMI_LOGGER_CONTEXT 结构。

  3. 注册提供者。每个提供者对应一个 _ETW_GUID_ENTRY 结构。该数据结构包含重要信息,如提供者 GUID、安全描述符、引用计数器、启用信息(针对启用提供者的每个 ETW 会话)和提供者注册列表。对于用户模式发起的注册提供者,需要验证 TRACELOG_REGISTER_GUIDS 访问权限。

    提供者 Provider 对应 _ETW_GUID_ENTRY 结构。

  4. 提供者成功添加到全局会话列表后,ETW 创建并初始化一个 ETW 注册对象,该对象表示一次注册。 该对象封装了一个 ETW_REG_ENTRY 数据结构,它将提供者与请求其注册的进程和会话联系起来。

    一个提供者可以被多个进程注册使用,每一个进程注册一次,就会生成一个注册对象 ETW_REG_ENTRY。

    5.png

  5. ETW Logger thread。

    Logger 线程是 ETW 中最重要的实体之一。 它的主要目的是将事件刷新到日志文件或将它们传递给实时消费者,跟踪传递和丢失事件的数量。 每次最初创建 ETW 会话时都会启动记录器线程,但前提是会话不使用循环日志模式。 它的执行逻辑很简单。 启动后,它会将自己链接到表示关联 ETW 会话的 _WMI_LOGGER_CONTEXT.LoggerThread 成员,并等待两个主要的同步对象。 每次属于会话的缓冲区变满时,ETW 都会发出 Flush 事件信号(这可能发生在提供者生成新事件之后——例如,如前一节“提供事件”中所讨论的),当一个新的实时消费者已请求连接,或者当记录器会话将要停止时。只有在会话是实时会话或用户在调用 StartTrace APl 以创建新会话时已明确要求它。

    当两个同步对象之一收到信号时,记录器线程重新装备它们并检查文件系统是否准备就绪。 如果不是,则主记录器线程再次返回睡眠状态(在早期引导阶段不应刷新任何会话)。 否则,它开始将属于会话的每个缓冲区刷新到日志文件或实时消费者。

    对于实时会话,记录器线程首先在 %SystemRoot%\System32\LogFiles\WMI\RtBackup 文件夹中创建一个临时的 per-session ETL 文件(如图10-35所示)。日志文件名是通过添加 EtwRT 生成的 实时会话名称的前缀。 该文件用于在将临时事件传递给实时消费者之前保存它们(日志文件还可以存储在适当的时间范围内未传递给消费者的丢失事件)。 启动时,实时自动记录器会从日志文件中恢复丢失的事件,目的是将它们传递给消费者。

    记录器线程是唯一能够在实时消费者和会话之间建立连接的实体。

  6. System loggers。

    有两个主要的系统记录器可用(提供者由系统提供注册,使用时启用即可),分别是 NT 内核记录器和循环内核上下文记录器(而全局记录器是 NT 内核记录器的子集)。 NT 内核最多支持八个系统记录器会话。 从系统记录器接收事件的每个会话都被视为系统会话。系统会话需要指定以下其一属性:

    • Properties.LogFileMode = EVENT_TRACE_SYSTEM_LOGGER_MODE。
    • 属性成员 Wnode.Guid 设置为 SystemTraceControlGuid 或 GlobalLoggerGuid (已弃用 - 不应用于新代码,因为它与其他还尝试使用这些 GUID 的组件冲突) 。

    下图列出了系统记录器及其 GUID。 内核中的 EtwpStartLogger 函数识别标志或特殊 GUID,并对 NT kernellogger 安全描述符执行额外检查。

    7.png

  7. ETW security。

    启动和停止 ETW 会话被视为高权限操作,因为事件可以包括可用于利用系统完整性的系统数据(这对于系统日志记录器尤其如此)。 Windows 安全模型已扩展为支持 ETW 安全。 如前几节所述,ETW 执行的每个操作都需要定义明确的访问权限,该访问权限必须由保护会话、提供程序或提供程序组(取决于操作)的安全描述符授予。 表 10-18 列出了为 ETW 引入的所有新访问权限及其用法。

    8.png

    ETW 所有安全描述符都以二进制格式存储在 HKLM\System\CurrentControlSet\Control\Wmi\Security 注册表项中。 在 ETW 中,GUID 表示的所有内容都可以通过自定义的安全描述符进行保护。 为了管理 ETW 安全,应用程序通常不直接与存储在注册表中的安全描述符交互,而是使用在 Sechost.dll 中实现的 EventAccessControl 和 EventAccessQuery API。

    EtwpStartLogger 函数对会话的安全描述符执行访问检查,如使用当前进程的访问令牌请求 TRACELOG_GUID_ENABLE 访问权限。

参考: