本文基于 .NET Framework 2.0(原来的代号为“Whidbey”)的预发布版本。文中包含的所有信息均有可能变更。
本文讨论:
| 
 | 本文使用下列技术:
 | 

本页内容
|   | 有哪些新功能? | 
|   | 哪些功能消失了? | 
|   | 获得线程信息 | 
|   | 堆栈快照 | 
|   | 增强的跟踪 | 
|   | 描述和解码函数参数 | 
|   | 小结 | 
从 .NET 开始,公共语言运行库 (CLR) Profiling API 已经成为用来检查运行库在幕后所做工作的机制。很多分析器只是简单地报告在给定例程、文件或类中花费了多少时间,但 Profiling API 在所提供的数据的数量和类型方面更为完整。有关进程中加载和使用的应用程序域、程序集和类的信息以及有关即时 (JIT) 编译器通知、内存使用率跟踪、事件跟踪、异常跟踪、托管到非托管代码转换以及运行库状态的信息 — 所有上述类别的信息都可以通过使用 Profiling API 为 .NET Framework 1.0 和 1.1 编写的分析器得到。
您将在 .NET Framework 2.0 Beta 1 中发现大大增强的 Profiling API。在本文中,我将讨论为什么进行这些更改,并且将说明开发人员如何利用它们。
如果您以前尚未使用过 Profiling API,那么在我开始讨论新的增强功能之前,请让我解释一下它的功能。首先,使用 Profiling API 需要有实现了 ICorProfilerCallback 接口的非托管 COM 服务器 DLL。此外,您需要设置两个环境变量(COR_PROFILER 和 COR_ENABLE_PROFILING),以便让 CLR 知道应当将您的 DLL 作为分析器加载。COR_PROFILER 被设置为 COM 服务器的类 ID (CLSID),而 COR_ENABLE_PROFILING 被设置为 1。
在 CLR 创建了分析器实例之后,您将在 ICorProfilerCallback 接口上收到的第一个通知是 Initialize 通知。您可以利用该机会,通过用由 COR_PRF_MONITOR 枚举定义的标志组合调用 ICorProfilerInfo::SetEventMask 来告诉 CLR 您希望接收的通知类型。ICorProfilerInfo 使您可以向 CLR 询问当前正在运行的程序并采取相应的行动。COR_PRF_MONITOR 中的标志在程序刚开始执行的时候启用,但是,在程序执行的晚些时候,可以通过用您需要的任何新标志再次调用 SetEventMask 来打开和关闭它们。
您在原来的 Profiling API 中可以完成的另一个任务是使用 ICorProfilerInfo::SetEnterLeaveFunctionHooks 调用设置回调。这使您可以向 CLR 提供回调函数,以便在每次进入或退出函数时或者在发生尾调用时调用(这些函数不返回)。当程序终止时,您会从 CLR 收到关机通知,此时您有机会对正在使用的任何资源执行最后的清理。使用原来的 Profiling API,可以用很多不同的方式对正在运行的程序进行研究,但是与所有事物一样,它还有改进的余地。
有关 Profiling API 的更多背景知识,请参阅我在 MSDN ®Magazine 的 2003 年 1 月刊中对它进行的概述(文章标题为 Inspect and Optimize Your Program's Memory Usage with the .NET Profiler API)以及 Aleksandr Mikunov 的有关使用 Profiling API 重新编写 MSIL 代码的文章(位于 MSDN Magazine 的 2003 年 9 月刊中,标题为 Rewrite MSIL Code on the Fly with the .NET Framework Profiling API )。
有哪些新功能?
Microsoft 的分析团队倾听了开发人员的呼声,并且在 2.0 中添加了很多出色的功能。您可以用 Profiling API 完成的新工作包括跟踪托管线程的名称和应用程序域,无须使用进程内调试即可获得函数参数和返回值,检查这些参数的值,以及快速获得堆栈跟踪。
大多数新功能是通过两个新接口公开的:ICorProfilerCallback2 和 ICorProfilerInfo2。ICorProfilerCallback2 接口从原来的 ICorProfilerCallback 接口继承,并且添加了一个新方法以跟踪线程名称更改。本文稍后将在有关线程信息的部分中对此进行讨论。ICorProfilerInfo2 接口从 ICorProfilerInfo 继承,并且添加了 13 个新方法以控制与运行库之间的交互。
可以调用 ICorProfilerInfo::SetEventMask 函数来告诉运行库希望为哪些事件接收回调。事件掩码由 COR_PRF_MONITOR 枚举组成,该枚举中已经为 .NET Framework 2.0 添加了两个新标志,如图 1 所示。
但是,这两个标志也已经被添加到 COR_PRF_IMMUTABLE 枚举值中,这意味着只能用 ICorProfilerCallback::Initialize 方法设置它们。任何稍后通过调用 ICorProilerInfo::SetEventMask 再次重置它们的尝试都将失败。COR_PRF_IMMUTABLE 由下列 COR_PRF_ MONITOR 标志组成(这些标志通过“或”运算组合在一起),如图 2 所示。
COR_PRF_ENABLE_FUNCTION_ARGS 打开了两个特定的功能。第一个是在对 FunctionEnter2 和 FunctionTailcall2(二者都是 .NET Framework 2.0 中的新增函数)的回调中获得函数参数的能力。第二个是使用 ICorProfilerInfo2::GetFunctionInfo2 函数获得一般函数的确切类 ID 的能力。如果该标志未使用并且您向 FunctionEnter2 回调进行了注册,则这些函数的 COR_PRF_FUNCTION_ARGUMENT_INFO* 和 COR_PRF_FRAME_INFO 参数将总是 NULL。COR_PRF_ ENABLE_FUNCTION_RETVAL 借助于 COR_PRF_FUNCTION_ARGUMENT_RANGE* 参数打开通过 FunctionLeave2 回调跟踪返回值的功能。如果 ICorProfilerInfo::SetEventMask 中没有设置该标志,则该参数将始终为 NULL。
让我们快速讨论一下有哪些功能从原来的 Profiling API 中消失了。然后,我们将研究如何使用 Visual Studio? 的强大的新功能。
哪些功能消失了?
该版本的 Profiling API 中淘汰的主要功能是进程内调试。这是分析器编写者用来在运行时分析函数调用的值、返回值以及程序状态的原始方法。这是通过四个 ICorProfilerInfo API 方法(如图 3 所示)和一个名为 ICorDebug 的接口完成的。ICorDebug 不再与 Profiling API 结合使用。
进程内调试的缺陷之一是,它无法在 FunctionEnter、FunctionLeave 和 FunctionTailcall 回调期间正确且可靠地提供方法参数和返回值。这是由于 ICorDebug 和 JIT 进程内编译器之间的交互具有相当复杂的性质。令人感到高兴的是,Microsoft Profiling API 团队已经开发出了性能更高的版本,该版本与原来为 ICorDebug 预想的版本具有相同的功能,从而消除了这一缺陷。
获得线程信息
以前为在分析器中获得线程信息而需要的标志是 COR_PRF_MONITOR_THREADS,该值在对 SetEventMask 进行调用期间设置。在新版本的 Profiling API 中,可以使用相同的标志,但是如果您已经实现了 ICorProfilerCallback2,则可以获得更多的信息。ThreadNameChanged 是 ICorProfilerCallback2 上的一个新方法:
HRESULT ICorProfilerCallback2::ThreadNameChanged( [in] ThreadID threadId, // ID of thread whose name was changed [in] ULONG cchName, // number of characters in new name [in] WCHAR name[]) // the new name of the thread
当通过将 System.Threading.Thread.Name 赋给一个新值,从而在 .NET 线程上设置友好的名称时,将调用 ThreadNameChanged。最初,.NET 线程未命名,但如果您要完成任何种类的真正的多线程处理,则我建议您命名它们;这在调试线程问题时可能提供无价的帮助。管理名为“GUI”和“Worker”的线程要比管理名为“0x98745837”和“0x22345873”的线程容易得多。
另一部分刚刚公开的线程信息是线程当前执行所在的 AppDomain。这可以通过对 ICorProfilerInfo2::GetThreadAppDomain 的调用检索得到:
HRESULT ICorProfilerInfo2::GetThreadAppDomain ( [in] ThreadID threadId, // ID of the thread whose AppDomain you require [out] AppDomainID *pAppDomainId) // AppDomainID for the requested thread
GetThreadAppDomain 使您可以获得与给定运行库线程相关联的当前 AppDomain 的 ID。这或许能够帮助您解决以下程序错误:当线程不再位于某个 AppDomain 中的时候,该线程试图访问特定于该 AppDomain 的资源。
堆栈快照
另一个新的 Profiling API 功能是能够请求堆栈快照。这使分析器可以通过获得每个托管帧的回调(还可能包括任何非托管帧的起始点 — 将其与非托管堆栈 Walker 结合使用非常理想)来创建堆栈跟踪。要开始获得当前线程的堆栈快照,请调用 ICorProfilerInfo2::DoStackSnapshot,如下所示:
HRESULT ICorProfilerInfo2::DoStackSnapshot (
[in] ThreadID thread,   // thread for which snapshot should be taken
[in] StackSnapshotCallback *callback, // callback for each frame
[in] ULONG32 infoFlags, // COR_PRF_SNAPSHOT_INFO flags to control
                        // the degree of information for each snapshot
clientData) // caller-specified data passed to each callback 
DoStackSnapshot 将执行当前线程的堆栈快照。在获取快照期间,对于托管堆栈的每个帧,都会调用其签名由 StackSnapshotCallback 定义的回调函数一次;在非托管链的开头,也会调用该回调函数一次。如果分析器除了跟踪托管代码以外,还跟踪非托管代码,则可以从这一位置开始执行非托管堆栈遍历;或者,它可以忽略非托管堆栈标记,而只是跟踪堆栈的托管帧。StackSnapshotCallback 函数的定义如下所示:
HRESULT StackSnapshotCallback( [in] FunctionID funcId, // function ID for reported frame; 0 for unmanaged [in] UINT_PTR ip, // instruction pointer for next instruction in frame [in] COR_PRF_FRAME_INFO frameInfo, // opaque value used to get frame info [in] ULONG32 contextSize, // size of buffer in context parameter [in] BYTE [] context, // register context of the frame; can be NULL [in] void* clientData),// data passed via DoStackSnapshot call
StackSnapshotCallback 使接收者可以分析快照当前所在堆栈的帧。这包括通过 COR_PRF_FRAME_INFO 值获得可用信息、查看寄存器信息以及读取在 clientData 字段的 DoStackSnapshot 中从原始调用方传入的任何支持数据。
不透明值 COR_PRF_FRAME_INFO 的定义如以下代码行所示:
typedef UINT_PTR COR_PRF_FRAME_INFO
COR_PRF_FRAME_INFO 用来表示堆栈帧,并且充当 ICorProfilerInfo2 上方法的标记。这使 ICorProfilerInfo2 可以检索参数并返回值信息。
COR_PRF_SNAPSHOT_INFO 枚举包含要作为 DoStackSnapshot 中的 infoFlags 参数值传递的值。图 4 中列出了这些值。
只要您不是过于经常地需要堆栈,那么在每次需要堆栈时使用 DoStackSnapshot 就很好。它还具有附带的好处,即使得交替非托管堆栈帧变得相对容易了;如果您需要托管和非托管代码范围的完整堆栈,则必须使用 DoStackSnapshot,因为它提供了非托管帧起始点。如果您只对托管堆栈遍历感兴趣,则可以使用新增的 FunctionEnter2、FunctionLeave2 和 FunctionTailcall2 跟踪函数的增强跟踪功能。通过这三个函数,可以监视代码的进度并生成“影子堆栈”。如果您经常需要堆栈跟踪,则生成影子堆栈开销较低,并且还有一项附带的好处,就是它向您提供了 DoStackSnapshot 方法从来不会提供给分析器的一般性类型参数信息。
增强的跟踪
函数跟踪是任何分析器的核心部分,因为它使您可以查看应用程序中每个函数的入口和出口。在 Profiling API 的 1.0 和 1.1 版本中,这是通过借助于 SetEnterLeaveFunctionHooks 方法设置 ICorProfilerInfo 接口上的回调函数完成的。该方法使分析器可以预订三种不同类型的函数跟踪回调:进入函数 (FunctionEnter)、离开函数 (FunctionLeave) 和执行尾调用 (FunctionTailcall)。当方法的最后一个操作是对另一个方法的调用时,发生尾调用。
这里,有趣的是堆栈从不记录对第一个方法的调用,而只是记录对第二个方法的调用。当该类型的调用或返回在 CLR 中发生时,这些回调将为每个函数执行,因此您可以获得有关应用程序在何处完成其工作的真实信息。每个回调都被传递一个函数 ID,因此分析器可以识别正在进入或退出哪个函数;可以通过使用同样为非托管访问提供的元数据 API 来解析该函数 ID。
这对只关心是否进入或退出函数的分析器而言已经足够了。但是,要弄清楚是用哪些参数调用该函数或者退出时的返回值是什么,分析器需要使用进程内调试接口。正如我在上文中提到的那样,进程内调试不是实现该目的的最佳方式,因为分析器没有办法检测这些值。
现在我们讨论 .NET Framework 2.0,在这里,Profiling API 团队已经为我们消除了使用 ICorDebug 进行进程内调试的负担。它们通过向名为 SetEnterLeaveFunctionHooks2 的 ICorProfilerInfo2 接口添加一个新方法来完成这一工作,如下所示:
HRESULT ICorProfilerInfo2::SetEnterLeaveFunctionHooks2 ( // ptr to FunctionEnter2 callback [in] FunctionEnter2 *pFuncEnter, // ptr to FunctionLeave2 callback [in] FunctionLeave2 *pFuncLeave, // ptr to FunctionTailcall2 callback [in] FunctionTailcall2 *pFuncTailcall)
为了使回调发生,必须使用 ICorProfilerInfo2::SetEventMask 设置 COR_PRF_MONITOR 标志 COR_PRF_MONITOR_ENTERLEAVE。为了获得能够检查函数参数的新功能,分析器必须设置新标志 COR_PRF_ENABLE_FUNCTION_ARGS。并且为了获得返回值,还必须设置 COR_PRF_ENABLE_FUNCTION_ RETVAL。
如果您过去已经用 Profiling API 做过什么,则您应该已经注意到原始 FunctionEnter、FunctionLeave 和 FunctionTailcall 的调用约定是 _declspec(naked)。这意味着函数的起始程序和结束程序不是由编译器为分析器设置的。分析器需要一点儿内联汇编语言来在函数的开头设置起始程序以及在函数的结尾设置结束程序。FunctionEnter2、FunctionLeave2 和 FunctionTailcall2 的新回调使用 _stdcall 调用约定,因而无须完成这些工作。
为了向新的函数跟踪回调注册,Profiling API 为 FunctionEnter2、FunctionLeave2 和 FunctionTailcall2 提供了回调实现,如下所示:
typedef void FunctionEnter2 ( [in] FunctionID funcId, // FunctionID for the function being entered [in] COR_PRF_FRAME_INFO func, // opaque value only valid during callback [in] COR_PRF_FUNCTION_ARGUMENT_INFO *argumentInfo) // function args info COR_PRF_ENABLE_FUNCTION_ARGS 在事件掩码中未进行设置,并且始终为 NULL。
在程序执行期间,FunctionEnter2 在进入几乎每个方法(尾调用除外)时调用。这发生在用以下回调指针集调用 SetEnterLeaveFunctionHooks2 之后:
typedef void FunctionLeave2 (
[in] FunctionID funcId, // FunctionID for function being left
[in] COR_PRF_FRAME_INFO func,  // opaque value only valid during callback
[in] COR_PRF_FUNCTION_ARGUMENT_RANGE *retvalRange) // info  about return
                                                   // values from function
COR_PRF_ENABLE_FUNCTION_RETVAL 在事件掩码中未进行设置,并且始终为 NULL。
在应用程序执行期间,FunctionLeave2 在退出每个方法时调用。同样,这发生在用以下回调指针集调用 SetEnterLeaveFunctionHooks2 之后:
typedef void FunctionTailcall2 ( [in] FunctionID funcId, // FunctionID for the function [in] COR_PRF_FRAME_INFO func) // opaque value only valid during callback
描述和解码函数参数
既然您在进入和退出函数的时候具有函数的回调,那么可以分析参数和返回值。Profiling API 将这些值公开为内存中的区域。可以使用函数的元数据标记来获得您要处理的参数的类型,以便可以对它们进行解码。.NET Framework 通过用 CorElementType 枚举中的值标记项目来跟踪它们。图 5 列出了所有这些值(红色项是在 .NET Framework 2.0 中添加的新枚举值)。值高于 0x22 (ELEMENT_TYPE_ MAX) 的 CorElementType 是用来在不同环境中描述类型的特殊修饰符。在参数和返回值中,您会看到基本的 .NET 类型(它们由 CorElementType 枚举表示)。这些类型显示在图 6 中。
FunctionEnter2 调用或 FunctionTailcall2 调用的参数在 COR_PRF_FUNCTION_ARGUMENT_ INFO* argumentInfo 参数中传递。COR_PRF_FUNCTION_ ARGUMENT_INFO 是一个结构,其定义如下所示:
typedef struct _COR_PRF_FUNCTION_ARGUMENT_INFO {
    ULONG numRanges; // number of argument ranges
    ULONG totalArgumentSize; // total size of arguments
    COR_PRF_FUNCTION_ARGUMENT_RANGE ranges[ 1 ]; // array of ranges
} 
COR_PRF_FUNCTION_ARGUMENT_INFO;
Ranges 字段被定义为一组结构,而这些结构被定义为 COR_PRF_FUNCTION_ARGUMENT_RANGE,如下所示:
typedef struct _COR_PRF_FUNCTION_ARGUMENT_RANGE {
    UINT_PTR startAddress; // starting memory address of first argument
    ULONG length; // length of this range of memory
}
COR_PRF_FUNCTION_ARGUMENT_RANGE;
如果分析器以后想要引用该信息的任何部分,则需要复制它,因为一旦您离开 FunctionEnter2 回调,这些结构将是无效的。COR_PRF_FUNCTION_ARGUMENT_INFO 在本质上是到函数参数值(对于值类型)或对象 ID(对于引用类型)的映射。如果参数在内存中按从左到右的顺序相邻,则将使用一个 COR_PRF_FUNCTION_ARGUMENT_RANGE 结构来表示它们。分割该范围的方法是:观察函数签名(这可以使用非托管元数据 API 从元数据中检索到),然后使用该知识遍历内存范围,并获得每个参数的值或对象 ID。
对于 FunctionLeave2 调用,在由 COR_PRF_FUNCTION_ARGUMENT_RANGE 结构指定的范围中只提供了返回值。对于诸如 ref 和 out 参数之类的项目,您需要在对 FunctionEnter2 的调用中保存参数的内存地址,然后在 Leave 回调中检查它。
既然您具有参数的值和/或对象 ID 以及返回值,那么您可以用它们做什么呢?打印简单的整数值是很容易的,但是如果该值是对象 ID,该怎么办呢?为了解码您正在处理哪个类型的对象以便访问值,需要相应的帮助。
ICorProfilerInfo2 接口中添加了一组全新的函数,以便帮助您检索这些参数的信息以及其他信息。该组函数包括下列添加到 ICorProfilerInfo2 接口中的新函数:
| • | GetFunctionInfo2 | 
| • | GetStringLayout | 
| • | GetClassLayout | 
| • | GetClassIDInfo2 | 
| • | GetClassFromTokenAndTypeArgs | 
| • | GetFunctionFromTokenAndTypeArgs | 
| • | GetArrayObjectInfo | 
| • | GetBoxClassLayout | 
让我们首先弄清楚有关被回调的函数的一些情况。在以前版本的 Profiling API 中,有一个名为 ICorProfilerInfo::GetFunctionInfo 的函数。GetFunctionInfo 使您可以使用传递到回调中的函数 ID 来确定函数的类和模块;它甚至可以获得函数的元数据标记。按照 API 版本控制的主要惯例,Microsoft 现在为我们提供了极其令人兴奋的方法名称 ICorProfilerInfo2::GetFunctionInfo2。
现在究竟可以访问哪些新颖而奇妙的内容?好,让我们观察一下 GetFunctionInfo2 的定义:
HRESULT GetFunctionInfo2 (
  [in] COR_PRF_FRAME_INFO frameInfo, // frame information for callback
  [out] ClassID *pClassId,   // class ID for function; can be NULL
  [out] ModuleID *pModuleId, // module ID for function; can be NULL
  [out] mdToken *pToken,     // metadata token for function; can be NULL
  [in] ULONG32 cTypeArgs,    // number of elements in the type arg array
  [out] ULONG32 *pcTypeArgs, // number of elements needed to hold type
                             // arguments to the function; can be NULL
  [out] ClassID typeArgs[])  // caller-allocated buffer to accept the
                             // the function's type arguments; can be NULL
如果您尝试分析函数签名的不同部分,则可以通过 GetFunctionInfo2 获得该函数所属的类、该函数所属的模块以及元数据标记。(后者可以用来通过元数据 API 查找所有种类的信息,但该主题有待另行讨论。)
现在,您能够查看一般函数上的类型参数。所有在名称中含有 TypeArgs 的参数都有助于描述该函数的这一实例化的类型参数的类。例如,请考虑具有如下定义的一般函数:
public class Printer {
    public void Print (T item) {
       // do some printing generically
    }
}
在运行时为该回调实例化该函数时,您可以发现类型 T。但这并非全部。鉴于 CLR 存储不同类型的可以作为参数的项目(字符串、数组、装箱的项目等等)的方式,分析器需要几个 Helper 方法来从内存中的正确位置采集适当的值。这是因为 CLR 不能保证该数据在内存中的布局,原因是该布局可能随着平台和处理器体系结构的改变而改变。
以下提供了 ICorProfilerInfo2::GetStringLayout 函数以分析字符串信息:
HRESULT GetStringLayout ( [out] ULONG *pBufferLengthOffset, // offset to DWORD containing buffer len [out] ULONG *pStringLengthOffset, // offset to DWORD containing string len [out] ULONG *pBufferOffset) // offset to the raw string buffer
由于 .NET 中的所有字符串都是基于 Unicode 的,因此字符串的缓冲区实际上包含宽字符 (WCHAR)。当您在检索字符串之后对其进行处理时,必须考虑到这一点。
并非所有参数都是值类型。如果分析器要搞清楚引用类型参数,则它需要遍历类或结构在内存中的布局,以查找单个成员值。为了有助于该值请求,请找到 ICorProfilerInfo2::GetClassLayout 方法,如下所示:
HRESULT GetClassLayout (
[in]  ClassID classID, // class ID for which the layout is requested
[in, out] COR_FIELD_OFFSET rFieldOffset[], // caller allocated layout
                                           // of fields in the class
[in] ULONG cFieldOffset,    // number of COR_FIELD_OFFSET elements in
                            // the caller allocated buffer in rFieldOffset
[out] ULONG *pcFieldOffset, // actual number of COR_FIELD_OFFSET elements
[out] ULONG *pulClassSize); // size in bytes of the class
对于该类中的每个字段,都有一个 COR_FIELD_OFFSET 结构,该结构的定义如下:
typedef struct _COR_FIELD_OFFSET {
    mdFieldDef ridOfField; // fieldDef metadata token for this field
    ULONG      ulOffset;   // offset of field from beginning of object
} 
COR_FIELD_OFFSET;
要使用从 GetClassLayout 返回的信息获得字段值,需要将 ulOffset 字段的 COR_FIELD_OFFSET 值添加到该字段的对象 ID 中。然后,您将具有该字段的值所在的内存地址。请注意,引用类型的字段所具有的值可以用作对象 ID,以便使用 GetClassLayout 进行进一步的检查。
为了获得有关某个类所属的父模块的信息以及该类的元数据标记,分析器以前调用 ICorProfilerInfo::GetClassIDInfo。现在,为了正确地支持泛型,它调用 ICorProfilerInfo2::GetClassIDInfo2:
HRESULT GetClassIDInfo2( [in] ClassID classId, // class ID for which information is required [out] ModuleID *pModuleId, // module to which this class belongs [out] mdTypeDef *pTypeDefToken, // metadata token for the class [in] ULONG32 cNumTypeArgs, // number of ClassID slots in typeArgs [out] ULONG32 *pcNumTypeArgs, // actual number of type arguments [out] ClassID typeArgs[]).. // type argument Class IDs for this class
GetClassIDInfo2 将所有类型(一般类型和非一般类型)的 GetClassIDInfo 一般化,这由获得类的类型参数的能力所证明。如果您在调用 GetClassIDInfo 时为 cNumTypeArgs 指定了零,但提供了一个指向 ULONG32 的指针以返回正确的数字,则可以计算出您需要多少空间。然后,您可以分配要在 typeArgs 中传递的类 ID 数组,并且用大小正确的数组进行实际的调用。
既然您知道了如何从类 ID 中获得类的元数据标记和类型参数,那么公平的做法似乎是向您说明如何反其道而行之,以获得某个元数据标记和一些类型参数的类 ID。这正是 GetClassFromTokenAndTypeArgs 的用途所在,如以下代码所示:
HRESULT GetClassFromTokenAndTypeArgs( [in] ModuleID moduleID, // module to which the class belongs [in] mdTypeDef typeDef, // metadata token for the class [in] ULONG32 cTypeArgs, // number of ClassID slots allocated in typeArgs [in, size_is(cTypeArgs)] ClassID typeArgs[], // argument Class IDs [out] ClassID* pClassID) // pointer which receives the Class ID
为了对函数执行相同的操作,让我们再次求助于 ICorProfilerInfo2 接口上的具有较长名称的方法,并且找到 GetFunctionFromTokenAndTypeArgs:
HRESULT GetFunctionFromTokenAndTypeArgs( [in] ModuleID moduleID, // module to which function belongs [in] mdTypeDef funcDef, // metadata token for the function [in] ClassID classId, // class ID for the class containing the function [in] ULONG32 cTypeArgs, // number of slots in typeArgs [in, size_is(cTypeArgs)] ClassID typeArgs[], // argument Class IDs [out] FunctionID* pFunctionID) // pointer which receives the Function ID
这就是您所要寻找的 — 一种通过类 ID 和函数 ID 中的一种信息来获取另一种信息的方法。
 
图 7 .NET数组
新的 Profiling API 还可以完成其他什么工作?唔,它可以帮助分析器发现如何访问各种类型数组中的元素。在 .NET Framework 中可以使用三个类型的数组:一维数组、传统的多维数组和交错数组(所有行可以具有不同数量列的多维数组)。一维和多维是 CLR 中的实际数组结构,而交错数组是使用一维数组的一维数组实现的。有关这些数组类型的图形表示,请参见图 7。
为了帮助分析器区分数组类型以及访问正确的元素值,Profiling API 提供了两个函数:ICorProfilerInfo::IsArrayClass 和 ICorProfilerInfo2::GetArrayObjectInfo。GetArrayObjectInfo 可以帮助分析器确定有关数组的结构信息,例如,元素的总数、维数和下限。IsArrayClass aids 可以帮助分析器识别数组的类型、数组的元素以及数组秩(维数)。首先,让我们观察一下 IsArrayClass:
HRESULT IsArrayClass ( [in] ClassID classID, // class ID for class in which we're interested [out] CorElementType *pBaseElemType, // describes array element type [out] ClassID *pBaseClassId, // class ID for array element type [out] ULONG *pcRank) // number of array dimensions, known as rank
CorElementType 是 .NET 的非托管 API 中的基本枚举之一,它表示可能遇到的所有类型的内容。它类似于 COM 中的 VARIANT VARTYPE 定义,但它们不是完全匹配,因为 .NET 规范不同于 COM。图 5显示了 CorElementType 及其值。在 IsArrayClass 中,CorElementType 用于 pBaseElemType 参数。
第一个参数是 classId,它代表我们要分析以查看其是否数组的类。第二个参数是 pBaseElemType,它指示数组中元素的 CorElementType。接下来是 pBaseClassId 参数,它为我们提供了数组中所有元素的实际类 ID。为了确定数组中的维数,我们观察 pcRank 参数,它使我们可以了解数组是如何构建的。
既然我们知道正在处理的维数(根据 pcRank 参数),那么我们可以使用该信息来帮助我们设置 GetArrayObjectInfo 的参数。GetArrayObjectInfo 如下所示:
HRESULT GetArrayObjectInfo( [in] ObjectID objectId, // object ID for array to examine [in] ULONG32 cDimensionSizes, // length of sizes and lower bounds arrays [out, size_is(cDimensionSizes), length_is(cDimensionSizes)] ULONG32 pDimensionSizes[], // sizes of each requested dimension [out, size_is(cDimensionSizes), length_is(cDimensionSizes)] int pDimensionLowerBounds[], // lower bound of each requested dimension [out] BYTE **ppData); // pointer to the raw buffer of array data
在调用了 GetArrayObjectInfo 之后,分析器将具有每个维的大小 (pDimensionSizes)、每个维的下限 (pDimensionLowerBounds) 以及数组值在内存中的起始位置的偏移量 (ppData)。使用该信息,并且使用 IsArrayClass 中的元素类型,分析器现在可以遍历该数组并分析每个元素的值。在找到每个值以后,就可以使用前面讨论的技术来分析它,以确定是否需要进行进一步的分析,或者该值实际上是否恰好位于元素位置。
分析器解码参数和返回值信息所需要的最后一部分布局信息是如何处理装箱值。当值类型被传递给需要引用类型的参数时,值被装箱。这些值被视为引用类型,其中,对象引用指向包含该值类型的 GC 堆上的一个箱子中。为了查看实际值位于箱子内的具体位置,我们使用 ICorProfilerInfo2::GetBoxClassInfo 方法:
HRESULT GetBoxClassLayout( [in] ClassID classId, // class ID for the boxed type [out] ULONG32 *pBufferOffset) // offset to the value in the box
GetBoxClassLayout 返回有关如何查找装箱类型的值的信息。如果您传递无法装箱的类的类 ID,则将获得失败的 HRESULT 返回码。为 pBufferOffset 参数中的装箱项接收的偏移量是以装箱项的对象 ID 为基准的偏移量。
正如您可以看到的那样,分析正在运行的程序的值是一项复杂的活动。但是,新的 Profiling API 使您可以很好地完成这一工作。
小结
Profiling API 已经进行了显著更新,以帮助您处理泛型,获得堆栈跟踪,查看有关线程的新信息,以及更细致地分析参数和返回值数据。C# 和托管代码可以完成很多相关工作,但是偶尔您可能希望重新使用 C++ 技能,以了解在幕后发生的事情。
Jay Hilyard 是 Compuware/NuMega Lab 的 BoundsChecker 小组的软件工程师,并且与他人合著了 C# Cookbook (O'Reilly)。Jay 经常与他的家人呆在一起或者观看 Patriots 的比赛,您可以通过 hilyard@comcast.net 与他联系。
 
                    
                 
                
 

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号