翻译Matt Pietrek 的 Under the Hood 专栏文章 The .NET Profiling API and the DNProfiler Tool

类别:VC语言 点击:0 评论:0 推荐:

The .NET Profiling API and the DNProfiler Tool

http://msdn.microsoft.com/msdnmag/issues/01/12/hood/default.aspx

微软的.NET Common Language Runtime(CLR)内部提供了很多机制来创建更容易使用、更面向对象的平台。包括垃圾回收、标准的跨语言异常处理、广泛的类库、元数据、和已存native代码的互操作性、远程处理。也包括跨cpu指令格式(中间语言,IL)和将IL编译成能够在目标cpu上运行的代码的即时编译器。

随着系统的发展变得越来越复杂,能够理解系统的内部工作机制变得越来越有价值。在Windows®,调查可执行程序装载器、内存管理器的操作机制将展示很多不同的技巧。另外,有些技巧在window 9X 平台下能够正常工作,但是在Windows NT® 和Windows 2000不能工作,反之亦然。在Win32®下,查看进程的内部操作的最好方式是使用调试API,但是它也只能是包含很少的一部分。

在.NET下就完全不同了。因为CLR运行时是任何.NET程序的中心,它提供了一个逻辑位置来插入钩子以便观察.NET的内部。预计到工具开发者、系统级程序员的需要,Microsoft使用.NET来提供了一个非常详细的、一致的方式去观察进程的内部操作。本月,我要描述这种机制、并提供一个程序(DNProfile)来记录.NET运行时的操作。

The .NET Profiling API

              观察.NET运行时动作的一种方式时使用剖析API。剖析API的命名不好,因为它对很多种事情都很有用,而不仅仅是剖析。考虑可以被剖析API观察的.NET动作列表,如下:

        Figure 1 Functions of the Profiling API

CLR startup and shutdown

Application domain creation/shutdown

Assembly loads/unloads

Module loads/unloads

Class loads/unloads

COM interop VTable creation/destruction

JIT-compiles, code pitching, and pre-JITed method searches

Thread creation/destruction/suspends

Remoting activity

Exception handling

Managed/unmanaged code transitions

Garbage collection and managed heap objects

Method entry/exit

       正如Keanu Reeves会说Whoooa! 相对于经过了多年的艰苦探索找到的如何给WINDOWS安装钩子来讲,Microsoft使观察CLR的行为变得非常容易了。列表中的每一个动作可以看作一个完全不同的CALLBACK。理论上讲,仅仅在CALLBACK函数中写需要的代码就可以了。

        现在,最好的剖析API文档是.NET SDK下的Profiling.doc,在Tool Developers Guide\docs目录下。对于Beta2 来讲,这个文档和实现稍微有些不同步。在本期刊中,我不会广泛的讨论剖析API的每一个方面,而是重点讨论剖析API能做的大的方面。

.NET的一个大的卖点是它不再使用COM。这不是真的,事实上,剖析API就是基于COM的。使用剖析API包括创建一个进程内服务器,它实现了单一的接口。每一个接口方法代表了Figure 1中的一个事件。在CALLBACK方法内部,代码可以使用另外的COM接口(由CLR提供)来得到观察事件的信息。

个人来讲,我喜欢更简单的API来对所感兴趣的每一个事件进行注册。将API实现作为一个COM对象强迫你写几乎同样的代码以便使用COM对象。既然剖析API是基于COM的,剖析API的用户很可能使用C++。

尽管使用典型的COM而不是使用托管.NET代码起初看起来有些奇怪,仔细考虑之后就会明白。如果用托管代码实现接口,它将对监视的所有事件的产生负面影响。例如,如果实现接口的托管代码触发了异常怎么办?在API没有激活的情况下,异常事件也不会发生。

一旦你有一个实现了Profiling接口的COM服务器,下一步是强迫CLR装载COM服务器、调用它的方法。技巧是设置环境变量。跟注册表、XML文件比较起来,环境变量是挺古老的。

