references:
-
https://learn.microsoft.com/EN-US/windows-hardware/drivers/devtest/event-tracing-for-windows--etw
-
https://github.com/microsoft/Windows-driver-samples/tree/main/general/tracing/evntdrv
-
https://learn.microsoft.com/en-us/windows/win32/wes/writing-an-instrumentation-manifest
约定:
- IMF ----> instrumentation manifest
ETW全称为Event Tracing for Windows
ETW中有三个重要的概念:
Term | Description |
---|---|
Providers | Applications or components that raise event tracing instrumentation. |
Controllers | Applications that start, stop, and configure event tracing sessions. |
Consumers | Applications that receive event tracing sessions (in real time) or from a file. |
即提供者、控制器、消费者,其实很好理解,提供者负责产生事件,控制器负责控制会话的开启和关闭吗,消费者接受提供者产生的事件
ETW提供了内核API,之前讲到的WPP就使用了ETW
驱动代码可以调用这些API来将自己注册成为一个提供者,产生的日志可以被消费者实时接收或者写入文件
和WPP一样,ETW也可以动态启停,对程序性能的影响非常小
在驱动程序中使用ETW
我们这里还是使用Windows File System Filter Driver中的代码来演示
其实最麻烦的就是写定义xml,下面着重讲一下
IMF就是一个XML文件,里面定义了一堆ETW相关概念的东西,通常以.man
作为文件后缀
定义provider
provider标签的5个属性:
- name
- guid
- resourceFileName
- messageFileName
- symbol
这5个属性必须赋值,语言本地化我们不用管,毕竟我也不是专门开发软件的😏
在一个man文件中可以写16个provider标签,如果多于16个,也可以定义,具体参考微软文档
<provider name="Microsoft-Windows-SampleProvider"
guid="{1db28f2e-8f80-4027-8c5a-a11f7f10f62d}"
symbol="PROVIDER_GUID"
resourceFileName="<path to the exe or dll that contains the metadata resources>"
messageFileName="<path to the exe or dll that contains the string resources>"
</provider>
定义频道
使用channel和importChannel标签,前者用于自定义频道,后者用于导入已经存在的频道
<channels>
<importChannel chid="c1"
name="Microsoft-Windows-BaseProvider/Admin"
symbol="CHANNEL_BASEPROVIDER_ADMIN"
/>
<channel chid="c2"
name="Microsoft-Windows-SampleProvider/Operational"
type="Operational"
enabled="true"
/>
</channels>
自定义频道的name属性的命名规范为provider标签的name属性+反斜杠+channel类型
channel类型一共就4个:
- Administrative
- Operational
- Analytical
- Debug
其中chid属性必须为唯一标识符,所以如果让我来写的话我会给他加一个GUID后缀
定义严重级别
- win:Critical
- win:Error
- win:Warning
- win:Informational
- win:Verbose
通常我们使用这5种级别就够了,没必要自己去定义,这个是windows sdk中帮我们定义的level:
<levels>
<level name="win:LogAlways" symbol="WINEVENT_LEVEL_LOG_ALWAYS" value="0" message="$(string.level.LogAlways)"> Log Always </level>
<level name="win:Critical" symbol="WINEVENT_LEVEL_CRITICAL" value="1" message="$(string.level.Critical)"> Only critical errors </level>
<level name="win:Error" symbol="WINEVENT_LEVEL_ERROR" value="2" message="$(string.level.Error)"> All errors, includes win:Critical </level>
<level name="win:Warning" symbol="WINEVENT_LEVEL_WARNING" value="3" message="$(string.level.Warning)"> All warnings, includes win:Error </level>
<level name="win:Informational" symbol="WINEVENT_LEVEL_INFO" value="4" message="$(string.level.Informational)"> All informational content, including win:Warning </level>
<level name="win:Verbose" symbol="WINEVENT_LEVEL_VERBOSE" value="5" message="$(string.level.Verbose)"> All tracing, including previous levels </level>
<!-- The following are unused. They are place holders so that users dont accidentally use those values in their own definitions -->
<level name="win:ReservedLevel6" symbol="WINEVENT_LEVEL_RESERVED_6" value="6"/>
<level name="win:ReservedLevel7" symbol="WINEVENT_LEVEL_RESERVED_7" value="7"/>
<level name="win:ReservedLevel8" symbol="WINEVENT_LEVEL_RESERVED_8" value="8"/>
<level name="win:ReservedLevel9" symbol="WINEVENT_LEVEL_RESERVED_9" value="9"/>
<level name="win:ReservedLevel10" symbol="WINEVENT_LEVEL_RESERVED_10" value="10"/>
<level name="win:ReservedLevel11" symbol="WINEVENT_LEVEL_RESERVED_11" value="11"/>
<level name="win:ReservedLevel12" symbol="WINEVENT_LEVEL_RESERVED_12" value="12"/>
<level name="win:ReservedLevel13" symbol="WINEVENT_LEVEL_RESERVED_13" value="13"/>
<level name="win:ReservedLevel14" symbol="WINEVENT_LEVEL_RESERVED_14" value="14"/>
<level name="win:ReservedLevel15" symbol="WINEVENT_LEVEL_RESERVED_15" value="15"/>
<!-- End of reserved values -->
</levels>
如果我们要用的话,直接复制回来就行了,没必要自己瞎折腾
定义任务和操作码
提供者使用任务和操作码来进行事件的分组
一般情况下,使用任务来标识一个软件组件,比如network、database等
使用操作码来标识某个组件执行的操作,如network组件的发包、收包操作,database组件的查询、插入等操作
这个都是自己来定义的,微软并不会强制要求你怎么写,只要你自己觉得符合逻辑即可
task和opcode的定义方法如下:
<tasks>
<task name="Disconnect"
symbol="TASK_DISCONNECT"
value="1"
message="$(string.Task.Disconnect)"/>
<task name="Connect"
symbol="TASK_CONNECT"
value="2"
message="$(string.Task.Connect)">
</task>
<task name="Validate"
symbol="TASK_VALIDATE"
value="3"
message="$(string.Task.Validate)">
</task>
</tasks>
<opcodes>
<opcode name="Initialize"
symbol="OPCODE_INITIALIZE"
value="12"
message="$(string.Opcode.Initialize)"/>
<opcode name="Cleanup"
symbol="OPCODE_CLEANUP"
value="13"
message="$(string.Opcode.Cleanup)"/>
</opcodes>
<localization>
<resources culture="en-US">
<stringTable>
<string id="Provider.Name" value="Sample Provider"/>
<string id="Task.Disconnect" value="Disconnect"/>
<string id="Task.Connect" value="Connect"/>
<string id="Task.Connect.ReadRegistry" value="ReadRegistry"/>
<string id="Task.Validate" value="Connect"/>
<string id="Task.Validate.GetRules" value="GetRules"/>
<string id="Opcode.Initialize" value="Initialize"/>
<string id="Opcode.Cleanup" value="Cleanup"/>
</stringTable>
</resources>
</localization>
定义关键字,用于给事件分类
关键字keyword是一个64-bit的掩码,每一个bit都是一个category,如果一个事件的某个bit置位,那么就表明这个事件归属于该bti位对应的category
其中低48-bit由开发者自定义,高16bit由微软定义,winmeta.xml
文件中有对高16bit的说明
这个就是高16bit每个bit位所对应的含义
ETW会话可以通过keyword来过滤掉自己不想搜集的事件,这个就跟WPP中的flag和level是一样的
ETW会话有一对针对keyword的过滤器:
- MatchAnyKeyword
- MatchAllKeyword
顾名思义,前者就是说只有事件的任意一个bit命中,就写入,后者意思是只有所有的bit都命中才会写入
如果MatchAllKeyword为0,那么只要符合MatchAnyKeyword规则,那么就会写入,如果MatchAllKeyword不为0,那么事件的keyword必须要同时符合两条规则才会被写入
我们可以举一个实际的例子
使用keyword标签来定义关键字,定义之后,后面的event标签就可以使用这里定义的所有关键字:
<keywords>
<keyword name="Read" mask="0x1" symbol="READ_KEYWORD"/>
<keyword name="Write" mask="0x2" symbol="WRITE_KEYWORD"/>
<keyword name="Local" mask="0x4" symbol="LOCAL_KEYWORD"/>
<keyword name="Remote" mask="0x8" symbol="REMOTE_KEYWORD"/>
</keywords>
定义过滤器
这个过滤器是给ETW会话使用的,过滤规则由会话提供
比如说我们的provider会生成进程相关ed事件,然后我们提供了一个根据进程ID进行事件过滤的过滤器,那么ETW会话就可以通过传给我们一个PID来过滤事件
使用filter标签来定义过滤器:
<filters>
<filter name="Pid"
value="1"
tid="t1"
symbol="FILTER_PID"/>
</filters>
<templates>
<template tid="t1">
<data name="Pid" inType="win:UInt32"/>
</template>
</templates>
定义Name/Value映射
用于将数字映射成字符串,这个没什么好说的,直接看例子:
<maps>
<valueMap name="TransferType">
<map value="1" message="$(string.TransferType.Download)"/>
<map value="2" message="$(string.TransferType.Upload)"/>
<map value="3" message="$(string.TransferType.UploadReply)"/>
</valueMap>
<bitMap name="DaysOfTheWeek">
<map value="0x1" message="$(string.DaysOfTheWeek.Sunday)"/>
<map value="0x2" message="$(string.DaysOfTheWeek.Monday)"/>
<map value="0x4" message="$(string.DaysOfTheWeek.Tuesday)"/>
<map value="0x8" message="$(string.DaysOfTheWeek.Wednesday)"/>
<map value="0x10" message="$(string.DaysOfTheWeek.Thursday)"/>
<map value="0x20" message="$(string.DaysOfTheWeek.Friday)"/>
<map value="0x40" message="$(string.DaysOfTheWeek.Saturday)"/>
</bitMap>
</maps>
<localization>
<resources culture="en-US">
<stringTable>
<string id="Provider.Name" value="Sample Provider"/>
<string id="TransferType.Download" value="Download"/>
<string id="TransferType.Upload" value="Upload"/>
<string id="TransferType.UploadReply" value="Upload-reply"/>
<string id="DaysOfTheWeek.Sunday" value="Sunday"/>
<string id="DaysOfTheWeek.Monday" value="Monday"/>
<string id="DaysOfTheWeek.Tuesday" value="Tuesday"/>
<string id="DaysOfTheWeek.Wednesday" value="Wednesday"/>
<string id="DaysOfTheWeek.Thursday" value="Thursday"/>
<string id="DaysOfTheWeek.Friday" value="Friday"/>
<string id="DaysOfTheWeek.Saturday" value="Saturday"/>
</stringTable>
</resources>
</localization>
定义事件数据模板
虽然我不知道name属性到底有啥用,但是根据文档,name属性是一定要有值的
注意里面的outtype,intype属性很好理解,解释我们调用EventWrite函数的时候传入的参数类型,而outtype是用来指示消费者如何渲染这条数据,不过outtype并不是一定要设置ed属性,如果你不指定,那么消费者会使用intype默认的对应outtype来进行渲染
<templates>
<template tid="t2">
<data name="TransferName" inType="win:UnicodeString"/>
<data name="Day" inType="win:UInt32" map="DaysOfTheWeek"/>
<data name="Transfer" inType="win:UInt32" map="TransferType"/>
</template>
<template tid="t3">
<data name="TransferName" inType="win:UnicodeString"/>
<data name="ErrorCode" inType="win:Int32" outType="win:HResult"/>
<data name="FilesCount" inType="win:UInt16" />
<data name="Files" inType="win:UnicodeString" count="FilesCount"/>
<data name="BufferSize" inType="win:UInt32" />
<data name="Buffer" inType="win:Binary" length="BufferSize"/>
<data name="Certificate" inType="win:Binary" length="11" />
<data name="IsLocal" inType="win:Boolean" />
<data name="Path" inType="win:UnicodeString" />
<data name="ValuesCount" inType="win:UInt16" />
<struct name="Values" count="ValuesCount" >
<data name="Value" inType="win:UInt16" />
<data name="Name" inType="win:UnicodeString" />
</struct>
</template>
</templates>
定义事件
使用event标签定义事件,value属性必须被定义,且需要是唯一的
如果事件中包含自定义的数据,那么就需要之前定义的数据模板,通过设置template属性为对应的templateid来实现
level/keyword/task/opcode用来给事件分类
这里有一个完整的定义文件
里面的symbol属性决定了我们到时候发布事件的时候所要使用的函数名称
另外一点就是所使用的template也决定了传参的形式
本地化消息字符串
这个没什么好讲的,就是localization标签,前面已经见到过很多次了
编译.man文件
使用消息编译器mc.exe来编译我们前面编写的.man文件
mc会创建出来一个头文件,里面包含了man文件中的定义
我们需要在源文件中包含这个头文件,同时mc还会生成.rc文件和一个.bin文件
有两种方法可以将上面的操作自动化:
- 添加消息编译器任务到驱动项目文件中
- 使用VS将man文件添加到项目中并配置消息编译器属性
对于第一种方法,可以在vcxproj文件中的MessageCompile标签中进行编辑:
<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>
代码编写
现在我们修改之前的过滤驱动项目来使用ETW进行tracing
其实我突然发现ETW并不是很适合用来记录调试信息,因为只要传参类型不同,就需要定义额外的template,很麻烦,除非说你定义一个很长的参数列表,来满足所有类型的参数类型,但是这样会导致代码非常臃肿
在编写完man文件之后我们需要修改文件属性
后面的过程我放到了视频中
项目地址
自定义的channel不知道啥原因不好使,使用importChannel,利用现成的channel就可以,而且就是驱动一运行,就会有源源不断的事件写入到event log中,根本不用tracelog
<importChannel chid="c1"
name="System"/>
所以是不是说,ETW并不是所谓的动态启停?只要注册了之后,有consumer就会生成事件?
这个我现在也没弄明白
2024-06-14 更新
其实ETW用起来并不复杂,我们并不一定非要把日志写入到系统log中
另外就是原始项目的代码注释中提到了一篇文章,我也顺便来看一下
笑死了,这篇文章通篇都在吐槽微软的屎山代码
不过问题是这个ETW代码只能在用户模式使用,驱动中是不能这么写的
但是Trace部分我们还是可以正常使用的,只需要把Trace项目的GUID修改为驱动项目中etwman.xml中的GUID即可
效果如下: