Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
.NET Framework: 634. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling
; https://www.sysnet.pe.kr/2/0/11132

.NET Framework: 635. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (2) - x86 환경의 __fastcall
; https://www.sysnet.pe.kr/2/0/11133

.NET Framework: 637. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (3) - x64 환경의 __fastcall과 Name mangling
; https://www.sysnet.pe.kr/2/0/11139

.NET Framework: 639. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (4) - CLR JIT 컴파일러의 P/Invoke 호출 규약
; https://www.sysnet.pe.kr/2/0/11141

.NET Framework: 642. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (부록 1) - CallingConvention.StdCall, CallingConvention.Cdecl에 상관없이 왜 호출이 잘 될까요?
; https://www.sysnet.pe.kr/2/0/11144




C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (부록 1) - CallingConvention.StdCall, CallingConvention.Cdecl에 상관없이 왜 호출이 잘 될까요?

지난 글에서,

C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (4) - CLR JIT 컴파일러의 P/Invoke 호출 규약
; https://www.sysnet.pe.kr/2/0/11141

CLR JIT 컴파일러가 생성한 P/Invoke 호출이 왜 __stdcall, __cdecl에 상관없이 잘 되는가에 대해 그냥 그러려니 하고 덮으려다가, 그래도 너무 궁금했습니다. 그래서 분석을 위해 다음의 DllImport 2개를 더 정의하고,

// 아래의 2개는 기존 __cdecl 호출 규약의 "ExternC_CDECL_Func_Arg5" 함수에 대해,

// - ExternC_CDECL_Func_Arg5_2는 명시적으로 "CallingConvention.Cdecl"을 지정하고,
[DllImport("Win32Project1.dll", EntryPoint = "ExternC_CDECL_Func_Arg5", CallingConvention = CallingConvention.Cdecl)]
internal unsafe static extern int ExternC_CDECL_Func_Arg5_2(int value1, int value2, int value3, int value4, int value5);

// - ExternC_CDECL_Func_Arg5_3는 명시적으로 "CallingConvention.StdCall"을 지정
[DllImport("Win32Project1.dll", EntryPoint = "ExternC_CDECL_Func_Arg5", CallingConvention = CallingConvention.StdCall)]
internal unsafe static extern int ExternC_CDECL_Func_Arg5_3(int value1, int value2, int value3, int value4, int value5);

호출을 다음과 같이 추가했습니다.

static unsafe void Main(string[] args)
{
    // JIT 컴파일 생성 용.
    ExternC_CDECL_Func_Arg5(1, 2, 3, 4, 5);
    ExternC_CDECL_Func_Arg5_2(11, 22, 33, 44, 55);
    ExternC_CDECL_Func_Arg5_3(111, 222, 333, 444, 555);
    ExternC_STD_Func_Arg5(6, 7, 8, 9, 10);

    // 실행 후, windbg를 붙이기 위해 일부러 호출
    Console.ReadLine();

    ExternC_CDECL_Func_Arg5(1, 2, 3, 4, 5);
    ExternC_CDECL_Func_Arg5_2(11, 22, 33, 44, 55);
    ExternC_CDECL_Func_Arg5_3(111, 222, 333, 444, 555);
    ExternC_STD_Func_Arg5(6, 7, 8, 9, 10);
}

Release 모드로 빌드하고, 실행하면 Console.ReadLine에서 응용 프로그램이 멈추는 데요, 이때 windbg.exe를 실행해 "Attach to Process..."를 해줍니다. 이후의 코드를 분석하면 어떤 수수께끼가 있는지 알게 됩니다. ^^

우선 sos 모듈을 로드하고, Managed Code를 실행하는 스레드로 문맥 변경을 합니다.

0:006> .loadby sos clr

0:008> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1  f50 013ca400     2a020 Preemptive  030D4F28:00000000 013bece8 1     MTA 
   5    2 3014 013d9920     2b220 Preemptive  00000000:00000000 013bece8 0     MTA (Finalizer) 

0:008> ~~[f50]s
eax=00000000 ebx=0000007c ecx=00000000 edx=00000000 esi=0113efc0 edi=00000000
eip=7774e61c esp=0113eea8 ebp=0113ef08 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtReadFile+0xc:
7774e61c c22400          ret     24h

0:000>

콜스택을 확인해 ReadLine을 호출한 부모 스택 프레임을 찾습니다.

0:000> !clrstack
OS Thread Id: 0xf50 (0)
Child SP       IP Call Site
0113ef28 7774e61c [InlinedCallFrame: 0113ef28] 
0113ef24 71e5b2b3 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0113ef28 72559df3 [InlinedCallFrame: 0113ef28] Microsoft.Win32.Win32Native.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
0113ef8c 72559df3 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
0113efc0 72559d02 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
0113efe0 71df7ae8 System.IO.StreamReader.ReadBuffer()
0113eff4 71e0d03c System.IO.StreamReader.ReadLine()
0113f010 726a04f1 System.IO.TextReader+SyncTextReader.ReadLine()
0113f020 72506b20 System.Console.ReadLine()
0113f028 02f104a5 *** WARNING: Unable to verify checksum for C:\ConsoleApplication1\bin\x86\Release\ConsoleApplication1.exe
Program.Main(System.String[]) [C:\ConsoleApplication1\Program.cs @ 73]
0113f198 72ceea96 [GCFrame: 0113f198] 

02f104a5 주소를 대상으로 역어셈블을 하면, 각각의 호출 코드를 확인할 수 있습니다.

0:000> !U /d 02f104a5
Normal JIT generated code
Program.Main(System.String[])
Begin 02f10448, size b4

C:\ConsoleApplication1\Program.cs @ 66:
02f10448 55              push    ebp

...[JIT 컴파일을 위한 코드 영역 생략]...

C:\ConsoleApplication1\Program.cs @ 71:
02f104a0 e867665f6f      call    mscorlib_ni+0xae6b0c (72506b0c) (System.Console.ReadLine(), mdToken: 06000a6a)

