ETW 详解与使用

关于etw,大部分非windows的开发者应该都是完全没有听说过。其实etw的本身开发用途是为了让开发者更好的了解操作系统内部情况而开发的监控工具。写作本文的原因在于很少有关于etw的中文文章,并且也很少有与etw开发者进行过交流的作者写作关于etw的相关文章,如果本文帮助您更好的理解etw并且帮助您绕过了etw的一些使用陷阱,那么就再好不过了。

众所周知的windows debug工具其实应该是windbg。但是windbg在debug过程中其实更倾向于静态,而etw则更倾向于动态。 因为本文其实与windbg并没有什么关系,所以这里就不详细比较两者区别了。ps:这里有个小贴士,如果你使用的是windbg next, 那么你在导入symbol的时候请尽量到windows官网下载相应的symbol文件,不要直接采用windows的symbol服务器。。。。

  1. 首先我们来介绍下etw的整体构造
    1.1. etw session
    1.1.1. etw session 的本质
    etw session的本质,很简单, 就是告诉你 你所trace的结果将被保存在哪里。
    1.1.2. 公有与私有session
    关于session另一个很重要的属性就是session的公有(public)和私有(private)属性。私有的session是非常少见的,而大部分的session都是公有的。除非你在声明StartTrace的property的时候一不小心或者很不小心或者傻逼兮兮的或者有那么百分之一的可能,有意为之,把属性设置成EVENT_TRACE_PRIVATE_LOGGER_MODE才可能让你的session变成private session。

    公有session就是全局session,这种session能够接收到全局所有与它相关的event,而私有session则只能接收到它所在process内的event。
    1.2 etw trace的两个常用模式与一个非常用模式
    关于这部分,最重要的文章就是这个:All constants for log mode
    1.2.1 文件log模式
    文件log模式就是将所获得的log内容保存进某个特定的etl file中。一般来说文件log模式在没有特定需求的情况下都是一个相当不错的选择。在内部开发的工具当中,文件log也是一个非常常用的选择。
    一般来说在使用文件log方式的时候, EVENT_TRACE_FILE_MODE_NONE是一个非常不错的选择,这个常量选择的弊端在于有可能会产生非常巨大的log文件。
    EVENT_TRACE_FILE_MODE_SEQUENTIAL也是一个不错的选择,这个选择的问题在于,它并不会按照你所想的停下来。嗯,重复一下。它并不会按照你想象的停下来。。。。。通过我查看其他组写的项目与观察我们组所使用的某个工具的行为,我可以负责任的说,这个模式在你按照网络上你能找到的大部分配置中都是不会停下来的。原因很简单,因为大部分配置都会遗忘配置MaximumFilesSIze这项。这项内容对于这个模式是不可缺少的。如果这项不配置,那么最后产生的文件就会无比庞大,有多大?30个G的etl你们见过么。。。。。。
    需要注意的是,在模式选择中,其实可以通过或运算为file mode选择realtime。这样选择的话就还需要注明flushtime保证实时写入文件。这个模式其实是一个非常不错的选择,也是针对某些有特定需求的系统实现realtime的一个选择。
    1.2.2 Real time模式
    real time模式本质上来说就是在提供了一个provider的前提下利用consumer进行对信息的消耗。在这个模式中,我们在声明session的时候必须指明flushtime。其他的设定与file mode没说什么很大区别。
    需要注意的一点是,这个模式只能用于public session,对于private session这个模式并不提供相关的支持。
    1.2.3 Buffer mode
    这个模式不多做介绍,因为我也没使用过。
    1.3 Etw provider
    etw在收集log的时候需要设定一个到多个provider的模式进行信息收集。provider与session之间通过EnableTraceEx2 这个函数进行某种程度上的联系。并且通过TraceHandle这个东西与etw的其他部分进行实质上的联系。
    1.4 etw consumer
    需要注意的是,几乎只有在real time mode的情况下我们才会使用consumer。但这不意味着consumer只可以应用于real time mode。很多情况下我们也是倾向于使用consumer来处理生成的etl文件。因为如果不使用consumer那么就只能使用xperf之类的软件来完成对etl的解析,这样一来会使整体非常繁琐。
    etw consumer是一个使用一定设置针对某个trace session进行消耗的消耗者。针对这个组件的一些问题我们会在下一部分统一说明。可以说这个组建的设计是存在很大问题的。

  2. etw的整体工作流程
    针对这个部分我认为还是不要具体细分小部分,最好一气呵成给大家一个整体的印象。建议在涉及到具体组件的时候大家向下一部分的对应组件部分进行详细阅读来了解具体组件功能以及需要注意的地方,当然,如果您想先有个完整印象再来详细看细节也可以在完整阅读本节之后再阅读下一节。
    首先要开始一个etw session最为重要的一个函数就是StartTrace, 我们需要三个变量来使用这个函数,在这三个变量中,最为重要的是Property变量,这个变量基本定义了所有trace session的选项。
    在session被启动之后接下来就是初始化provider。在这里有两个变量其实来说很具有迷惑性,在下一部分中有相应描述。这里的关键变量就是trace handle和provider id。需要特别说明的是,provider id其实可以和start trace部分的guid不是一个值,并且在我的实践中,我认为这两个最好不要是同一个值,对于性能没有任何影响,但是我直觉的感觉如果他们两个使用同一个guid会在某些场景下产生些问题。 在这一部分还有controlcode这个变量,这个变量其实功能只有控制provider的启动和停止,并没有什么其他特别的地方。另外最后就是enableparameters这个变量可以说非常的复杂,我会在下一部分进行详细描述。其实provider内部还有一些很重要的东西,这些东西主要让provider被分成四个类型,并且通过不同的注册函数将自己进行注册来完成log的目的。
    在这里我来分享一个自己的实际经历来说明一个在provider的注册上容易犯的错误。博主曾经参与了一个有关一个windows下特殊部件的项目,因为相关的触发函数位于os repo内部且逻辑非常复杂,配置极其变态,所以博主当时一直认为是自己的provider没有进行注册所以才导致了问题。博主事后发现,当你使用了enabletraceex2这个函数的时候,只要使用了正确的配置,那么你的provider是会自动被注册上去的。所以如果各位在网上看到什么EventRegister 或者EventWrite函数疑惑自己为什么没有使用,不要疑惑,你本来就不必使用。。。。
    在这里说下,session与provider与consumer之间的对应关系。session相当于配置了一个目的地,而provider则是通过一些系统函数,通过自身配置的parameter来将自己注册于系统之内来获得相应的log。所以和平时的想象不同,其实provider与session之间的关系其实是一个provider可以对应着多个session。而consumer与session的关系则是一一对应的关系。至于这个整体架构是否合理我们在本部分的最后会加以评价,我们先来看看最后一个部分consumer。

    consumer可以说是这个系统之中使用最少的一个部件,我在很多项目中都见过只声明provider和session,生成etl file然后使用xperf等其他工具来解析etl file的情况。原因我们也在本部分最后进行讲述。
    consumer中我们使用一个EVENT_TRACE_LOGFILE变量来进行属性配置。最后我们会将这个变量的以指针形式传入traceHandle。并且我们还将两个回调函数传入这个变量,从而在ProcessTrace这个函数被调用的时候我们可以使用回调函数来对相关的内容进行处理。同时consumer中还有开始时间和结束时间这两个选项。博主最开始真的想到过利用这两个属性来完成一些分时间段的任务,但是最后的尝试让我选择放弃,因为这两个属性在接受system time的设定上都太蠢了。如果你没有很好的测试机制请务必远离这两个东西。

    为什么很少有人使用consumer,很简单,因为这个设计实在太渣了。
    在这里我们来看一下整个系统的设计:
    首先provider和session部分的设计可以说是匠心独具非常不错,既保证了部件之间的松耦合,又保证了资源的有效利用,并且以独特的思维很好的将两部分结合了起来。唯一的问题可能就是provider在文档中暴露了过多的内部函数可能会给新手造成一些理解上的困难。再有也就是配置比较容易搞错之类的问题。
    而consumer则是一个彻头彻尾的失败设计。首先手动配置logfile还比较正常,但是将配置属性以指针形式传入tracehandle就令人无法理解了。这个位置完全可以用正常方式对tracehandle进行配置从而保证易读性。而之后的两个回调函数则可以说是语言自带的痛,这两个回调函数使得很多操作都变得异常复杂。博主在做自己项目的时候甚至使用了一个全局变量以绕过传递参数的不便之处。而这个部分最不能理解的一个设计则在于消息解析的设计,如果你使用了tdh的解析器,你会发现如果要获得想要的变量的信息,你需要使用指针位移的方式才能获得,不仅需要二次传入原始参数,而且还非常容易搞错offset的量。这个设计可以说是非常匪夷所思了。

    以上便是etw的相应工作流程以及其中不甚合理的地方。

  3. etw在记录过程中必不可少的元素
    3.1 session中的EVENT_TRACE_PROPERTIES
    这个元素出现在StartTrace这个函数的第三个位置。相关的参数中,最为重要的有LogFileMode, LoggerNameOffset这两个,前者可以针对不同的需求设计不同的log模式。后者则是因为设定与LogFileNameOffset有牵连所以说下,一般来说就是session name的长度。而LogFileNameOffset则要接续在它的后面。
    相关链接EVENT_TRACE_PROPERTIES 文档

    3.2 provider中的MatchAnyKeyword 与 MatchAllKeyword
    这两个变量我翻遍内部例子也没见到任何人使用除了0x20之外的值。

    3.3 EnableParameters
    在这里我先抛出一个链接ENABLE_TRACE_PARAMETERS
    这个链接里面有所有的属性应该怎么写。其中最为难写的就是FileterDesc这一项。这一项针对不同项目所选择的属性都是完全不同的,甚至可以自己声明settings这个参数,来使用自己定义的filter。其实博主就是在os repo中自己定义了一个filter然后使用的自己的setting完成的。有需求的朋友可以查查怎么在不动系统的情况下实现自己的filter,但是整体来说并不太建议

    3.4 EventRecordCallback
    这个回调函数可以说是全系列的败笔。所有针对pEvent的处理都在这个里面。可以说是为所欲为,但是因为它本身需要在LOGFILE属性里面用指针形式声明,所以其扩展性瞬间缩水到0,连带的对其进行参数传入也变得非常的被动。

    3.5 BufferCallback
    这个函数的主要作用在于可以用额外手段将consume这个过程停下来,只要这个函数return false,则可以直接完成停止consume这个过程而不需额外的closeTrace。

    3.6 pEvent
    这个参数内部内容之复杂,解析之困难可以说是顶级。
    首先这个参数内部包含有四种可能结构,分别是array, struct, map, value。其中map和value可以视为同一样东西。同时如果想要获得value的话必须使用TdhFormatProperty函数并且借助获得的pFormattedData与offset来设定。

以上并非etw全部的使用指南,大部分都是笔者在使用过程中体验过的网上没有明确说明的东西,在这里加以记录,如果有人遇到类似坑可以进行参考。