Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 7개 있습니다.)
.NET Framework: 969. .NET Framework 및 .NET 5 - UnmanagedCallersOnly 특성 사용
; https://www.sysnet.pe.kr/2/0/12412

.NET Framework: 970. .NET 5 / .NET Core - UnmanagedCallersOnly 특성을 사용한 함수 내보내기
; https://www.sysnet.pe.kr/2/0/12413

.NET Framework: 971. UnmanagedCallersOnly 특성과 DNNE 사용
; https://www.sysnet.pe.kr/2/0/12415

.NET Framework: 972. DNNE가 출력한 NE DLL을 직접 생성하는 방법
; https://www.sysnet.pe.kr/2/0/12421

.NET Framework: 973. .NET 5, .NET Framework에서만 허용하는 UnmanagedCallersOnly 사용예
; https://www.sysnet.pe.kr/2/0/12422

.NET Framework: 976. UnmanagedCallersOnly + C# 9.0 함수 포인터 사용 시 x86 빌드에서 오동작하는 문제
; https://www.sysnet.pe.kr/2/0/12431

닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합
; https://www.sysnet.pe.kr/2/0/13464




UnmanagedCallersOnly + C# 9.0 함수 포인터 사용 시 x86 빌드에서 오동작하는 문제

결론만 먼저 말하면 x86 환경에서는 이렇게 정리가 됩니다.

  1. .NET 5 런타임인 경우, stdcall, cdecl 호출 규약만 지원
  2. .NET Framework 런타임인 경우, 모든 호출 규약에서 오동작
  3. (.NET Core는 어차피 지원하지 못하므로.)

이유는, x64의 경우 모든 호출 규약이 통일되었으므로 native -> managed function 호출 시 특별히 런타임이 끼어들지 않고도 인자 전달이 잘 됩니다. 반면 x86의 경우 native -> [marshaller] -> managed function 식으로 호출이 되는데, 이때 UnmanagedCallersOnly 특성이 부여된 managed function은 단일하게 clrcall 호출 규약을 따르도록 JIT가 코드를 생성합니다. 따라서 native에서의 stdcall, cdecl, fastcall 등의 호출에 대해 중간에 [marshaller]가 적절한 변환을 해야 하는데, 이 코드의 생성을 책임지는 런타임이 .NET 5가 유일하기 때문입니다.

결국, 기존 .NET Framework 런타임은 이에 대한 배려가 없으므로 모든 호출 규약에 대해 managed function으로 정상적인 인자 전달을 하지 못합니다.




이제 위의 사항들을 가지고 기술적으로 한번 접근해 볼까요? ^^

예를 들어, C++ DLL에 C# 메서드를 콜백하는 함수를 하나 만들고,

typedef void(__stdcall* CALLBACK_PROC_STDCALL)(int n1, int n2, int n3, int n4, int n5, int n6);

__declspec(dllexport) void __stdcall Callback_stdcall(/* C#에서 전달 */ CALLBACK_PROC_STDCALL callback)
{
    callback(1, 2, 3, 4, 5, 6);
}

C# 측에서 Callback_stdcall을 호출해 콜백 메서드를 넘겨주는 코드를,

[DllImport("Dll1.dll", SetLastError = true, CharSet = CharSet.Auto, EntryPoint = "Callback_stdcall")]
unsafe static extern bool Callback_with_function_ptr_stdcall(delegate* unmanaged[Stdcall]<int, int, int, int, int, int, void> callback);

unsafe
{
    Callback_with_function_ptr_stdcall(&callback_stdcall);
}

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
public static void callback_stdcall(int n1, int n2, int n3, int n4, int n5, int n6)
{
    int sum = n1 + n2 + n3 + n4 + n5 + n6;
    Console.WriteLine(sum);
}

x64에서 실행해 보겠습니다. 그럼, C++의 callback 호출에서,

     9:     callback(1, 2, 3, 4, 5, 6);
