references:
-
https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/wpp-software-tracing
-
https://github.com/Microsoft/Windows-driver-samples/tree/main/general/tracing/tracedriver
约定:
- TMF --> trace message function
本章我们将会介绍如何使用WPP
WPP的全称为Windows software trace PreProcessor
他可以用来跟踪trace provider的操作,一个trace provider可以是一个内核驱动,也可以是一个用户模式的软件
WPP是WMI event tracing的补充和增强,他简化了追踪trace provider操作的方式
WPP非常高效,可以实时记录二进制日志数据,并且可以转换为human-readable trace log
WPP和ETW都可以用来搜集调试信息,两者的区别在于前者可以保证我们的调试信息不被用户看到,因为WPP产生的二进制日志文件需要使用TMF文件来进行解码,而TMF是通过PDB文件生成的,正常情况下我们是不会发布PDB文件的
WPP软件跟踪过程
WPP的基本过程可以分为下面这几步:
- 定义control GUID,用于唯一标识一个trace provider
- 添加WPP相关的C预处理指令和WPP宏来调用源文件中的函数
- 修改VS项目配置以启动WPP预处理
- 安装驱动,开启trace session并记录消息
需要注意的是,并不是所有的驱动类型都可以使用WPP,这一点可以通过在DriverEntry中加入WPP宏WPP_INIT_TRACING
然后编译代码来进行验证,如果编译通不过,说明该类型的驱动不受WPP支持
WPP trace provider同一时刻只能存在于一个trace session中
在驱动代码中使用WPP
我们需要对原本的驱动代码进行更改,这里我们使用Windows File System Filter Driver中的代码来演示
如果使用了VS提供的KMDF模板,可以知己跳到第五步
第一步,定义control GUID和trace flag
一般的做法是把WPP相关的宏定义放到一个单独的头文件中,然后在所有需要用到WPP trace的源文件中包含这个头文件
wpp.h:
#define WPP_CONTROL_GUIDS \
WPP_DEFINE_CONTROL_GUID( \
myDriverTraceGuid, (84bdb2e9,829e,41b3,b891,02f454bc2bd7), \
WPP_DEFINE_BIT(MYDRIVER_ALL_INFO) /* bit 0 = 0x00000001 */ \
WPP_DEFINE_BIT(TRACE_DRIVER) /* bit 1 = 0x00000002 */ \
WPP_DEFINE_BIT(TRACE_DEVICE) /* bit 2 = 0x00000004 */ \
WPP_DEFINE_BIT(TRACE_QUEUE) /* bit 3 = 0x00000008 */ \
)
可以看到trace flag的值是2的次幂,从0开始
在使用TMF的时候,我们会带上trace flag,tracelog.exe可以通过指定trace flag来控制正在运行的驱动中哪些TMF会被执行
形式如下:
DoTraceMessage(TRACE_DRIVER, "Hello World!\n");
当tracelog.exe通过-flag
选项指定flag为2的时候,这条语句就会执行,对应的message就会被记录下来
第二步,选择一个函数作为trace message function
所谓trace message function就和dbgprint差不多,就是用来写调试信息的
微软提供了一个默认的函数,DoTraceMessage宏,如果你使用这个默认函数,那么就不需要再定义WPP_LEVEL_ENABLED和WPP_LEVEL_LOGGER宏了
如果你使用自定义的函数,或者转换现存的函数,那么就需要进行以下操作
-
比如你想把KdPrint函数转换成TMF,那么你需要定义WPP宏来标识并启用这个函数
-
定义WPP宏来启用这个函数,每一个TMF都必须要有一对宏定义,这对宏用于标识trace provider并指定在什么条件下会产生trace message,形式如下:
c WPP_<condition>_LOGGER WPP_<condition>_ENABLED
这里的<condition>
就是你的TMF的参数列表,比如你的TMF签名为:
DoTraceLevelMessage(LEVEL, FLAGS, MESSAGE, ...);
那么对应的宏就应该长下面这个样子
#define WPP_LEVEL_FLAGS_LOGGER
#define WPP_LEVEL_FLAGS_ENABLED
默认的TMF是DoTraceMessage:
void DoTraceMessage(
TraceFlagName,
Message,
[ VariableList ]
);
它对应的宏就是
WPP_LEVEL_ENABLED
WPP_LEVEL_LOGGER
这就是遗留问题了,虽然DoTraceMessage支持的是trace flag,但是他这里宏定义上写的却是LEVEL,而且实际他也是不支持LEVEL的,而是支持trace flag
如果我们使用上面提到的DoTraceLevelMessage函数作为我们的TMF,那么最终的宏定义应该为:
#define WPP_LEVEL_FLAGS_LOGGER(lvl,flags) \
WPP_LEVEL_LOGGER(flags)
#define WPP_LEVEL_FLAGS_ENABLED(lvl, flags) \
(WPP_LEVEL_ENABLED(flags) && WPP_CONTROL(WPP_BIT_ ## flags).Level >= lvl)
只有在达到指定等级之后,才会启用loggger
下面我们需要使用begin_wpp
和end_wpp
来声明我们的TMF,你没看错,他就是被注释掉的,因为这个不是给编译器用的,是给WPP预处理器用的配置
DTLM是DoTraceLevelMessage的缩写
// begin_wpp config
// FUNC DTLM(LEVEL, FLAGS, MSG, ...);
// end_wpp
可以直接将KdPrint转换成TMF,如下:
// begin_wpp config
// FUNC KdPrint{LEVEL=TRACE_LEVEL_INFORMATION, FLAGS=TRACE_DRIVER}((MSG, ...));
// end_wpp
只有在TRACE_LEVEL_INFORMATION等级和TRACE_DRIVER标志启用时,才会记录日志,此时的KdPrint将不再直接输出到DbgView中,而是记录到日志中
我们也可以直接将KdPrintEx转换为我们的TMF
// begin_wpp config
// FUNC KdPrintEx((Flags, LEVEL, MSG, ...));
// end_wpp
不过KdPrintEx和KdPrint不太一样,多了几个参数
DbgPrintEx (
_In_ ULONG ComponentId,
_In_ ULONG Level,
_In_z_ _Printf_format_string_ PCSTR Format,
...
);
因此我们要在WPP_CONTROL_GUIDS
定义一个ComponentId的BIT,不然tracelog也没法用,对应的WPP宏对有需要更改,FLAGS跑到了LEVEL前面
#define WPP_CONTROL_GUIDS \
WPP_DEFINE_CONTROL_GUID(GUIDFriendlyName, (DE1AB18A, 8A47, 43FB, B3BB, E503D293A33F), \
WPP_DEFINE_BIT(DPFLTR_IHVDRIVER_ID) )
#define WPP_FLAGS_LEVEL_LOGGER(flags,lvl) \
WPP_LEVEL_LOGGER(flags)
#define WPP_FLAGS_LEVEL_ENABLED(flags,lvl) \
(WPP_LEVEL_ENABLED(flags) && WPP_CONTROL(WPP_BIT_ ## flags).Level >= lvl)
第三步,包含头文件wpp.h和自动生成的*.tmh到会用到trace的源文件中
准确来说*.tmh文件还没有生成,只有在我们build驱动的时候才会生出来
build之前需要先在项目属性中开启WPP
我们有多少个.c文件,就会有多少个.tmh文件被生出来,WPP预处理器为每一个源文件都生成了一个.tmh文件
第四步,初始化和clean up WPP
分别对应WPP_INIT_TRACING和WPP_CLEANUP
WPP_INIT_TRACING( DriverObject, RegistryPath );
WPP_CLEANUP(DriverObject);
前者放在DriverEntry里,后者放在Unload里
当然了,如果遇到意外程序提前结束了,也要在退出时先CLEANUP
测试
extra
#define WPP_CONTROL_GUIDS \
WPP_DEFINE_CONTROL_GUID(GUIDFriendlyName, (DE1AB18A, 8A47, 43FB, B3BB, E503D293A33F), \
WPP_DEFINE_BIT(FOR_DEBUG) \
WPP_DEFINE_BIT(FOR_FILTER_FILENAME) \
WPP_DEFINE_BIT(FOR_DRIVER_STATUS) )
这里的BIT我们可以随便定义,不一定非要和KdPrintEx的ComponentID保持一致,这样并不影响KdPrintEx正常工作,而且可以通过-flag选项控制trace log输出
将tracelog的-flag选项分别设置为1、2、4可以控制不同类型的trace log
完美!
项目文件
TraceView
TraceView还支持过滤器,我们可以设置过滤器来过滤掉不想看到的trace message
这里我们discard掉了所有不包含fsfilter
字符串的message(不区分大小写),可以看到捕获的文件名并没有显示在TraceView中
TraceViewPlus
推荐使用TraceVIewPlus,用了都说好,界面看着舒服,输出也能很方便导出,直接在这里添加pdb文件,然后启动即可