C:\ConsoleApplication1\Program.cs @ 73:
>>> 02f104a5 6a03            push    3
02f104a7 6a04            push    4
02f104a9 6a05            push    5
02f104ab b901000000      mov     ecx,1
02f104b0 8d5101          lea     edx,[ecx+1]
02f104b3 e8acfcffff      call    02f10164 (Program.ExternC_CDECL_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000006)

C:\ConsoleApplication1\Program.cs @ 74:
02f104b8 6a21            push    21h
02f104ba 6a2c            push    2Ch
02f104bc 6a37            push    37h
02f104be b90b000000      mov     ecx,0Bh
02f104c3 8d510b          lea     edx,[ecx+0Bh]
02f104c6 e8a5fcffff      call    02f10170 (Program.ExternC_CDECL_Func_Arg5_2(Int32, Int32, Int32, Int32, Int32), mdToken: 06000007)

C:\ConsoleApplication1\Program.cs @ 75:
02f104cb 684d010000      push    14Dh
02f104d0 68bc010000      push    1BCh
02f104d5 682b020000      push    22Bh
02f104da b96f000000      mov     ecx,6Fh
02f104df 8d516f          lea     edx,[ecx+6Fh]
02f104e2 e895fcffff      call    02f1017c (Program.ExternC_CDECL_Func_Arg5_3(Int32, Int32, Int32, Int32, Int32), mdToken: 06000008)

C:\ConsoleApplication1\Program.cs @ 77:
02f104e7 6a08            push    8
02f104e9 6a09            push    9
02f104eb 6a0a            push    0Ah
02f104ed b906000000      mov     ecx,6
02f104f2 8d5101          lea     edx,[ecx+1]
02f104f5 e88efcffff      call    02f10188 (Program.ExternC_STD_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000010)

C:\ConsoleApplication1\Program.cs @ 103:
02f104fa 5d              pop     ebp
02f104fb c3              ret

개별 call들의 대상 주소에 대해 disassembly 코드를 확인해 보면 jmp 문으로 이어지는 것을 확인할 수 있습니다.,

02f104b3 e8acfcffff      call    02f10164 (Program.ExternC_CDECL_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000006)

                            02f10164 b8d04d3501      mov     eax,1354DD0h
                            02f10169 89ed            mov     ebp,ebp
                            02f1016b e9a0030000      jmp     02f10510

02f104c6 e8a5fcffff      call    02f10170 (Program.ExternC_CDECL_Func_Arg5_2(Int32, Int32, Int32, Int32, Int32), mdToken: 06000007)

                            02f10170 b8fc4d3501      mov     eax,1354DFCh
                            02f10175 89ed            mov     ebp,ebp
                            02f10177 e994030000      jmp     02f10510

02f104e2 e895fcffff      call    02f1017c (Program.ExternC_CDECL_Func_Arg5_3(Int32, Int32, Int32, Int32, Int32), mdToken: 06000008)

                            02f1017c b8284e3501      mov     eax,1354E28h
                            02f10181 89ed            mov     ebp,ebp
                            02f10183 e938040000      jmp     02f105c0

02f104f5 e88efcffff      call    02f10188 (Program.ExternC_STD_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000010)

                            02f10188 b8884f3501      mov     eax,1354F88h
                            02f1018d 89ed            mov     ebp,ebp
                            02f1018f e92c040000      jmp     02f105c0

그런데, 재미있는 규칙이 있습니다. 위의 2개는 02f10510로 점프하는 반면 아래의 2개는 02f105c0로 점프합니다. 왜일까요? 그렇습니다. __cdecl로 알고 있는 호출에 대해서는 02f10510로, __stdcall이라고 알려진 호출에 대해서는 02f105c0로 처리가 된 것입니다. 이처럼, 호출 규약이 같고 함수의 인자 수가 동일한 호출에 대해서는 같은 래퍼 함수가 처리합니다.

이제 분석 대상은 jmp 문으로 이어지는 코드가 됩니다. 먼저 __cdecl 호출 규약을 처리하는 방식을 조사해 볼 텐데요. 이를 위해 "jmp 02f10510"의 대상 주소에 BreakPoint를 설정하고 Run을 합니다.

0:008> bp 02f10510

0:008> g
Breakpoint 0 hit
...[생략]...

그럼, 다시 응용 프로그램이 실행되는데 현재 Console.ReadLine으로 입력을 받고 있는 상태이므로 엔터 키를 한번 쳐 줍니다. 그와 동시에 BP에 걸린 windbg 화면이 나오는데, 일단 현재의 @esp에 쌓인 스택을 보면 다음과 같습니다.

// 스택은 상위 주소에서 하위 주소로 쌓이므로,
// 아래에서 위로 읽어 나가면 됩니다.

Address  Value
0113f018 02f104b8  // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005  // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004  // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003  // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034  // Main 메서드 완료 후 복구할 stackframe EBP 주소
...

이런 스택 상황과 함께 실행될 02f10510의 코드는 다음과 같습니다.

