성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Roll A Lisp In C - Reading ; https...
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
글쓰기
제목
이름
암호
전자우편
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'>OpenCover 코드 커버리지 도구의 동작 방식을 통해 살펴보는 Calli IL 코드 사용법</h1> <p> 지난번에 OpenCover 도구를 설명했는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > OpenCover 오픈 소스를 이용한 .NET 코드 커버리지(Code coverage) ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/2881'>http://www.sysnet.pe.kr/2/0/2881</a> </pre> <br /> .NET Profiler를 이용해 런타임에 변경한다는 사실만 다를 뿐 결과적으로 보면 "<a target='tab' href='http://www.sysnet.pe.kr/2/0/2880'>Semantic Designs</a>" 제품이 컴파일 타임에 코드를 변경한 것과 유사한 방식으로 동작합니다.<br /> <br /> 예를 들어, OpenCover.Console.exe로 다음의 콘솔 프로그램을 실행해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { if (args.Length == 2) { Console.WriteLine("Args == 2"); } else { Console.WriteLine("Args != 2"); } TestIt(); Console.WriteLine("Main"); } <span style='color: blue; font-weight: bold'> private static void TestIt() { Console.WriteLine("TestIt"); TestIt2(); }</span> private static void TestIt2() { Console.WriteLine("TestIt2"); } } } </pre> <br /> TestIt 메서드의 IL 코드가 원래는 이렇게 빌드되었지만,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > L_0000: /* 00 */ nop L_0001: /* 72 */ ldstr 0x70001227 L_0006: /* 28 */ call STAT System.Console.WriteLine [mscorlib] // 0x0a00006d L_000b: /* 00 */ nop L_000c: /* 28 */ call STAT ConsoleApplication1.Program.TestIt2 [ConsoleApplication1] // 0x06000027 L_0011: /* 00 */ nop L_0012: /* 2a */ ret </pre> <br /> OpenCover.Profiler.dll .NET Profiler의 동작으로 인해 이렇게 변경된 것을 확인할 수 있습니다.<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='color: blue; font-weight: bold'> L_0000: /* 20 */ ldc.i4 0x00000706 L_0005: /* 28 */ call STAT System.CannotUnloadAppDomainException.SafeVisited [mscorlib] // 0x0a00007b</span> L_000a: /* 00 */ nop <span style='color: blue; font-weight: bold'>L_000b: /* 20 */ ldc.i4 0x00000707 L_0010: /* 28 */ call STAT System.CannotUnloadAppDomainException.SafeVisited [mscorlib] // 0x0a00007b</span> L_0015: /* 72 */ ldstr 0x70001227 L_001a: /* 28 */ call STAT System.Console.WriteLine [mscorlib] // 0x0a00006d L_001f: /* 00 */ nop <span style='color: blue; font-weight: bold'>L_0020: /* 20 */ ldc.i4 0x00000708 L_0025: /* 28 */ call STAT System.CannotUnloadAppDomainException.SafeVisited [mscorlib] // 0x0a00007b</span> L_002a: /* 28 */ call STAT ConsoleApplication1.Program.TestIt2 [ConsoleApplication1] // 0x06000027 L_002f: /* 00 */ nop <span style='color: blue; font-weight: bold'>L_0030: /* 20 */ ldc.i4 0x00000709 L_0035: /* 28 */ call STAT System.CannotUnloadAppDomainException.SafeVisited [mscorlib] // 0x0a00007b</span> L_003a: /* 2a */ ret </pre> <br /> mscorlib.dll에 있는 System.CannotUnloadAppDomainException 타입의 SafeVisited 메서드를 호출하고 있는데요. 원래 이 메서드는 CannotUnloadAppDomainException 타입에 존재하지 않습니다. 즉, OpenCover.Profiler.dll .NET Profiler가 런타임시에 CannotUnloadAppDomainException 타입에 SafeVisited 메서드를 만들어 넣은 것입니다. 이어서 그 메서드의 IL 코드를 들어가면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > L_0000: /* 02 */ ldarg.0 L_0001: /* 28 */ call STAT System.CannotUnloadAppDomainException.VisitedCritical [mscorlib] // 0x06006bbc L_0006: /* 2a */ ret </pre> <br /> 이렇게 VisitedCritical 메서드로 호출을 전달하는 역할만 합니다. (그 이유는, <a target='tab' href='http://www.sysnet.pe.kr/2/0/1680'>.NET 4.0의 새로운 보안 모델</a> 때문입니다.)<br /> <br /> 다시 VisitedCritical 메서드의 내부로 들어가면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > L_0000: /* 02 */ ldarg.0 L_0001: /* 21 */ ldc.i8 0x7ffa4f745ad0 L_000a: /* 29 */ calli 0x11000eaf L_000f: /* 2a */ ret </pre> <br /> 이렇게 구성되어 있는데, 바로 여기서 ^^ calli 명령어가 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > OpCodes.Calli Field ; <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.calli'>https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.calli</a> </pre> <br /> calli 명령어는 사용하기 전, 그 메서드에 전달할 인자에 대한 처리가 먼저 선행됩니다. (물론, 대상 메서드가 인자를 받지 않는다면 생략~~~!) 위의 명령어에서, "ldarg.0"이 바로 calli로 호출되는 메서드에 전달할 인자입니다. 즉, VisitedCritical 메서드는 그 스스로에게 전달되었던 첫 번째 인자를 calli 메서드에 다시 전달합니다.<br /> <br /> 인자 전달 후에는 호출하게 될 메서드의 주소가 스택에 놓여집니다. 즉, "ldc.i8 0x7ffa4f745ad0" 명령에서 "0x7ffa4f745ad0" 주소가 바로 메서드의 주소입니다. "Process Explorer"에서 이 주소를 확인해 보면 OpenCover.Profiler.dll 모듈의 매핑 주소 내에 있음을 알 수 있습니다.<br /> <br /> 마지막으로 calli 명령어는 그 자신의 명령어에 대한 operand로 0x11000eaf 값을 받고 있는데요. 이 값이 참~~~ 미스테리합니다. ^^; "<a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.calli'>OpCodes.Calli Field</a>" 문서에 보면, "29 <T>" 형식으로 operand 영역에 대해 "callSiteDescr"라고 표현하고 있습니다. 도대체 "callSiteDescr"이 무슨 인자란 말입니까? 문서에는 이 값이 signature에 대한 메타데이터 토큰 값임을 밝히고 있습니다.<br /> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> The method entry pointer is assumed to be a specific pointer to native code (of the target machine) that can be legitimately called with the arguments described by the calling convention (a metadata token for a stand-alone signature)<br /> </div><br /> <br /> 즉, 0x7ffa4f745ad0 주소에 있는 메서드의 static/instance 유형 및 인자 수, 반환 타입, 각각의 인자 타입을 알 수 있는 signature 항목의 토큰 값을 calli에 전달하고 있는 것입니다.<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;' > IL rewriting : calli opcode and metadata token for a stand-alone signature ; <a target='tab' href='https://social.msdn.microsoft.com/Forums/en-US/ada5917d-e714-40e9-b441-eb9074842b06/il-rewriting-calli-opcode-and-metadata-token-for-a-standalone-signature?forum=netfxtoolsdev'>https://social.msdn.microsoft.com/Forums/en-US/ada5917d-e714-40e9-b441-eb9074842b06/il-rewriting-calli-opcode-and-metadata-token-for-a-standalone-signature?forum=netfxtoolsdev</a> </pre> <br /> 위의 글에 보면, native 메서드로 __fastcall 방식의 C++ 코드가 준비되어 있고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static void __fastcall UnmanagedInspectValue(void) { printf("Hello\n"); } </pre> <br /> 이 메서드에 대한 signature를 .NET Profiler에서 동적으로 signature 테이블에 등록한 후, 등록된 그 항목의 메타데이터 토큰값을 구한 다음,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > static COR_SIGNATURE unmanagedInspectValueSignature[] = { IMAGE_CEE_CS_CALLCONV_DEFAULT, // Default CallKind! 0x00, // Parameter count ELEMENT_TYPE_VOID // Return type }; void (__fastcall *pt)(void) = &UnmanagedInspectValue ; <span style='color: blue; font-weight: bold'>mdSignature pmsig;</span> metaDataEmit->GetTokenFromSig(unmanagedInspectValueSignature, sizeof(unmanagedInspectValueSignature), <span style='color: blue; font-weight: bold'> &pmsig</span>)); </pre> <br /> 이를 기반으로 calli 명령어를 구성하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > BYTE ilCode[10]; ilCode[0] = 0x20; // ldc.i4 memcpy( ilCode[1], (void*)&pt, sizeof(pt) ); // ftn pointer ilCode[5]= 0x29; // calli memcpy( ilCode[6] (void*)&pmsig, sizeof(pmsig) ); // call site descr </pre> <br /> 역시, 코드를 보니까 이해가 빠르군요. ^^<br /> <br /> 다시 정리해 보면, OpenCover 도구는 .NET Profiler를 이용해 런타임시에 코드의 구분 구획마다 코드 커버리지가 되었음을 알 수 있는 식별자를 심고, 그 식별자를 .NET Profiler 내부에서 구현해둔 C/C++ 함수에 전달해 처리하는 방식을 취하고 있습니다.<br /> <br /> 실제로 OpenCover 소스 코드를 뒤져보면 "\main\OpenCover.Profiler\CodeCoverage.cpp" 파일이 있는데, 바로 아래의 함수가 System.CannotUnloadAppDomainException.VisitedCritical 메서드의 호출내부에서 최종 불리게 되는 native 메서드인 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > /// <summary>An unmanaged callback that can be called from .NET that has a single I4 parameter</summary> /// <remarks> /// void (__fastcall *pt)(long) = &SequencePointVisit ; /// mdSignature pmsig = GetUnmanagedMethodSignatureToken_I4(moduleId); /// </remarks> static void __fastcall InstrumentPointVisit(ULONG seq) { CCodeCoverage::g_pProfiler->AddVisitPoint(seq); } </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1470
(왼쪽의 숫자를 입력해야 합니다.)