CLR 完全介绍

处理损坏状态异常

Andrew Pardoe

本专栏基于 Visual Studio 2010 的预发布版本。如更改,恕所有信息。

内容

完全什么异常?
Win 32 SEH 异常和 System.Exception
托管代码和 SEH
损坏的状态异常
很仍错误使用 Catch (Exception e)
明智地代码

有曾经编写的代码,并不完全正确,但足够关闭?必须编写代码来正常工作,如果一切顺利,但不完全确定出现问题时会发生?很可能坐在一些代码,您编写或不得不维护一个简单的、 不正确语句: catch (Exception e)。似乎 innocent 直观,但此小语句可以会导致许多问题它无法执行您的期望时。

如果您可以看到使用异常的代码方式下面的代码会,则需要阅读此专栏:

public void FileSave(String name)
{
    try
    {
      FileStream fs = new FileStream(name, FileMode.Create);
    }
    catch (Exception)
    {
      throw new System.IO.IOException("File Open Error!");
    }
}

在此代码错误是常见: 简单编写代码以捕捉所有异常比它旨在捕获完全未能通过代码执行在 try 块中引发异常。 但通过捕捉的异常层次结构,将得到忽略任何类型的异常,并将其转换为一个 IOException。

异常处理是哪些大多数人有知识而不是 intimate 了解那些区域之一。 我将开头的背景信息说明异常对于那些可能更加熟悉从本机编程或一个旧的大学 textbook 处理的异常处理从 CLR 的角度。 如果您是旧的专业人员在托管的异常处理,scott 跳至不同类型的异常上节继续或者,一个在托管代码,结构化异常处理 (SEH)。 请确保通过读取最后的几个部分。

完全什么异常?

一个例外是信号检测到一个条件时引发不应在程序线程的正常执行的。 多个代理可以检测错误条件,并引发异常。 程序代码 (或它使用的库代码) 可以引发从 System.Exception 派生的类型、 CLR 执行引擎可以引发异常,和非托管的代码可以引发异常以及。 执行的线程上引发的异常跨 AppDomain,执行本机和托管代码,线程,和如果不处理此的程序被视为未处理的异常操作系统。

异常指示错误发生。 虽然每个托管的异常有一个类型 (如 System.ArgumentException 或 System.ArithmeticException),类型只是在其中引发异常的上下文中有意义。 如果它了解导致异常发生的条件,程序可以处理异常。 但如果程序不处理该异常,则可能表示任意数量的错误的操作。 并且一旦该异常已离开该程序,只有一个非常一般的含义: 错误发生。

当 Windows 看到程序不处理异常,它会尝试保护程序的持续通过终止进程的数据 (磁盘、 注册表设置和等等上的文件)。 即使该异常最初表示 (如无法从空的堆栈中弹出) 的一些良性的意外程序状态它显示为严重的问题,当 Windows 看到它因为操作系统没有上下文来正确解释该异常。 一个 AppDomain 中的单个线程可以关闭整个 CLR 实例将通过未处理异常 (请参见 图 1 )。

fig01.gif

图 1 一个线程 ’s 未异常会导致处理在整个进程终止

因此危险异常时为什么会在如此流行? 如机车和链 saws,例外的原始功能使它们非常有用。 在程序的线程上的普通数据流从函数函数经过调用并返回。 每个调用函数创建了堆栈上的执行的框架 ; 每个返回销毁该框架。 除了更改全局状态,仅在程序中的数据流是通过为函数参数或返回值的连续框架之间传递数据实现的。 在异常处理缺少,每个调用方需要检查它所调用的函数的成功,或只是假设所有内容始终是确定。

大多数的 Win32 API 返回非零的值,以指示失败,因为 Windows 不使用异常处理。 程序员必须包装代码检查被调用的函数的返回值的每个函数调用。 是例如从 MSDN 文档,了解此代码,目录中列出的文件显式会检查每个调用成功。 在调用 FindNextFile(...) 被包装在检查,以查看返回是否不为零。 如果在调用不成功单独函数调用,GetLastError (),提供异常的条件的详细信息。 请注意,因为返回值是一定限于本地作用域必须成功下一帧上中检查每次调用:

// FindNextFile requires checking for success of each call 
while (FindNextFile(hFind, &ffd) != 0); 
dwError = GetLastError(); 
if (dwError != ERROR_NO_MORE_FILES) 
{ 
  ErrorHandler(TEXT("FindFirstFile")); 
} 
FindClose(hFind); 
return dwError; 

从包含意外的情况,给该函数的调用方函数只能传递错误条件。 异常具有强大功能来传递函数的执行结果超出当前函数范围给堆栈上的每个帧至知道如何处理意外的情况的框架。 CLR 的异常系统 (称为两次通过异常系统) 将异常传递到线程的调用堆栈上, 每个前置以调用方开头,并继续直到某些功能说它会处理的异常 (这称为第一轮) 中。

然后,异常系统将展开其中引发异常和则为处理 (称为第二步中) 之间的调用堆栈上的每个框架的状态。 为堆栈 unwinds,CLR 将 finally 子句和 fault 子句以运行每个框架按原样 DPN。 然后,执行处理框架中的到 catch 子句。

因为 CLR 检查调用堆栈上的每个前置任务的调用方具有 catch 块不需要,可以被堆栈上任何位置捕获该异常。 而不是让代码以立即检查每个函数调用的结果,程序员可以处理远从引发异常的一个位置中的错误。 使用错误代码,将需要程序员可以检查和错误代码在每个堆栈帧传递至可进行处理错误条件的位置。 异常处理释放从检查在堆栈上的每个帧异常程序员。

有关详细引发自定义的异常类型,请参阅" 错误处理: 从托管 COM+ 服务器应用程序中引发自定义异常类型".

Win 32 SEH 异常和 System.Exception

还有来自能够捕获远从引发异常的异常的有趣副作用。 程序线程可以接收来自任何活动的框架,它调用堆栈上的程序例外不知道引发异常。 但例外不始终代表程序检测到的错误条件: 程序线程也可能导致程序之外的异常。

如果一个线程执行导致错误的处理器,然后将控制转到操作系统内核提供了到为 SEH 例外线程错误。 正如您的 catch 块不知道其中引发了异常的线程的堆栈上, 它不需要知道完全什么时候操作系统内核引发 SEH 异常。

Windows 将通知有关使用 SEH OS 例外的程序线程。 托管的代码的程序员很少看这些因为 CLR 通常会阻止该类型的由 SEH 异常的错误。 但是,如果 Windows 引发 SEH 异常,CLR 将提供它为托管代码。 尽管在托管代码中的 SEH 异常是极少,不安全的托管的代码可以生成一个 STATUS_­ACCESS_VIOLATION 表示程序试图访问无效内存。

有关 SEH 的详细信息,请参阅 Matt Pietrek 文章" 在 Win 32 的深度的崩溃课程结构化异常处理"1997 年 1 月发布的 Microsoft Systems Journal .

SEH 异常都不同的类从由程序引发这些异常。 某个程序可能会引发异常,因为它试图中弹出空牌叠中的某个项目或尝试打开一个文件,不存在。 所有这些异常的意义的程序的执行上下文中。 SEH 异常指向外部的程序上下文。 访问冲突 (AV),例如,表示无效的内存的尝试的写入。 与程序错误不同 SEH 异常指示运行库的过程的完整性可能已被破坏。 但即使 SEH 异常是不同于 CLR 将 SEH 异常传递到托管的线程时,从 System.Exception 中, 派生的异常它可以被捕获使用 catch (Exception e) 语句。

某些系统尝试分隔这些两种类型的异常。 Microsoft Visual C++ 编译器区分 C++ throw 语句和 Win 32 SEH 异常如果编译使用 /EH 开关程序引发的异常。 这种分离是有用的因为一个普通的程序不知道如何处理没有不引发的错误。 如果 C++ 程序尝试将元素添加到一个 std::vector,应的操作可能不足内存,无法由于但正确的程序使用编写良好的库不应该处理的访问冲突。

这种分离,则适合程序员。 AV 是一个严重的问题: 关键的系统内存的意外的写入时可能产生不可预料的结果会影响过程的任何一部分。 但一个零除错误,导致的错误和未检查用户输入,等某些 SEH 错误是严重。 正确使用一个由零除的某个程序时就不这会影响系统的任何其他部分。 实际上,很可能 C++ 程序可以处理不 destabilizing 系统的其余部分的一个零除错误。 因此有用这种分离时它不会很表示托管的语义程序员需要。

托管代码和 SEH

CLR 有总会 SEH 异常发送到使用相同的机制为由程序引发的异常的托管代码。 这不是问题,只要代码不会尝试处理无法合理处理的异常情况。 大多数程序不能安全地继续访问冲突后的执行。 遗憾的是,CLR 的异常处理模型具有始终鼓励用户要允许程序捕获顶部 System.Exception 层次结构的任何异常捕获这些严重错误。 但这是很少要做正确的事情。

编写 catch (Exception e) 是一个常见的编程错误,因为未处理的异常有严重的后果。 但是,您可能认为如果您不知道由一个函数,将产生错误,应保护对所有可能的错误在程序调用该函数时。 这似乎类似的操作的合理过程直到您对的含义继续执行,当您的流程可能处于损坏的状态。 有时中止,尝试再次是最佳选择: 没有人喜欢看到一个 Watson 对话框但最好重新启动您的程序比要有数据损坏。

程序捕捉异常由他们不了解是一个严重的问题的上下文。 但是您不能使用异常规范或某些其他合同机制解决此问题。 和重要托管的程序能够接收 SEH 异常的通知,因为 CLR 是一个平台,许多种类的应用程序和主机。 有些主机,such as SQL Server,需要总控制应用程序的过程。 与本机代码有时托管的代码的相互操作必须处理本机 C++ 异常或 SEH 异常。

但大多数程序员编写 catch (Exception e) 确实不希望捕获访问冲突。 他们更愿意发生灾难性错误时其程序停止而不是让处于未知状态 limp 以及该程序的执行。 对于程序尤其如此该主机托管外接程序如 Visual Studio 或 Microsoft Office。 如果加载项会导致访问冲突并且然后 swallows 异常,主机可能会执行损坏自己州 (或用户文件) 不过意识到出现问题。

在 CLR 的版本 4,产品团队进行表示不同于所有其他异常的损坏的进程状态的异常。 我们将以指示已损坏的进程的状态指定有关个 SEH 异常。 指定与的上下文相对于异常类型本身引发异常。 这意味着从 Windows 收到访问冲突将被标记为一个损坏的状态异常 (CSE),但由新 System.AccessViolation­exception 将未标记为一个 CSE 编写的 throw 引发用户代码中的一个。 如果您参加了 PDC 2008,您将收到一个社区技术预览的 Visual Studio 2010 包括这些更改。

一定要注意该异常不会损坏进程: 进程状态中检测到损坏后,引发异常。 是例如通过不安全的代码中的指针写入引用不属于该程序的内存时, 引发访问冲突。 非法写入实际上没有发生,操作系统检查内存的所有权并阻止该操作从记录的位置。 访问冲突表示本身的指针已损坏在一个线程的执行。

损坏的状态异常

在版本 4 及更高版本,CLR 异常系统将不提供 CSEs 为托管代码除非代码明确指出它可以处理损坏的进程状态异常。 这意味着在托管代码中的 catch (Exception e) 的实例不会有提供给它的 CSE。 中的 CLR 异常系统更改,从而您不必更改异常层次结构,或更改任何托管的语言的异常处理语义。

出于兼容性的原因 CLR 团队提供可以运行您的旧代码旧行为下的一些方法:

  • 如果要重新编译代码在 Microsoft.NET Framework 3.5 中创建并运行它在.NET Framework 4.0 而不必更新源,可以在应用程序配置文件中添加一个条目: legacyCorruptedState­­ExceptionsPolicy = true。
  • 针对.NET Framework 3.5 或更早版本运行库的编译的程序集将能够处理损坏的状态异常 (换句话说,维护旧行为).NET Framework 4.0 上运行时。

如果您希望在代码中来处理 CSEs,您必须指定您需要通过标记包含带有新的属性在异常子句 (catch,最后或错误),函数: System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions。 如果引发一个 CSE,CLR 将执行的搜索匹配的 catch 子句,但将仅搜索用 HandleProcessCorruptedStateExceptions 属性标记的函数中 (参见 图 2 )。

图 2 使用 HandleProcessCorruptedStateExceptions

// This program runs as part of an automated test system so you need
// to prevent the normal Unhandled Exception behavior (Watson dialog).
// Instead, print out any exceptions and exit with an error code.
[HandledProcessCorruptedStateExceptions]
public static int Main()
{
    try
    {
        // Catch any exceptions leaking out of the program
        CallMainProgramLoop();
    }
    catch (Exception e) // We could be catching anything here
    {
        // The exception we caught could have been a program error
        // or something much more serious. Regardless, we know that
        // something is not right. We'll just output the exception
        // and exit with an error. We won't try to do any work when 
        // the program or process is in an unknown state!
        System.Console.WriteLine(e.Message);
        return 1;
    }
    return 0;
  } 

如果找到合适的 catch 子句,CLR 将展开堆栈,正常,但只将最后执行和 fault 块 (和 C# 中,隐式最后阻止在使用的语句) 函数标记为属性中。 遇到在部分受信任或透明的代码中,因为受信任的宿主不希望的不受信任外接程序捕获和忽略这些严重异常时,都忽略该 HandleProcessCorruptedStateExceptions 属性。

很仍错误使用 Catch (Exception e)

即使 CLR 异常系统将最坏的例外情况标记为已 CSE,它最好仍不在您在代码中写 catch (Exception e)。 异常表示意外情况的整个的系列。 CLR 可以检测最坏的异常,指示可能已损坏的进程状态的 SEH 异常。 但其他意外的情况仍可以有害如果忽略或常规处理。

在进程损坏缺少,CLR 提供了一些非常强程序的正确性和内存的安全保证。 执行以安全的 Microsoft 中间语言 (MSIL) 代码您可以确定某个程序时在程序中的所有指令将正确都执行。 但执行程序说明假设为是通常不同执行,程序员想。 根据给 CLR 完全正确的程序可能会损坏如程序文件以写入到磁盘之类的持久的状态。

以一个简单的示例为管理一个高中的考试分数的数据库的程序。 程序使用面向对象的设计原则封装数据,并引发托管的异常,以指示意外的事件。 学校 Secretary 太点击 Enter 键一的一天,很多时间生成成绩文件时。 程序尝试将值从一个空队列中弹出,并引发一个 QueueEmptyException 在调用堆栈上是通过框架处理的。

某处靠近堆栈的顶部是函数,GenerateGrades(),使用 try/catch 子句捕捉异常的。 遗憾的是,GenerateGrades() 有学生存储在队列中,不会有任何想法如何处理一个 QueueEmpty­exception 不知道。 但编写 GenerateGrades() 的程序员不想而不保存的计算到目前为止的数据的程序崩溃。 所有内容将安全地写入到磁盘并退出程序。

使用此程序问题是它使许多可能不正确的假设。 什么是说学生队列中缺少的条目位于末尾? 可能是第一个学生记录清楚跳过,或最十分位。 该异常只告诉程序员程序不正确。 采取任何操作,将数据保存到磁盘或"恢复"和继续执行,是只是普通的错误。 不不可能上下文中引发异常的不知情的情况下任何正确的操作。

如果该程序已捕获接近于引发该异常的特定异常,它可能已能够采取适当措施。 该程序知道一个 QueueEmptyException 试图出列学生的函数中的含义。 如果函数捕获该异常类型,而不是捕捉整个类异常类型,它将尝试进行正确的程序状态的更好位置中。

一般情况下,捕获特定异常是正确做,因为它提供异常处理程序在大多数上下文。 如果您的代码可能可以捕获两种例外情况,然后它必须能够同时处理。 编写代码,指出 catch (Exception e) 必须为能够处理真正任何异常的情况。 这是一个很难保留的承诺。

某些语言尝试阻止程序员捕获异常一大类。 例如,C++ 有一机制,允许程序员指定哪些异常可引发的函数中的异常规范。 Java 采用这进一步但选中的情况例外编译器强制要求指定异常类。 在这两个的语言列表超出函数声明中的此函数可以流动的异常,并调用方需要处理这些异常。 异常规范是一个不错的主意,但它们具有了混合模式操作的结果。

还有为为了任何托管的代码应能够处理 CSEs vigorous 争论。 这些异常通常表示一个系统级错误,和只应了解系统级上下文的代码处理。 虽然大多数人不需要能够处理 CSEs,有几个方案,有必要。

一个方案是当您已经非常接近于在异常发生。 例如,考虑调用的已知 buggy 的本机代码的程序。 调试您了解它有时零的指针访问它之前的代码,这会导致访问冲突。 可以调用本机代码,因为您知道指针损坏的原因,并认为是维护进程完整性安全使用 P/Invoke 函数上使用 HandleProcessCorruptedStateExceptions 属性。

使用此属性可能会调用的其他方案时您就可以从错误。 实际上,您就几乎可以退出您的进程。 假设您已编写一个主机或一个框架,想要执行错误的一些自定义日志记录。 可以封装您的 Main 函数与 try/catch / finally 块,并将其标记与 HandleProcessCorruptedStateExceptions。 错误意外使得一直上程序的主函数,您是否写入一些数据您的日志作为少地,必须并退出此过程。 任何工作当过程的完整性为有问题可能是危险,但如果自定义日志记录失败有时可接受。

请看一下 图 3 所示的图表。 此函数 1 (fn1()) 以便其 catch 子句捕获访问冲突属性与 [HandleProcess­CorruptedStateExceptions]。 在 finally 块函数 3 不会执行即使该异常将被捕获函数 1 中。 在堆栈底部函数 4 引发访问冲突。

fig03.gif

图 3 异常和访问冲突

没有在这些情况下以及您正操作是完全安全,但还有就终止进程的不可接受的方案任一保证。 但是,如果您决定处理一个 CSE 没有大的负担对您作为程序员正确地执行该操作。 请记住 CLR 异常系统不会甚至交给一个 CSE 没有标记为新的属性是在第一轮搜索匹配的 catch 子句) 时或第二步 (时 unwinds 每个框架的状态和执行 finally 和 fault 块) 过程中的任何函数。

在 finally 块存在为了保证该代码始终运行,有是否异常。 (fault 块只时运行异常发生,但是它们具有类似保证始终正在执行)。 这些构造用于清理如释放文件句柄或反转模拟环境的重要资源。

甚至编写使用可靠的代码限制的执行,除非它是已被标记为 HandleProcessCorruptedStateExceptions 属性的函数中引发一个 CSE 时不会执行区域 (CER)。 很难非常编写正确的代码来处理一个 CSE 并继续安全地运行该进程。

仔细查看 图 4 查看内容可以转错误代码。 如果此代码不能处理 CSEs 的函数中则 finally 块不能运行发生访问冲突时。 很好在进程终止,将发布打开的文件句柄。 但如果某个其他代码捕获访问冲突,并尝试还原状态,需要知道它必须关闭该文件,以及恢复该程序已更改任何其他外部状态。

图 4 </a0>-finally 块可能不运行

void ReadFile(int index)
    {
      System.IO.StreamReader file = 
        new System.IO.StreamReader(filepath);
          try
          {
            file.ReadBlock(buffer, index, buffer.Length);
          }
          catch (System.IO.IOException e)
          {
            Console.WriteLine("File Read Error!");
          }
          finally
          {
            if (file != null)
                {
                    file.Close()
                }
          }
    }

如果您决定您要处理一个 CSE,您的代码需要预期没有的未 DPN 的关键状态的大量。 最后,fault 块不具有已运行。 不执行受约束的执行区域。 该程序) 和过程处于未知状态。

如果您知道您的代码将执行正确的操作时,就会知道如何操作。 但如果您不确定状态的程序在内,执行,则最好只是让您退出的进程。 或如果您的应用程序承载,调用您的主机已指定该升级策略。 请参阅 Alessandro Catorcini 和 Brian Grunkemeyer 从 2007 年 12 月的 CLR 透彻列 有关编写可靠代码和 CER 的更多信息。

明智地代码

尽管 CLR 阻止您 naively 捕获 CSEs,是仍不一个好主意要捕获的异常的过大类。 但 catch (Exception e) 显示在大量代码中,,就不可能这将更改。 通过不提供表示一个损坏的进程状态 naively 捕捉所有异常的代码的异常,可防止此代码进行严重情况更糟糕。

在下次您编写或维护捕捉异常的代码时考虑该异常的含义。 捕获哪些程序 (和它使用的库) 都会引发的匹配类型? 您知道如何处理该异常,以便程序可以正确并安全地继续执行吗?

异常处理是一个功能强大的工具,应仔细和 thoughtfully 使用。 如果您确实要使用此功能,如果您真正需要处理可能已损坏的进程的异常,CLR 将信任您并可以执行此操作。 只需小心,并正确地执行该操作。

将您的问题和提出的意见发送至 clrinout@Microsoft.com.

Andrew Pardoe 是 Microsoft 的 CLR 的程序经理。 他适用于桌面和 Silverlight 的运行库执行引擎的许多方面。 在可以访问他 Andrew.Pardoe@Microsoft.com.