02f10510 55              push    ebp
02f10511 8bec            mov     ebp,esp
02f10513 57              push    edi
02f10514 56              push    esi
02f10515 53              push    ebx
02f10516 83ec20          sub     esp,20h
02f10519 8945f0          mov     dword ptr [ebp-10h],eax
02f1051c 648b35280e0000  mov     esi,dword ptr fs:[0E28h]
02f10523 c745d828face72  mov     dword ptr [ebp-28h],offset clr!InlinedCallFrame::`vftable' (72cefa28)
02f1052a c745d4a47e3651  mov     dword ptr [ebp-2Ch],51367EA4h
02f10531 8b460c          mov     eax,dword ptr [esi+0Ch]
02f10534 8945dc          mov     dword ptr [ebp-24h],eax
02f10537 896dec          mov     dword ptr [ebp-14h],ebp
02f1053a c745e800000000  mov     dword ptr [ebp-18h],0
02f10541 8d45d8          lea     eax,[ebp-28h]
02f10544 89460c          mov     dword ptr [esi+0Ch],eax
02f10547 8bd9            mov     ebx,ecx
02f10549 8bfa            mov     edi,edx
02f1054b 8b4df0          mov     ecx,dword ptr [ebp-10h]
02f1054e e87d38ed6f      call    clr!StubHelpers::DemandPermission (72de3dd0)
02f10553 8b45f0          mov     eax,dword ptr [ebp-10h]
02f10556 8b4014          mov     eax,dword ptr [eax+14h]
02f10559 8b10            mov     edx,dword ptr [eax]
02f1055b ff7508          push    dword ptr [ebp+8]
02f1055e ff750c          push    dword ptr [ebp+0Ch]
02f10561 ff7510          push    dword ptr [ebp+10h]
02f10564 57              push    edi
02f10565 53              push    ebx
02f10566 c745e000000000  mov     dword ptr [ebp-20h],0
02f1056d 8965e4          mov     dword ptr [ebp-1Ch],esp
02f10570 c745e87d05f102  mov     dword ptr [ebp-18h],2F1057Dh
02f10577 c6460800        mov     byte ptr [esi+8],0
02f1057b ffd2            call    edx
02f1057d 83c414          add     esp,14h
02f10580 c6460801        mov     byte ptr [esi+8],1
02f10584 833d4080357300  cmp     dword ptr [clr!g_TrapReturningThreads (73358040)],0
02f1058b 7407            je      02f10594
02f1058d 50              push    eax
02f1058e e87d63f76f      call    clr!JIT_RareDisableHelper (72e86910)
02f10593 58              pop     eax
02f10594 c745e800000000  mov     dword ptr [ebp-18h],0
02f1059b 8b7ddc          mov     edi,dword ptr [ebp-24h]
02f1059e 897e0c          mov     dword ptr [esi+0Ch],edi
02f105a1 8d65f4          lea     esp,[ebp-0Ch]
02f105a4 5b              pop     ebx
02f105a5 5e              pop     esi
02f105a6 5f              pop     edi
02f105a7 5d              pop     ebp
02f105a8 c20c00          ret     0Ch

호흡 한번 가다듬고! 디버거를 이용해 코드 한줄씩 실행해 보겠습니다. ^^ 우선, 02f10516 주소의 "sub esp,20h" 호출까지 실행 후 다시 stack을 확인합니다.

0113efe8 71e0cff5 // 쓰레기 값
0113efec 00000000 // 쓰레기 값
0113eff0 00000000 // 쓰레기 값 
0113eff4 030d1228 // 쓰레기 값 
0113eff8 00000000 // 쓰레기 값 
0113effc 0113f0c4 // 쓰레기 값 
0113f000 030d4ee4 // 쓰레기 값 
0113f004 72cee516 // 이후 위의 스택은 "sub esp, 20h"로 추가된 32바이트(8개의 WORD 영역)
0113f008 0113f0c4 // push ebx
0113f00c 00000000 // push esi
0113f010 0113f040 // push edi
0113f014 0113f028 // push ebp
0113f018 02f104b8  // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005  // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004  // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003  // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034  // Main 메서드 완료 후 복구할 stackframe EBP 주소

보시는 바와 같이, (원래 우리의 C# 코드에서 전달된 처음 2개의 인자를 보관하는 ecx, edx는 보존된 상태이고) 3~5번째 인자는 스택의 저 아래까지 내려간 상태입니다. 당연하겠지만, 이대로는 C++ DLL의 export 함수를 호출하지 못합니다.

그럼, 아래의 호출까지 완료한 다음의,

02f1054e e87d38ed6f      call    clr!StubHelpers::DemandPermission (72de3dd0)

스택 상태를 다시 보겠습니다.

// [문맥] ebp == 0x113f014

0113efe8 51367ea4 // mov dword ptr [ebp-2Ch],51367EA4h
0113efec 72cefa28 // mov dword ptr [ebp-28h],offset clr!InlinedCallFrame::`vftable' (72cefa28)
0113eff0 0113f198 // mov esi,dword ptr fs:[0E28h] // FS 레지스터 - TEB (Thread Environment Block))
                  // mov eax,dword ptr [esi+0Ch]  // esi == 0x13ca400
                  // mov dword ptr [ebp-24h],eax
0113eff4 030d1228 // 쓰레기 값 
0113eff8 00000000 // 쓰레기 값 
0113effc 00000000 // mov dword ptr [ebp-18h],0 
0113f000 0113f014 // dword ptr [ebp-14h],ebp
0113f004 01354dd0 // 01354dd0 - jmp 02f10510 호출 전 특별히 담아놓았던 eax 값
0113f008 0113f0c4 // push ebx
0113f00c 00000000 // push esi
0113f010 0113f040 // push edi
0113f014 0113f028 // push ebp
0113f018 02f104b8  // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005  // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004  // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003  // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034  // Main 메서드 완료 후 복구할 stackframe EBP 주소

아직 C/C++ 호출을 위한 인자 전달에 별다른 변화는 없습니다. 부수적으로 clr!StubHelpers::DemandPermission 함수 처리 중에 ecx, edx 인자가 변경될 수 있으므로 각각 ebx, edi 레지스터에 별도로 보관하는 작업이 수행되었습니다.

02f10547 8bd9            mov     ebx,ecx
02f10549 8bfa            mov     edi,edx

DemandPermission 호출이 의미있는 것은, P/Invoke 대상이 되는 DLL 측의 함수에 대한 주소를 반환해 준다는 것입니다. 위의 코드에서는 그 결괏값을 다음의 코드를 통해 edx에 보관하고 있습니다.

02f1054e e87d38ed6f      call    clr!StubHelpers::DemandPermission (72de3dd0)
02f10553 8b45f0          mov     eax,dword ptr [ebp-10h]
02f10556 8b4014          mov     eax,dword ptr [eax+14h]
02f10559 8b10            mov     edx,dword ptr [eax]

즉, C++ 측의 ExternC_CDECL_Func_Arg5 함수 주소가 담겨지게 됩니다.

자, 이걸로 CLR 래퍼함수는 어느 정도 사전 처리작업을 완료했습니다. 이제부터는 본격적으로 P/Invoke 함수를 호출하기 위한 인자 값 복사 작업을 합니다. 바로 이곳이 실제적인 __cdecl 호출 규약에 의한 스택 인자 전달 코드가 수행되는 곳입니다.