为什么不用注册表(配置文件)告诉CLR应该调用Profiling API?我从Microsoft开发者那里听说的一个原因是那要求.NET程序在启动时检查注册表将会有性能影响。考虑到.NET启动时有很多事情要做,这样的原因考虑的也是很慎重的。虽然如此,使用环境变量可以对所有程序起作用(如果在系统环境变量)或单独的进程。在后者情况下,调试器、剖析器等工具可以在启动程序时指定环境变量。稍后我将讨论所需要的环境变量。

The .NET Profiling API Interfaces

              我检查的主要的接口是ICorProfilerCallback,在.NET Framework SDK的Include目录下的CorProf.IDL文件中定义。接口的实现需要使用剖析API。你的工作是提供实现。尽管有很多的方法,不要怕,对于他们中的大多说,可以简单的返回S_OK或E_NOIMPL。对于明确的事件的动作,在合适的CALLBACK方法中写合适的代码。

        检查CALLBACK方法,你会发现大部分接收有关于那个事件的另外信息的参数。例如,JITCompilationStarted方法接收了一个名为functionId的UINT类型的参数。你可以对它做什么?答案在于接口ICorProfilerInfo。

        ICorProfilerInfo可以提供任何剖析信息。考虑刚才提及的functionId,你可以调用ICorProfilerInfo::GetTokenAndMetaDataFromFunction,它返回给你元数据接口、函数的元数据token。使用元数据接口和token,你可以查询函数名、它所在的类、及你所知道的任何信息。

        简单的讲,剖析API包含了两个COM接口。引入接口,由你来实现,是ICorProfilerCallback。当CLR事件发生时,CLR会调用接口中的某一个方法。引出接口,是ICorProfilerInfo,由CLR提供给你,让你在CALLBACK内部使用。

        使用ICorProfilerCallback参数的普遍的模式是广泛的使用ID来代表函数、类、模块、程序集,等。ID是不透明的句柄。关于它的有意义的信息从ICorProfilerInfo接口来获得。程序执行过程中如果函数的代码被卸载,然后又被装载、并编译,特定的ID值(如functionId)可能会改变。但是有CALLBACK让你知道这个发生了。

        ICorProfilerCallback方法可以分成逻辑的几组。大部分情况下,事件都有Started、Finished方法,并且成对的调用。让我们进行深入研究它,更好的了解我们可以通过CLR观察到什么。除非有特殊说明,下面列出的所有方法都属于ICorProfilerCallback接口。

Initialize/Shutdown Methods

              在使用剖析API的进程中,Initialize是第一个被调用的方法。你的代码从这个方法得到ICorProfilerInfo指针。唯一的参数是LPUNKNOWN,对它调用QueryInterface方法得到ICorProfilerInfo指针。Initialize是你告诉剖析API你对哪些事件感兴趣的地方。

        为了指出你感兴趣的事件,调用ICorProfilerInfo::SetEventMask方法,传递一个设置了合适的bit的DWORD类型的参数。这些标志来自于CorProf.h文件中的COR_PROF_MONITOR枚举变量。低位值标志被命名为COR_PRF_MONITOR_XXX,告诉CLR调用ICorProfilerCallback的哪些方法。例如,如果你想让ClassLoadStarted方法被调用,你必须设置COR_PRF_MONITOR_CLASS_LOADS标志。

        ICorProfilerInfo::SetEventMask方法的参数的另外一些标志以一种方式、或者另一种方式改变CLR的行为.例如,如果你想在执行时监视对象的分配,必须设置COR_PRF_ENABLE_OBJECT_ALLOCATED标志。相似的,COR_PRF_DISABLE_INLINING告诉CLR不要内联任何函数。如果一个方法内联了,你将得不到ENTER、LEAVE通知。

        你可以在以后的某个时刻调用ICorProfilerInfo::SetEventMask来修改你所感兴趣的事件。然而,某些事件是不可以改变的,意味着你一旦在Initialize设置了,他们将不能被修改。

        当CLR终止进程时,Shutdown方法被调用。在某些情况下,它不会被调用,但是对于一个正常终止的.NET程序来讲,它应该被调用。

