성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (부록 1) - CallingConvention.StdCall, CallingConvention.Cdecl에 상관없이 왜 호출이 잘 될까요?</h1> <p> 지난 글에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (4) - CLR JIT 컴파일러의 P/Invoke 호출 규약 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11141'>http://www.sysnet.pe.kr/2/0/11141</a> </pre> <br /> CLR JIT 컴파일러가 생성한 P/Invoke 호출이 왜 __stdcall, __cdecl에 상관없이 잘 되는가에 대해 그냥 그러려니 하고 덮으려다가, 그래도 너무 궁금했습니다. 그래서 분석을 위해 다음의 DllImport 2개를 더 정의하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 아래의 2개는 기존 __cdecl 호출 규약의 "ExternC_CDECL_Func_Arg5" 함수에 대해, // - ExternC_CDECL_Func_Arg5_2는 명시적으로 "CallingConvention.Cdecl"을 지정하고, [DllImport("Win32Project1.dll", <span style='color: blue; font-weight: bold'>EntryPoint = "ExternC_CDECL_Func_Arg5", CallingConvention = CallingConvention.Cdecl</span>)] internal unsafe static extern int <span style='color: blue; font-weight: bold'>ExternC_CDECL_Func_Arg5_2</span>(int value1, int value2, int value3, int value4, int value5); // - ExternC_CDECL_Func_Arg5_3는 명시적으로 "CallingConvention.StdCall"을 지정 [DllImport("Win32Project1.dll", <span style='color: blue; font-weight: bold'>EntryPoint = "ExternC_CDECL_Func_Arg5", CallingConvention = CallingConvention.StdCall</span>)] internal unsafe static extern int <span style='color: blue; font-weight: bold'>ExternC_CDECL_Func_Arg5_3</span>(int value1, int value2, int value3, int value4, int value5); </pre> <br /> 호출을 다음과 같이 추가했습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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); } </pre> <br /> Release 모드로 빌드하고, 실행하면 Console.ReadLine에서 응용 프로그램이 멈추는 데요, 이때 windbg.exe를 실행해 "Attach to Process..."를 해줍니다. 이후의 코드를 분석하면 어떤 수수께끼가 있는지 알게 됩니다. ^^<br /> <br /> 우선 sos 모듈을 로드하고, Managed Code를 실행하는 스레드로 문맥 변경을 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:006> <span style='color: blue; font-weight: bold'>.loadby sos clr</span> 0:008> <span style='color: blue; font-weight: bold'>!threads</span> 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 <span style='color: blue; font-weight: bold'>f50</span> 013ca400 2a020 Preemptive 030D4F28:00000000 013bece8 1 MTA 5 2 3014 013d9920 2b220 Preemptive 00000000:00000000 013bece8 0 MTA (Finalizer) 0:008> <span style='color: blue; font-weight: bold'>~~[f50]s</span> 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> </pre> <br /> 콜스택을 확인해 ReadLine을 호출한 부모 스택 프레임을 찾습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!clrstack</span> 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() <span style='color: blue; font-weight: bold'>0113f028 02f104a5 *** WARNING: Unable to verify checksum for C:\ConsoleApplication1\bin\x86\Release\ConsoleApplication1.exe Program.Main(System.String[]) [C:\ConsoleApplication1\Program.cs @ 73]</span> 0113f198 72ceea96 [GCFrame: 0113f198] </pre> <br /> 02f104a5 주소를 대상으로 역어셈블을 하면, 각각의 호출 코드를 확인할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!U /d 02f104a5</span> 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] <span style='color: blue; font-weight: bold'>02f104b3 e8acfcffff call 02f10164 (Program.ExternC_CDECL_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000006)</span> 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] <span style='color: blue; font-weight: bold'>02f104c6 e8a5fcffff call 02f10170 (Program.ExternC_CDECL_Func_Arg5_2(Int32, Int32, Int32, Int32, Int32), mdToken: 06000007)</span> 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] <span style='color: blue; font-weight: bold'>02f104e2 e895fcffff call 02f1017c (Program.ExternC_CDECL_Func_Arg5_3(Int32, Int32, Int32, Int32, Int32), mdToken: 06000008)</span> 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] <span style='color: blue; font-weight: bold'>02f104f5 e88efcffff call 02f10188 (Program.ExternC_STD_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000010)</span> C:\ConsoleApplication1\Program.cs @ 103: 02f104fa 5d pop ebp 02f104fb c3 ret </pre> <br /> 개별 call들의 대상 주소에 대해 disassembly 코드를 확인해 보면 jmp 문으로 이어지는 것을 확인할 수 있습니다.,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 02f104b3 e8acfcffff call <span style='color: blue; font-weight: bold'>02f10164</span> (Program.ExternC_CDECL_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000006) <span style='color: blue; font-weight: bold'>02f10164</span> b8d04d3501 mov eax,1354DD0h 02f10169 89ed mov ebp,ebp <span style='color: blue; font-weight: bold'>02f1016b e9a0030000 jmp 02f10510</span> 02f104c6 e8a5fcffff call <span style='color: blue; font-weight: bold'>02f10170</span> (Program.ExternC_CDECL_Func_Arg5_2(Int32, Int32, Int32, Int32, Int32), mdToken: 06000007) <span style='color: blue; font-weight: bold'>02f10170</span> b8fc4d3501 mov eax,1354DFCh 02f10175 89ed mov ebp,ebp <span style='color: blue; font-weight: bold'>02f10177 e994030000 jmp 02f10510</span> 02f104e2 e895fcffff call <span style='color: blue; font-weight: bold'>02f1017c</span> (Program.ExternC_CDECL_Func_Arg5_3(Int32, Int32, Int32, Int32, Int32), mdToken: 06000008) <span style='color: blue; font-weight: bold'>02f1017c</span> b8284e3501 mov eax,1354E28h 02f10181 89ed mov ebp,ebp <span style='color: blue; font-weight: bold'>02f10183 e938040000 jmp 02f105c0</span> 02f104f5 e88efcffff call <span style='color: blue; font-weight: bold'>02f10188</span> (Program.ExternC_STD_Func_Arg5(Int32, Int32, Int32, Int32, Int32), mdToken: 06000010) <span style='color: blue; font-weight: bold'>02f10188</span> b8884f3501 mov eax,1354F88h 02f1018d 89ed mov ebp,ebp <span style='color: blue; font-weight: bold'>02f1018f e92c040000 jmp 02f105c0</span> </pre> <br /> 그런데, 재미있는 규칙이 있습니다. 위의 2개는 02f10510로 점프하는 반면 아래의 2개는 02f105c0로 점프합니다. 왜일까요? 그렇습니다. __cdecl로 알고 있는 호출에 대해서는 02f10510로, __stdcall이라고 알려진 호출에 대해서는 02f105c0로 처리가 된 것입니다. 이처럼, 호출 규약이 같고 함수의 인자 수가 동일한 호출에 대해서는 같은 래퍼 함수가 처리합니다.<br /> <br /> 이제 분석 대상은 jmp 문으로 이어지는 코드가 됩니다. 먼저 __cdecl 호출 규약을 처리하는 방식을 조사해 볼 텐데요. 이를 위해 "jmp 02f10510"의 대상 주소에 BreakPoint를 설정하고 Run을 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:008> <span style='color: blue; font-weight: bold'>bp 02f10510</span> 0:008> <span style='color: blue; font-weight: bold'>g</span> Breakpoint 0 hit ...[생략]... </pre> <br /> 그럼, 다시 응용 프로그램이 실행되는데 현재 Console.ReadLine으로 입력을 받고 있는 상태이므로 엔터 키를 한번 쳐 줍니다. 그와 동시에 BP에 걸린 windbg 화면이 나오는데, 일단 현재의 @esp에 쌓인 스택을 보면 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 스택은 상위 주소에서 하위 주소로 쌓이므로, // 아래에서 위로 읽어 나가면 됩니다. 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 주소 ... </pre> <br /> 이런 스택 상황과 함께 실행될 02f10510의 코드는 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> 호흡 한번 가다듬고! 디버거를 이용해 코드 한줄씩 실행해 보겠습니다. ^^ 우선, 02f10516 주소의 "sub esp,20h" 호출까지 실행 후 다시 stack을 확인합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 주소 </pre> <br /> 보시는 바와 같이, (원래 우리의 C# 코드에서 전달된 처음 2개의 인자를 보관하는 ecx, edx는 보존된 상태이고) 3~5번째 인자는 스택의 저 아래까지 내려간 상태입니다. 당연하겠지만, 이대로는 C++ DLL의 export 함수를 호출하지 못합니다.<br /> <br /> 그럼, 아래의 호출까지 완료한 다음의,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 02f1054e e87d38ed6f call clr!StubHelpers::DemandPermission (72de3dd0) </pre> <br /> 스택 상태를 다시 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // [문맥] 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 주소 </pre> <br /> 아직 C/C++ 호출을 위한 인자 전달에 별다른 변화는 없습니다. 부수적으로 clr!StubHelpers::DemandPermission 함수 처리 중에 ecx, edx 인자가 변경될 수 있으므로 각각 ebx, edi 레지스터에 별도로 보관하는 작업이 수행되었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 02f10547 8bd9 mov ebx,ecx 02f10549 8bfa mov edi,edx </pre> <br /> DemandPermission 호출이 의미있는 것은, P/Invoke 대상이 되는 DLL 측의 함수에 대한 주소를 반환해 준다는 것입니다. 위의 코드에서는 그 결괏값을 다음의 코드를 통해 edx에 보관하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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] </pre> <br /> 즉, C++ 측의 ExternC_CDECL_Func_Arg5 함수 주소가 담겨지게 됩니다.<br /> <br /> 자, 이걸로 CLR 래퍼함수는 어느 정도 사전 처리작업을 완료했습니다. 이제부터는 본격적으로 P/Invoke 함수를 호출하기 위한 인자 값 복사 작업을 합니다. 바로 이곳이 실제적인 __cdecl 호출 규약에 의한 스택 인자 전달 코드가 수행되는 곳입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 인자 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 </pre> <br /> 여기까지의 스택 변화를 반영한 결과입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // [문맥] 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 주소 </pre> <br /> 그런 다음 C++의 함수 호출은 02f1057b 주소의 "call edx"에서 이뤄지는 데, __cdecl의 호출 규약으로 인해 edx 대상이 되는 함수에서는 스택 정리를 하지 않습니다. 대신 "call edx" 수행 후 호출자 측의 "add esp, 14h"를 통해 __cdecl의 호출 규약에 맞게 전달한 인자에 해당하는 스택을 정리합니다. 그럼, 이렇게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 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 주소 </pre> <br /> 이후, 흥미로운 코드가 하나 있는데 바로 "lea esp, [ebp-0Ch]" 입니다. 이 호출 하나로 ESP 레지스터가 현재의 래퍼 함수가 호출되는 시점으로 곧바로 복원됩니다. 마지막 ret 코드가 수행되기 전까지 스택 상황은 다음과 같이 바뀝니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='text-decoration: line-through'>0113efe8 51367ea4 0113efec 72cefa28 0113eff0 0113f198 0113eff4 00000000 0113eff8 0113efd4 0113effc 00000000 // mov dword ptr [ebp-18h],0 0113f000 0113f014 0113f004 01354dd0</span> <span style='color: blue; font-weight: bold'>// lea esp,[ebp-0Ch]로 esp 레지스터가 0113f008 주소로 잘림</span> <span style='color: blue; font-weight: bold'>0113f008</span> 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 주소 </pre> <br /> C# 래퍼 함수 자체의 스택 처리 방식은 (__stdcall, __fastcall처럼) callee가 처리하는 방식입니다. 따라서 "ret 0Ch" 코드가 실행되면서 그 자체에 전달된 인자를 위한 스택을 모두 비워버립니다. 결국 ExternC_CDECL_Func_Arg5 호출 이전의 스택 상태로 완벽하게 복원됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그럼, __cdecl 함수로 정의된 것을 __stdcall로 호출했을 때는 왜 잘 실행이 되는 것일까요? (참고로, Visual Studio에서 디버깅 모드로 실행하면 PInvokeStackImbalance MDA 예외가 발생합니다.)<br /> <br /> 다음은 __cdecl 대상의 함수를 C# 측에서 CallingConvention.StdCall로 호출했을 때의 "jmp 02f105c0"에 있는 CLR 래퍼 함수의 어셈블리입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 <span style='color: blue; font-weight: bold'>02f1062b ffd2 call edx</span> 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 </pre> <br /> 주된 차이점은, __cdecl로 작성된 C++ 함수이므로 스택 정리를 안해줌과 동시에 호출자 측에서도 "call edx" 실행 후 "add esp, 14h"와 같은 정리 작업을 안해 주기 때문에 다음과 같이 C++에 전달된 인자가 ESP에 그대로 남아있다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 주소 </pre> <br /> 하지만 그래도 실행에 지장이 없는 이유는, 전에 흥미롭다고 했던 "lea esp,[ebp-0Ch]" 코드 덕분입니다. 이 코드 한 줄로 인해 esp 레지스터의 값이 CLR 래퍼 함수의 초기 상태로 복원됩니다. 즉, 현재 호출된 코드의 스택 프레임(0x12ff374)을 기준으로 -12바이트 (0x012ff368) 위치로 순식간에 잘려집니다. 이렇게!<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // lea esp,[ebp-0Ch]로 esp 레지스터가 012ff368 주소로 치환</span> 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 주소 </pre> <br /> 이후 일련의 pop ... 명령과 ret 0Ch로 역시 CLR 래퍼 함수가 호출 이전의 스택 상태로 복원됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 다시 정리해 보면!<br /> <br /> __cdecl 함수를 __stdcall로 호출해도 괜찮은 것은, __cdecl 규약을 갖는 대상 함수 측에서도 스택 정리를 안 하고 CLR Wrapper 코드 측에서도 스택 정리를 안하지만, 마지막의 "lea esp, [ebp-0Ch]" 코드 덕분에 동일하게 초기화가 돼 버리기 때문입니다.<br /> <br /> 그렇다면 그 반대의 경우는 어떨까요? __stdcall 함수를 __cdecl 규약으로 호출한다면?<br /> <br /> 이렇게 되면 C/C++ 측의 함수에서도 스택 정리를 하고, CLR Wrapper 코드 측에서도 스택 정리를 하므로 2배의 스택이 날아가 버립니다. 어쩌면 스택이 완전히 깨져 버릴 수 있는 것입니다. 그런데 실제로 해보면 예상치 못한 결과가 나옵니다. 가령, __stdcall로 정의된 C++ 함수를 다음과 같이 억지로 (또는 실수로) CallingConvention.Cdecl이라고 지정하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("Win32Project1.dll", <span style='color: blue; font-weight: bold'>CallingConvention = CallingConvention.Cdecl</span>)] internal unsafe static extern int ExternC_STD_Func_Arg5(int value1, int value2, int value3, int value4, int value5); </pre> <br /> CLR은 "ExternC_STD_Func_Arg5" 함수 이름으로 찾게 됩니다. 하지만, 이전 장에서 설명한 것처럼,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11132'>http://www.sysnet.pe.kr/2/0/11132</a> </pre> <br /> extern "C"로 묶은 __stdcall 함수는 "_ExternC_STD_Func_Arg5@20"으로 name mangling이 되기 때문에 이 함수를 찾지 못하게 됩니다. 그래서 실제로는 다음과 같은 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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'. </pre> <br /> 오호... 이렇게까지 예방(?)이 되었지만 아직도 문제가 남아 있습니다. 즉 .def로 export 시킨 함수는 이름 그대로 사용되기 때문에 CLR Wrapper 측에서도 그 이름으로 풀이해 호출할 수밖에 없게 됩니다.<br /> <br /> 실제로 테스트를 해보겠습니다. 스택이 잘(?) 깨질 수 있도록 전달하는 인자의 수를 10개로 늘린 함수를 마련했습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [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); </pre> <br /> 이때 생성된 CLR 래퍼 함수는 다음과 같습니다. (이전에 언급했지만, CLR은 P/Invoke 함수의 인자 수에 따라 이런 wrapper 함수를 개별로 생성합니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> 그래서 call edx에서 반환하자 마자 다음과 같이 인자가 전달된 스택이 날아가 ESP 레지스터가 가리키는 위치가 바뀌고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 주소 </pre> <br /> 이후, 다시 "add esp, 28h"를 하는 바람에 10개의 WORD만큼 ESP 위치가 날아갑니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 주소 </pre> <br /> 위의 상태로만 보면, ESP가 가리키는 스택이 깨졌으므로 이후의 실행이 엉망이 될 것입니다. 실제로 날아가버린 스택으로 인해 실행이 잘못될만한 코드가 CLR Wrapper에 보면 "push eax, call clr!JIT_RareDisableHelper, pop eax"가 있습니다. 하지만, 다행인 점이 있다면 clr!g_TrapReturningThreads 전역 변수는 (어떻게 설정하는지 모르겠지만) 0 값을 가지기 때문에 "je 02520553"으로 인해 그 부분의 코드를 건너 뛰고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0252053a ffd2 call edx 0252053c 83c428 add esp,28h 0252053f c6460801 mov byte ptr [esi+8],1 <span style='color: blue; font-weight: bold'>02520543 833d4080357300 cmp dword ptr [clr!g_TrapReturningThreads (73358040)],0 0252054a 7407 je 02520553</span> 0252054c 50 push eax 0252054d e8be639670 call clr!JIT_RareDisableHelper (72e86910) 02520552 58 pop eax <span style='color: blue; font-weight: bold'>02520553</span> 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 </pre> <br /> 결국, "lea esp, [ebp-0ch]"로 인해 다시 ESP 레지스터가 정상으로 복구된다는 점입니다. 그러니까, g_TrapReturningThreads 전역 변수가 0이 아닌 상황에서는 저런 식으로 __cdecl 함수를 __stdcall로 잘못 지정해 호출하면 프로그램이 비정상 종료될 수 있습니다.<br /> <br /> 하지만, 대개의 경우 저 코드는 아주 잘 실행될 것이므로 Visual Studio만이 디버그 모드에서 P/InvokeStackImbalance MDA 예외로 경고를 표시해 주는 것입니다.<br /> <br /> 결론을 내리면, 지정한 CallingConvention.StdCall, CallingConvention.Cdecl 호출 규약이 잘못되어도 특수한 상황이 아니라면 P/Invoke 호출은 정상적으로 완료됩니다. (혹시, g_TrapReturningThreads 값을 제어하는 방법을 아시는 분은 덧글 부탁드립니다. CoreCLR을 보면 알 수 있을지도! ^^)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, clr!StubHelpers::DemandPermission 함수의 역 어셈블을 실어봅니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> 이 함수 내에서 "call clr!HelperMethodFrameRestoreState (72ceeabf)" 호출을 하는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 </pre> <br /> 위에서 최종적으로 edx에 값이 들어가는데, 그 값이 바로 P/Invoke 함수에 대한 C/C++ 측의 주소입니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1694
(왼쪽의 숫자를 입력해야 합니다.)