00007FF982E61826 C7 44 24 28 06 00 00 00 mov         dword ptr [rsp+28h],6  
00007FF982E6182E C7 44 24 20 05 00 00 00 mov         dword ptr [rsp+20h],5  
00007FF982E61836 41 B9 04 00 00 00    mov         r9d,4  
00007FF982E6183C 41 B8 03 00 00 00    mov         r8d,3  
00007FF982E61842 BA 02 00 00 00       mov         edx,2  
00007FF982E61847 B9 01 00 00 00       mov         ecx,1  
00007FF982E6184C FF 95 E0 00 00 00    call        qword ptr [0x00007ff9369b05a0] 

최초 호출 시 0x00007ff9369b05a0 주소를 보면 PrecodeFixupThunk 단계를 거치지만,

00007FF9369B05A0 E8 6B 3F 54 5F       call        PrecodeFixupThunk (07FF995EF4510h)  

호출 이후에는 곧바로 UnmanagedCallersOnly 메서드로 점프하는 코드로 바뀝니다.

00007FF9369B05A0 E9 8B 0D 00 00       jmp         ConsoleApp1.Program.callback_stdcall(Int32, Int32, Int32, Int32, Int32, Int32) (07FF9369B1330h)  

단일한 호출 규약으로 통일이 된 덕분에 함수 포인터를 사용해도 native에서 managed까지 자연스럽게 흘러갈 수 있는 구조입니다.




반면, x86은 어떨까요? .NET Framework + x86 환경에서 위의 코드를 동일하게 호출해 보면,

     9:     callback(1, 2, 3, 4, 5, 6);
790B17C8 8B F4                mov         esi,esp  
790B17CA 6A 06                push        6  
790B17CC 6A 05                push        5  
790B17CE 6A 04                push        4  
790B17D0 6A 03                push        3  
790B17D2 6A 02                push        2  
790B17D4 6A 01                push        1  
790B17D6 FF 55 08             call        dword ptr [0x02870520]

x64와 마찬가지로 PrecodeFixupThunk 코드를 거치고,

02870520 E8 EB EB 19 71       call        _PrecodeFixupThunk@0 (73A0F110h)  

호출 이후에는 x64와 마찬가지로 닷넷 코드로 바로 점프합니다.

02870520 E9 D3 0A 00 00       jmp         ConsoleApp1.Program.callback_stdcall(Int32, Int32, Int32, Int32, Int32, Int32) (02870FF8h)  

