Windows软件调试初探-事件追踪ETW

入门

ETW使用提供器-消耗器-控制器设计模式。输出追踪消息的目标程序是提供器,接收和查看追踪消息的工具或文件是消耗器,负责控制追踪会话的工具是控制器。用于支持ETW传输追踪信息的通信连接称为ETW会话,Windows最多支持64个ETW会话,其中有两个是系统专用的,分别是启动早期用的Global Logger Session和记录系统预定义事件的NT Kernel Logger Session。

ETW基础设施核心部分是实现在内核模块的,作为WMI一个部分实现。有一部分函数在DDK中公开,供驱动调用,用户模式API在ADVAPI32.DLL输出。

1
x nt!Wmi*trace*

提供ETW消息

ETW提供器通过RegisterTraceGuids向系统注册自己的GUID,这样ETW控制器才能通过GUID找到该提供器。若ETW控制器启动或停止该提供器,则系统调用回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
#include <evntrace.h>
#define RegisterTraceGuids RegisterTraceGuidsW
EXTERN_C ULONG WMIAPI RegisterTraceGuidsW (
_In_ WMIDPREQUEST RequestAddress, //回调函数
_In_opt_ PVOID RequestContext, //回调函数参数
_In_ LPCGUID ControlGuid, //提供器GUID
_In_ ULONG GuidCount, //TraceGuidReg数组元素个数
_In_reads_opt_(GuidCount) PTRACE_GUID_REGISTRATION TraceGuidReg, //表示追踪事件GUID的数组
_In_opt_ LPCWSTR MofImagePath, //NULL
_In_opt_ LPCWSTR MofResourceName, //NULL
_Out_ PTRACEHANDLE RegistrationHandle //返回句柄
);

一个典型ETW提供器回调函数:

1
2
3
4
5
6
7
8
9
10
ULONG WINAPI MyControlCallback(WMIDPREQUESTCODE RequestCode,PVOID Context,ULONG* Reserved,PVOID Buffer){
if(RequestCode==WMI_ENABLE_EVENTS){ //启用追踪
g_hTrace=GetTraceLoggerHandle(Buffer); //获取会话句柄
g_dwFlags=GetTraceEnableFlags(Buffer); //读取控制器设置的启用标志
g_dwLevel=GetTraceEnableLevel(Buffer); //读取控制器设置的启用级别
}
else if(RequestCode==WMI_DISABLE_EVENTS) //禁止追踪
g_hTrace=NULL;
return 0;
};

有了ETW会话句柄便可用TraceEventTraceMessageTraceMessageVa向ETW会话输出信息,他们都调用NtTraceEvent内核服务,后者又调用ETW内核函数WmiTraceMessage

1
2
3
4
5
6
7
8
9
10
11
EXTERN_C ULONG WMIAPI TraceEvent (
_In_ TRACEHANDLE TraceHandle, //追踪会话句柄
_In_ PEVENT_TRACE_HEADER EventTrace //追踪信息的缓冲区
);
EXTERN_C ULONG __cdecl TraceMessage (
_In_ TRACEHANDLE LoggerHandle,
_In_ ULONG MessageFlags, //消息标志
_In_ LPCGUID MessageGuid, //消息所属分类的GUID
_In_ USHORT MessageNumber, //消息编号
... //消息负载数据
);

一个组织缓冲区、填写头结构并调用TraceEvent的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _MyEvent{
EVENT_TRACE_HEADER m_Header; //标准追踪信息头
ULONG m_ulDataInfo; //要输出的事件数据
}MyEvent;
MyEvent e;
e.m_Header.Size=sizeof(e);
e.m_Header.Guid=MyEventGUID; //所属事件类别GUID
e.m_Header.Class.Type=uType; //事件类型
e.m_Header.Flags=WNODE_FLAG_TRACED_GUID; //标志选项
e.m_Header.Level=TRACE_LEVEL_INFORMATION; //事件重要级别
e.m_uDataInfo=uVarValue; //事件负载数据
Status=TraceEvent(g_hTrace,(PEVENT_TRACE_HEADER)&e); //发送消息

控制ETW会话

ETW控制器程序用StartTrace开始一个ETW追踪会话,并取得该会话的信息和句柄。

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
28
29
30
#define StartTrace              StartTraceW
EXTERN_C ULONG WMIAPI StartTraceW (
_Out_ PTRACEHANDLE TraceHandle,
_In_ LPCWSTR InstanceName, //ETW会话名
_Inout_ PEVENT_TRACE_PROPERTIES Properties //会话选项
);
typedef struct _EVENT_TRACE_PROPERTIES {
WNODE_HEADER Wnode; //头架构 包含提供器的GUID值等属性
ULONG BufferSize; //缓冲区大小
ULONG MinimumBuffers; //缓冲区最小个数
ULONG MaximumBuffers; //缓冲区最大个数
ULONG MaximumFileSize; //追踪文件的最大容量
ULONG LogFileMode; //ETW消息传递模式 文件、实时等
ULONG FlushTimer; //多久冲转缓冲区一次 单位秒
ULONG EnableFlags; //要输出的事件
union {
LONG AgeLimit; //废弃
LONG FlushThreshold; // Number of buffers to fill before flushing
} DUMMYUNIONNAME;
//以下为输出
ULONG NumberOfBuffers; //已分配缓冲区数
ULONG FreeBuffers; //分配但未使用的缓冲区数
ULONG EventsLost; //丢失未能记录的事件个数
ULONG BuffersWritten; //已写缓冲区个数
ULONG LogBuffersLost; //文件模式未能写入日志的缓冲区数
ULONG RealTimeBuffersLost; //实时模式未能发送给消耗器的缓冲区数
HANDLE LoggerThreadId; //ETW提供器线程ID
ULONG LogFileNameOffset; //.etl追踪文件名偏移量 0不使用
ULONG LoggerNameOffset; //ETW会话名称偏移量
} EVENT_TRACE_PROPERTIES, *PEVENT_TRACE_PROPERTIES;

