Rotor里的异常处理

我看到了一些关于Rotor(和CLR)中使用的异常处理机制的问题。下面是关于Rotor异常处理的另一个注意事项列表。目的是帮助Rotor开发人员调试和理解CLR中的异常。

异常生成和抛出

此步骤在很大程度上取决于异常的类型以及从何处引发异常:

jitted代码中的软件异常---使用C#中throw new MyException(…)引发的异常

  • 异常对象的创建方式与任何其他托管对象的创建方式相同
  • vm\jitiface.cpp中调用JIT_Throw
  • HelperFrame被推送以标记从jitted到EE代码的转换。这是可靠的stack walk工作所必需的——安全性和垃圾收集是可靠stackwalk的主要用户。
  • vm\excep.cpp中调用了RaiseTheException
  • 异常对象存储在当前线程对象中
  • EXCEPTION_COMPLUS异常是使用Win32的RaiseException API引发的

执行引擎代码中的软件异常---在clr\src\vm目录中使用COMPlusThrow(…)引发异常

  • 如果尚未提供给调用,则异常对象的创建方式与任何其他托管对象的创建方式相同
  • vm\excep.cpp:RaiseTheException被调用,故事的其余部分与第一个案例相同。
  • 注意:不需要推送任何转换框架
  • 注意:在托管异常堆栈跟踪中不包含C++ stacttrace。

jitted代码中的硬件异常-如访问空对象、除以零等。

  • 将调用jitted代码的异常处理程序。大多数时候它是vm\i386\excepx86.cpp:COMPlusFrameHandler。
  • 异常被标识为直接来自vm\i386\excepx86.cpp:CPFH_HandleManagedFault中的jitted代码。
  • 推送FaultingExceptionFrame以标记从jitted到EE代码的特殊转换。
  • 使用vm\i386\excepx86.cpp:LinkFrameAndRow和关联的asm帮助程序重新引发异常。
    对于Win32/i386 SEH,通过将IP修改为指向NakedThrowHelper的异常过滤器返回EXCEPTION_CONTINUE_EXECUTION来完成重新触发。
    对于PAL_PORTABLE_SEH,通过简单调用LaunchNakedThrowHelper来完成重新触发。
  • 第二次在异常过滤器中,为硬件异常创建正确类型的异常对象。硬件异常到BCL异常类型的映射在vm\excep.cpp:MapWin32FaultToCOMPlusException中完成。
  • 注意:硬件异常重试是一个实现细节,它应该允许可继续的异常(异常继续执行)无缝工作。CLI目前不支持可延续的异常,但将来可能会有所改变。此外,可能存在一些互操作场景,其中异常继续执行非常重要。
  • 注意:对于PAL_PORTABLE_SEH,硬件异常的重新触发是不同的,因为PAL不支持异常继续执行。稍后为调试器支持添加了异常继续执行。

执行引擎代码中的硬件异常