// 인자 5개를 right-to-left 순서로 전달

02f1055b ff7508          push    dword ptr [ebp+8]
02f1055e ff750c          push    dword ptr [ebp+0Ch]
02f10561 ff7510          push    dword ptr [ebp+10h]
02f10564 57              push    edi
02f10565 53              push    ebx

여기까지의 스택 변화를 반영한 결과입니다.

// [문맥] ebp == 0x113f014, esi == 0x13ca400 == FS:0e28
//        edx == 6c6510f0 (C++측의 ExternC_CDECL_Func_Arg5 함수 주소)

0113efd4 00000001 // 다시 복사된 1번째 인자 push dword ptr [ebp+8]
0113efd8 00000002 // 다시 복사된 2번째 인자 push dword ptr [ebp+0Ch]
0113efdc 00000003 // 다시 복사된 3번째 인자 push dword ptr [ebp+10h]
0113efe0 00000004 // 다시 복사된 4번째 인자 push edi
0113efe4 00000005 // 다시 복사된 5번째 인자 push ebx
0113efe8 51367ea4 
0113efec 72cefa28 
0113eff0 0113f198 
0113eff4 00000000 // mov dword ptr [ebp-20h],0 
0113eff8 0113efd4 // mov dword ptr [ebp-1Ch],esp
0113effc 02f1057d // mov dword ptr [ebp-18h],2F1057Dh
0113f000 0113f014 
0113f004 01354dd0 
0113f008 0113f0c4 // push ebx
0113f00c 00000000 // push esi
0113f010 0113f040 // push edi
0113f014 0113f028 // push ebp
0113f018 02f104b8 // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005 // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004 // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003 // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034 // Main 메서드 완료 후 복구할 stackframe EBP 주소

그런 다음 C++의 함수 호출은 02f1057b 주소의 "call edx"에서 이뤄지는 데, __cdecl의 호출 규약으로 인해 edx 대상이 되는 함수에서는 스택 정리를 하지 않습니다. 대신 "call edx" 수행 후 호출자 측의 "add esp, 14h"를 통해 __cdecl의 호출 규약에 맞게 전달한 인자에 해당하는 스택을 정리합니다. 그럼, 이렇게 됩니다.

// add esp, 14h 호출 후

0113efe8 51367ea4 
0113efec 72cefa28 
0113eff0 0113f198 
0113eff4 00000000 // mov dword ptr [ebp-20h],0 
0113eff8 0113efd4 // mov dword ptr [ebp-1Ch],esp
0113effc 02f1057d // mov dword ptr [ebp-18h],2F1057Dh
0113f000 0113f014 
0113f004 01354dd0 
0113f008 0113f0c4 // push ebx
0113f00c 00000000 // push esi
0113f010 0113f040 // push edi
0113f014 0113f028 // push ebp
0113f018 02f104b8 // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005 // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004 // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003 // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034 // Main 메서드 완료 후 복구할 stackframe EBP 주소

이후, 흥미로운 코드가 하나 있는데 바로 "lea esp, [ebp-0Ch]" 입니다. 이 호출 하나로 ESP 레지스터가 현재의 래퍼 함수가 호출되는 시점으로 곧바로 복원됩니다. 마지막 ret 코드가 수행되기 전까지 스택 상황은 다음과 같이 바뀝니다.

0113efe8 51367ea4 
0113efec 72cefa28 
0113eff0 0113f198 
0113eff4 00000000
0113eff8 0113efd4
0113effc 00000000 // mov dword ptr [ebp-18h],0
0113f000 0113f014 
0113f004 01354dd0
                  // lea esp,[ebp-0Ch]로 esp 레지스터가 0113f008 주소로 잘림
0113f008 0113f0c4 // pop ebx 
0113f00c 00000000 // pop esi
0113f010 0113f040 // pop edi
0113f014 0113f028 // pop ebp
0113f018 02f104b8 // CLR Wrapper 함수를 호출 후 돌아갈 주소
0113f01c 00000005 // ExternC_CDECL_Func_Arg5에 전달된 5번째 인자
0113f020 00000004 // ExternC_CDECL_Func_Arg5에 전달된 4번째 인자
0113f024 00000003 // ExternC_CDECL_Func_Arg5에 전달된 3번째 인자
0113f028 0113f034 // Main 메서드 완료 후 복구할 stackframe EBP 주소

C# 래퍼 함수 자체의 스택 처리 방식은 (__stdcall, __fastcall처럼) callee가 처리하는 방식입니다. 따라서 "ret 0Ch" 코드가 실행되면서 그 자체에 전달된 인자를 위한 스택을 모두 비워버립니다. 결국 ExternC_CDECL_Func_Arg5 호출 이전의 스택 상태로 완벽하게 복원됩니다.




그럼, __cdecl 함수로 정의된 것을 __stdcall로 호출했을 때는 왜 잘 실행이 되는 것일까요? (참고로, Visual Studio에서 디버깅 모드로 실행하면 PInvokeStackImbalance MDA 예외가 발생합니다.)

다음은 __cdecl 대상의 함수를 C# 측에서 CallingConvention.StdCall로 호출했을 때의 "jmp 02f105c0"에 있는 CLR 래퍼 함수의 어셈블리입니다.

