Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

(번역글) .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/

(번역글) .NET Internals Cookbook Part 2 - GC-related things
; https://www.sysnet.pe.kr/2/0/11869

(번역글) .NET Internals Cookbook Part 3 - Initialization tricks
; https://www.sysnet.pe.kr/2/0/11871

(번역글) .NET Internals Cookbook Part 4 - Type members
; https://www.sysnet.pe.kr/2/0/11872

(번역글) .NET Internals Cookbook Part 5 - Methods, parameters, modifiers
; https://www.sysnet.pe.kr/2/0/11873

(번역글) .NET Internals Cookbook Part 6 - Object internals
; https://www.sysnet.pe.kr/2/0/11874

(번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others
; https://www.sysnet.pe.kr/2/0/11876

(번역글) .NET Internals Cookbook Part 8 - C# gotchas
; https://www.sysnet.pe.kr/2/0/11877

(번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff
; https://www.sysnet.pe.kr/2/0/11878

(번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others
; https://www.sysnet.pe.kr/2/0/11879

(번역글) .NET Internals Cookbook Part 11 - Various C# riddles
; https://www.sysnet.pe.kr/2/0/11882

(번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
; https://www.sysnet.pe.kr/2/0/11891

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

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





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 구현이 아닙니다.




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

[연관 글]





[최초 등록일: ]
[최종 수정일: 5/9/2019 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.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)
11900정성태5/18/2019732오류 유형: 537. "sfc /scannow" 실행 중 시스템이 부팅되는 현상
11899정성태5/17/2019916Linux: 9. Linux에서 윈도우의 OutputDebugString 대신 사용할 수 있는 syslog [1]
11898정성태5/20/2019816VC++: 130. C++ string의 c_str과 data 함수의 차이점 [3]
11897정성태5/16/20191377오류 유형: 536. Visual Studio - "Developer Pack"을 설치했는데도 "대상 프레임워크" 목록에 나오지 않는 경우 [1]
11896정성태5/15/2019939개발 환경 구성: 440. C#, C++ - double의 Infinity, NaN 표현 방식파일 다운로드1
11895정성태5/12/2019886.NET Framework: 832. ML.NET Model Builder - 회귀(Regression), 다중 분류(Multi-class classification) 예제파일 다운로드1
11894정성태5/12/20191198VS.NET IDE: 135. Visual Studio - ML.NET Model Builder 소개 [1]
11893정성태5/10/2019848오류 유형: 535. C# 6.0 이상의 문법을 컴파일 시 오류가 발생한다면?
11892정성태5/10/2019647웹: 38. HTTP Cookie의 expires 시간 형식(RFC7231)
11891정성태5/9/2019821.NET Framework: 831. (번역글) .NET Internals Cookbook Part 12 - Memory structure, attributes, handles
11890정성태5/8/2019654개발 환경 구성: 439. "Visual Studio Enterprise is required to execute the test." 메시지와 관련된 코드 기록
11889정성태5/8/2019679개발 환경 구성: 438. mstest, QTAgent의 로그 파일 설정 방법
11888정성태5/8/2019941.NET Framework: 830. C# - 비동기 호출을 취소하는 CancellationToken의 간단한 예제 코드파일 다운로드1
11887정성태5/8/2019922.NET Framework: 829. C# - yield 문을 사용할 수 있는 메서드의 조건
11886정성태5/8/2019931오류 유형: 534. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 [2]
11885정성태5/7/2019524오류 유형: 533. mstest.exe 실행 시 "File extension specified '.loadtest' is not a valid test extension." 오류 발생
11884정성태5/5/2019983.NET Framework: 828. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 두 번째 이야기
11883정성태5/3/20191060.NET Framework: 827. C# - 인터넷 시간 서버로부터 받은 시간을 윈도우에 적용하는 방법파일 다운로드1
11882정성태5/9/2019700.NET Framework: 826. (번역글) .NET Internals Cookbook Part 11 - Various C# riddles파일 다운로드1
11881정성태4/28/2019965오류 유형: 532. .NET Core 프로젝트로 마이그레이션 시 "CS0579 Duplicate 'System.Reflection.AssemblyCompanyAttribute' attribute" 오류 발생
11880정성태4/25/2019609오류 유형: 531. 이벤트 로그 오류 - Task Scheduling Error: m->NextScheduledSPRetry 1547, m->NextScheduledEvent 1547
11879정성태10/21/2019821.NET Framework: 825. (번역글) .NET Internals Cookbook Part 10 - Threads, Tasks, asynchronous code and others파일 다운로드1
11878정성태5/9/2019784.NET Framework: 824. (번역글) .NET Internals Cookbook Part 9 - Finalizers, queues, card tables and other GC stuff파일 다운로드1
11877정성태5/9/2019774.NET Framework: 823. (번역글) .NET Internals Cookbook Part 8 - C# gotchas파일 다운로드1
11876정성태5/9/2019676.NET Framework: 822. (번역글) .NET Internals Cookbook Part 7 - Word tearing, locking and others파일 다운로드1
11875정성태4/21/2019566오류 유형: 530. Visual Studo에서 .NET Core 프로젝트를 열 때 "One or more errors occurred." 오류 발생
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...