Application Domain Creation/Shutdown

              这个种类的方法有AppDomainCreationStarted, AppDomainCreationFinished, AppDomainShutdownStarted, and AppDomainShutdownFinished。他们的名字是自描述的。这些方法的主要Token是AppDomainID。需要注意的是在AppDomainCreationStarted 回调方法中,不能使用AppDomainID,因为AppDomain还不存在。然而,一旦收到了AppDomainCreationFinished通知,就可以以AppDomainID为参数调用以为参数调用ICorProfilerInfo::GetAppDomainInfo 来得到新AppDomain的信息。

Assembly Loads/Unloads

              在装载、卸载程序集时,AssemblyLoadStarted, AssemblyLoadFinished, AssemblyUnloadStarted,  AssemblyUnloadFinished这些方法被调用。主要的Token是AssemblyID。需要注意的是在AssemblyLoadStarted方法中不能使用Token AssemblyID,因为程序集还不存在。然而,一旦收到AssemblyLoadFinished通知,就可以以AssemblyID为参数调用ICorProfilerInfo::GetAssemblyInfo来得到新程序集的信息。

Module Loads/Unloads

            装载模块相关的函数有ModuleLoadStarted, ModuleLoadFinished, ModuleUnloadStarted, ModuleUnloadFinished, 和ModuleAttachedToAssembly。前四个函数的名字是自描述的。主要的Token是ModuleID。和AssemblyLoadStarted时的Token一样,传递给ModuleLoadStarted方法的Token也是不可用的。因为模块不存在。但是当收到ModuleLoadFinished通知后,就可以以ModuleID为参数调用ICorProfilerInfo::GetModuleInfo来得到新模块的信息。

当CLR将一个模块和一个程序集相关联起来时,最后一个函数,ModuleAttachedToAssembly,被调用。尽管模块和程序集经常是同一个文件,一个程序集也可能有多个模块。

Class Loads/Unloads

装载类相关的函数有ClassLoadStarted, ClassLoadFinished, ClassUnloadStarted, 和ClassUnloadFinished。这些函数的名字是自描述的。传递给ClassLoadStarted方法的Token也是不可用的。因为类不存在。但是当收到ClassLoadFinished通知后,就可以以ClassID为参数调用ICorProfilerInfo::GetClassIDInfo来得到新类的信息。

JIT Compilation

JIT编译方法(如图2)用到的主要Token是FunctionID。

JITCompilationStarted

JITCompilationFinished

JITCachedFunctionSearchStarted

JITCachedFunctionSearchFinished

JITFunctionPitched

JITInlining

这里,术语函数、方法交互的使用。以FunctionID为参数调用 ICorProfilerInfo::GetFunctionInfo来获得函数的信息。

方法JITCompilationStarted是很有趣的,因为它允许你在JITed之前查看、修改IL。查看ICorProfilerInfo:: GetILFunctionBody来获得细节,不要认为它很容易。方法JITCachedFunctionSearchStarted表示CLR寻找已经被JITed编译成native代码的函数。通过设置输出参数pbUseCachedFunction为FALSE,你可以强制运行时不考虑Pre-JITd的状态,使用最新的JITed版本。

方法JITFunctionPitched表示从内存中删除一个以前JITed的方法。只有在内存很少的情况下,才会发生。最后方法JITInlining表示JITer要内联函数。如果你要计算那个方法的enter/leave通知个数,可以设置输出参数pfShouldInline为FALSE来禁止内联。也可以通过传递给ICorProfilerInfo::SetEventMask的一个标志来禁止进程范围内的内联。

Threading

线程方法如图3

ThreadCreated

ThreadDestroyed

ThreadAssignedToOSThread

RuntimeSuspendStarted

RuntimeSuspendFinished

RuntimeSuspendAborted

RuntimeResumeStarted

RuntimeResumeFinished

RuntimeThreadSuspended

RuntimeThreadResumed

