Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 11개 있습니다.)

(번역글) .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 구현이 아닙니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/4/2023]

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

비밀번호

댓글 작성자
 



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

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13500정성태12/24/20232176디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232816닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232293오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232306Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232316Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232499Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232543닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232261개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232231Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232344개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232132개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232066오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232381개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232194개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232088오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232161개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232295닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232812닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232265개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232601개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232284개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232465닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232202닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232261닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232133개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232309닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...