02f105c0 55              push    ebp
02f105c1 8bec            mov     ebp,esp
02f105c3 57              push    edi
02f105c4 56              push    esi
02f105c5 53              push    ebx
02f105c6 83ec20          sub     esp,20h
02f105c9 8945f0          mov     dword ptr [ebp-10h],eax
02f105cc 648b35280e0000  mov     esi,dword ptr fs:[0E28h]
02f105d3 c745d828face72  mov     dword ptr [ebp-28h],offset clr!InlinedCallFrame::`vftable' (72cefa28)
02f105da c745d4a47e3651  mov     dword ptr [ebp-2Ch],51367EA4h
02f105e1 8b460c          mov     eax,dword ptr [esi+0Ch]
02f105e4 8945dc          mov     dword ptr [ebp-24h],eax
02f105e7 896dec          mov     dword ptr [ebp-14h],ebp
02f105ea c745e800000000  mov     dword ptr [ebp-18h],0
02f105f1 8d45d8          lea     eax,[ebp-28h]
02f105f4 89460c          mov     dword ptr [esi+0Ch],eax
02f105f7 8bd9            mov     ebx,ecx
02f105f9 8bfa            mov     edi,edx
02f105fb 8b4df0          mov     ecx,dword ptr [ebp-10h]
02f105fe e8cd37ed6f      call    clr!StubHelpers::DemandPermission (72de3dd0)
02f10603 8b45f0          mov     eax,dword ptr [ebp-10h]
02f10606 8b4014          mov     eax,dword ptr [eax+14h]
02f10609 8b10            mov     edx,dword ptr [eax]
02f1060b ff7508          push    dword ptr [ebp+8]
02f1060e ff750c          push    dword ptr [ebp+0Ch]
02f10611 ff7510          push    dword ptr [ebp+10h]
02f10614 57              push    edi
02f10615 53              push    ebx
02f10616 c745e014000000  mov     dword ptr [ebp-20h],14h
02f1061d 8965e4          mov     dword ptr [ebp-1Ch],esp
02f10620 c745e82d06f102  mov     dword ptr [ebp-18h],2F1062Dh
02f10627 c6460800        mov     byte ptr [esi+8],0
02f1062b ffd2            call    edx
02f1062d c6460801        mov     byte ptr [esi+8],1
02f10631 833d4080357300  cmp     dword ptr [clr!g_TrapReturningThreads (73358040)],0
02f10638 7407            je      02f10641
02f1063a 50              push    eax
02f1063b e8d062f76f      call    clr!JIT_RareDisableHelper (72e86910)
02f10640 58              pop     eax
02f10641 c745e800000000  mov     dword ptr [ebp-18h],0
02f10648 8b7ddc          mov     edi,dword ptr [ebp-24h]
02f1064b 897e0c          mov     dword ptr [esi+0Ch],edi
02f1064e 8d65f4          lea     esp,[ebp-0Ch]
02f10651 5b              pop     ebx
02f10652 5e              pop     esi
02f10653 5f              pop     edi
02f10654 5d              pop     ebp
02f10655 c20c00          ret     0Ch

주된 차이점은, __cdecl로 작성된 C++ 함수이므로 스택 정리를 안해줌과 동시에 호출자 측에서도 "call edx" 실행 후 "add esp, 14h"와 같은 정리 작업을 안해 주기 때문에 다음과 같이 C++에 전달된 인자가 ESP에 그대로 남아있다는 점입니다.

012ff334 0000006f // 1번째 인자
012ff338 000000de // 2번째 인자
012ff33c 0000014d // 3번째 인자
012ff340 000001bc // 4번째 인자
012ff344 0000022b // 5번째 인자
012ff348 dfbfb46c 
012ff34c 72cefa28 clr!InlinedCallFrame::`vftable'
012ff350 012ff500 
012ff354 00000014 
012ff358 012ff334 
012ff35c 015b062d 
012ff360 012ff374 
012ff364 01444e28 
012ff368 012ff42c // push ebx
012ff36c 00000000 // push esi 
012ff370 012ff3a0 // push edi
012ff374 012ff388 // push ebp
012ff378 015b04e7 // CLR Wrapper 함수를 호출 후 돌아갈 주소
012ff37c 0000022b // 5번째 인자  
012ff380 000001bc // 4번째 인자  
012ff384 0000014d // 3번째 인자 
012ff388 012ff394 // Main 메서드 완료 후 복구할 stackframe EBP 주소 

하지만 그래도 실행에 지장이 없는 이유는, 전에 흥미롭다고 했던 "lea esp,[ebp-0Ch]" 코드 덕분입니다. 이 코드 한 줄로 인해 esp 레지스터의 값이 CLR 래퍼 함수의 초기 상태로 복원됩니다. 즉, 현재 호출된 코드의 스택 프레임(0x12ff374)을 기준으로 -12바이트 (0x012ff368) 위치로 순식간에 잘려집니다. 이렇게!

// lea esp,[ebp-0Ch]로 esp 레지스터가 012ff368 주소로 치환

012ff368 012ff42c // pop ebx
012ff36c 00000000 // pop esi 
012ff370 012ff3a0 // pop edi 
012ff374 012ff388 // pop ebp 
012ff378 015b04e7 // CLR Wrapper 함수를 호출 후 돌아갈 주소
012ff37c 0000022b // 5번째 인자 
012ff380 000001bc // 4번째 인자 
012ff384 0000014d // 3번째 인자
012ff388 012ff394 // Main 메서드 완료 후 복구할 stackframe EBP 주소

이후 일련의 pop ... 명령과 ret 0Ch로 역시 CLR 래퍼 함수가 호출 이전의 스택 상태로 복원됩니다.




다시 정리해 보면!

__cdecl 함수를 __stdcall로 호출해도 괜찮은 것은, __cdecl 규약을 갖는 대상 함수 측에서도 스택 정리를 안 하고 CLR Wrapper 코드 측에서도 스택 정리를 안하지만, 마지막의 "lea esp, [ebp-0Ch]" 코드 덕분에 동일하게 초기화가 돼 버리기 때문입니다.

그렇다면 그 반대의 경우는 어떨까요? __stdcall 함수를 __cdecl 규약으로 호출한다면?

이렇게 되면 C/C++ 측의 함수에서도 스택 정리를 하고, CLR Wrapper 코드 측에서도 스택 정리를 하므로 2배의 스택이 날아가 버립니다. 어쩌면 스택이 완전히 깨져 버릴 수 있는 것입니다. 그런데 실제로 해보면 예상치 못한 결과가 나옵니다. 가령, __stdcall로 정의된 C++ 함수를 다음과 같이 억지로 (또는 실수로) CallingConvention.Cdecl이라고 지정하면,

[DllImport("Win32Project1.dll", CallingConvention = CallingConvention.Cdecl)]
internal unsafe static extern int ExternC_STD_Func_Arg5(int value1, int value2, int value3, int value4, int value5);