ThreadCreated/Destroyed方法包含了线程的生命周期。例如,概念上讲,一个CLR线程在其生命周期中可以运行在多个Win32线程之上。ThreadAssignedToOSThread方法指示了CLR正在运行在哪个Win32线程之上。

              当CLR执行时,有时必须挂起部分或者所有的线程以便执行垃圾收集。RuntimeSuspend,RuntimeResume系列的方法指示CLR线程被挂起(实际比这更复杂,我在这里部深入细节了)。传递给RuntimeSuspendStarted的一个参数指示了线程为什么被挂起。最后两个RuntimeThread方法指示一个线程正在被挂起,并且那总在事件RuntimeSuspend内发生。

COM Interop

            当CLR和普通的COM对象互操作时,需要代理接口。两个方法指示代理被创建、销毁。传递到这两个函数的参数是.NET ClassID,相应的COM接口IID,一个指向代理的虚函数表,虚函数表的项数。

Managed/Unmanaged Code Transitions

              当托管代码调用非托管代码,或者非托管代码调用托管代码时,函数UnmanagedToManagedTransition 、ManagedToUnmanagedTransition被调用。传递给每个方法FunctionID来代表调用者,可以利用它调用ICorProfilerInfo::GetFunctionInfo来得到更多信息。

Garbage Collection and Managed Heap Objects

        尽管没有实际的垃圾收集方法,ObjectAllocated, MovedReferences, ObjectsAllocatedByClass, ObjectReferences,  RootReferences这些事件的触发指示了系统正在进行垃圾收集。这些通知携带的信息非常复杂,我不会解释他的全部,仅仅指出一些关键点。

              当从托管堆中分配一个对象时,ObjectAllocated方法被调用(需要设置COR_PRF_ENABLE_OBJECT_ALLOCATED标志)。对象被一个ObjectID标示,同时也提供ClassID。可以利用ClassID来调用以便ICorProfilerInfo::GetClassIDInfo获得更多信息。

              MovedReferences方法指出一个对象(由ObjectID来标示)已经被移动到内存中。ObjectsAllocatedByClass方法指出最后一次垃圾收集之后,又创建了哪个类的实例。ObjectReferences方法提供了一个特定对象引用的对象列表。最后,RootReferences方法指出了所有根对象引用的对象列表。

Remoting Activity

              远程方法如图4

RemotingClientInvocationStarted

RemotingClientSendingMessage

RemotingClientReceivingReply

RemotingClientInvocationFinished

RemotingServerReceivingMessage

RemotingServerInvocationStarted

RemotingServerInvocationReturned

RemotingServerSendingReply

它指出了在远程方法调用的过程中的各种执行点。当程序作为客户端时,方法被调用;当程序作为远程服务器时,方法被调用。在这两种情况下,远程方法的调用,实际的传输消息被区分开来。

Exception Handling

              剖析API对异常处理的支持相当复杂,并且非常完全。详细的细节请参考文档。本质上来讲,在异常处理的每一个阶段,API在之前、之后通知你。图5显示了异常方法。

ExceptionThrown

ExceptionSearchFunctionEnter

ExceptionSearchFunctionLeave

ExceptionSearchFilterEnter

ExceptionSearchFilterLeave

ExceptionSearchCatcherFound

ExceptionOSHandlerEnter

ExceptionOSHandlerLeave

ExceptionUnwindFunctionEnter

ExceptionUnwindFunctionLeave

ExceptionUnwindFinallyEnter

ExceptionUnwindFinallyLeave

ExceptionCatcherEnter

ExceptionCatcherLeave

ExceptionCLRCatcherFound

ExceptionCLRCatcherExecute

 

        ExceptionThrown方法是你收到异常的第一个指示。以ObjectID为参数调用m_pICorProfilerInfo::GetClassFromObjec来得到异常的类型。对于堆栈中的每一个托管方法,当CLR查找方法中的try块,执行方法中的filter,或者找到处理异常一个方法时,CLR通知你。当堆栈展开时,执行finally块时,或者执行处理代码时,CLR也会通知你。

Receiving Method Entry and Exit Notifications

