返回
顶部

references:

约定:

  • 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位所对应的含义

image-20240510232426433

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属性决定了我们到时候发布事件的时候所要使用的函数名称

image-20240511023802802

image-20240511023721474

image-20240511023612314

另外一点就是所使用的template也决定了传参的形式

image-20240511024225605

image-20240511024414157

本地化消息字符串

这个没什么好讲的,就是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文件之后我们需要修改文件属性

image-20240511033024152

后面的过程我放到了视频中

part1

part2

项目地址

自定义的channel不知道啥原因不好使,使用importChannel,利用现成的channel就可以,而且就是驱动一运行,就会有源源不断的事件写入到event log中,根本不用tracelog

<importChannel  chid="c1"
                name="System"/>

所以是不是说,ETW并不是所谓的动态启停?只要注册了之后,有consumer就会生成事件?

这个我现在也没弄明白

2024-06-14 更新

其实ETW用起来并不复杂,我们并不一定非要把日志写入到系统log中

这里有一个从这个项目中抄来的ETW代码,大家可以参考一下

另外就是原始项目的代码注释中提到了一篇文章,我也顺便来看一下

笑死了,这篇文章通篇都在吐槽微软的屎山代码

不过问题是这个ETW代码只能在用户模式使用,驱动中是不能这么写的

但是Trace部分我们还是可以正常使用的,只需要把Trace项目的GUID修改为驱动项目中etwman.xml中的GUID即可

效果如下:

4y5u6kuytreiuytrew45t6yuasdadas