堆栈遍历
本文面向的是对构建用于检查托管应用程序的分析器感兴趣的读者。我将描述如何编写分析器,以在 .NET Framework 的公共语言运行库 (CLR) 中遍历托管堆栈。我将尽力保持轻松的心情,因为主题本身的进展有时会非常艰难。
在 CLR 的 2.0 版本中分析 API 有一个名为 DoStackSnapshot 的新方法,它允许分析器遍历正在分析的应用程序的调用堆栈。CLR 的 1.1 版通过进程内调试接口提供了类似功能。但使用 DoStackSnapshot 遍历调用堆栈更容易、更准确且更稳定。DoStackSnapshot 方法使用的堆栈遍历器与垃圾收集器、安全系统、异常系统等使用的堆栈遍历器相同。因此,您知道它必须运转正常。
访问完整的堆栈跟踪可使分析器用户在发生值得关注的事件时,对应用程序运行情况有一个全面的了解。根据应用程序及用户想要分析的内容,您可以假设用户在分配对象、加载类、引发异常时需要调用堆栈。即使所获得的是应用程序事件以外事件(例如计时器事件)的调用堆栈,也仍然会引起采样分析器的关注。如果您看到谁调用了包含热点的函数,则查看代码形式的热点将会变得更加有启迪作用。
我将侧重于通过 DoStackSnapshot API 获取堆栈跟踪。获取堆栈跟踪的另一方法是通过构建影子堆栈:可挂接 FunctionEnter 和 FunctionLeave,以保存当前线程的托管调用堆栈的副本。如果您在应用程序执行期间始终需要堆栈信息,如果您不介意在每次执行托管调用及返回时运行分析器的代码所产生的性能成本,则影子堆栈构建将会非常有用。如果很少需要报告堆栈(例如,为了响应事件),则 DoStackSnapshot 将是极佳的方法。即使采样分析器每隔几毫秒便拍一次堆栈快照,其频率也要比构建影子堆栈低。因此,DoStackSnapshot 非常适合采样分析器。
谨慎地进行堆栈遍历
如果您希望能够在需要时随时获取调用堆栈,这将非常有用。但是与能力随之而来的还有责任。分析器用户不会希望堆栈遍历在运行时导致访问违例 (AV) 或死锁。作为分析器编写者,您必须谨慎行使您的权力。我将讨论如何使用 DoStackSnapshot,以及如何小心地执行此操作。如您所见,您想利用此方法执行的操作越多,操作就越难以正确执行。
让我们看一下我们的主题。以下是分析器调用的内容(可在 Corprof.idl 的 ICorProfilerInfo2 接口中找到):
- HRESULT DoStackSnapshot(
- [in] ThreadID thread,
- [in] StackSnapshotCallback *callback,
- [in] ULONG32 infoFlags,
- [in] void *clientData,
- [in, size_is(contextSize), length_is(contextSize)] BYTE context[],
- [in] ULONG32 contextSize);
下列代码是 CLR 在分析器上调用的内容(也可在 Corprof.idl 中找到)。向上例的 callback 参数中的此函数实现传递指针。
- typedef HRESULT __stdcall StackSnapshotCallback(
- FunctionID funcId,
- UINT_PTR ip,
- COR_PRF_FRAME_INFO frameInfo,
- ULONG32 contextSize,
- BYTE context[],
- void *clientData);
这像是一块三明治。在分析器想要遍历堆栈时,调用 DoStackSnapshot。在 CLR 从该调用返回之前,它调用 StackSnapshotCallback 函数多次,即,为堆栈上的每一个托管帧或每一组非托管帧调用一次该函数。图 1 显示了此三明治结构。
堆栈遍历: 分析期间的调用“三明治”
正如您从我的注释中所看到的,CLR 会将这些帧告知给您,但告知的顺序与这些帧被推入到堆栈中的顺序正好相反。即,最先告知叶节点帧(被最后推入),最后告知主节点帧(被最先推入)。
这些函数的所有参数有何意义?我不准备对它们进行逐一讨论,但我将从 DoStackSnapshot 开始讨论其中的一部分(我将利用一小部分时间讨论余下部分)。infoFlags 值来自 Corprof.idl 中的 COR_PRF_SNAPSHOT_INFO 枚举,它允许您控制 CLR 是否为您提供它所报告的帧的寄存器上下文。您可为 clientData 指定您所需要的任何值,并且 CLR 将在 StackSnapshotCallback 调用中返回该值。
在 StackSnapshotCallback 中,CLR 使用 funcId 参数向您传递当前遍历的帧的 FunctionID 值。如果当前帧是一组非托管帧,则该值为 0(我稍后会加以介绍)。如果 funcId 值是一个非零值,则您可向其他方法(例如 GetFunctionInfo2 和 GetCodeInfo2)传递 funcId 和 frameInfo,以获得有关该函数的更多信息。您可在堆栈遍历过程中立即获得此函数信息,或者保存 funcId 值并在以后获取函数信息,以减少对运行中的应用程序的影响。如果您是在以后获取函数信息,请记住 frameInfo 值仅在为您提供的回调内有效。尽管可以保存 funcId 值以供以后使用,但是切勿保存 frameInfo 值以备日后使用。
当您从 StackSnapshotCallback 返回时,通常会返回 S_OK,并且 CLR 将继续遍历堆栈。如果需要的话,也可返回 S_FALSE,这将停止堆栈遍历。然后,DoStackSnapshot 调用会返回 CORPROF_E_STACKSNAPSHOT_ABORTED。
【编辑推荐】