分析概述

探查器是一个监视另一应用程序的执行情况的工具。 公共语言运行时 (CLR) 探查器是一个由函数组成的动态链接库 (DLL),这些函数通过使用分析 API 接收来自 CLR 的消息或将消息发送给 CLR。 探查器 DLL 由 CLR 在运行时加载。

传统的分析工具主要用于测量应用程序的执行情况。 也就是说,这些工具将测量花在每个函数上的时间或一段时间内应用程序的内存使用量。 分析 API 针对较广泛的一类诊断工具,如代码覆盖率实用工具,甚至是高级调试辅助工具。 这些工具的用途本质上都是为了进行诊断。 分析 API 不仅测量而且还监视应用程序的执行情况。 因此,应用程序本身决不应该使用分析 API,并且应用程序的执行情况不应该取决于探查器(或受其影响)。

与分析常规编译的机器码相比,分析 CLR 应用程序需要更多的支持。 这是因为 CLR 引入了一些概念,例如应用程序域、垃圾回收、托管的异常处理、代码实时 (JIT) 编译(将 Microsoft 中间语言 (MSIL) 代码转换成本机代码)和类似功能。 常规分析机制无法识别或提供有关这些功能的有用信息。 分析 API 可以有效地提供此缺少的信息,并且对 CLR 和分析的应用程序的性能影响最小。

通过进行运行时 JIT 编译,可以很好地进行分析。 通过使用分析 API,探查器在对内存中的 MSIL 代码流进行 JIT 编译之前可以为例程更改此代码流。 通过这种方式,探查器可以向需要更深入调查的特定例程动态添加检测代码。 虽然这种方法在一般情况下可行,但是对于 CLR 来说,使用分析 API 进行实现更加简单。

本概述包含以下几部分:

  • 分析 API

  • 支持的功能

  • 通知线程

  • 安全性

  • 在代码探查器中组合托管代码和非托管代码

  • 分析非托管代码

  • 使用 COM

  • 调用堆栈

  • 回调和堆栈深度

  • 相关主题

分析 API

通常,分析 API 用于编写“代码探查器”,代码探查器是一个监视托管应用程序执行情况的程序。

探查器 DLL 将使用此分析 API,前者将加载到与正在被分析的应用程序相同的进程中。 此探查器 DLL 将实现一个回调接口(在 .NET Framework 1.0 和 1.1 版本中为 ICorProfilerCallback,在 2.0 版本及更高版本中为 ICorProfilerCallback2)。 CLR 将调用该接口中的方法来通知探查器有关分析进程中的事件的情况。 通过使用 ICorProfilerInfoICorProfilerInfo2 接口中的方法获取有关分析应用程序状态的信息,此探查器可以回调到运行时中。

注意注意

在与分析应用程序相同的进程中只应该运行探查器解决方案的数据收集部分。所有用户接口和数据分析都应该在单独的进程中执行。

下图演示探查器 DLL 如何与正在被分析的应用程序以及 CLR 进行交互。

分析体系结构

分析体系结构

通知接口

ICorProfilerCallbackICorProfilerCallback2 可被视为通知接口。 这些接口由诸如 ClassLoadStartedClassLoadFinishedJITCompilationStarted 之类的方法组成。 CLR 在每次执行加载或卸载类、编译函数等操作时,它将在探查器的 ICorProfilerCallbackICorProfilerCallback2 接口中调用相应的方法。

例如,探查器可以通过以下两个通知函数来测量代码性能:FunctionEnter2FunctionLeave2。 它仅对每个通知加时间戳、累积结果并输出一个列表,该列表指出了在执行应用程序过程中耗用大部分 CPU 或时钟时间的函数。

信息检索接口

分析中涉及的其他主要接口为 ICorProfilerInfoICorProfilerInfo2。 探查器会根据需要调用这些接口,以获取更多信息来帮助进行分析。 例如,每当 CLR 调用 FunctionEnter2 函数时,它都会提供函数标识符。 通过调用 ICorProfilerInfo2::GetFunctionInfo2 方法来发现函数的父类、其名称等,探查器可以获得有关该函数的更多信息。

返回页首

支持的功能

分析 API 提供有关在公共语言运行时中发生的各种事件和操作的信息。 您可以使用此信息监视进程的内部工作并分析 .NET Framework 应用程序的性能。