하지만, 해당 메서드는 clrcall 호출 규약을 따르도록 .NET Framework 런타임이 코드를 생성해 두기 때문에,

    66:         [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
    67:         public static void callback_stdcall(int n1, int n2, int n3, int n4, int n5, int n6)
    68:         {
02870FF8 55                   push        ebp  
02870FF9 8B EC                mov         ebp,esp  
02870FFB 57                   push        edi  
02870FFC 56                   push        esi  
02870FFD 53                   push        ebx  
02870FFE 83 EC 38             sub         esp,38h  
02871001 8B F1                mov         esi,ecx  
02871003 8D 7D C8             lea         edi,[ebp-38h]  
02871006 B9 0B 00 00 00       mov         ecx,0Bh  
0287100B 33 C0                xor         eax,eax  
0287100D F3 AB                rep stos    dword ptr es:[edi]  
0287100F 8B CE                mov         ecx,esi  
02871011 89 4D C4             mov         dword ptr [ebp-3Ch],ecx  
02871014 89 55 C0             mov         dword ptr [ebp-40h],edx  
02871017 83 3D F8 4A F3 00 00 cmp         dword ptr ds:[0F34AF8h],0  
0287101E 74 05                je          ConsoleApp1.Program.callback_stdcall(Int32, Int32, Int32, Int32, Int32, Int32)+02Dh (02871025h)  
02871020 E8 5B ED 53 71       call        JIT_DbgIsJustMyCode (73DAFD80h)  
02871025 33 D2                xor         edx,edx  
02871027 89 55 BC             mov         dword ptr [ebp-44h],edx  
0287102A 90                   nop  
    69:             int sum = n1 + n2 + n3 + n4 + n5 + n6;
0287102B 8B 45 C4             mov         eax,dword ptr [ebp-3Ch]  
0287102E 03 45 C0             add         eax,dword ptr [ebp-40h]  
02871031 03 45 14             add         eax,dword ptr [ebp+14h]  
02871034 03 45 10             add         eax,dword ptr [ebp+10h]  
02871037 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
0287103A 03 45 08             add         eax,dword ptr [ebp+8]  
0287103D 89 45 BC             mov         dword ptr [ebp-44h],eax  
    70:             Console.WriteLine(sum);
02871040 8B 4D BC             mov         ecx,dword ptr [ebp-44h]  
02871043 E8 F0 08 5C 70       call        System.Console.WriteLine(Int32) (72E31938h)  
02871048 90                   nop  
    71:         }
02871049 90                   nop  
0287104A 8D 65 F4             lea         esp,[ebp-0Ch]  
0287104D 5B                   pop         ebx  
0287104E 5E                   pop         esi  
0287104F 5F                   pop         edi  
02871050 5D                   pop         ebp  
02871051 C2 10 00             ret         10h  

당연히 호출 규약이 맞지 않아 닷넷 메서드에 대한 콜백 이후에는 MDA 오류가 발생합니다.

Managed Debugging Assistant 'FatalExecutionEngineError' 
  Message=Managed Debugging Assistant 'FatalExecutionEngineError' : 'The runtime has encountered a fatal error. The address of the error was at 0x73ac59c8, on thread 0x6010. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. Common sources of this bug include user marshaling errors for COM-interop or PInvoke, which may corrupt the stack.'




동일한 코드를 .NET 5 런타임에서 x86으로 구동하면,

     9:     callback(1, 2, 3, 4, 5, 6);
790E17C8 8B F4                mov         esi,esp  
790E17CA 6A 06                push        6  
790E17CC 6A 05                push        5  
790E17CE 6A 04                push        4  
790E17D0 6A 03                push        3  
790E17D2 6A 02                push        2  
790E17D4 6A 01                push        1  
790E17D6 FF 55 08             call        dword ptr [0x02D4345E]  

함수 포인터가 건네준 주소에는 이런 코드가 있고,

02D4345E B8 4C 34 D4 02       mov         eax,2D4344Ch  
02D43463 E9 B4 FB FF FF       jmp         02D4301C  

첫 호출을 한 이후 여전히 이렇게 stub 코드를 경유하도록 패치가 됩니다.

02D4345E B8 4C 34 D4 02       mov         eax,offset Pointer to: CLRStub[MethodDescPrestub]@d8f94f87085d24f8 (02D4344Ch)  
02D43463 E9 0C 9C 00 00       jmp         CLRStub[StubLinkStub]@d8f94f8702d4d074 (02D4D074h)  

위에서 "02D4344Ch" 주소에는 085d24f8 값이 담겨 있고 이것은 닷넷 측의 UnmanagedCallersOnly 메서드로 점프하는 코드를 가리킵니다.

085D24F8 E9 CB 60 00 00       jmp         ConsoleApp1.Program.callback_stdcall(Int32, Int32, Int32, Int32, Int32, Int32) (085D85C8h)  

즉, .NET 5 런타임은 C++ 측에 넘겨진 함수 포인터의 주소가 stdcall 호출 방식으로 호출될 것이기 때문에 managed 코드의 clrcall 호출 방식으로 변환을 하는 stub 코드를,

02D4D074 55                   push        ebp  
02D4D075 89 E5                mov         ebp,esp  
02D4D077 53                   push        ebx  
02D4D078 83 EC 08             sub         esp,8  
02D4D07B 50                   push        eax  
02D4D07C 64 8B 1D 2C 00 00 00 mov         ebx,dword ptr fs:[2Ch]  
02D4D083 8B 5B 14             mov         ebx,dword ptr [ebx+14h]  
02D4D086 8B 5B 08             mov         ebx,dword ptr [ebx+8]  
02D4D089 83 FB 00             cmp         ebx,0  
02D4D08C 74 70                je          CLRStub[StubLinkStub]@d8f94f8702d4d0fe (02D4D0FEh)  
02D4D08E 89 D9                mov         ecx,ebx  
02D4D090 58                   pop         eax  
02D4D091 C6 41 08 01          mov         byte ptr [ecx+8],1  
02D4D095 83 3D D4 CF 63 7C 00 cmp         dword ptr [g_TrapReturningThreads (7C63CFD4h)],0  
02D4D09C 75 69                jne         CLRStub[StubLinkStub]@d8f94f8702d4d107 (02D4D107h)  
02D4D09E FF 71 0C             push        dword ptr [ecx+0Ch]  
02D4D0A1 68 10 D4 50 7C       push        offset FastNExportExceptHandler (7C50D410h)  
02D4D0A6 64 FF 35 00 00 00 00 push        dword ptr fs:[CLRStub[StubLinkStub]@d8f94f8702d4d0a9 (00h)]  
02D4D0AD 64 89 25 00 00 00 00 mov         dword ptr fs:[CLRStub[StubLinkStub]@d8f94f8702d4d0b0 (00h)],esp  
02D4D0B4 8D 5D 08             lea         ebx,[ebp+8]  
02D4D0B7 51                   push        ecx  
02D4D0B8 83 EC 04             sub         esp,4  
02D4D0BB FF 73 08             push        dword ptr [ebx+8]  
02D4D0BE FF 73 0C             push        dword ptr [ebx+0Ch]  
02D4D0C1 FF 73 10             push        dword ptr [ebx+10h]  
02D4D0C4 FF 73 14             push        dword ptr [ebx+14h]  
02D4D0C7 8B 53 04             mov         edx,dword ptr [ebx+4]  
02D4D0CA 8B 0B                mov         ecx,dword ptr [ebx]  
02D4D0CC 8B 00                mov         eax,dword ptr [eax]  
02D4D0CE 89 44 24 10          mov         dword ptr [esp+10h],eax  
02D4D0D2 FF 54 24 10          call        dword ptr [esp+10h]  
02D4D0D6 83 C4 04             add         esp,4  
02D4D0D9 89 43 F0             mov         dword ptr [ebx-10h],eax  
02D4D0DC 89 53 EC             mov         dword ptr [ebx-14h],edx  
02D4D0DF 59                   pop         ecx  
02D4D0E0 C6 41 08 00          mov         byte ptr [ecx+8],0  
02D4D0E4 F6 41 04 1B          test        byte ptr [ecx+4],1Bh  
02D4D0E8 75 24                jne         CLRStub[StubLinkStub]@d8f94f8702d4d10e (02D4D10Eh)  
02D4D0EA 8B 14 24             mov         edx,dword ptr [esp]  
02D4D0ED 64 89 15 00 00 00 00 mov         dword ptr fs:[CLRStub[StubLinkStub]@d8f94f8702d4d0f0 (00h)],edx  
02D4D0F4 83 C4 0C             add         esp,0Ch  
02D4D0F7 5A                   pop         edx  
02D4D0F8 58                   pop         eax  
02D4D0F9 5B                   pop         ebx  
02D4D0FA 5D                   pop         ebp  
02D4D0FB C2 18 00             ret         18h  

경유하므로 호출이 정상적으로 이뤄지는 것입니다. 또한, stdcall의 경우 피호출 측에서 스택을 정리하기 때문에 마지막에 "ret 18h"로 스택을 정리하는데, 위의 코드를 cdecl 콜백으로 바꿔 테스트를 해보면, 모든 stub 코드가 동일한 상태에서 마지막 코드가 "ret"으로 스택 정리를 하지 않고 끝냅니다.

그래서 stdcall과 cdecl에 대해서는 .NET 5 런타임이 적절한 stub 코드를 경유하게 만듦으로써 정상적인 호출을 가능케 합니다.




재미있는 것은, delegate* unmanaged[Fastcall]입니다. fastcall의 경우 C++ 호출 측에서 ecx, edx를 이용해 인자 전달을 해 사실상 스택을 0x10 바이트만큼 사용하지만,

[C++ 호출 측]
    19:     callback(1, 2, 3, 4, 5, 6);
790E1748 8B F4                mov         esi,esp  
790E174A 6A 06                push        6  
790E174C 6A 05                push        5  
790E174E 6A 04                push        4  
790E1750 6A 03                push        3  
790E1752 BA 02 00 00 00       mov         edx,2  
790E1757 B9 01 00 00 00       mov         ecx,1  
790E175C FF 55 08             call        dword ptr [0x033f345e]  

[최초 호출 시 코드]
033F345E B8 4C 34 3F 03       mov         eax,33F344Ch  
033F3463 E9 B4 FB FF FF       jmp         033F301C  

[JIT 후 코드]
033F345E B8 4C 34 3F 03       mov         eax,offset Pointer to: CLRStub[MethodDescPrestub]@4e40adda059f24e8 (033F344Ch)  
033F3463 E9 54 9C 00 00       jmp         CLRStub[StubLinkStub]@4e40adda033fd0bc (033FD0BCh)  

특이하게도 .NET 5 런타임에서는 stub 코드를 cdecl과 동일하게 처리합니다. 이 때문에 마지막 "ret 18h"로 인한 스택 정리의 불균형으로 (디버깅 중에는) Run-Time Check Failure 오류가 발생합니다.

Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.


반면, 동일한 코드를 .NET Framework 런타임에서 호출하면 정상적으로 (운이 좋아) 동작합니다. 왜냐하면, .NET Framework 런타임은 delegate* unmanaged[Fastcall]에 대한 stub 코드를 생성하지 않기 때문에 native -> managed로의 호출이 그대로 이뤄집니다. 그런 와중에 fastcall과 clrcall의 호출 규약의 유사함으로 인해 스택이 깨지는 현상이 없어 표면상 잘 동작하는 것입니다.

하지만, clrcall과 fastcall의 3번째 인자부터 역순 관계이기 때문에 실제로 닷넷 코드 측에서 넘어온 인자를 보면,

[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvFastcall) })]
public static void callback_fastcall(int n1, int n2, int n3, int n4, int n5, int n6)
{
    // n1 == 1, n2 == 2, n3 == 6, n4 == 5, n5 == 4, n6 == 3
    int sum = n1 + n2 + n3 + n4 + n5 + n6;
    Console.WriteLine(sum);
}