执行引擎代码中的硬件异常通常不应该发生。如果他们这样做了,通常意味着事情真的出了问题,需要大量的运气才能从这种情况中恢复过来。不过,这条规则有几个例外:

  • 写入屏障帮助程序(vm\i386\jithelp.asm:JIT_write barrier and friends)中的访问冲突被转换为调用程序中的访问冲突。转换在vm\i386\excepx86.cpp:CPFH_AdjustContextForWriteBarrier中完成。这允许JIT在调用写回载波时优化空检查。FJIT当前不使用此功能。
  • 尽管存在一些防止堆栈溢出异常(vm\stackprobe.h)的屏蔽,但它们可能发生在执行引擎代码中,这通常非常糟糕。这是为.NET v1设计的,CLR团队正在为Whidbey开发更好的解决方案。便携式堆栈溢出处理不在转子-我们只是死时堆栈溢出发生。可移植堆栈溢出处理的设计和实现留给读者练习
  • 元数据代码用于访问监视(#define ZAP_MONITOR),以允许按配置文件引导的元数据布局优化。检测是通过使所有元数据访问A/V、记录它们并通过异常继续执行从它们恢复来完成的。(是的,这个解决方案的性能很差。)元数据工具不是作为Rotor的一部分提供的,但是必须记住它以避免代码损坏。

FCalls中的软件异常--使用FCThrow(…)及其类似调用引发的异常

Fcall闻起来像jitted代码,但实际上不是,因此Fcall中的异常处理是特殊的。

  • 如果未推送helper框架,则使用FCThrow helpers从fcall抛出异常。(如果推送helperframe,则它是使用complushrow在执行引擎代码中抛出软件异常的简单方法)
  • FCThrow helper使用特殊帧ATTR_CAPTURE_DEPTH_2推动辅助帧。因此,只能直接从FCALL调用FCThrow。不能从从FCALL调用的函数调用它。
  • 一旦helper框架被推送,complushrow就会像执行引擎代码中的任何其他软件异常一样实际抛出异常。
  • 注意:FCThrow只是HELPER_METHOD_FRAME_BEGIN(…);complusthrow(…);HELPER_METHOD_FRAME_END()的快捷方式。如果您看到了长代码模式,那么在尝试使用FCThrow对其进行优化时要小心—它可能是有原因的。fcall依赖于一个非常脆弱的x86解释器。这个翻译有时需要帮助.

可以看到,所有异常都转换为一个看起来像常规托管异常的统一案例。从托管代码引发的每个异常(无论是硬件异常还是软件异常)都会下放到Win32 RaiseException层。这样做是为了保持一致性。它被认为可以简化互操作性的实现。

处理异常

异常的处理遵循两次机会的Win32模型。
第一次,stackwalker为堆栈上的每个方法调用回调。一旦某个过滤器返回它处理异常,异常展开的第二次传递就开始了。
第二次,堆栈已展开。展开代码的结构在本机Win32 i386异常处理和PAL可移植异常处理之间有所不同。

异常和垃圾收集器

请注意,对RaiseException的调用是由EE代码发出的,并且此代码以协作模式运行。因此,在异常分派期间不会阻止垃圾收集。

异常和帧

帧的链接列表(Frame-vm\frames.h类的后代)与每个线程对象关联。这些是可靠的堆栈遍历基础设施的关键部分。执行引擎代码正在使用一种特殊的机制来释放异常时的链。COMPLUS-TRY/COMPLUS-CATCH宏(vm\exceptmacros.h)通过在异常时调用unwind frame chain(vm\excepp.cpp)来展开帧链。此外,这些宏还释放嵌套的异常信息(vm\excep.cpp:UnwindExInfo)并还原GC模式,这非常方便。
请注意,UnwindFrameChain正在访问已展开的堆栈空间。这是可行的,但它取决于堆栈展开的微妙的低级实现细节。由于Win32/i386 SEH和PAL_PORTABLE_SEH之间堆栈展开的低级实现细节不同,因此COMPLUS_TRY/COMPLUS_CATCH宏对于这两个宏是不同的。它还解释了为什么有必要在执行引擎中使用COMPLUS-TRY/COMPLUS-catch捕获异常,而不使用常规的PAL-TRY/PAL-catch除外。
CLR开发人员没有选择通过为类框架创建C++析构函数来解开帧链,这将是一个非常简单的解决方案。这可能是一个纯粹的历史遗留物;它们可能有一些性能童话;或者对象析构函数与结构化异常处理的混合,而MSC不支持的结构异常处理会在编写代码时造成太多的不适。

异常与C++对象展开

在异常解除时,PAL_PORTABLE_SEH 不调用堆栈上的对象的C++析构函数。这是可以的,因为执行引擎代码似乎没有利用当前的C++特性。嗯,我们还没有找到它依赖于被调用的C++的地方。这可能会因Whidbey而改变

异常和安全

由于存在安全隐患,因此从异常处理中正确执行堆栈遍历非常重要。对于过滤器来说,这是特别棘手的,因为在调用过滤器时,真正的堆栈是不展开的。在抛出异常到最后调用异常之间,可以调用来自堆栈上源代码的任意用户代码。

 

 

 

 

Rotor

posted on 2019-12-16 17:02  活着的虫子  阅读(247)  评论(0编辑  收藏  举报

导航