分析 API 检索有关在 CLR 中发生的以下操作和事件的信息:

  • CLR 启动和关闭事件。

  • 应用程序域的创建和关闭事件。

  • 程序集的加载和卸载事件。

  • 模块的加载和卸载事件。

  • COM vtable 的创建和析构事件。

  • 实时 (JIT) 编译和代码间距调整事件。

  • 类的加载和卸载事件。

  • 线程的创建和析构事件。

  • 函数的进入和退出事件。

  • 异常。

  • 在执行托管代码与执行非托管代码之间进行的转换。

  • 在不同运行时上下文之间进行的转换。

  • 有关运行时挂起的信息。

  • 有关运行时内存堆和垃圾回收活动的信息。

可通过任何与(非托管)COM 兼容的语言来调用分析 API。

在 CPU 和内存消耗方面,API 的效率高。 分析不包括对分析的应用程序所做的更改,这些更改很重要,足以产生令人误解的结果。

分析 API 对于取样和非取样探查器都很有用。 “取样探查器”每隔一定的时钟滴答数(例如每隔 5 毫秒)便会检查一次配置文件。 “非取样探查器”与引起事件的线程同步获悉事件。

不支持的功能

分析 API 不支持以下功能:

  • 非托管代码,必须使用常规的 Win32 方法进行分析。 但是,CLR 探查器添加了过渡事件以确定托管代码与非托管代码之间的界限。

  • 出于某些目的(如面向方面的编程)而修改其自己的代码的自修改应用程序。

  • 界限检查,这是因为分析 API 未提供此信息。 CLR 为所有托管代码的界限检查提供了内部支持。

  • 远程分析,出于以下原因,不支持远程分析:

    • 远程分析延长了执行时间。 在使用分析接口时,必须使执行时间降到最少,以便分析结果不会受到不良影响。 特别是当正在监视执行性能时,更应该这么做。 但是,在使用分析接口监视内存使用情况或者获取有关堆栈帧、对象等方面的运行时信息时,远程分析不是限制。

    • CLR 代码探查器必须向正在运行分析应用程序的本地计算机上的运行时注册一个或多个回调接口。 这限制了创建远程代码探查器的能力。

  • 在具有高可用性要求的生产环境中进行分析。 为了支持开发时诊断,已经创建了分析 API。 尚未进行支持生产环境所需的严格测试。

返回页首

通知线程

大多数情况下,生成事件的线程也会执行通知。 此类通知(例如,FunctionEnterFunctionLeave)无需提供显式的 ThreadID。 同时,探查器可能决定使用线程本地存储区来存储和更新其分析块,而不是基于受影响线程的 ThreadID 对全局存储区中的分析块进行索引。

请注意,这些回调未经过序列化。 用户必须通过创建线程安全的数据结构,并通过在必须防止从多个线程中进行并行访问的位置锁定探查器代码来保护其代码。 因此,在某些情况下,您可能会收到不正常的回调序列。 例如,假设托管应用程序正在生成执行相同代码的两个线程。 这种情况下,在收到 ICorProfilerCallback::JITCompilationFinished 回调之前,将可能会从一个线程中收到某个函数的 ICorProfilerCallback::JITCompilationStarted 事件,并从另一个线程中收到 FunctionEnter 回调。 在这种情况下,对于可能尚未完全实时 (JIT) 编译的函数,用户将会收到 FunctionEnter 回调。

返回页首

安全性

探查器 DLL 是一个作为公共语言运行时执行引擎的一部分运行的非托管 DLL。 因此,探查器 DLL 中的代码不受托管代码访问安全性的限制。 操作系统对运行分析应用程序的用户施加的限制是探查器 DLL 上仅有的限制。

探查器作者应该采取合适的预防措施以避免产生与安全有关的问题。 例如,在安装过程中,应该将探查器 DLL 添加到访问控制列表 (ACL) 中,以便恶意用户无法修改它。

返回页首

在代码探查器中组合托管代码和非托管代码

编写不正确的探查器可能会造成循环引用自身,从而导致不可预知的行为。

通过查看 CLR 分析 API,会让人产生这样一个印象:即您可以编写包含托管组件和非托管组件的探查器,这些组件通过 COM 互操作或间接调用互相调用。

尽管从设计角度来说这是可行的,但分析 API 不支持托管组件。 CLR 探查器必须完全处于非托管状态。 尝试在 CLR 探查器中组合托管代码和非托管代码可能会导致访问冲突、程序故障或死锁。 探查器的托管组件会激发事件回到其非托管组件,后者随后又将再次调用托管组件,从而导致循环引用。

CLR 探查器中唯一可以安全调用托管代码的位置是在方法的 Microsoft 中间语言 (MSIL) 体中。 在函数的实时 (JIT) 编译完成之前,探查器可以在方法的 MSIL 体中插入托管调用,然后对其进行 JIT 编译(请参见 ICorProfilerInfo::GetILFunctionBody 方法)。 这种技术可以成功地用于托管代码的选择性检测,或者用于收集有关 JIT 的统计信息和性能数据。

