分析概述

探查器是监视另一个应用程序的执行的工具。 公共语言运行时(CLR)探查器是一个动态链接库(DLL),它由从 CLR 接收消息并使用分析 API 将消息发送到 CLR 的函数组成。 探查器 DLL 在运行时由 CLR 加载。

传统的分析工具侧重于测量应用程序的执行。 也就是说,它们测量在每次函数中花费的时间或应用程序随时间推移的内存使用情况。 分析 API 面向更广泛的诊断工具类,例如代码覆盖率实用工具,甚至高级调试辅助工具。 这些用途本质上都是诊断性的。 分析 API 不仅度量值,还监视应用程序的执行。 因此,分析 API 不应由应用程序本身使用,并且应用程序的执行不应依赖于探查器(或受探查器影响)。

分析 CLR 应用程序需要比分析常规编译的计算机代码更多的支持。 这是因为 CLR 引入了应用程序域、垃圾回收、托管异常处理、代码实时(JIT)编译(将公共中间语言或 CIL、代码转换为本机计算机代码)等概念,以及类似的功能。 传统分析机制无法识别或提供有关这些功能的有用信息。 分析 API 有效地提供了此缺失的信息,对 CLR 和分析应用程序的性能影响最小。

运行时的 JIT 编译为分析提供了良好的机会。 分析 API 使探查器能够在 JIT 编译之前更改例程的内存中 CIL 代码流。 通过这种方式,探查器可以将检测代码动态添加到需要更深入调查的特定例程。 尽管这种方法在传统方案中是可能的,但使用分析 API 为 CLR 实现要容易得多。

分析 API

通常,分析 API 用于编写 代码探查器,它是监视托管应用程序的执行的程序。

分析 API 由探查器 DLL 使用,该 DLL 加载到与所分析的应用程序相同的进程中。 探查器 DLL 实现回调接口(.NET Framework 版本 1.0 和 1.1 中的 ICorProfilerCallback2 版本 2.0 及更高版本中)。 CLR 调用该接口中的方法,以通知探查器分析过程中的事件。 探查器可以使用 ICorProfilerInfo 和 ICorProfilerInfo2 接口中的方法来获取有关所分析应用程序状态的信息,从而回调运行时。

注释

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

下图显示了探查器 DLL 如何与正在分析的应用程序和 CLR 交互。

显示分析体系结构的屏幕截图。

通知接口

ICorProfilerCallbackICorProfilerCallback2 可被视为通知接口。 这些接口由 ClassLoadStartedClassLoadFinishedJITCompilationStarted 等方法组成。 每次 CLR 加载或卸载类、编译函数等时,都会调用探查器 ICorProfilerCallbackICorProfilerCallback2 接口中的相应方法。

例如,探查器可以通过两个通知函数来度量代码性能: FunctionEnter2FunctionLeave2。 它只是对每个通知进行时间戳、累积结果并输出一个列表,该列表指示哪些函数在执行应用程序期间消耗了最大 CPU 或时钟时间。

信息检索接口

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

支持的功能

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

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

  • CLR 启动和关闭事件。

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

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

  • 模块加载和卸载事件。

  • COM vtable 创建和销毁事件。

  • 实时 (JIT) 编译和代码投向事件。

  • 类加载和卸载事件。

  • 线程创建和销毁事件。

  • 函数进入和退出事件。

  • 异常。

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

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

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

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

可以从任何(非托管)COM 兼容语言调用分析 API。

API 在 CPU 和内存消耗方面非常高效。 分析不涉及对已分析应用程序所做的更改,这些更改足以引起误导性的结果。

分析 API 对采样和非采样探查器都很有用。 采样探查器在常规时钟时钟刻度检查配置文件,例如,相距 5 毫秒。 非采样探查器会与导致事件的线程同步通知事件。

不支持的功能