C++ 측에서 fastcall로 넘겨준 인자를 닷넷 메서드에서 잘못 받고 있습니다. (위의 경우 3, 4, 5, 6 인자가 타입이 같고 내부 코드가 인자 순서에 상관없어 운이 좋게 정상 동작했지만, 다른 경우라면 오동작을 하게 됩니다.)




콜백 말고, 직접 GetProcAddress로 구하는 것은 어떨까요? 테스트를 위해 다음의 코드를,

unsafe
{
    IntPtr ptrKernel = LoadLibrary("Dll1.dll");
    IntPtr ptr_stdcall = GetProcAddress(ptrKernel, "stdcall_func");
    IntPtr ptr_cdeclcall = GetProcAddress(ptrKernel, "cdecl_func");

    var stdCallFunc = (delegate* unmanaged[Stdcall]<int, int, int, int, int, int, void>)ptr_stdcall;
    var cdeclCallFunc = (delegate* unmanaged[Cdecl]<int, int, int, int, int, int, void>)ptr_cdeclcall;
    stdCallFunc(1, 2, 3, 4, 5, 6);
    cdeclCallFunc(1, 2, 3, 4, 5, 6);
}

실행해 보면 .NET 5/Framework/.NET Core 런타임 모두 64비트/32비트 빌드에 상관없이 잘 동작합니다. 콜백 방식과는 달리 위와 같이 .NET 측에서 호출하는 경우에는 GenericPInvokeCalliHelper라는 내부 코드를 반드시 거치도록 변경하기 때문에,

    45:                 stdCallFunc(1, 2, 3, 4, 5, 6);
