Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
닷넷에서 호출 스택의 메서드에 대한 인자 값 확인이 가능할까?

예전에, 지인으로부터 문의를 받았던 내용입니다. 닷넷에서 예외가 발생한 경우 친절하게 "메서드 호출 스택" 및 "소스 코드 라인 번호"를 구할 수 있는 것은 좋으나, 해당 메서드에 전달된 인자 값까지도 알고 싶다는 것이었습니다.

오호~~~ 일리 있습니다. 저도 궁금해졌거든요. ^^

그 당시에는, 얼마 간의 웹 서핑 끝에 아무런 자료를 찾을 수 없어서 안된다고 잠정 결론을 내리고 문제를 덮었는데, 어느 순간 .NET의 메타 데이터 도움을 받으면 가능하지 않을까 싶은 생각이 들어 다시 그 문제를 파악해 보았습니다. 이 글은, 그 결과에 대한 기록입니다. ^^ 한번 같이 볼까요? ^^

예를 들어, 다음과 같이 메서드 호출이 연결된다고 가정해 보겠습니다.

class Program
{
    static void Main(string[] args)
    {
        TestMethod(0, 1);
    }

    static void TestMethod(int arg1, int arg2)
    {
        TestMethod2(arg1 + arg2);
    }

    static void TestMethod2(int arg1)
    {
        // Do...
    }
}

TestMethod2 메서드에서, 자신이 호출되어온 구조를 알고 싶다면 다음과 같이 코드를 만들어 줄 수 있습니다.

static void TestMethod2(int arg1)
{
    System.Diagnostics.StackTrace trace = 
        new System.Diagnostics.StackTrace(Thread.CurrentThread, true);
    Console.WriteLine(trace.ToString());
}

그럼, 다음과 같은 정보를 얻을 수 있습니다.

at ConsoleApplication1.Program.TestMethod2(Int32 arg1) in D:\...\Program.cs:line 22
at ConsoleApplication1.Program.TestMethod(Int32 arg1, Int32 arg2) in D:\...\Program.cs:line 17
at ConsoleApplication1.Program.Main(String[] args) in D:\...\Program.cs:line 12

위의 문자열은 예외 개체로부터도 역시 동일하게 구할 수 있습니다. 나아가서 인자 값을 구해보면!

해결의 실마리는 생각보다 간단합니다. C/C++을 공부하신 분들이라면 이런 경우 대번에 BP를 생각해 낼 텐데요. 사실 C/C++에서는 BP 값을 안다고 해도 해당 메서드에 대한 정보를 알 수 없기 때문에 몇 개의 인자가 스택에 쌓여있는지 알 수 없으므로 오히려 써 먹을 수 없었지만, .NET 세계에서는 메타 데이터 덕분에 BP와 함께 적절하게 코드를 작성하면 호출 스택에 있는 모든 메서드에 전달된 인자를 살펴 보는 것이 가능합니다.

.NET CLR의 IL 코드 자체는 "스택" 기반의 동작을 모델로 하지만, JIT 컴파일 단계를 지나서 기계어로 번역된 시점에는 어쩔 수 없는 C/C++ 함수 구조와 동일하게 이뤄져 있습니다. 실제로, 아래는 Visual Studio에서 디버깅 시에 디스 어셈블 코드 창을 이용해서 확인해 본 것입니다.

net_stack_arg_check_1.png

낯익은 코드가 보이죠? ^^

[Release 로 빌드 / Debug JIT 시]

