Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (kevin13@chol.net)
홈페이지
첨부 파일

(번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes

이번 글은 다음의 시리즈에 대한 번역 글입니다. ^^ (참고로, 1:1 식으로 번역하지 않았습니다.)

.NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes
; https://blog.adamfurmanek.pl/2019/02/16/net-internals-cookbook-part-1/

글의 내용이 너무 좋아서 ^^ 많은 분들에게 알리고 싶어 원 저자의 허락을 받아 게시하는 것인데, 혹시 내용이 틀린 부분이 있다면 덧글 부탁드립니다.

(첨부 파일은 이 글의 예제 프로젝트를 포함합니다.)





1. System.Exception으로부터 상속받지 않은 객체를 throw로 던지면 어떻게 될까요?

C#의 경우에는 가능하지 않지만 C++/CLI와 같은 언어에서는 System.Exception 객체로부터 상속받지 않은 객체도 throw하는 것이 가능합니다. 예를 들어, integer, string, byte 등의 인스턴스를 직접 throw할 수 있는데 CLR은 이것을 일반 예외와 구분하기 위해 System.Runtime.CompilerServices.RuntimeWrappedException이라는 예외를 따로 제공합니다.

하지만 초기 .NET 1.x 버전에서는 그와 같은 (System.Exception 상속 이외의) 객체들이 RuntimeWrappedException 예외로 처리되지 않고 조건이 없는 catch 문으로만 다룰 수 있었습니다. 그래서 그 당시에는 다음과 같은 식의 예외 처리가 의미가 있었습니다.

// .NET 1.x
try
{
}
catch (Exception e) // System.Exception 및 그의 상속 객체들
{
}
catch // C++/CLI 등의 언어에서 throw할 수 있는 System.Exception을 상속하지 않은 객체들
{
}

위의 코드는 C#의 경우 .NET 2.0부터 컴파일 경고가 발생하는데,

warning CS1058: A previous catch clause already catches all exceptions. All non-exceptions thrown will be wrapped in a System.Runtime.CompilerServices.RuntimeWrappedException.

마지막 catch 문이 절대 실행되지 않기 때문입니다. 즉, .NET 2.0부터는 다음과 같이 마이그레이션을 해야 합니다.

// .NET 2.0 ~ (now)
try
{
}
catch (RuntimeWrappedException e) // C++/CLI 등의 언어에서 throw할 수 있는 System.Exception을 상속하지 않은 객체들
{
}
catch (Exception) // System.Exception 및 그의 상속 객체들
{
}

만약 호환을 위해 .NET 1.x과 같이 처리하고 싶다면 RuntimeCompatibilityAttribute 특성을 설정해야 합니다.

[assembly: RuntimeCompatibilityAttribute(WrapNonExceptionThrows = false)]

실제로 테스트를 해볼까요? ^^

다음의 C/C++ CLI 코드가 있을 때,

public ref class MyTest
{
public:
    void DoMethod()
    {
        throw 5;
    }

    void DoMethod2()
    {
        throw gcnew System::Int32(5);
    }
};

C#에서 이 코드를 다음과 같이 처리해 보면,

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true)]

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            MyTest my = new MyTest();

            Test(() => my.DoMethod());
            Test(() => my.DoMethod2());
        }

        private static void Test(Action action)
        {
            try
            {
                action();
            }
            catch (RuntimeWrappedException e1) // C++/CLI 등의 언어에서 throw할 수 있는 System.Exception을 상속하지 않은 관리 객체들
            {
                Console.WriteLine(e1.ToString());
            }
            catch (SEHException e2) // C++/CLI 등의 언어에서 throw할 수 있는 System.Exception을 상속하지 않은 비-관리 객체들
            {
                Console.WriteLine(e2.ToString());
            }
            catch (Exception e3) // System.Exception 및 그의 상속 객체들
            {
                Console.WriteLine(e3.ToString());
            }
            catch
            {
                Console.WriteLine("untyped catch");
            }
        }
    }
}

WrapNonExceptionThrows = true인 실행 결과와,

throw 5
    System.Runtime.InteropServices.SEHException (0x80004005): External component has thrown an exception.
       at _CxxThrowException(Void* , _s__ThrowInfo* )
       at MyTest.DoMethod() in F:\...\ConsoleApp1\Dll1\pch.h:line 19
       at ConsoleApp1.Program.<>c__DisplayClass0_0.<Main>b__0() in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 15
       at ConsoleApp1.Program.Test(Action action) in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 23

throw gcnew System::Int32(5)
    System.Runtime.CompilerServices.RuntimeWrappedException: An object that does not derive from System.Exception has been wrapped in a RuntimeWrappedException.
       at MyTest.DoMethod2() in F:\...\ConsoleApp1\Dll1\pch.h:line 23
       at ConsoleApp1.Program.<>c__DisplayClass0_0.<Main>b__1() in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 16
       at ConsoleApp1.Program.Test(Action action) in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 23

WrapNonExceptionThrows = false인 결과가 다릅니다.

throw 5
    System.Runtime.InteropServices.SEHException (0x80004005): External component has thrown an exception.
       at _CxxThrowException(Void* , _s__ThrowInfo* )
       at MyTest.DoMethod() in F:\...\ConsoleApp1\Dll1\pch.h:line 19
       at ConsoleApp1.Program.<>c__DisplayClass0_0.<Main>b__0() in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 15
       at ConsoleApp1.Program.Test(Action action) in F:\...\ConsoleApp1\ConsoleApp1\Program.cs:line 23

throw gcnew System::Int32(5)
    untyped catch





2. ThreadAbortException을 무시할 수 있을까요?

ThreadAbortException 예외는 잡을 수는 있지만 기본적으로 무시하진 않습니다. 만약 무시하고 싶다면 명시적으로 Thread.ResetAbort() 메서드를 호출해야 합니다. 다시 말하면, Thread.Abort를 호출했다고 해서 언제나 해당 스레드가 종료된다고 보장할 수는 없습니다.

또 한가지 재미있는 점은, Thread.Abort를 호출하면 스레드의 나머지 부분은 실행되지 않지만 finally 구문에 작성한 코드는 여전히 실행된다는 점입니다. 결국 대상 스레드가 finally 절의 코드를 수행 중이라면 Thread.Abort는 그것조차도 막을 수 없다는 것입니다. 실제로 CER (Constrained Execution Regions) 처리 시 반드시 수행되어야 하는 보호 코드를 finally 블록에 넣어야 하는 것에는 이런 이유도 있습니다.

참고로, Thread.Abort의 내부 구현은 다음과 같은 식입니다.

  1. OS 스레드를 중지(Suspend)시키고,
  2. 스레드 중지가 요청되었음을 알리는 메타데이터 비트를 설정 후,
  3. 해당 스레드의 APC 큐에 작업을 추가하고,
  4. OS 스레드를 다시 시작(Resume)

따라서 Thread.Abort가 호출되면 스레드는 잠시 멈췄다가 다시 실행이 되는데, APC 큐의 작업은 이후 스레드가 alertable 상태로 진입할 때 실행됩니다. 그때 위의 2번 작업에서 수행한 비트 설정 유무를 체크해서 실질적으로 스레드 종료 작업을 밟게 되는데, 만약 이 과정에서 스레드가 자발적인 alertable 상태로 진입하지 않으면 CLR은 스레드의 IP 레지스터를 강제로 설정하는 방식으로 결국 APC 작업이 수행되도록 합니다.





3. AccessViolationException 류의 예외를 잡을 수 있을까요?

그와 같은 예외를 잡으려면 해당 메서드에 대해 HandleProcessCorruptedStateExceptionsAttribute 특성을 지정해야 합니다. 예를 들어 볼까요? ^^

using System;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            MyMethod();
        }

        public static int MyMethod()
        {
            try
            {
                Marshal.Copy(new byte[] { 42 }, 0, (IntPtr)1000, 1);
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Catched: " + e.ToString());
                return 1;
            }

            return 0;
        }
    }
}

위의 예제는, System.AccessViolationException 예외가 발생하지만 catch 문의 코드가 실행되지 않습니다. 반면 HandleProcessCorruptedStateExceptions 특성을 MyMethod에 다음과 같이 지정하면,

[HandleProcessCorruptedStateExceptions]
public static int MyMethod()
{
    // ...[생략]...
}

이번에는 MyMethod 안에 있는 catch 문의 코드가 실행됩니다. 위의 상황은 .NET 2.0부터 바뀐 것이고 .NET 1.x 시절에는 무조건 catch 문으로 AV 예외를 잡을 수 있었습니다. 만약 .NET 2.0에서도 HandleProcessCorruptedStateExceptions 특성 없이 AV 예외를 잡고 싶다면 app.config에 다음과 같은 설정을 하면 됩니다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <runtime>
        <legacyCorruptedStateExceptionsPolicy enabled="true" />
    </runtime>
</configuration>

이렇게 바꾸고 HandleProcessCorruptedStateExceptions 특성을 주석 처리해도 catch 문의 코드가 실행되는 걸 확인할 수 있습니다. 그런데 사실 .NET 1.x에서는 System.AccessViolationException 타입도 없었습니다. 원래는 .NET 1.x의 경우 AV 예외를 System.NullReferenceException으로 처리했는데요, 이것도 .NET 2.0 이상에서 하위 호환을 위해 1.x처럼 AV 예외를 null 참조 예외로 바꿀 수 있도록 옵션이 제공됩니다.

<legacyNullReferenceExceptionPolicy enabled="true" />

따라서, legacyCorruptedStateExceptionsPolicy, legacyNullReferenceExceptionPolicy 옵션이 모두 true라면 HandleProcessCorruptedStateExceptions 특성이 없어도 AV 예외가 .NET 1.x와 동일하게 System.NullReferenceException으로 잡히게 됩니다.





4. finally 블록의 코드가 실행되지 않거나, finally가 중첩된 경우 특정 finally만 실행되는 경우도 있나요?

두 가지 상황 모두 발생할 수 있습니다.

우선 첫 번째 상황의 경우, 예를 들어 응용 프로그램을 작업 관리자 등을 이용해 강제 종료를 하면 finally 코드가 수행되지 않는 것을 확인할 수 있습니다. 또는, 코드로 테스트해 보려면 Environment.FailFast 메서드로 재현할 수 있습니다.

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Thread.Sleep(1000 * 5);
            Environment.FailFast("FailFast called!");
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}

이 프로그램은 5초 후에 Environment.FailFast가 호출되지만 "Finally" 문자열 출력을 볼 수 없습니다.

두 번째 상황의 경우는 이전에 설명한 HandleProcessCorruptedStateExceptions의 도움을 받아 재현할 수 있습니다. 가령 다음과 같이 try/finally 블록이 있는 경우,

private static void Method1()
{
    try
    {
        Method2();
    }
    finally
    {
        Console.WriteLine("Method 1");
    }
}

private static void Method2()
{
    try
    {
        Method3();
    }
    finally
    {
        Console.WriteLine("Method 2");
    }
}

private static void Method3()
{
    throw new ApplicationException("TEST");
}

Method3에서의 예외로 인해 Method2의 finally와 Method1의 finally가 차례대로 실행됩니다. 반면, Method3에서 System.AccessViolationException이 발생하는 상황에서 Method1에만 HandleProcessCorruptedStateExceptions 특성을 지정했다면,

[HandleProcessCorruptedStateExceptions]
private static void Method1()
{
    try
    {
        Method2();
    }
    finally
    {
        Console.WriteLine("Method 1");
    }
}

private static void Method2()
{
    try
    {
        Method3();
    }
    finally
    {
        Console.WriteLine("Method 2");
    }
}

private static void Method3()
{
    Marshal.Copy(new byte[] { 42 }, 0, (IntPtr)1000, 1);
}

이번에는 화면에 Method1의 finally 블록만 실행되는 것을 확인할 수 있습니다.





5. fault란?

C#에서 지원하는 예외 처리 블록은 try, catch, finally로 3가지가 있습니다. 반면 IL 수준으로 내려가면 fault 블록을 하나 더 정의할 수 있는데 이는 예외가 발생하는 경우에만 실행됩니다. (finally의 경우 예외 유무에 상관없이 실행되는 것과 비교됩니다.)

따라서 IL 코드로 다음과 같이 작성해 ilasm.exe로 컴파일하면 fault 영역을 테스트할 수 있습니다.

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly 'bacccde7-828a-4616-94e8-da4332f65171'
{
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module 'bacccde7-828a-4616-94e8-da4332f65171.dll'
// MVID: {63E2991D-2F64-4FE2-8CF3-0A1E1F6A1FD1}
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x01280000

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .entrypoint
    .maxstack  1
    .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}'
    .line 6,6 : 2,3 ''
    IL_0000:  nop
    .line 7,7 : 6,7 ''
    .try
    {
      IL_0001:  nop
      .line 9,9 : 3,4 ''
      IL_0002:  nop
      IL_0003:  leave.s    IL_0013

      .line 9,9 : 11,12 ''
    }  // end .try
    fault
    {
      IL_0005:  nop
      .line 10,10 : 4,52 ''
      IL_0006:  ldstr      "This will not be executed."
      IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_0010:  nop
      .line 11,11 : 3,4 ''
      IL_0011:  nop
      IL_0012:  endfinally
      .line 16707566,16707566 : 0,0 ''
    }  // end handler
    IL_0013:  nop
    .line 13,13 : 6,7 ''
    .try
    {
      IL_0014:  nop
      .line 14,14 : 4,42 ''
      IL_0015:  ldstr      "Some exception"
      IL_001a:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
      IL_001f:  throw

      .line 15,15 : 11,12 ''
    }  // end .try
    fault
    {
      IL_0020:  nop
      .line 16,16 : 4,44 ''
      IL_0021:  ldstr      "You will see this."
      IL_0026:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_002b:  nop
      .line 17,17 : 3,4 ''
      IL_002c:  nop
      IL_002d:  endfinally
    }  // end handler
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class Program





6. 예외 필터(exception filter)란?

예외를 바로 처리하는 대신에, 해당 예외의 조건을 미리 검사한 후 처리할 수 있는 구문이 바로 "예외 필터"입니다. 다른 시리즈의 글(Part 2 - Handling and rethrowing exceptions in C#)에서 예외 처리 구조에 대해 언급했지만 필터의 경우 두 단계로 이뤄진 예외 처리 구조에서 첫 번째 시점에 실행된다는 차이점이 있습니다.

그렇다면 이것이 기존 구문의 조합으로 가능한 간편 표기법(syntax sugar)일까요? 이를 위해 다음의 코드를 빌드하고,

using System;

public class Program
{
    public static void Main()
    {
        try
        {
        }
        catch (Exception e) when (e.Message == "Message") // C# 6.0부터 예외 필터 지원
        {
        }
    }
}

IL 코드로 보면,

.try
{
  IL_0001:  nop
  .line 9,9 : 3,4 ''
  IL_0002:  nop
  IL_0003:  leave.s    IL_002e

  .line 16707566,16707566 : 0,0 ''
}  // end .try
filter
{
  IL_0005:  isinst     [mscorlib]System.Exception
  IL_000a:  dup
  IL_000b:  brtrue.s   IL_0011

  IL_000d:  pop
  IL_000e:  ldc.i4.0
  IL_000f:  br.s       IL_0027

  IL_0011:  stloc.0
  .line 10,10 : 22,51 ''
  IL_0012:  ldloc.0
  IL_0013:  callvirt   instance string [mscorlib]System.Exception::get_Message()
  IL_0018:  ldstr      "Message"
  IL_001d:  call       bool [mscorlib]System.String::op_Equality(string,
                                                                 string)
  IL_0022:  stloc.1
  .line 16707566,16707566 : 0,0 ''
  IL_0023:  ldloc.1
  IL_0024:  ldc.i4.0
  IL_0025:  cgt.un
  IL_0027:  endfilter
  .line 16707566,16707566 : 0,0 ''
}  // end filter
{  // handler
  IL_0029:  pop
  .line 10,10 : 51,52 ''
  IL_002a:  nop
  .line 11,11 : 3,4 ''
  IL_002b:  nop
  IL_002c:  leave.s    IL_002e

  .line 12,12 : 2,3 ''
}  // end handler

별도의 filter 영역이 지정된 것을 볼 수 있습니다. 즉, 간단하게 또 다른 C# 코드로 엮어낼 수 있는 syntax sugar 구현이 아닙니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]





[최초 등록일: ]
[최종 수정일: 3/8/2019 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:kevin13@chol.com

비밀번호

댓글 쓴 사람
 



2019-03-13 12시20분
[kernel] filter 는 stack unwinding 이 안되어서 덤프만들때 잘 쓰고 있습니다! (https://www.thomaslevesque.com/2015/06/21/exception-filters-in-c-6/ ) 번역해주셔서 고맙습니다!
[손님]

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
11852정성태3/20/201934개발 환경 구성: 434. 존재하지 않는 IP 주소에 대한 Dns.GetHostByAddress/gethostbyaddr/GetNameInfoW 실행이 느리다면?파일 다운로드1
11851정성태3/19/201968Linux: 8. C# - 리눅스 환경에서 DllImport 대신 라이브러리 동적 로드 처리
11850정성태3/18/201997.NET Framework: 813. C# async 메서드에서 out/ref/in 유형의 인자를 사용하지 못하는 이유
11849정성태3/18/201956.NET Framework: 812. pscp.exe 기능을 C#으로 제어하는 방법파일 다운로드1
11848정성태3/17/201964스크립트: 14. 윈도우 CMD - 파일이 변경된 경우 파일명을 변경해 복사하고 싶다면?
11847정성태3/17/201950Linux: 7. 리눅스 C/C++ - 공유 라이브러리 동적 로딩 후 export 함수 사용 방법파일 다운로드1
11846정성태3/15/201958Linux: 6. getenv, setenv가 언어/운영체제마다 호환이 안 되는 문제
11845정성태3/15/201956Linux: 5. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency)
11844정성태3/14/201968개발 환경 구성: 434. Visual Studio 2019 - 리눅스 프로젝트를 이용한 공유/실행(so/out) 프로그램 개발 환경 설정파일 다운로드1
11843정성태3/14/201940기타: 75. MSDN 웹 사이트를 기본으로 영문 페이지로 열고 싶다면?
11842정성태3/13/201962개발 환경 구성: 433. 마이크로소프트의 CoreCLR 프로파일러 예제를 Visual Studio CMake로 빌드하는 방법파일 다운로드1
11841정성태3/13/201947VS.NET IDE: 132. Visual Studio 2019 - CMake의 컴파일러를 기본 g++에서 clang++로 변경
11840정성태3/13/201933오류 유형: 526. 윈도우 10 Ubuntu App 환경에서는 USB 외장 하드 접근 불가
11839정성태3/12/2019106디버깅 기술: 124. .NET Core 웹 앱을 호스팅하는 Azure App Services의 프로세스 메모리 덤프 및 windbg 분석 개요 [1]
11838정성태3/8/2019211.NET Framework: 811. (번역글) .NET Internals Cookbook Part 1 - Exceptions, filters and corrupted processes [1]파일 다운로드1
11837정성태3/14/201967기타: 74. [예약]
11836정성태3/5/2019119오류 유형: 525. Visual Studio 2019 Preview 4/RC - C# 8.0 Missing compiler required member 'System.Range..ctor' [1]
11835정성태3/5/2019211.NET Framework: 810. C# 8.0의 Index/Range 연산자를 .NET Framework에서 사용하는 방법 및 비동기 스트림의 컴파일 방법파일 다운로드1
11834정성태3/4/2019170개발 환경 구성: 432. Visual Studio 없이 최신 C# (8.0) 컴파일러를 사용하는 방법
11833정성태3/4/201999개발 환경 구성: 431. Visual Studio 2019 - CMake를 이용한 공유/실행(so/out) 리눅스 프로젝트 설정파일 다운로드1
11832정성태3/4/201948오류 유형: 524. Visual Studio CMake - rsync: connection unexpectedly closed
11831정성태3/4/201987오류 유형: 523. Visual Studio 2019 - 새 창으로 뜬 윈도우를 닫을 때 비정상 종료
11830정성태2/26/201970오류 유형: 522. 이벤트 로그 - Error opening event log file State. Log will not be processed. Return code from OpenEventLog is 87.
11829정성태2/26/201990개발 환경 구성: 430. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 - 리눅스 환경
11828정성태2/26/201969개발 환경 구성: 429. Component Services 관리자의 RuntimeBroker 설정이 2개 있는 경우
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...