00007FF9369A09A9 CC                   int         3  
00007FF9369A09AA 8B 95 C0 00 00 00    mov         edx,dword ptr [rbp+0C0h]  
00007FF9369A09B0 4C 89 95 A8 00 00 00 mov         qword ptr [rbp+0A8h],r10  
00007FF9369A09B7 C7 44 24 20 05 00 00 00 mov         dword ptr [rsp+20h],5  
00007FF9369A09BF C7 44 24 28 06 00 00 00 mov         dword ptr [rsp+28h],6  
00007FF9369A09C7 4C 8B 95 A8 00 00 00 mov         r10,qword ptr [rbp+0A8h]  
00007FF9369A09CE 41 BB 60 12 0B 01    mov         r11d,10B1260h  
00007FF9369A09D4 B9 01 00 00 00       mov         ecx,1  
00007FF9369A09D9 BA 02 00 00 00       mov         edx,2  
00007FF9369A09DE 41 B8 03 00 00 00    mov         r8d,3  
00007FF9369A09E4 41 B9 04 00 00 00    mov         r9d,4  
00007FF9369A09EA E8 51 09 55 5F       call        GenericPInvokeCalliHelper (07FF995EF1340h)  

잘 동작합니다. 그리고 fastcall의 경우에는,

IntPtr ptr_fastcall = GetProcAddress(ptrKernel, "fastcall_func");
var fastCallFunc = (delegate* unmanaged[Fastcall]<int, int, int, int, int, int, void>)ptr_fastcall;
fastCallFunc(1, 2, 3, 4, 5, 6);