CLR은 "ExternC_STD_Func_Arg5" 함수 이름으로 찾게 됩니다. 하지만, 이전 장에서 설명한 것처럼,

C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling
; https://www.sysnet.pe.kr/2/0/11132

extern "C"로 묶은 __stdcall 함수는 "_ExternC_STD_Func_Arg5@20"으로 name mangling이 되기 때문에 이 함수를 찾지 못하게 됩니다. 그래서 실제로는 다음과 같은 예외가 발생합니다.

An unhandled exception of type 'System.EntryPointNotFoundException' occurred in ConsoleApplication1.exe

Additional information: Unable to find an entry point named 'ExternC_STD_Func_Arg5' in DLL 'Win32Project1.dll'.

오호... 이렇게까지 예방(?)이 되었지만 아직도 문제가 남아 있습니다. 즉 .def로 export 시킨 함수는 이름 그대로 사용되기 때문에 CLR Wrapper 측에서도 그 이름으로 풀이해 호출할 수밖에 없게 됩니다.

실제로 테스트를 해보겠습니다. 스택이 잘(?) 깨질 수 있도록 전달하는 인자의 수를 10개로 늘린 함수를 마련했습니다.

[DllImport("Win32Project1.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int ExternC_STD_Func_Arg10_By_DEF(int value1, int value2, int value3, int value4, int value5, int value6, int value7, int value8, int value9, int value10);

ExternC_STD_Func_Arg10_By_DEF(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

이때 생성된 CLR 래퍼 함수는 다음과 같습니다. (이전에 언급했지만, CLR은 P/Invoke 함수의 인자 수에 따라 이런 wrapper 함수를 개별로 생성합니다.)

025204c0 55              push    ebp
025204c1 8bec            mov     ebp,esp
025204c3 57              push    edi
025204c4 56              push    esi
025204c5 53              push    ebx
025204c6 83ec20          sub     esp,20h
025204c9 8945f0          mov     dword ptr [ebp-10h],eax
025204cc 648b35280e0000  mov     esi,dword ptr fs:[0E28h]
025204d3 c745d828face72  mov     dword ptr [ebp-28h],offset clr!InlinedCallFrame::`vftable' (72cefa28)
025204da c745d44d22728c  mov     dword ptr [ebp-2Ch],8C72224Dh
025204e1 8b460c          mov     eax,dword ptr [esi+0Ch]
025204e4 8945dc          mov     dword ptr [ebp-24h],eax
025204e7 896dec          mov     dword ptr [ebp-14h],ebp
025204ea c745e800000000  mov     dword ptr [ebp-18h],0
025204f1 8d45d8          lea     eax,[ebp-28h]
025204f4 89460c          mov     dword ptr [esi+0Ch],eax
025204f7 8bd9            mov     ebx,ecx
025204f9 8bfa            mov     edi,edx
025204fb 8b4df0          mov     ecx,dword ptr [ebp-10h]
025204fe e8cd388c70      call    clr!StubHelpers::DemandPermission (72de3dd0)
02520503 8b45f0          mov     eax,dword ptr [ebp-10h]
02520506 8b4014          mov     eax,dword ptr [eax+14h]
02520509 8b10            mov     edx,dword ptr [eax]
0252050b ff7508          push    dword ptr [ebp+8]
0252050e ff750c          push    dword ptr [ebp+0Ch]
02520511 ff7510          push    dword ptr [ebp+10h]
02520514 ff7514          push    dword ptr [ebp+14h]
02520517 ff7518          push    dword ptr [ebp+18h]
0252051a ff751c          push    dword ptr [ebp+1Ch]
0252051d ff7520          push    dword ptr [ebp+20h]
02520520 ff7524          push    dword ptr [ebp+24h]
02520523 57              push    edi
02520524 53              push    ebx
02520525 c745e000000000  mov     dword ptr [ebp-20h],0
0252052c 8965e4          mov     dword ptr [ebp-1Ch],esp
0252052f c745e83c055202  mov     dword ptr [ebp-18h],252053Ch
02520536 c6460800        mov     byte ptr [esi+8],0
0252053a ffd2            call    edx
0252053c 83c428          add     esp,28h
0252053f c6460801        mov     byte ptr [esi+8],1
02520543 833d4080357300  cmp     dword ptr [clr!g_TrapReturningThreads (73358040)],0
0252054a 7407            je      02520553
0252054c 50              push    eax
0252054d e8be639670      call    clr!JIT_RareDisableHelper (72e86910)
02520552 58              pop     eax
02520553 c745e800000000  mov     dword ptr [ebp-18h],0
0252055a 8b7ddc          mov     edi,dword ptr [ebp-24h]
0252055d 897e0c          mov     dword ptr [esi+0Ch],edi
02520560 8d65f4          lea     esp,[ebp-0Ch]
02520563 5b              pop     ebx
02520564 5e              pop     esi
02520565 5f              pop     edi
02520566 5d              pop     ebp
02520567 c22000          ret     20h

그래서 call edx에서 반환하자 마자 다음과 같이 인자가 전달된 스택이 날아가 ESP 레지스터가 가리키는 위치가 바뀌고,

006feeb4 8c72224d 
006feeb8 72cefa28 clr!InlinedCallFrame::`vftable'
006feebc 006ff078 
006feec0 00000000 
006feec4 006fee8c 
006feec8 0252053c 
006feecc 006feee0 
006feed0 00a84f5c // 이후 위의 스택은 "sub esp, 20h"로 추가된 32바이트(8개의 WORD 영역)
006feed4 006fefa4 // push ebx
006feed8 00000000 // push esi
006feedc 006fef20 // push edi
006feee0 006fef08 // push ebp
006feee4 0252048a // CLR Wrapper 함수를 호출 후 돌아갈 주소
006feee8 0000000a // 여기까지는 10개 인자 중 8개가 C# 코드로부터 전달된 스택
006feeec 00000009 // 물론, 나머지 2개는 ecx, edx에 전달됨.
006feef0 00000008 
006feef4 00000007 
006feef8 00000006 
006feefc 00000005 
006fef00 00000004 
006fef04 00000003
006fef08 006fef14 // Main 메서드 완료 후 복구할 stackframe EBP 주소

이후, 다시 "add esp, 28h"를 하는 바람에 10개의 WORD만큼 ESP 위치가 날아갑니다.

006feedc 006fef20 // push edi
006feee0 006fef08 // push ebp
006feee4 0252048a // CLR Wrapper 함수를 호출 후 돌아갈 주소
006feee8 0000000a // 여기까지는 10개 인자 중 8개가 C# 코드로부터 전달된 스택
006feeec 00000009 
006feef0 00000008 
006feef4 00000007 
006feef8 00000006 
006feefc 00000005 
006fef00 00000004 
006fef04 00000003 
006fef08 006fef14 // Main 메서드 완료 후 복구할 stackframe EBP 주소

위의 상태로만 보면, ESP가 가리키는 스택이 깨졌으므로 이후의 실행이 엉망이 될 것입니다. 실제로 날아가버린 스택으로 인해 실행이 잘못될만한 코드가 CLR Wrapper에 보면 "push eax, call clr!JIT_RareDisableHelper, pop eax"가 있습니다. 하지만, 다행인 점이 있다면 clr!g_TrapReturningThreads 전역 변수는 (어떻게 설정하는지 모르겠지만) 0 값을 가지기 때문에 "je 02520553"으로 인해 그 부분의 코드를 건너 뛰고,

0252053a ffd2            call    edx
0252053c 83c428          add     esp,28h
0252053f c6460801        mov     byte ptr [esi+8],1
02520543 833d4080357300  cmp     dword ptr [clr!g_TrapReturningThreads (73358040)],0
0252054a 7407            je      02520553
0252054c 50              push    eax
0252054d e8be639670      call    clr!JIT_RareDisableHelper (72e86910)
02520552 58              pop     eax
02520553 c745e800000000  mov     dword ptr [ebp-18h],0
0252055a 8b7ddc          mov     edi,dword ptr [ebp-24h]
0252055d 897e0c          mov     dword ptr [esi+0Ch],edi
02520560 8d65f4          lea     esp,[ebp-0Ch]
02520563 5b              pop     ebx
02520564 5e              pop     esi
02520565 5f              pop     edi
02520566 5d              pop     ebp
02520567 c22000          ret     20h

결국, "lea esp, [ebp-0ch]"로 인해 다시 ESP 레지스터가 정상으로 복구된다는 점입니다. 그러니까, g_TrapReturningThreads 전역 변수가 0이 아닌 상황에서는 저런 식으로 __cdecl 함수를 __stdcall로 잘못 지정해 호출하면 프로그램이 비정상 종료될 수 있습니다.

하지만, 대개의 경우 저 코드는 아주 잘 실행될 것이므로 Visual Studio만이 디버그 모드에서 P/InvokeStackImbalance MDA 예외로 경고를 표시해 주는 것입니다.

결론을 내리면, 지정한 CallingConvention.StdCall, CallingConvention.Cdecl 호출 규약이 잘못되어도 특수한 상황이 아니라면 P/Invoke 호출은 정상적으로 완료됩니다. (혹시, g_TrapReturningThreads 값을 제어하는 방법을 아시는 분은 덧글 부탁드립니다. CoreCLR을 보면 알 수 있을지도! ^^)




참고로, clr!StubHelpers::DemandPermission 함수의 역 어셈블을 실어봅니다.

clr!StubHelpers::DemandPermission:
72de3dd0 68bc000000      push    0BCh
72de3dd5 b868982773      mov     eax,offset clr! ?? ::FNODOBFM::`string'+0x26290 (73279868)
72de3dda e841b4f0ff      call    clr!_EH_prolog3_catch (72cef220)
72de3ddf 8bf1            mov     esi,ecx
72de3de1 bfd03dde72      mov     edi,offset clr!StubHelpers::DemandPermission (72de3dd0)
72de3de6 85f6            test    esi,esi
72de3de8 0f85ccb1f2ff    jne     clr!StubHelpers::DemandPermission+0x1e (72d0efba)
72de3dee 33db            xor     ebx,ebx
72de3df0 c7853cfffffff4f0ce72 mov dword ptr [ebp-0C4h],offset clr!HelperMethodFrame::`vftable' (72cef0f4)
72de3dfa 899d48ffffff    mov     dword ptr [ebp-0B8h],ebx
72de3e00 89bd50ffffff    mov     dword ptr [ebp-0B0h],edi
72de3e06 8d8d54ffffff    lea     ecx,[ebp-0ACh]
72de3e0c e84fb4f0ff      call    clr!LazyMachStateCaptureState (72cef260)
72de3e11 85c0            test    eax,eax
72de3e13 756a            jne     clr!StubHelpers::DemandPermission+0x6a (72de3e7f)
72de3e15 8d8d3cffffff    lea     ecx,[ebp-0C4h]
72de3e1b e85fb4f0ff      call    clr!HelperMethodFrame::Push (72cef27f)
72de3e20 8b8d4cffffff    mov     ecx,dword ptr [ebp-0B4h]
72de3e26 c745fc03000000  mov     dword ptr [ebp-4],3
72de3e2d 803d3080357300  cmp     byte ptr [clr!g_StackProbingEnabled (73358030)],0
72de3e34 0f8523922400    jne     clr!StubHelpers::DemandPermission+0x15d (7302d05d)
72de3e3a c645fc04        mov     byte ptr [ebp-4],4
72de3e3e 53              push    ebx
72de3e3f 33d2            xor     edx,edx
72de3e41 33c9            xor     ecx,ecx
72de3e43 41              inc     ecx
72de3e44 e8196ef2ff      call    clr!SecurityStackWalk::SpecialDemand (72d0ac62)
72de3e49 885de0          mov     byte ptr [ebp-20h],bl
72de3e4c c645fc03        mov     byte ptr [ebp-4],3
72de3e50 803d3080357300  cmp     byte ptr [clr!g_StackProbingEnabled (73358030)],0
72de3e57 0f851f922400    jne     clr!StubHelpers::DemandPermission+0x192 (7302d07c)
72de3e5d 834dfcff        or      dword ptr [ebp-4],0FFFFFFFFh
72de3e61 8d8d3cffffff    lea     ecx,[ebp-0C4h]
72de3e67 e83cb4f0ff      call    clr!HelperMethodFrame::Pop (72cef2a8)
72de3e6c 8d8d54ffffff    lea     ecx,[ebp-0ACh]
72de3e72 e848acf0ff      call    clr!HelperMethodFrameRestoreState (72ceeabf)
72de3e77 85c0            test    eax,eax
72de3e79 0f8571ffffff    jne     clr!StubHelpers::DemandPermission+0x113 (72de3df0)
72de3e7f e85cb2f0ff      call    clr!_EH_epilog3 (72cef0e0)
72de3e84 c3              ret

이 함수 내에서 "call clr!HelperMethodFrameRestoreState (72ceeabf)" 호출을 하는데,

clr!HelperMethodFrameRestoreState:
72ceeabf 8bc1            mov     eax,ecx
72ceeac1 83782400        cmp     dword ptr [eax+24h],0
72ceeac5 7427            je      clr!HelperMethodFrameRestoreState+0x2f (72ceeaee)
72ceeac7 8d500c          lea     edx,[eax+0Ch]
72ceeaca 395008          cmp     dword ptr [eax+8],edx
72ceeacd 7502            jne     clr!HelperMethodFrameRestoreState+0x12 (72ceead1)
72ceeacf 8b32            mov     esi,dword ptr [edx]
72ceead1 8d5004          lea     edx,[eax+4]
72ceead4 3910            cmp     dword ptr [eax],edx
72ceead6 7502            jne     clr!HelperMethodFrameRestoreState+0x1b (72ceeada)
72ceead8 8b3a            mov     edi,dword ptr [edx]
72ceeada 8d5014          lea     edx,[eax+14h]
72ceeadd 395010          cmp     dword ptr [eax+10h],edx
72ceeae0 7502            jne     clr!HelperMethodFrameRestoreState+0x25 (72ceeae4)
72ceeae2 8b1a            mov     ebx,dword ptr [edx]
72ceeae4 8d501c          lea     edx,[eax+1Ch]
72ceeae7 395018          cmp     dword ptr [eax+18h],edx
72ceeaea 7502            jne     clr!HelperMethodFrameRestoreState+0x2f (72ceeaee)
72ceeaec 8b2a            mov     ebp,dword ptr [edx]
72ceeaee 33c0            xor     eax,eax
72ceeaf0 c3              ret

위에서 최종적으로 edx에 값이 들어가는데, 그 값이 바로 P/Invoke 함수에 대한 C/C++ 측의 주소입니다.




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







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

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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  [54]  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12268정성태7/15/202010498오류 유형: 631. .NET Core 웹 응용 프로그램 오류 - HTTP Error 500.35 - ANCM Multiple In-Process Applications in same Process
12267정성태7/15/202012158.NET Framework: 927. C# - 윈도우 프로그램에서 Credential Manager를 이용한 보안 정보 저장파일 다운로드1
12266정성태7/14/20209822오류 유형: 630. 사용자 계정을 지정해 CreateService API로 서비스를 등록한 경우 "Error 1069: The service did not start due to a logon failure." 오류발생
12265정성태7/10/20209041오류 유형: 629. Visual Studio - 웹 애플리케이션 실행 시 "Unable to connect to web server 'IIS Express'." 오류 발생
12264정성태7/9/202018043오류 유형: 628. docker: Error response from daemon: Conflict. The container name "..." is already in use by container "...".
12261정성태7/9/202010957VS.NET IDE: 148. 윈도우 10에서 .NET Core 응용 프로그램을 리눅스 환경에서 실행하는 2가지 방법 - docker, WSL 2 [5]
12260정성태7/8/20209422.NET Framework: 926. C# - ETW를 이용한 ThreadPool 스레드 감시파일 다운로드1
12259정성태7/8/20208947오류 유형: 627. nvlddmkm.sys의 BAD_POOL_HEADER BSOD 문제 [1]
12258정성태7/8/202012096기타: 77. DataDog APM 간략 소개
12257정성태7/7/20209154.NET Framework: 925. C# - ETW를 이용한 Monitor Enter/Exit 감시파일 다운로드1
12256정성태7/7/20209570.NET Framework: 924. C# - Reflection으로 변경할 수 없는 readonly 정적 필드 [4]
12255정성태7/6/20209972.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/20209875오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/202010937.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/202012868.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
12251정성태7/1/202010834.NET Framework: 920. C# - 파일의 비동기 처리 유무에 따른 스레드 상황 [1]파일 다운로드2
12250정성태6/30/202013452.NET Framework: 919. C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법 [1]파일 다운로드1
12249정성태6/29/20209585오류 유형: 625. Microsoft SQL Server 2019 RC1 Setup - 설치 제거 시 Warning 26003 오류 발생
12248정성태6/29/20208041오류 유형: 624. SQL 서버 오류 - service-specific error code 17051
12247정성태6/29/20209560.NET Framework: 918. C# - 불린 형 상수를 반환값으로 포함하는 3항 연산자 사용 시 단축 표현 권장(IDE0075) [2]파일 다운로드1
12246정성태6/29/202010403.NET Framework: 917. C# - USB 관련 ETW(Event Tracing for Windows)를 이용한 키보드 입력을 감지하는 방법
12245정성태6/24/202010900.NET Framework: 916. C# - Task.Yield 사용법 (2) [2]파일 다운로드1
12244정성태6/24/202010644.NET Framework: 915. ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용 [1]파일 다운로드1
12243정성태6/23/20208306VS.NET IDE: 147. Visual C++ 프로젝트 - .NET Core EXE를 "Debugger Type"으로 지원하는 기능 추가
12242정성태6/23/20209029오류 유형: 623. AADSTS90072 - User account '...' from identity provider 'live.com' does not exist in tenant 'Microsoft Services'
12241정성태6/23/202012293.NET Framework: 914. C# - Task.Yield 사용법파일 다운로드1
... 46  47  48  49  50  51  52  53  [54]  55  56  57  58  59  60  ...