消息投递模式又文件模式和实时模式。前者将ETW消息写入ETL文件,此时EVENT_TRACE_PROPERTIES后应放置完整路径文件名。后者实时将ETW消息递送给ETW消耗器。

EnableTrace启动ETW提供器,使其向该ETW会话输出追踪消息:

1
2
3
4
5
6
7
EXTERN_C ULONG WMIAPI EnableTrace (
_In_ ULONG Enable, //TRUE启动 否则停止ETW提供器
_In_ ULONG EnableFlag, //启动标志
_In_ ULONG EnableLevel, //追踪消息级别
_In_ LPCGUID ControlGuid, //ETW提供器GUID
_In_ TRACEHANDLE TraceHandle //ETW会话句柄
);

启动会话后ETW控制器用ControlTrace查询ETW会话状态或执行其他控制动作:

1
2
3
4
5
6
EXTERN_C ULONG WMIAPI ControlTraceW (
_In_ TRACEHANDLE TraceHandle, //会话句柄
_In_opt_ LPCWSTR InstanceName, //会话名称
_Inout_ PEVENT_TRACE_PROPERTIES Properties, //属性结构
_In_ ULONG ControlCode //控制代码
);

TraceHandle和InstanceName选一个就行。ControlCode如下:

含义
EVENT_TRACE_CONTROL_FLUSH 冲转会话缓冲区
EVENT_TRACE_CONTROL_QUERY 读取会话属性和统计信息
EVENT_TRACE_CONTROL_STOP 停止会话
EVENT_TRAFCE_CONTROL_UPDATE 更新会话属性

ETW控制器还可用QueryAllTraces查询当前系统内启动的所有ETW会话,用EnumerateTraceGuids枚举系统内注册的所有ETW提供器。

消耗ETW消息

ETW消耗器用OpenTrace打开一个ETL文件或实时ETW会话,同时注册自己用于接收消息的回调函数:

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
28
29
EXTERN_C ETW_APP_DECLSPEC_DEPRECATED TRACEHANDLE WMIAPI OpenTraceW (
_Inout_ PEVENT_TRACE_LOGFILEW Logfile
);
typedef struct _EVENT_TRACE_LOGFILEW EVENT_TRACE_LOGFILEW, *PEVENT_TRACE_LOGFILEW;
struct _EVENT_TRACE_LOGFILEW {
LPWSTR LogFileName; //文件模式下为ETL文件名 否则NULL
LPWSTR LoggerName; //实时模式下为ETW控制器定义的会话名称
//下面是输出
LONGLONG CurrentTime; //当前时间
ULONG BuffersRead; //已读缓冲区数
union {
ULONG LogFileMode; //输入 消息递送方式
ULONG ProcessTraceMode; // Processing flags used on Vista and above
} DUMMYUNIONNAME;
EVENT_TRACE CurrentEvent; //指向被处理的最后一个时间EVENT_TRACE结构
TRACE_LOGFILE_HEADER LogfileHeader; // logfile header structure
PEVENT_TRACE_BUFFER_CALLBACKW BufferCallback; //输入 要注册的BufferCallback函数 可选
// following variables are filled for BufferCallback.
ULONG BufferSize; //每个ETW会话缓冲区大小
ULONG Filled; //缓冲区中包含的有效信息字节数
ULONG EventsLost; //未使用
// following needs to be propagated to each buffer
union {
PEVENT_CALLBACK EventCallback; //输入 要注册的EventCallback函数
PEVENT_RECORD_CALLBACK EventRecordCallback; // Callback with EVENT_RECORD on Vista and above
} DUMMYUNIONNAME2;
ULONG IsKernelTrace; //ETW会话为NT Kernel Logger为1 否则0
PVOID Context; //保留
};

之后ETW消耗器用ProcessTrace通知系统启动消息递送:

1
2
3
4
5
6
EXTERN_C ETW_APP_DECLSPEC_DEPRECATED ULONG WMIAPI ProcessTrace (
_In_reads_(HandleCount) PTRACEHANDLE HandleArray, //会话句柄数组
_In_ ULONG HandleCount, //HandleArray数组元素个数
_In_opt_ LPFILETIME StartTime, //开始接收事件的时间
_In_opt_ LPFILETIME EndTime //结束接收事件的时间
);

ETW消耗器可注册BufferCallback、EventCallback和EventClassCallback回调,第一个用于接收会话缓冲区统计信息,后两个用来接收追踪事件。

1
2
3
4
typedef VOID (WINAPI *PEVENT_CALLBACK)(
PEVENT_TRACE pEvent
);
typedef ULONG (WINAPI * PEVENT_TRACE_BUFFER_CALLBACKW)(PEVENT_TRACE_LOGFILEW Logfile);

系统将指定的所有会话的所有事件都发给EventCallback,ETW允许用SetTraceCallback通知系统按事件所属分类发给不同回调函数,此时EventCallback也会收到所有事件:

1
2
3
4
EXTERN_C ULONG WMIAPI SetTraceCallback (
_In_ LPCGUID pGuid, //一类事件GUID
_In_ PEVENT_CALLBACK EventCallback //接收该类事件的回调函数
);