--- D:\...\Program.cs 
    21:         {
00000000 55                   push        ebp     // 스택 프레임 구성
00000001 8B EC                mov         ebp,esp 
00000003 83 EC 40             sub         esp, 8 // 지역 변수를 위한 스택 공간 마련

전형적인 함수의 prologue 코드입니다.

따라서, .NET에서 호출 스택에 전달된 인자값들을 확인하고 싶다면 EBP 값을 알아내어 그로부터 구성된 스택 프레임을 추적하면서 해당 메서드의 메타 데이터로부터 인자 수에 맞게 값을 추출하는 것으로 해결할 수 있습니다.

그런데 ^^ 불행이 시작되는 군요. 바로 CLR의 JIT 컴파일 결과물이 __fastcall 방식을 따른다는 점입니다.

따라서, 함수 호출 시에 최초 2개의 인자에 대해서는 ecx, edx 레지스터를 통해서 전달하고 이후의 인자들은 스택에 오른쪽에서 왼쪽 방향으로 저장됩니다. (아울러 static 메서드가 아닌 인스턴스 메서드 호출인 경우에는 최초 인자 전달에 this가 예약됩니다.)

이런 이유로 인해, 스택 프레임을 이용하여 호출 스택에 전달된 인자값을 확인한다면 다음의 경우만 가능합니다.

  • 현재 메서드 호출의 인자값
  • 이전 메서드 호출의 (this를 포함한) 3번째 인자값

애석하게도 "이전 메서드 호출의 2번째 인자값"들은 구하는 방법이 묘연합니다. 왜냐하면, 그 당시의 ecx, edx 값에 저장되기 때문에 "현재의 메서드" 단계에서는 ecx, edx 값이 현재 메서드에 전달된 인자값으로 덮어써졌기 때문입니다. 대신 특정 빌드 유형에 따라서 ecx, edx 값을 다음과 같이 스택에 다시 저장해 주는 코드를 확인할 수 있습니다.

[Release로 빌드 / Debug JIT 시]

00000000 55                   push        ebp 
00000001 8B EC                mov         ebp,esp 
00000003 83 EC 08             sub         esp,8 

00000006 89 4D FC             mov         dword ptr [ebp-4],ecx 
00000009 89 55 F8             mov         dword ptr [ebp-8],edx 

0000000c 83 3D 50 8D 1D 00 00 cmp         dword ptr ds:[001D8D50h],0 
00000013 74 05                je          0000001A 
00000015 E8 CF 9D 74 5C       call        5C749DE9 

JIT 컴파일이 어떻게 되느냐에 따라서 스택 프레임에서 정보를 구할 수 있는 방식이 달라지기 때문에, 인자 값을 구해오기 위해서는 아래와 같은 몇 가지 제약을 두어야 합니다.

  • 해당 어셈블리는 릴리스 빌드로 되어 있고,
  • 플랫폼은 x86,
  • 첫 번째 인자의 시작은 [ebp-4] 부터! (참고로, 디버그 빌드에서는 [ebp-3Ch]입니다.)
  • 실행은, Visual Studio의 "F5" 키를 이용하여 디버그 모드로 실행 (즉, IL 자체는 Release 빌드되었지만, JIT 컴파일러는 Debug 모드로 빌드)

어쨌든... 이번 건은 상업적인 목적이 아닌 개인적인 호기심에서 알아보는 것이기 때문에, ^^ 재미삼아서 읽어주시면 되겠습니다.




자, 그럼 시작은 BP 레지스터의 값을 알아내는 것부터인데요.

물론, C#에서 레지스터 값을 직접 구해오는 방법은 없습니다. 이런 경우에는 C++/CLI를 통해서 접근해야 하는데요. C#에서 사용할 수 있도록 다음과 같은 메서드를 노출해주는 C++/CLI 메서드를 만들어주어야 합니다.

namespace CppCliTest 
{
    public ref class Class1
    {

    public:        
        __declspec(noinline) int GetBPValue()
        {

            return innerGetBPValue();
        }
    };
}

아무리 C++/CLI라고는 해도 인라인 어셈블리 구문을 사용할 수는 없기 때문에 위와 같이 별도의 unmanaged C++ 함수를 불러주는 단계가 필요합니다. 그래서, innerGetBPValue 메서드 구현을 보면,

__declspec(noinline) int innerGetBPValue()
{
    int bpValue;

    __asm
    {
        mov bpValue, ebp
    }

    int *GetBPValueBP = (int *)bpValue; // innerGetBPValue를 호출한 GetBPValue 메서드의 BP
    int *managedCppFuncBP = (int *)(*GetBPValueBP); // GetBPValue를 호출한 .NET TestMethod2의 BP

    return *managedCppFuncBP;
}

위와 같이 자신의 BP를 구한 후, 스택 프레임을 따라서 .NET TestMethod2까지 구해서 반환합니다.

.NET C# 메서드에서는 반환받은 BP 값을 이용해서 아래와 같이 스택 프레임을 접근할 수 있습니다.

static unsafe void TestMethod2(int arg1)
{
    CppCliTest.Class1 cl1 = new CppCliTest.Class1();
    int bpValue = cl1.GetBPValue();

    IntPtr ptrBasePointer = new IntPtr(bpValue);
    int *pt = (int *)ptrBasePointer.ToPointer();

    Console.WriteLine(string.Format("TestMethod BP : 0x{0:x}", *pt));
    Console.WriteLine(string.Format("Return Address: 0x{0:x}", *(pt + 1)));
    Console.WriteLine(string.Format("Argument    #1: 0x{0:x}", *(pt - 1)));

    ...[생략]...
}

BP 값을 이용하여, 자신에게 넘어온 인자 값과 함수의 반환 주소 값까지도 알아낼 수 있습니다. 위에서는 인자의 수를 미리 안다는 가정으로 코딩되었는데, 만약 TestMethod2를 호출한 TestMethod까지 자동으로 처리하고 싶다면 다음과 같이 CLR에서 제공되는 메타데이터를 곁들이면 됩니다.

static unsafe void TestMethod2(int arg1)
{
    ...[생략]...

    IntPtr ptrTestMethodBasePointer = new IntPtr(*pt); // 자신을 호출한 상위 메서드의 BP 값을 알아내고,
    int* testMethodPt = (int*)ptrTestMethodBasePointer.ToPointer();

    Console.WriteLine(string.Format("TestMethod BP : 0x{0:x}", *testMethodPt));
    Console.WriteLine(string.Format("Return Address: 0x{0:x}", *(testMethodPt + 1)));

    // System.Diagnostics.StackFrame을 이용해서 스택의 상위 메서드의 MethodBaseInfo를 구하고,
    StackFrame sf = new StackFrame(1);
    int paramCount = sf.GetMethod().GetParameters().Length;

    // 파라미터 수에 따라 스택 프레임에서 값을 가져와서 출력
    for (int i = 0; i < paramCount; i++)
    {
        Console.WriteLine(
            string.Format("Argument    #{0}: 0x{1:x}", i + 1, *(testMethodPt - (1 + i))));
    }
}

이제, Visual Studio에서 "F5" 키를 눌러 실행하면 다음과 유사한 결과를 볼 수 있습니다.

net_stack_arg_check_2.png

제한된 환경(Release 빌드, Debug JIT)에서 결과를 내었기 때문에, 실제 업무 환경에서 사용하는 것은 거의 불가능에 가깝다고 볼 수 있습니다. JIT 컴파일러의 최적화 기능 때문인데요. 단적인 예로, "Release 빌드, Release JIT" 상황에서는 PDB 파일의 도움을 받는 Visual Studio조차도 함수에 전달된 인자를 못 보여줍니다.

정리해 보면, .NET의 메타데이터 정보를 이용한다고 해도 "종단 메서드" 측에서 스택 프레임을 따라가면서 메서드의 인자 전달을 추적하는 것은 불가능합니다. 따라서, 굳이 이런 기능이 필요하다면 "사용자"가 제어할 수 있는 모든 메서드들에서 자신들의 인자 값을 보관하는 어떤 기능을 구현해서 연계해 주어야 합니다.

첨부한 파일은, 제가 테스트한 소스 코드입니다.



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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2014-01-30 12시00분
결국 불가능하다는 답변이지만.

In a .net Exception how to get a stacktrace with argument values
; http://stackoverflow.com/questions/157911/in-a-net-exception-how-to-get-a-stacktrace-with-argument-values
정성태
2015-02-27 11시58분
정성태

... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...
NoWriterDateCnt.TitleFile(s)
12111정성태1/12/202020537디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [16]파일 다운로드1
12110정성태1/11/202019839디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/202016578오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/202017413오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/202018596.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/202019634VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/202018139디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/7/202019343DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태1/7/202022453DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/202018796디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/5/202019054.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/202016545.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
12099정성태1/3/202019409디버깅 기술: 151. Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회 [1]
12098정성태1/2/202019147.NET Framework: 874. C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법 [3]
12097정성태1/2/202017207디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태12/30/201919875디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [1]
12095정성태12/27/201921582VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/201919332.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/201918889.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/201917666디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/201919965디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/201920074.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점 [1]파일 다운로드1
12089정성태12/23/201919009디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/201917966Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/201918431디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
12086정성태12/20/201920949디버깅 기술: 144. windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례
... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...