(.NET 5/Framework/.NET Core 런타임 모두) 이런 예외가 발생하는데요,

System.TypeLoadException
  HResult=0x80131522
  Message=Invalid unmanaged calling convention: must be one of stdcall, cdecl, or thiscall.
  Source=<Cannot evaluate the exception source>
  StackTrace:
<Cannot evaluate the exception stack trace>

  This exception was originally thrown at this call stack:
    ConsoleApp1.Program.Main(string[]) in Program.cs

이것은 예전에 정리한 글의 제약을 그대로 따른다는 데에서 일관성은 있는 규칙입니다.

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

그래도 이해가 안 되는 것은, 어째서 unmanaged[Fastcall]은 컴파일이 가능하게 했냐는 점입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




그러고 보니, 이번 실습을 하면서 아래의 글이 생각났습니다.

C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)
; https://www.sysnet.pe.kr/2/0/12151

위의 글에서도 Win32 API를 닷넷 메서드로 가로챌 때 호출 규약의 문제로 인해 x64에서만 실습을 했었습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/27/2020]

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13298정성태3/27/20233681Windows: 236. Win32 - MessageBeep 소리가 안 들린다면?
13297정성태3/26/20234355Windows: 235. Win32 - Code Modal과 UI Modal
13296정성태3/25/20233693Windows: 234. IsDialogMessage와 협업하는 WM_GETDLGCODE Win32 메시지 [1]파일 다운로드1
13295정성태3/24/20233962Windows: 233. Win32 - modeless 대화창을 modal처럼 동작하게 만드는 방법파일 다운로드1
13294정성태3/22/20234135.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234200오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234329Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234742.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234246.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233438Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233552Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233707Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234164Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233757Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233955Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233496오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233820Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233727Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234494개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20234043오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20234015개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234658개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234336.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234692.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234276.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233973.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...