剖析API最酷的特性之一是当任意一个托管方法开始执行时,要返回到调用者时,都能够通知你。因为在执行.NET方法之前需要JITed。如果你对方法的enters,leaves感兴趣,可以通知JITer,JITer在方法的开始、结束时,调用你提供的特定的方法。同时也有一个tailcal通知,概念上讲它和优化函数推出相似。不过我从没有在.NET下看到该事件的产生。

不象我描述的所有其他的事件,enter/leave回调方法部属于ICorProfilerCallback接口。而是使用接口ICorProfilerCallback,ICorProfilerInfo来进行设置,以便JITed调用你提供的方法。有趣的是,回调函数有个限制,不能修改寄存器的值。所以你提供的代码很有可能包含一段汇编语言,使用__declspec naked函数。请参考图6的一个例子。

void __declspec( naked ) EnterNaked()

{   

__asm

    {

   push eax

        push ecx

        push edx

        push [esp+16]

        call RealEnterFunctionInCPP

        pop edx

        pop ecx

        pop eax

        ret 4

    }

}

只有一个参数(FunctionID)被传递到enter/leave回调函数中。文档警告,如果回调函数阻塞或很长时间没有返回,将会导致可怕的后果。但是,就我的经验来看,使用ICorProfilerInfo来查询FunctionID的名字,并写入文件中,并被有问题。

投入一些努力,你就可以利用enter/leave写出一个象样的监听程序来显示每一个方法的执行。如果你写了这样的程序的话,你就会发现,仅仅启动一个小.NET程序就有很多的方法调用。因为同时有enter,leave回调函数,可以看到有好多嵌套方法调用。

需要注意的是.NET运行时可能使用pre-JITed的函数。除非你采取特殊的步骤,否则这些方法不会产生回调。有时可以通过监视JITCachedFunctionSearchStarted并且返回FALSE来修正这个问题。

Caveats with the Profiling API

            如果想使用剖析API来实现某些功能,需要知道某些很重要的限制。首先,它假定COM服务器是free-threaded。在调用ICorProfilerCallback方法时,.NET运行时不回作任何的同步。如果你用全局数据,必要时你需要使用临界区保护数据。或者使用线程局部存储,请参考DNProfiler例子程序。

           另一个限制是在ICorProfilerCallback方法中,不能调用任何托管代码,不管是直接调用还是间接调用。剖析API被有被设计为可重入的,可调用托管代码的,如果违反了,将会很麻烦的。

           剖析API提供了获得ICorDebug的方法。这允许你得到特定的调用堆栈细节。但是当你执行进程内调试时,并不是所有的ICorDebug方法都是可用的。查看ICorDebug文档来确定哪些在进程内调试时是可以调用的,哪些是不可以调用的。

              最后,剖析API是如此的酷,我想可以同时用它写好多工具。很不幸,某一时刻,只能有一个剖析COM服务器。如果你追求时间信息,你可能在某一时刻只想有一个剖析器。然而,考虑到非剖析工具,如果Microsoft提供一个方案,允许一个工具处理这些事件,并且可以将事件继续发送到下一个工具就好了。

The DNProfiler Sample

              为了显示剖析API的很酷的性质,我写了一个简单的实现,记录每一个CALLBACK方法。对于一些方法,代码执行一些额外的工作使它更有意义。例如,当触发了ClassLoadFinished时,DNProfiler接收了一个ClassID,写出了类的名字。

        DNProfiler不会显示每一个CALLBACK的所有的信息,但是它做了相当的工作显示发生了什么。本期刊中,我没有实现函数的ENTER/LEAVE CALLBACK,但是Microsoft的例子实现了这些。

        DNProfiler将结果写入到和父进程同一目录下的DNProfiler.out的文本文件中。DNProfiler观察到的启动事件的一部分如下:

Figure 8 Sample Output

Initialize

ThreadCreated

ThreadAssignedToOSThread

ThreadCreated

ThreadAssignedToOSThread

AssemblyLoadStarted

  ModuleLoadStarted

  ModuleLoadFinished:c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll

  ModuleAttachedToAssembly: mscorlib

AssemblyLoadFinished: mscorlib  Status: 00000000