或者,代码探查器可以在调入非托管代码的每个托管函数的 MSIL 体中插入本机挂钩。 这种技术可用于检测和覆盖。 例如,代码探查器可以在每个 MSIL 块之后插入检测挂钩,以确保该块已执行。 修改方法的 MSIL 体是一项非常细致的操作,有许多因素应考虑在内。

返回页首

分析非托管代码

公共语言运行时 (CLR) 分析 API 为分析非托管代码提供了最低支持。 提供了下列功能:

  • 堆栈链的枚举。 此功能使代码探查器能够确定托管代码和非托管代码之间的界限。

  • 确定堆栈链是否与托管代码或本机代码相对应。

在 .NET Framework 1.0 和 1.1 版中,可通过 CLR 调试 API 的进程内子集使用这些方法。 它们在 CorDebug.idl 文件中定义,CLR 调试概述中对此进行了介绍。

在 .NET Framework 2.0 及更高版本中,您可以使用 ICorProfilerInfo2::DoStackSnapshot 方法以实现此功能。

返回页首

使用 COM

尽管分析接口被定义为 COM 接口,但公共语言运行时 (CLR) 不会实际初始化 COM 以使用这些接口。 原因是为了避免在托管应用程序有机会指定其所需的线程模型之前不得不通过使用 CoInitialize 函数设置线程模型。 同样,探查器本身不应调用 CoInitialize,因为它可能会选取与所分析应用程序不兼容的线程模型,并可能会导致应用程序失败。

返回页首

调用堆栈

分析 API 提供了两种方法来获取调用堆栈:允许以分散方式收集调用堆栈的堆栈快照方法,以及时刻跟踪调用堆栈的隐藏堆栈方法。

堆栈快照

堆栈快照是线程堆栈在某一时刻的描图。 分析 API 支持在堆栈上跟踪托管函数,但它会将跟踪非托管函数的工作交给探查器自己的堆栈审核器来完成。

有关如何对探查器进行编程以审核托管堆栈的更多信息,请参见本文档集中的 ICorProfilerInfo2::DoStackSnapshot 方法,以及 MSDN Library 中的 Profiler Stack Walking in the .NET Framework 2.0: Basics and Beyond(.NET Framework 2.0 中的探查器堆栈审核:基础和超越)。

隐藏堆栈

过于频繁地使用快照方法可能会很快导致性能问题。 如果您想要频繁进行堆栈跟踪,探查器应通过使用 FunctionEnter2FunctionLeave2FunctionTailcall2ICorProfilerCallback2 异常回调改为生成隐藏堆栈。 隐藏堆栈始终是最新的,并且可以在需要堆栈快照时随时快速复制到存储区。

隐藏堆栈可以获取函数参数、返回值以及有关泛型实例化的信息。 只有通过隐藏堆栈才能使用此信息,并且可以在控制传递到函数时获取此信息。 但是,此信息在稍后函数运行过程中可能不可用。

返回页首

回调和堆栈深度

在堆栈受到严重约束的情形下,可能会发出探查器回调,并且探查器回调中的堆栈溢出将导致进程立即退出。 探查器应确保尽可能少使用堆栈来响应回调。 如果想要依据面对堆栈溢出时非常稳固的进程使用探查器,探查器本身还应避免触发堆栈溢出。

返回页首

相关主题

标题

说明

.NET Framework 2.0 中的分析

介绍 .NET Framework 2.0 及更高版本中对分析功能的更改和改进。

设置分析环境

说明如何初始化探查器、设置事件通知和分析 Windows 服务。

分析 API 中的加载程序回调

讨论为加载应用程序域、程序集、模块和类而发出的回调。

分析 API 中的垃圾回收

说明如何触发、检测和阻止垃圾回收。

分析 API 中的对象跟踪

说明如何能够跟踪在垃圾回收过程中移动的对象。

分析 API 中的对象检查

说明控查器如何能够使用元数据来获取有关对象的信息。

分析 API 中的异常处理

讨论探查器如何能够监控异常事件。

分析 API 中的代码生成

描述探查器如何能够控制自动和手动代码生成。

分析和运行时通知 ID

讨论公共语言运行时传递给探查器的类、线程和应用程序域 ID。

分析 API 方法约定

讨论 HRESULT 返回值、如何分配返回缓冲区以供分析 API 使用,以及可选输出参数的使用。

分析接口

描述分析 API 使用的非托管接口。

分析全局静态函数

描述分析 API 使用的非托管全局静态函数。

分析枚举

描述分析 API 使用的非托管枚举。

分析结构

描述分析 API 使用的非托管结构。

返回页首