Windows 事件跟踪:将珠穆朗玛峰变成派克斯峰





5.00/5 (11投票s)
历史上,ETW 一直被描绘成具有非常陡峭的学习曲线,类似于攀登珠穆朗玛峰的努力。意识到其潜在价值后,我着手将所需的努力降低到攀登派克峰的程度。
引言
Windows 事件跟踪(以下简称 ETW)是一个低延迟的 API,旨在调试在 Microsoft Windows 上运行的各种代码,从最普通的“hello, world”用户应用程序到最晦涩的设备驱动程序。它建立在 Windows 事件日志服务底层的基础设施之上,提供了一个灵活、松耦合的框架,用于记录 Windows 应用程序或库生命周期中可能发生的各种事件。
图 1 显示了一组 ETW 事件日志,由 Microsoft Message Analyzer 渲染。
聪明的读者会立即认出 图 1 中显示的 Microsoft Message Analyzer 窗口与标准的 Windows 事件查看器显示非常相似。虽然这里那里有一些差异,但它们共同的起源是显而易见的。根本的区别在于,上面显示的事件是由一个自定义 ETW 提供程序引发的,该提供程序内置于促使本文调查的 DLL 中。
ETW 事件与更熟悉的 Windows 事件之间最显著的区别在于 图 1 窗口的下三分之一处的详细信息网格,该网格列出了馈送到引发事件的数据包中的可替换参数。这些参数及其标签使您能够调用内部详细信息,例如在事件发生时代码中存在的内存地址和枚举类型。
背景
尽管事件跟踪自 Windows 2000 起就可用,但 Vista 引入了一个新的高性能引擎,该引擎与常规事件报告(“事件”——我没编造这个词,微软编造的)基础设施共享,并利用新的事件提供程序清单,取代了 Windows 2000 中 ETW 的核心的繁琐的托管对象格式文件。虽然“经典”事件跟踪(使用 MOF 文件)仍然受支持,但 Vista 随之而来的基于清单的报告放宽了许多限制,这些限制使得 ETW 既不灵活也不实用。
我为本文选择的标题反映了我最初对学习曲线的看法,直到昨天,我发现了 Microsoft Message Analyzer(下载)。将其与事件命令行实用程序wevtutil.exe
以及我对 Windows 事件日志工作原理相对熟悉的了解结合起来,将似乎是攀登珠穆朗玛峰顶峰的探险,变成了攀登派克峰顶峰的可管理得多的徒步。
为了准备我们的攀登,理解构成清单的众多组成部分以及它们之间的关系至关重要。为此,我将在下面提供表 1,然后是图 2。请快速浏览一下两者,然后滚动到图片下方,叙述继续。
实体名称 | 相关实体名称 | 关系类型 | 解释 |
---|---|---|---|
清单 | 事件 | 一对多 | 一个清单通常定义两个或多个事件。 |
事件 | 清单 | 多对一 | |
模板 | 清单 | 多对多 | 使用相同参数列表的两个或多个事件可以共享一个模板,但一个事件最多与一个模板相关。 |
字段 | 模板 | 多对一 | 一个模板有一个或多个关联字段。不幸的是,字段定义不能在模板之间共享。 |
映射 | 字段 | 多对多 | 一个或多个模板中的多个字段可以共享一个映射。 |
操作码 | 事件 | 多对一 | 每个事件最多关联一个操作码,但一个操作码可以应用于多个事件。 |
关键字 | 事件 | 多对多 | 每个事件关联零个或多个关键字,一个关键字可以应用于多个事件。 |
图 2 显示了构成事件清单的对象的之间的关系。
对于小型项目,您可以省略 Opcodes
和 Keywords
。两者都支持对包含两个或多个事件提供程序的复杂应用程序生成的大日志进行筛选,但对于促使我学习 ETW 和撰写本文的项目规模来说,它们是过度的。(仔细检查后,清单实现了操作码,但我很快就意识到它们是过度的。)
但是,如果您的应用程序报告任何与枚举类型相似的内容,那么映射则另当别论。尽管这个应用程序很简单,但它需要其中三个。
如果您将清单视为一个小数据库,将图 2 视为其模式,那么模式会决定实体的定义顺序,主要是从下往上,大致如下。
- 第一个必需的任务是定义清单,它具有名称、符号和 GUID 属性。
- 命名遵循既定约定,
公司-产品-组件
,用连字符分隔。指定的名称将成为清单文件的名称,.man
,以及您导入提供程序实现模块的 C/C++ 头文件(.h
)和 Message Compiler 生成的资源脚本文件(.rc
)。 - 由于符号属性被包含在 C/C++ 头文件中出现的许多名称中,因此它应遵循您已建立的符号命名约定。至少,连字符必须被下划线取代,因为符号必须是有效的 C 名称。
- GUID 为您的清单提供了一个无歧义的标识;请使用“Manuscript Generator”上的“New”按钮填充此框。
- 命名遵循既定约定,
- 一旦您有一个清单来容纳它们,
maps
必须在除Keywords
和Opcode
(如果您需要其中任何一个)之外的所有内容之前定义。由于您可能会使用它们,因此请将映射定义视为您的第一个必需的设计活动。 - 由于
Templates
依赖于零个或多个Maps
,因此它们是下一个逻辑实体,可以引起您的注意。 - 在定义了其关联的
Template
(如果有)、其Opcode
(如果需要)和其Keywords
(如果有)之后,才能定义Event
。
由于可以按任意顺序编辑任何实体,因此上述顺序是最有效的定义顺序。实际上,您可能会进行几次迭代,尤其是在您的前几个清单上。即使有周密的计划,我也进行了几次迭代才得到了一个令我满意的产品。
您的徒步装备清单
继续使用徒步的比喻,正如攀登派克峰是一次非凡的徒步,需要装备检查清单一样,实现 ETW 也是如此。
ecmangen.exe
,Instrumentation manifest generation tool(仪器清单生成工具),包含在 Microsoft Platform SDK 的最新版本中。这个图形工具是生成 ETW Provider 清单最快、最简单、最准确的方法,这是一个复杂的 XML 文档,必须遵循相当严格的 DTD。严格来说,事件提供程序(引发跟踪事件的程序)和事件使用者(监听、保存和格式化事件的程序)都直接或间接使用清单。mc.exe
,Message Compiler(消息编译器),将ecmangen.exe
生成的仪器清单转换为二进制Message资源,您的事件提供程序使用该资源来指导构建发送到事件日志服务的包。由于它也包含在 Platform SDK 中,如果您有其中一个,您就有另一个。虽然它可以在命令提示符下使用,但在 Visual Studio 项目中使用它要容易得多。设置起来很简单,但并不完全显而易见,所以届时我会告诉您如何做。rc.exe
,Resource Compiler(资源编译器),负责将 Message、Manifest 和 Version 资源合并到一个二进制资源对象文件(COFF 格式文件)中,该文件被送入链接器。它与前两个一起,作为 Platform SDK 的一部分提供。虽然它可以在命令提示符下使用,但在 Visual Studio 项目中使用它要容易得多。我怀疑 Resource Compiler 包含在 PSDK 中是因为即使您使用 GCC 等其他编译器和链接器代替 Visual Studio,也需要它。link.exe
,Microsoft Incremental Linker(Microsoft 增量链接器),将所有内容整合到一个 Portable Executable (PE) 文件中,Windows 可以将其加载到进程中。这是随 Microsoft Visual Studio 一起提供的仅有的两个清单项之一。如果您使用其他编译器,您将需要它的链接器;它们通常成对提供。cl.exe
,Microsoft C/C++ Optimizing Compiler(Microsoft C/C++ 优化编译器),是添加到您清单中的另一个 Visual Studio 组件。您需要它,或一个替代的 C/C++ 编译器,来编译您的事件提供程序源代码。wevtutil.exe
,Eventing Command Line Utility(事件命令行实用程序),是此清单上唯一随操作系统一起提供的项,也是唯一通常可通过 WindowsPATH
目录列表访问的项。MessageAnalyzer.exe
,Microsoft Message Analyzer,需要单独下载,可从 http://www.microsoft.com/en-us/download/details.aspx?id=44226 获取。
把夏尔巴人留在尼泊尔;有我作为向导,您会做得很好的。
如果您拥有 Windows Vista 或更高版本的 Platform SDK,您可以勾选项目 1 到 3,表示已安装并准备就绪。虽然将前两个以及项目 6 和 7 的桌面快捷方式放在同一个目录中会节省时间,但这样使用它们要容易得多。如果您拥有任何版本的 Visual Studio,则项目 4 和 5 已涵盖,无需进一步关注。如果您的首选 C/C++ 编译器是 GCC,情况也是如此。剩下项目 6,它随 Windows 一起提供,位于您的 System32
目录中,该目录位于 PATH
目录列表中,以及项目 7,它需要下载和安装。
规划您的攀登
虽然远不如攀登珠穆朗玛峰那样艰苦,但有些路段很狭窄,并且做一些准备会事半功倍。
以下任务按照您应该在 ecmangen.exe
中执行的顺序排列。我偏好的规划工具是 Microsoft Excel。它方便的字符串函数非常适合将对象名称转换为符号名称。我还使用条件格式来提醒我注意一组本应唯一的项目中的重复项。通常,我还会使用公式为事件和资源字符串等内容分配序列号。
- 将枚举类型映射到文字。每组文字构成一个Map。每个 Map 成为您Template中任何整数项的一个新的可选输出格式。我通常通过将 enum 复制到工作表中来创建我的映射,在那里可以对其进行解析和编号。
- 确定要包含在每个事件中的数据(参数);这些数据进入 Template。每个参数都有输入类型和输出类型,大多数都有与输入类型对应的默认值,并且可以按原样接受。
- 支持的输入类型为字符串(ANSI 或 Unicode)、任意大小的有符号或无符号整数(支持 16、32 和 64 位)、浮点数、双精度数、布尔值、指针、
GUID
、FILETIME
、SYSTEMTINE
、SID
和 Binary。 - 除了表示枚举的整数外,默认输出类型通常可以接受。
- 表示枚举的整数被指定为它们的输出类型;当这些映射保存时,它们会自动添加到列表中。当由 Microsoft Message Analyzer 等工具呈现参数时,将使用映射来替换数值显示相应的枚举文本。
- 支持的输入类型为字符串(ANSI 或 Unicode)、任意大小的有符号或无符号整数(支持 16、32 和 64 位)、浮点数、双精度数、布尔值、指针、
- 定义 Templates。您想要报告一个或多个参数的每个事件都需要一个 Template。
- Template 是一个命名对象,它列出了消息中出现的参数,并在 Message Analyzer 窗口底部的详细信息框中显示。
- Template 列出了与事件相关的所有参数,按它们在引发事件的宏中出现的顺序。
- 如果多个事件接受相同的参数,并且顺序相同,它们可以共享一个模板。
- 在 Template 中输入的标签用于 Message Analyzer 在选定消息的下部窗口中显示的列表中。
- 在消息正文中,您必须提供自己的标签,除非上下文足以标识参数。
- 定义监听事件的 Channel。与 Manifests 一样,Channels 也有名称和符号,但 Channel 不需要 GUID。
- Channel 的命名似乎不太有规律。为了最大限度地减少名称的扩散,我将我的 Channel 命名为在清单名称后附加
-Debugging
。 - 与清单符号名称一样,Channel 符号名称必须是有效的 C 符号,因此连字符变成了下划线。遵循我的命名约定,我的符号名称全部大写,Excel 再次发挥了作用。
- Type 从列表框中选择,您希望您的 Channel 的 Type 为
Debug
。 - Description 是为普通用户准备的;据我所知,没有东西使用它。
- Channel 的命名似乎不太有规律。为了最大限度地减少名称的扩散,我将我的 Channel 命名为在清单名称后附加
- 识别事件。每个事件都被分配一个数字,该数字在单个清单中定义的事件集合中必须是唯一的。将每个事件与最多一个 Template 相关联,该 Template 包含包含参数的转换说明。
- 您可以包含未在主消息正文中显示的参数,但关联的 Template 必须处理每个参数。
- 尽管事件 ID必须唯一,但它们不必连续。遵循一个起源于打孔卡时代的长期建立的程序,我通常以 10 或 100 的间隔分配事件 ID,这允许我在不重新编号现有事件的情况下插入数字。
- 撰写消息。一条好的消息应该简要但清晰地描述关联的事件,然后为伴随它的每个参数(数据点)提供一个简短、有意义的标签。
- 参数由占位符标记(
%1
到%n
,其中n
是参数总数)指示。 - 参数编号对应于它们在引发事件的宏调用中出现的顺序。
- 参数编号必须从 1 开始(注意,C# 开发者!),并且必须连续,但它们可以在消息正文中以任何顺序出现。
- 参数由占位符标记(
- 命名您的提供程序。命名约定是
CompanyName-ProductName-ComponentName
;这是为注册表项、Program Files 中的应用程序目录以及其他与应用程序相关的目录和对象命名的约定方案。
攀登
在大多数山峰上,您可以选择多条路线到达山顶。由于 C# 路线标记相当清晰,以其他文章的形式出现,而我碰巧要进行仪器化的项目是用 C++ 实现的,所以我将走 C++ 路线。尽管这条路线的部分路段更陡峭,但这并没有(太多地)阻止我!
攀登分为四个阶段。
- 使用
ecmangen.exe
创建清单。 - 使用 Message Compiler 和 Visual Studio 将清单编译成仅资源 DLL。在某些方面,我更关心同名的 C/C++ 头文件,这是构建 DLL 的副产品。
- 将副产品 C/C++ 头文件合并到应用程序中,并编写宏调用来引发事件。
- 使用 Microsoft Message Analyzer 启用您的调试事件通道,监听事件并记录它们。
接下来的四个部分涵盖了每个步骤的关键点。由于本文是为高级 C 和 C++ 程序员受众撰写的,因此我将重点放在工具的介绍上,将细节留给您去探索。
山脚:创建清单
图 3 显示了加载了
LeakStop_EventTraceProvider.man
的 Manifest Generator。
上面的图 3 显示了完成的清单加载后,并且选择了 Events 部分时,主 Manifest Generator 窗口的外观。
- 令我有些惊讶的是,
ecmangen.exe
没有最近使用 (MRU) 列表,尽管它会记住您上次加载文件的目录的名称。 - 由于
ecmangen.exe
同时处理事件跟踪和性能计数器的清单,因此有两个分支,两者最初都折叠起来。当您将清单加载到其中时,相关的分支会获得一个节点,您将其展开以显示上面所示的视图。 - 展开树后,此程序的行为类似于典型的 MMC snap-in。
- 尽管存在我在上一节中提到的层次结构,但事件及其组件占据树中的单个级别,极大地简化了编辑。
重要提示:请密切注意主窗口右边缘的细长窗口,您会在其中看到看起来像超链接的小部件,每个小部件旁边都有一个加号(它只是占用空间,不做任何事情)。当其中一个图标显示Edit时,您必须单击它,然后才能在中间窗口编辑任何内容,在那里会发生所有有趣的事情。选择Edit后,该按钮将被一对Save和Cancel按钮替换。完成更改后,请务必单击Save按钮,以免丢失您的编辑。显然,Cancel按钮允许您放弃更改。
图 4 显示了完成的映射定义。
图 5 显示了
LeakStop_EventTraceProvider.man
清单中完成的 LS_SOURCE_HEAP
映射定义。
图 4 和图 5 显示了 Maps 集合以及 LeakStop_EventTraceProvider.man
清单中完成的 LS_SOURCE_HEAP
映射。请注意,在图 5中,文本是自由格式的;它不必符合 C 变量名的约束。相比之下,映射的名称必须是有效的 C 变量名,因为它会包含在生成的代码中。
图 6 是完成的
DataBlockRegistration
参数模板。
图 7 是
pfIsProtected
参数,处于编辑模式,显示了列表框,并选择了 LS_BLOCK_STATE
映射。
图 6 和图 7 分别显示了完成的 DataBlockRegistration
参数模板及其 pfIsProtected
参数。在这两张图中,请注意 penmSourceHeapType
的 OutType
是 LS_SOURCE_HEAP
,而 pfIsProtected
参数的 OutType
是 LS_BLOCK_STATE
。尽管技术上是布尔值,但我将其视为一个枚举,以更好地控制报告中显示的文本。此外,图 7 显示了 OutType
列表框,您可以在列表中底部看到三个映射名称。
最后,我请您注意窗口中间的Save按钮;在图 7中显示;此按钮可保存对单个参数定义的更改。您还必须使用右侧窗格中的Save按钮来保存模板本身。
这条路充满了盲目的发夹弯和松动的岩石,我提到过吗?现在我提过了;请谨慎。
短暂而陡峭的攀登:使用 Visual Studio 生成清单二进制文件和头文件
攀登的下一阶段发生在 Microsoft Visual Studio 中。由于我希望在继续前进之前创建 DLL,并且我已开始倾向于将大部分资源保留在专用 DLL 中,因此我创建了一个仅资源 DLL 项目。由于它们位于一个名为 Win32_ResGen.XLXM
的工作簿中,该工作簿是我在整理清单材料的过程中创建的,因此该 DLL 不仅包含事件提供程序清单,还包含一组常规字符串资源以及一个版本资源。有关 Excel 工作簿的详细信息,请参阅“改进的资源字符串生成器实战:你将吃自己的狗粮,并且喜欢它!”
配置仅资源 DLL 项目几乎与配置任何其他 DLL 项目相同。只有一个属性,在项目属性表“Linker”页面上标记为“No Entry Point”,必须设置为“Yes”,这是一个默认覆盖,由粗体显示(图 8)表示。其他几个属性的设置偏离了默认值,但这仅是创建仅资源 DLL 的要求。
图 8 显示了将 DLL 标记为仅资源的唯一必须更改的属性。
由于事件清单文件 LeakStop_EventTraceProvider.man
是在 Visual Studio 之外创建的,因此必须将其手动添加到项目中,可以通过“Add Existing Item”工具轻松完成,该工具在主“Project”菜单和项目上下文(右键单击)菜单中都可用。我更喜欢使用上下文菜单,因为它能确保文件被添加到预期的项目中(我曾有过文件被添加到错误项目的情况!)。
将文件添加到项目后,在 Solution Explorer
中展开项目,突出显示该文件(参见图 9)。使用右键单击激活其上下文菜单,然后选择“Properties”,它出现在菜单底部附近。此操作会打开 LeakStop_EventTraceProvider.man
的专用属性表,显示在图 10中。
- 确保“Item Type”是“Custom Build Tool”。
- 展开“Custom Build Tool”项,该项出现在具有初始焦点的“General”页面下方。您的属性表现在应该类似于图 11。
- 将表 2 中显示的值输入属性表,然后单击“Apply”按钮来记录编辑。
- 构建您的项目。叙述在表 2下方继续。
图 9 显示了 LeakStop_Message_Strings
项目,其中 LeakStop_EventTraceProvider.man
(由在 ecmangen,exe
中创建的清单保存生成的文件)被选中。
图 10 显示了
LeakStop_EventTraceProvider.man
的主属性表。
图 11 显示了
LeakStop_EventTraceProvider.man
属性表的“Custom Build Tool”配置页面。下面的表 2 列出了属性,这样您就无需从图片中读取它们。
IDE 中的字段标签 | 字段值 | 注释 |
---|---|---|
命令行 | mc %(FullPath) -um | 精确输入此命令。-um 开关是编译用户模式事件跟踪清单所必需的。还有许多其他开关;有关完整列表,请参阅 MSDN 库中的Message Compiler (MC.exe)。 |
描述 | 编译事件清单资源 | 此文本出现在构建日志中,主要面向普通用户。 |
附加依赖项 | 将此字段留空。 | |
链接对象 | 是 | 这是默认值。 |
将输出视为内容 | 否 | 这是默认值。 |
LeakStop_EventTraceProvider.rc
和 LeakStop_EventTraceProvider.h
进入库主资源脚本 LeakStop_Message_Strings.rc
的“Resource Includes”部分。
- 打开 Solution Explorer,右键单击
LeakStop_Message_Strings.rc
,然后选择“Add Resource Includes”。 - 合并它们,遵循
Win32_ResGen.XLSM
中 VBA 宏留下的痕迹,该宏合并了LeakStop_Message_Strings_Reource_Script.H
和LeakStop_Message_Strings_Reource_Script.RC2
。
构建此项目以生成下一阶段攀登所需的头文件。
为了总结这一阶段,我提请您注意此项目的几个功能。
- 构建项目会生成
LeakStop_EventTraceProvider.h
,您必须将其合并到引发事件的代码中,以及LeakStop_EventTraceProvider.rc
和其他一些中间文件,您可以安全地忽略它们。 - 构建的另一个主要输出是
LeakStop_Message_Strings.dll
,它位于 Release 输出目录中。- 在示例包中,我将其副本放在 Debug 输出目录中,以便调试版本可以找到它,以防我需要加载它来读取其字符串资源。
- 在我的工作源代码树中,我使用
FSUTIL.EXE
创建了一个符号链接或硬链接到 Release 目录中的 DLL。
- 没有 Debug 配置。这是故意的(我的设计,不是他们的!),因为为资源 DLL 拥有一个 Debug 配置没有什么好处,因为这种 DLL 总是作为数据加载,并被其他可执行文件中的代码读取。即使您分别构建了一个,字节到字节的比较也会显示它们是相同的。
- 我生成了一个 Map 文件,因为我总是为我创建的每个程序(DLL 或 EXE)都这样做。
- 我最近养成的一个习惯是启用 COMDAT 折叠。对于资源 DLL,我认为这个开关不起作用。标记 DLL 为具有安全结构化异常处理块也是如此。
- 由于此库中没有可执行代码,因此 C/C++ 编译器属性页保留其默认值;我没有费心打开它。
将 Visual Studio 保持打开状态,为下一阶段的攀登做准备。
较长、更平缓的攀登:编写引发事件的代码
下一阶段的重点转移到引发跟踪事件的代码上,该代码位于 LeakStop32
项目中。三个文件中的两个,LeakStop32_DllMainain.CPP
和 LS_Memory_Block_Registrars.CPP
,定义了引发跟踪事件的例程。
我的大多数项目都包含一个私有头文件,例如 LeakStop32_pvt.h
,它取代了我没有使用的 stdafx.h
,因为我不需要预编译头。当我开发支持 ANSI(窄字符)和 Unicode(宽字符)字符串实现和/或返回的函数时,我开始不再使用预编译头。此类 DLL 不能使用预编译头,因为在编译宽字符代码时必须使用 UNICODE
定义头文件,而在编译窄字符代码时必须取消定义 UNICODE
。
由于我没有使用预编译头,我将所有内容都放入了私有头文件中,甚至包括 resource.h
,大多数开发人员会根据需要将其包含到单个源模块中。
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#define _WIN32_WINNT 0x0601 // Make the functions that require Windows 7 or newer visible.
#include "..\Common\StandardMacros_DAG.H" // Standard headers in the correct order
#include "..\Common\LeakStop32.H" // Library declarations
#include ".\FreeSlotsStack.H" // CFreeSlotsStack class declaration
#include "..\LeakStop_Message_Strings\LeakStop_EventTraceProvider.h" // This header declares the Event Tracing for Windows interface.
#include "..\LeakStop_Message_Strings\LeakStop_Message_Strings_Reource_Script.H" // This header grants access to everything as regular resource strings.
#include ".\resource.h" // Library resources (mostly strings) - Redundant, now that everything is in LeakStop_Message_Strings.dll.
列表 1 演示了我包含与 LeakStop_Message_Strings.dll
关联的两个头文件的巧妙方法。
上面的列表 1 显示了 LeakStop32_pvt.h
的有趣部分,它出现在文件顶部附近,在那里我集中了我的 #include
指令。LeakStop_Message_Strings_Reource_Script.H
是与资源 DLL 关联的第二个头文件,它是由 Win32_ResGen.XLSM
中的两个 VBA 宏之一生成的,该宏也生成了 LeakStop_Message_Strings_Reource_Script.RC2
,并编辑了 LeakStop_Message_Strings.rc
以包含两者。那个 VBA 宏消除了在资源包含文件中使用和维护只读资源字符串的繁琐工作和高风险错误。
列表 2 显示了 LeakStop_EventTraceProvider.h
中最有趣的部分,它定义了一组宏,消除了 C 和 C++ 代码中引发跟踪事件的猜测。
//
// Register with ETW Vista +
//
#ifndef EventRegisterDavidAGray_CDeclVsPascal_StopLeak
#define EventRegisterDavidAGray_CDeclVsPascal_StopLeak() McGenEventRegister(&DAVIDAGRAY_CDECLVSPASCAL_STOPLEAK, McGenControlCallbackV2, &DAVIDAGRAY_CDECLVSPASCAL_STOPLEAK_Context, &DavidAGray_CDeclVsPascal_StopLeakHandle)
#endif
//
// UnRegister with ETW
//
#ifndef EventUnregisterDavidAGray_CDeclVsPascal_StopLeak
#define EventUnregisterDavidAGray_CDeclVsPascal_StopLeak() McGenEventUnregister(&DavidAGray_CDeclVsPascal_StopLeakHandle)
#endif
//
// Event Macro for ERR_CFREESLOTSSTACK_GROWSTACK_NO_MEMORY
//
#define EventWriteERR_CFREESLOTSSTACK_GROWSTACK_NO_MEMORY()\
EventEnabledERR_CFREESLOTSSTACK_GROWSTACK_NO_MEMORY() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_CFREESLOTSSTACK_GROWSTACK_NO_MEMORY)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_CFREESLOTSSTACK_CFREESLOTSSTACK_NO_MEMORY
//
#define EventWriteERR_CFREESLOTSSTACK_CFREESLOTSSTACK_NO_MEMORY()\
EventEnabledERR_CFREESLOTSSTACK_CFREESLOTSSTACK_NO_MEMORY() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_CFREESLOTSSTACK_CFREESLOTSSTACK_NO_MEMORY)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_SWEEPSTACK_BAD_HEAP_129
//
#define EventWriteERR_SWEEPSTACK_BAD_HEAP_129()\
EventEnabledERR_SWEEPSTACK_BAD_HEAP_129() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_SWEEPSTACK_BAD_HEAP_129)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_SWEEPSTACK_BAD_HEAP_146
//
#define EventWriteERR_SWEEPSTACK_BAD_HEAP_146()\
EventEnabledERR_SWEEPSTACK_BAD_HEAP_146() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_SWEEPSTACK_BAD_HEAP_146)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_SWEEPSTACK_BAD_HEAP_162
//
#define EventWriteERR_SWEEPSTACK_BAD_HEAP_162()\
EventEnabledERR_SWEEPSTACK_BAD_HEAP_162() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_SWEEPSTACK_BAD_HEAP_162)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_SWEEPSTACK_ARITH_FAIL
//
#define EventWriteERR_SWEEPSTACK_ARITH_FAIL()\
EventEnabledERR_SWEEPSTACK_ARITH_FAIL() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_SWEEPSTACK_ARITH_FAIL)\
: ERROR_SUCCESS\
//
// Event Macro for ERR_PVT_GETSLOTINDEX_NO_MEMORY
//
#define EventWriteERR_PVT_GETSLOTINDEX_NO_MEMORY()\
EventEnabledERR_PVT_GETSLOTINDEX_NO_MEMORY() ?\
TemplateEventDescriptor(DavidAGray_CDeclVsPascal_StopLeakHandle, &ERR_PVT_GETSLOTINDEX_NO_MEMORY)\
: ERROR_SUCCESS\
//
// Event Macro for EVT_DataBlockRegistration
//
#define EventWriteEVT_DataBlockRegistration(plpDataBlock, penmSourceHeapType, phHeap, pfIsProtected, ullWhenAdded)\
EventEnabledEVT_DataBlockRegistration() ?\
Template_qqqqx(DavidAGray_CDeclVsPascal_StopLeakHandle, &EVT_DataBlockRegistration, plpDataBlock, penmSourceHeapType, phHeap, pfIsProtected, ullWhenAdded)\
: ERROR_SUCCESS\
//
// Event Macro for EVT_SweepStackState
//
#define EventWriteEVT_SweepStackState(ullmLastSweepStart, ullmThisSweepStart, intLastOccupiedSlot, intTotalSlots)\
EventEnabledEVT_SweepStackState() ?\
Template_xxdd(DavidAGray_CDeclVsPascal_StopLeakHandle, &EVT_SweepStackState, ullmLastSweepStart, ullmThisSweepStart, intLastOccupiedSlot, intTotalSlots)\
: ERROR_SUCCESS\
//
// Event Macro for EVT_SweepItem
//
#define EventWriteEVT_SweepItem(SlotIndex, BlockAddress, BlockHeap, ullWhenRegistered, ullAge, Disposition, VacantSlotIndex)\
EventEnabledEVT_SweepItem() ?\
Template_dqqxxqd(DavidAGray_CDeclVsPascal_StopLeakHandle, &EVT_SweepItem, SlotIndex, BlockAddress, BlockHeap, ullWhenRegistered, ullAge, Disposition, VacantSlotIndex)\
: ERROR_SUCCESS\
列表 2 显示了 LeakStop_EventTraceProvider.h
的有趣片段,它包含生成引发跟踪事件的函数调用的宏。由于它们的用法是内部的,我跳过了启用检查宏。
列表 2 中显示的事件引发宏分为两组。
- 以
EventWriteERR
开头的宏旨在在控制权移交给已注册的结构化异常处理例程(通常终止进程)之前报告异常。这些事件会引发到Application通道,该通道始终在监听;因此,在极其不可能发生异常的情况下,它们会在Application事件日志中记录一个STOP错误。 - 名称以
EventWriteEVT
开头的宏会引发跟踪事件。除非有内容监听DavidAGray-CDeclVsPascal-StopLeak-Debugging
自定义通道,否则不会引发这些事件,代码的行为就好像它们不存在一样。
这些函数式宏可以轻松地为您的代码添加仪器,以进行尽可能详细的跟踪日志记录。
由于这是较复杂的情况之一,我将演示 EventWriteEVT_DataBlockRegistration
,它接受表 3 中列出和描述的五个参数。
名称 | 值 |
---|---|
plpDataBlock | 指向从某个堆分配的数据块的 void 指针 |
penmSourceHeapType | 枚举类型,标识支持的五个堆类型中的哪一个 |
phHeap | 对于任何进程堆,返回 GetProcessHeap() 的默认进程堆的句柄,或 HeapCreate() 返回的私有堆的句柄,对于所有其他类型为 NULL |
pfIsProtected | 如果块受保护,则为 TRUE ,不应在下次扫描时丢弃 |
ullWhenAdded | 自上次 Windows 重新启动以来,块添加时的 100 纳秒精确时间 |
我最初的设计将 ullWhenAdded
实现为由 GetSystemTimeAsFiletime 设置的 FILETIME
结构。当我阅读有关高分辨率计时器的一些内容时,它提醒我系统时钟可能随时被 Windows 时间服务(或替换它的其他 NTP 时间服务)重置,并且调整可能是向上或向下。由于两者都可能产生不准确的结果,因此进一步研究导致我替换了 QueryUnbiasedInterruptTime 函数,该函数返回一个 ULONGLONG
,一个 64 位无符号整数,表示自上次 Windows 重新启动以来的滴答数,每个滴答为 100 纳秒。QueryUnbiasedInterruptTime
是 GetTickCount 和 GetTickCount64 的高分辨率实现,因为每秒有 107 个滴答,而只有 103 毫秒。这实现了我的理想,即给我一个值,我可以将其视为 BCL System.DateTime
结构中的 Ticks
成员。
EventWriteEVT_DataBlockRegistration ( ( LONG_PTR ) plpDataBlock ,
penmSourceHeapType ,
( CDWORD ) phHeap ,
pfIsProtected ,
lpVacantSlot->ullWhenAdded );
列表 4 是对宏 EventWriteEVT_DataBlockRegistration
的直接调用,它出现在 LS_Memory_Block_Registrars.CPP
中定义的函数 LS_pvt_Register_Heap_Ptr
中。
BOOL APIENTRY DllMain
(
HMODULE phModule,
DWORD pul_reason_for_call,
LPVOID plpReserved
)
{
switch ( pul_reason_for_call )
{
case DLL_PROCESS_ATTACH:
if ( m_hProcHeap = GetProcessHeap ( ) )
{ // Process heap handle is in hand. Save the module handle, and exit.
m_hDllModule = phModule;
// ------------------------------------------------------------
// Initialize a critical section for synchronizing access to
// the stack pointer (the pointer defined at the base address
// of the structure defined by m_lsStackInfo, as opposed to the
// thread stack that is involved in function calls.
// ------------------------------------------------------------
if ( InitializeCriticalSectionAndSpinCount ( m_lsStackInfo.lpCritSect ,
m_lsStackInfo.dwCritSectSpinCount ) )
{
if ( m_lsStackInfo.lpStackBase = ( LPLS_PTR_STACK_ITEM ) AllocBytes_WW ( LS_INITIAL_STACK_SIZE_DEFAULT * sizeof ( LS_PTR_STACK_ITEM ) ) )
{
if ( m_hThreadSweep = CreateThread ( DW_UNUSED_WW ,
LS_DEFAULT_THREAD_STACK_SIZE ,
SweepStack ,
&m_lsStackInfo ,
LS_START_THREAD_IMMEDIATELY ,
&m_IDSweepertHD ) )
{
return ( !EventRegisterDavidAGray_CDeclVsPascal_StopLeak ( ) );
} // TRUE (anticipated outcome) block, if ( m_hThreadSweep = CreateThread ( DW_UNUSED_WW , LS_DEFAULT_THREAD_STACK_SIZE , SweepStack , &m_lsStackInfo , LS_START_THREAD_IMMEDIATELY , &m_IDSweepertHD ) )
else
{
return FALSE ;
} // FALSE (unanticipated outcome) block, if ( m_hThreadSweep = CreateThread ( DW_UNUSED_WW , LS_DEFAULT_THREAD_STACK_SIZE , SweepStack , &m_lsStackInfo , LS_START_THREAD_IMMEDIATELY , &m_IDSweepertHD ) )
} // TRUE (anticipated outcome) block, if ( m_lsStackInfo.lpStackBase = ( LPLS_PTR_STACK_ITEM ) AllocBytes_WW ( LS_INITIAL_STACK_SIZE_DEFAULT * sizeof ( LS_PTR_STACK_ITEM ) ) )
else
{
return FALSE ;
} // FALSE (unanticipated outcome) block, if ( m_lsStackInfo.lpStackBase = ( LPLS_PTR_STACK_ITEM ) AllocBytes_WW ( LS_INITIAL_STACK_SIZE_DEFAULT * sizeof ( LS_PTR_STACK_ITEM ) ) )
} // TRUE (anticipated outcome) block, if ( m_lsStackInfo.hStackMutex = CreateMutex ( LP_UNUSED_WW , MTX_CREATE_WITHOUT_ACQUIRING , LP_UNUSED_WW ) )
else
{
return FALSE ;
} // FALSE (unanticipated outcome) block, if ( m_lsStackInfo.hStackMutex = CreateMutex ( LP_UNUSED_WW , MTX_CREATE_WITHOUT_ACQUIRING , LP_UNUSED_WW ) )
} // TRUE (anticipated outcome) block, if ( m_hProcHeap = GetProcessHeap ( ) )
else
{ // Process heap handle is MIA; abandon ship.
return FALSE ;
} // FALSE (unanticipated outcome) block, if ( m_hProcHeap = GetProcessHeap ( ) )
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
m_dwLastThread = GetCurrentThreadId ( );
break;
case DLL_PROCESS_DETACH:
return ( !EventUnregisterDavidAGray_CDeclVsPascal_StopLeak ( ) ) ;
DEFAULT_UNREACHABLE;
} // switch ( pul_reason_for_call )
UNREFERENCED_PARAMETER ( plpReserved ) ;
return TRUE;
} // DllMain
列表 5 是 LeakStop32.dll
的整个入口点例程 DllMain,它定义在源代码模块 LeakStop32_DllMainain.CPP
中。
调用宏 EventWriteEVT_DataBlockRegistration
(如列表 4所示)非常直接。尽管函数式宏会返回值,但我选择忽略它,因为它指示的失败的概率非常低,而且除非有东西实际监听调试事件通道,否则宏将解析为无操作事件。
列表 5 显示了整个 DLLMain
入口点例程,尽管只有两行很重要。
DLL_PROCESS_ATTACH
块中的最后一条可执行语句调用了另一个宏EventRegisterDavidAGray_CDeclVsPascal_StopLeak
,该宏将事件提供程序注册到两个通道。这个函数式宏类似于 RegisterEventSource Windows API 函数,该函数在传统 Windows 事件日志域中将程序注册到事件源。- 同样,
DLL_PROCESS_DETACH
块在进程与 DLL 分离时运行,调用逆向宏EventUnregisterDavidAGray_CDeclVsPascal_StopLeak
,类似于经典的事件日志记录 DeregisterEventSource 函数。
由于这两个函数都返回 ERROR_SUCCESS
(零)来表示成功,它在布尔上下文中评估为 FALSE
,并返回一个非零系统状态代码,该代码评估为布尔值 TRUE
,一元Not运算符(!
)反转返回值,以匹配 DLL 入口点例程成功时必须返回 TRUE
的要求。这种方法比使用 IF
语句来评估返回值并使入口点例程返回 True 或 False 的传统方法更简洁、更高效。我喜欢抓住这样的机会从函数中紧凑地返回值,而布尔函数通常适合这种处理。
将事件及其通道注册到 Windows
还有一个最后的步骤,可以集成到构建过程中,那就是将事件清单注册到 Windows。
wevtutil im C:\Users\DAVE\Documents\Articles_2017\Anatomy_of_a_Stack_Frame\CvsPascal\LeakStop_Message_Strings\LeakStop_EventTraceProvider.man
列表 6 是将事件清单注册到 Windows 的命令行。没有消息显示来指示函数是否成功。
列表 6 中显示的命令注册了清单,假设解决方案安装在目录 C:\Users\DAVE\Documents\Articles_2017\Anatomy_of_a_Stack_Frame
中。由于 wevtutil.exe
位于每个 Windows 安装的 System32
目录中,该目录在 Windows PATH
列表中,因此该命令可以不带限定符。虽然此命令可以在 LeakStop_Message_Strings
项目或解决方案中的任何其他项目的 Post-Build 步骤中实现,但我没有费心,因为它是一次性事件。
到达顶峰
得益于 Windows Message Analyzer,最后的冲顶变得快速而几乎毫不费力。
从您的桌面快捷方式启动 MessageAnalyzer.exe
。一两分钟后,您将看到图 12 所示的窗口。按照图 12 到 16 的标题中描述的步骤配置您的会话。
配置好会话后,按Start按钮(右下角,图 14),然后运行您的程序。结果将类似于图 1。
您已到达顶峰。开始您的顶峰仪式和其他活动,并停留您想停留的时间。
图 12 显示了 Microsoft Message Analyzer 启动时显示的窗口。
图 13 显示了从图 12 的初始屏幕选择 New Session 时出现的窗口。
图 14 显示了从图 13 的表单中选择 Live Trace,然后选择 Add Providers 功能时出现的窗口。
图 15 是 Add Custom Provider
对话框,它在您第一次使用自定义提供程序时出现。您需要提供程序元素中的Name和GUID属性,它们是 LeakStop_EventTraceProvider.man
中的第三级元素。
关注点
我通常很难回答我是否做了什么“聪明或古怪”的事情,因为我发现我认为正常的行为常常被别人认为是一两项。尽管如此,以下是我认为值得注意的几点。
- 我已经在
DLLMain
中提到了return ( !EventRegisterDavidAGray_CDeclVsPascal_StopLeak ( ) );
和return ( !EventUnregisterDavidAGray_CDeclVsPascal_StopLeak ( ) ) ;
。 - 在我代码的各个地方,都有许多形式为
if (m_hProcHeap = GetProcessHeap())
的语句,它们合并了保存和测试函数返回值。 - 更聪明的是形式为
if ( InitializeCriticalSectionAndSpinCount ( m_lsStackInfo.lpCritSect , m_lsStackInfo.dwCritSectSpinCount ) )
的语句,其中返回值在原地进行测试,而无需先将其从EAX
寄存器复制到内存位置,而该内存位置从未被再次引用。这种测试会解析为两条非常快的机器指令,一条CMP
(比较)指令,后跟一条条件跳转指令。 - 由于它直接引用结构成员,因此
DllMain
中的代码不会浪费时间在运行时计算成员偏移地址。相同的技术也可以应用于线程过程SweepStack
,因为它们都在LeakStop32_DllMainain.CPP
中定义。将其保留为当前状态是为了演示其区别。由于该优化超出了本文的范围,因此将此调查留给感兴趣的读者,他们可以研究LeakStop32
项目的任何中间输出目录中的LeakStop32_DllMainain.asm
。
有人告诉我,研究我的代码就像剥洋葱或卷心菜或生菜一样。尽情研究吧!
历史
2017 年 6 月 8 日星期四,标志着首次发布给 Code Project 编辑。