ClassLoadStarted: System.Object

ClassLoadFinished

ClassLoadStarted: System.ValueType

ClassLoadFinished

ClassLoadStarted: System.ICloneable

ClassLoadFinished

... // lines omitted

ObjectAllocated: System.OutOfMemoryException

ObjectAllocated: System.StackOverflowException

        注意到DNProfiler通过缩进表示嵌套。在这个例子中,AssemblyLoadStarted和AssemblyLoadFinished之间,DNProfiler接收到另外三个事件,已经被合适的缩进。

        DNProfiler仅仅是个DLL形式的COM组件,没有EXE执行。你或者自己编译它,或者使用REGSVR32对它进行注册。一旦注册成功,最简单的使用方式时使用一个控制台窗口运行Profiling_on.bat文件。

Figure 9 profiling_on.bat

set Cor_Enable_Profiling=0x1

set COR_PROFILER={9AB84088-18E7-42F0-8F8D-E022AE3C4517}

 

@REM    COR_PRF_MONITOR_FUNCTION_UNLOADS    = 0x1,

@REM    COR_PRF_MONITOR_CLASS_LOADS    = 0x2,

@REM    COR_PRF_MONITOR_MODULE_LOADS    = 0x4,

@REM    COR_PRF_MONITOR_ASSEMBLY_LOADS    = 0x8,

@REM    COR_PRF_MONITOR_APPDOMAIN_LOADS    = 0x10,

@REM    COR_PRF_MONITOR_JIT_COMPILATION    = 0x20,

@REM    COR_PRF_MONITOR_EXCEPTIONS    = 0x40,

@REM    COR_PRF_MONITOR_GC    = 0x80,

@REM    COR_PRF_MONITOR_OBJECT_ALLOCATED    = 0x100,

@REM    COR_PRF_MONITOR_THREADS    = 0x200,

@REM    COR_PRF_MONITOR_REMOTING    = 0x400,

@REM    COR_PRF_MONITOR_CODE_TRANSITIONS    = 0x800,

@REM    COR_PRF_MONITOR_ENTERLEAVE    = 0x1000,

@REM    COR_PRF_MONITOR_CCW    = 0x2000,

@REM    COR_PRF_MONITOR_REMOTING_COOKIE    = 0x4000 |

@REM    COR_PRF_MONITOR_REMOTING,

@REM    COR_PRF_MONITOR_REMOTING_ASYNC    = 0x8000 |

@REM    COR_PRF_MONITOR_REMOTING,

@REM    COR_PRF_MONITOR_SUSPENDS    = 0x10000,

@REM    COR_PRF_MONITOR_CACHE_SEARCHES    = 0x20000,

@REM    COR_PRF_MONITOR_CLR_EXCEPTIONS    = 0x1000000,

@REM    COR_PRF_MONITOR_ALL    = 0x107ffff,

@REM    COR_PRF_ENABLE_REJIT    = 0x40000,

@REM    COR_PRF_ENABLE_INPROC_DEBUGGING    = 0x80000,

@REM    COR_PRF_ENABLE_JIT_MAPS    = 0x100000,

@REM    COR_PRF_DISABLE_INLINING    = 0x200000,

@REM    COR_PRF_DISABLE_OPTIMIZATIONS    = 0x400000,

@REM    COR_PRF_ENABLE_OBJECT_ALLOCATED    = 0x800000,

@REM    COR_PRF_ALL    = 0x1ffffff,

 