分析 API 不支持以下功能:

  • 非托管代码,必须使用传统的 Win32 方法进行分析。 但是,CLR 探查器包括转换事件,以确定托管代码和非托管代码之间的边界。

  • 出于面向方面编程等目的,自行修改其代码的应用程序。

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

  • 远程分析由于以下原因不受支持:

    • 远程分析延长执行时间。 使用分析接口时,必须尽量减少执行时间,以便分析结果不会受到过度影响。 当正在监视执行性能时,这尤其如此。 但是,当分析接口用于监视内存使用情况或获取有关堆栈帧、对象等的运行时信息时,远程分析并不受到限制。

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

通知线程

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

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

安全性

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

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

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

写入错误的探查器可能会导致循环引用本身,从而导致不可预知的行为。

对 CLR 分析 API 的评审可能会给人留下一种印象,即可以编写包含通过 COM 互作或间接调用相互调用的托管和非托管组件的探查器。

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

CLR 探查器可以安全地调用托管代码的唯一位置是方法的公共中间语言(CIL)正文。 修改 CIL 正文的建议做法是在 ICorProfilerCallback4 接口中使用 JIT 重新编译方法。

也可以使用较旧的检测方法修改 CIL。 在函数的实时 (JIT) 编译完成之前,探查器可以在方法的 CIL 正文中插入托管调用,然后 JIT 编译它(请参阅 ICorProfilerInfo::GetILFunctionBody 方法)。 此方法可以成功用于对托管代码进行选择性检测,或收集有关 JIT 的统计信息和性能数据。

或者,代码探查器可以在调用非托管代码的每个托管函数的 CIL 正文中插入本机挂钩。 此方法可用于检测和覆盖范围。 例如,代码探查器可以在每个 CIL 块之后插入检测挂钩,以确保已执行该块。 方法的 CIL 体修改是一项非常微妙的作,需要考虑许多因素。

分析非托管代码

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

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

  • 确定堆栈链是对应于托管代码还是本机代码。

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

在 .NET Framework 2.0 及更高版本中,可以对此功能使用 ICorProfilerInfo2::D oStackSnapshot 方法。

使用 COM

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

调用堆栈

分析 API 提供了两种方法来获取调用堆栈:一种堆栈快照方法,该方法可实现调用堆栈的稀疏收集,以及一个阴影堆栈方法,该方法在每次跟踪调用堆栈。

堆栈快照

堆栈快照是线程堆栈的跟踪,即刻。 分析 API 支持跟踪堆栈上的托管函数,但它会将非托管函数的跟踪留给探查器自己的堆栈演练程序。

有关如何对探查器进行编程以遍历托管堆栈的详细信息,请参阅本文档集中的 ICorProfilerInfo2::D oStackSnapshot 方法,以及 .NET Framework 2.0 中的 Profiler Stack Walk:Basics and Beyond

阴影堆栈

使用快照方法过于频繁,可以快速创建性能问题。 如果要频繁执行堆栈跟踪,探查器应改用 FunctionEnter2FunctionLeave2FunctionTailcall2ICorProfilerCallback2 异常回调来生成阴影堆栈。 阴影堆栈始终是最新的,每当需要堆栈快照时,都可以快速复制到存储。

阴影堆栈可以获取有关泛型实例化的函数参数、返回值和信息。 此信息只能通过阴影堆栈获取,当控制权移交给函数时,可能会获取此信息。 但是,在函数运行过程中,此信息可能稍后不可用。

回调和堆栈深度

探查器回调可能会在非常受堆栈约束的情况下发出,探查器回调中的堆栈溢出将导致即时进程退出。 探查器应确保尽可能少地使用堆栈来响应回调。 如果探查器适用于针对堆栈溢出可靠的进程,探查器本身还应避免触发堆栈溢出。

Title Description
设置分析环境 介绍如何初始化探查器、设置事件通知和分析 Windows 服务。
分析接口 描述分析 API 使用的非托管接口。
分析全局静态函数 描述分析 API 使用的非托管全局静态函数。
分析枚举 描述分析 API 使用的非托管枚举。
分析结构 描述分析 API 使用的非托管结构。