set DN_PROFILER_MASK=0x1A7ffff

        一旦运行,所有的从那个控制台窗口启动的.NET程序将使用DNProfiler。为了设回正常模式,运行相应的Profilng_off.bat文件。

        Profiling_On.bat设置了三个环境变量,其中两个由CLR使用。进程启动时,当 Cor_Enable_Profiling环境变量设置为非零时,CLR试图装载由COR_PROFILER环境变量指出的COM服务器。COR_PROFILER环境变量可以是CLSID或ProgID.

        第三个环境变量是DN_PROFILER_MASK,由DNProfiler使用。它允许你动态的配置你所感兴趣的方法,而不是硬编码到代码中。在DNProfiler的Initialize方法,它获得DN_PROFILER_MASK的值,并传递到ICorProfilerInfo::SetEventMask中。在.bat文件中,我已经将mask设置为包含任何事件,但是我鼓励你测试其它的值。

        Figure 7是DNProfiler的主要的代码段。在CCorProfiler::Initialize方法的开始附近,它是进行初始化的地点。Initialize方法调用了一个私有函数,GetInitializationParameters来读取环境变量、生成输出文件的全路径。

        ProfilerCallback.cpp文件中的大部分函数是简单的实现ICorProfilerCallback接口方法。如果函数时一个开始函数,代码将调用ChangeNestingLevel帮助函数。函数更新每线程的跟踪嵌套层数的变量。当相应的函数完成时,代码再次调用ChangeNestingLevel来恢复原始值。

        文件的结尾处是一些私有函数,来做共同的功能,如从ClassID查询类名字。ProfilerPrintf函数和fprintf相同,只是它考虑了每个现成的嵌套层数,并且在实际文本之前使用空格填充缩进。

        ProfilerCallback.h是描述CProfilerCallback类的头文件。注意CProfilerCallback是如何从ICorProfilerBack接口继承的。在类定义的尾部时一些私有函数、成员变量。

        最后的源文件是COMStuff.CPP。它创建了DNProfiler,我想使事情尽可能简单,没有使用ATL,MFC,或不是必需的任何东西。我是一个Framework爱好者。任何情况下,Microsoft的例子采取了相似的方法。我偷用相关的部分代码,并且使它更简单些,结果就是COMStuff.CPP。

Interop Fun with DNProfiler

           很容易花费一些时间学习DNProfiler的输出,了解.NET程序的事件顺序。我不会学习所有的感兴趣的事件。有一个需要提及,.NET WINDOWS FORMS包在USER32.DLL的基础上进行构件,你可能怀疑当发送消息时,需要在托管代码、非托管代码间进行来回转换。          

           确实是这样的。在托管代码、非托管代码间如此之多的递归真令人吃惊。我意思是说,托管代码调用非托管代码,非托管代码接着又调用托管代码。在托管代码内,又需要调用非托管代码,如此下去。当然最后,堆栈展开到原始的级别。

           察看Figure 10

ManagedToUnmanagedTransition:

System.Windows.Forms.UnsafeNativeMethods::DispatchMessageW

  UnmanagedToManagedTransition: WndProc::Invoke

    ManagedToUnmanagedTransition:

    System.Windows.Forms.UnsafeNativeMethods::CallWindowProc

      UnmanagedToManagedTransition: WndProc::Invoke

        ManagedToUnmanagedTransition:

        System.Windows.Forms.UnsafeNativeMethods::IntDestroyWindow

          UnmanagedToManagedTransition: WndProc::Invoke

            ManagedToUnmanagedTransition:

            System.Windows.Forms.UnsafeNativeMethods::SendMessage

              UnmanagedToManagedTransition: WndProc::Invoke

                ManagedToUnmanagedTransition:

                System.Windows.Forms.UnsafeNativeMethods::CallWindowProc

              它显示了销毁窗口消息的处理过程。在托管代码中,UnsafeNativeMethods::DispatchMessageW被调用,它传递到非托管代码的WndProc::Invoke方法,WndProc::Invoke内部需要调用托管代码UnsafeNativeMethods::CallWindowProc。这个函数接着调用非托管方法WndProc::Invoke。这个方法在托管代码、非托管代码间来回调用,在UnsafeNativeMethods::DispatchMessageW函数返回前,达到了很深的调用层。

              Microsoft为.NET提供的剖析接口对工具开发者来讲在很长的一段时间内是很好的。很广泛的、相当有灵活性的.NET工具将都是基于它开发的。这里我给出了它的能力的整体预揽,一定要察看文档、通过Microsoft提供的Profiler例子获得更多的信息。

本文地址:http://com.8s8s.com/it/it355.htm