F.R.I.D.A.Y.

C#에서 WinAPI 호출 시 유의할 점 본문

DEV/.Net

C#에서 WinAPI 호출 시 유의할 점

F.R.I.D.A.Y. 2022. 4. 29. 14:03
반응형

 C#에서 네이티브 코드인 WinAPI를 이용해 무언가를 작성하려 할 때, 꼭 주의해야 할 점이 있다.


관리되는 코드

 C#은 C/C++처럼 기계어로 컴파일되는 것이 아닌 .NET 혹은 .Net Framework에서 작동하는 바이트코드, CRL(Common Runtime Language)로 변경된다.

 C/C++과 C#의 가장 큰 차이를 고르라면 아마도 메모리를 사용자(개발자)가 직접 관리하는가에 대한 여부일 것이다. 여태 알듯 C/C++은 개발자가 객체의 메모리 관리를 직접 해야 하는 것과 달리, C#의 경우 언어 사용자가 직접 메모리 관리를 할 필요가 없다. 내부적으로 GC(Garbage Collector)가 돌아가면서 사용되지 않는 메모리를 제거하는 방식으로 누수 메모리를 자동으로 잡는다.

 

 이렇게 .NET 에서 메모리 관리를 해주는 언어를 <관리되는 코드>라고 부른다. 반대로, C/C++과 같이 .NET에서 메모리 관리를 하지 않는 코드를 관리되지 않는 코드라고 한다.

 

네이티브 코드 혼용

 대개 소규모 프로젝트를 진행한다면 한 가지 언어를 이용해 개발을 하는 것이 보편적이다. 핵심 기능에 알맞는, 혹은 본인이 잘하는 언어 하나를 기반으로 만드는 게 일반적인 셈. 단 하나의 언어를 이용하는 만큼, 언어 하나만 알면 된다는 점, 그리고 선택한 언어의 장점을 그대로 흡수해 사용할 수 있다는 점이 좋지만, 다른 한편으로는 그 언어가 가진 제약을 벗어날 수 없다는 문제가 발생한다.

 예를 들어 다른 프로세스의 정보를 가져온다든가, 하드웨어 정보를 가져온다든가 하는 경우가 대표적이다. 그렇다고 비교적 저수준[# 실제로 C/C++은 언어 레벨에서 볼 때 고수준 언어가 맞다. 그러나 여기에서는 상대적이 관점에서의 저수준이며, 포스트의 이후 설명에서도 C/C++은 저수준으로 안내하겠다.]의 C/C++로 개발을 하자면 개발 효율이 떨어진다. 특히나 GUI 프로그래밍을 겸해야 한다면 난이도는 CLI 기반 프로그래밍보다 더 어려짐은 자명하다.

 

 그래서 GUI 프로그래밍이 필요한 경우라면 GUI는 고수준 언어를 사용해서, 실제 정보를 가져오고 처리하는 등의 행위는 저수준 언어로 처리하는 것이 일반적이다.

 

 외국 사람과 대화를 할 때는, 외국어를 배우거나 다른 방법 등으로 서로 지켜야 할 규칙을 만들어 의사소통을 한다. 당장, 자국에서도 사투리가 존재해 사투리를 모르면 자국어라도 이해가 되지 않는 경우도 존재하는 것 처럼.

작성 당시 유행하던 밈. 무슨 말씀이신지 자막을 보지 않으면 전혀 모르겠다

마샬링

 marshalling.[# 경우에 따라 단일 l의 marshaling이라고도 부른다고] 언어마다 처리하는 방식에 차이를 보이므로 이를 하나의 규약으로서 정해주는 것을 마샬링이라고 부른다. C/C++ 코드를 C#에서 사용하려면 이와같이 마샬링이 필수적이다.

 

[DllImport("user32", SetLastError= false)]
public static extern int GetwindowText(
	IntPtr hWnd, 
	StringBuilder lpText, 
    in in maxCount = 256
);

 예컨데 WinAPI 중 윈도우의 상단 타이틀, 혹은 버튼의 텍스트 등을 가져오는 GetWindowText 함수를 C#에서 사용하려면 위와 같이 Dll Import를 진행하여[# GetWindowText가 user32.dll에 존재한다] 외부 dll에 존재하며 이를 사용하겠다는 선언을 해주어야 한다.

 

 이와 같은 API에서는 문제가 발생하지 않으나, .NET과 GC의 구조를 모르는 경우 이해하기 난해한 부분이 존재하기도 한다. 나의 경우 CallbackOnCollectedDelegate 에러를 뱉고는 그대로 프로그램이 죽어버리는 오류를 경험했다.

 

 이 버그는 NullPointerException이나 DivideByZeroException과 같이 눈에 보이는 문제가 아니라는 점에서 더 골 때리는데, 어찌보면 코드의 문제라기보단 GC의 특성을 이해하지 않고 코드를 작성한 개발자의 탓이 크다.[# 코드 문제 맞잖아?]

 

 오류가 발생하는 경우는 대개 윈도우 프로시저 함수를 넘겨주는 경우. 즉, C#으로 작성된 callback 함수를 WinAPI에 전달할 때 발생한다. 나의 경우엔 수강중인 강의의 팀 프로젝트를 진행하던 중 경험하였으며, 문제가 되었던 코드는 다음이었다.

        public Tracer RunTrace()
        {
            hForegroundHookCode = ah.SetHook(wa.EventCode.EVENT_SYSTEM_FOREGROUND, GetForeGroundWindow);

            if(hForegroundHookCode == 0) Trace.WriteLine("Tracer 후킹 코드 바인딩 실패");
            else Trace.WriteLine("Tracer 후킹 코드 바인딩 성공");

            return this;
        }

 SetHook 함수[# 더 파고들면 SetWinEventHook이다. 사용하기 편하게 wrapping한 것]의 callback으로 GetForeGroundWindow 함수를 넘겨주었는데, 잘 동작하다 어느 시점이 되어버리면 위와 같은 CallbackOnCollectedDelegate 오류를 뱉곤 프로그램이 뻗어버린다.

 문제 조건이 GC에 의해 결정이 되다보니 언제 죽을지도 모르고, 문제 재현을 위해서는 어느정도 GC가 필요할 때까지 프로그램을 방치해두어야 하니 디버깅 하는 것도 한나절이다.[# GC와 연계한 문제임을 알고 있다면, 대충 어느 부분에서 문제가 날지도 알고 있을것이고 해결하는 방법도 대강 알고 있을테니 이는 문제가 되지 않는다.] 일반적인 오류처럼 에러 영역에 바로 중단점을 잡아주는 것도 아니니, 개발에 미숙한 경우라면 배는 힘들 것이다.

 

 SetHook 코드를 살펴보면 아래와 같이 되어있다.

        public static uint SetHook(wae EventCode, wa.WinEventProc WndProc, int procId = 0)
        {
            return wa.SetWinEventHook(EventCode, EventCode,
                IntPtr.Zero, WndProc, procId, 0,
                wa.SetWinEventHookFlags.WINEVENT_OUTOFCONTEXT | wa.SetWinEventHookFlags.WINEVENT_SKIPOWNPROCESS);
        }

 파라미터인 WndProc가 SetWinEventHook에 의해 인자로 설정, 특정 윈도우 이벤트가 감지되면 호출이 이루어지는 hook 함수이다. 즉, hook 함수의 주소를 파라미터 변수 WndProc가 가지고 있는 셈이다.

 SetHook이 실행될 때 위와 같이 세 함수가 실행중이라고 한다면, WndProc는 GetForeGroundWindow를 포인트하고 있을 것이고, 이는 달리 말해 GetForeGroundWindow의 레퍼런스 카운트가 0이 아니라는 소리가 된다. 관리되는(managed) 코드인 SetHook에서 관리되지 않는 코드(unmanaged) SetWinEventHook을 실행하면 아래와 같이 OS 후킹 리스트에 GetForeGroundWindow가 들어간다.

 문제는, SetWinEventHook은 관리되지 않는 코드라는 점이다. 관리되지 않는 코드는 GC에서 관리하지 않기 때문에 가리키는 대상의 레퍼런스 카운트를 증감시키지 않는다. 즉, SetHook이 끝나고 나면 GetForeGroundWindow의 레퍼런스 카운트는 0이 되어버리는 셈이다.

 따라서 GC는 GetForeGroundWindow를 더이상 사용하지 않는 개체로 보고 지워버리거나 메모리 압축 과정에서 대상 개체를 SetWinEventHook이 가리키는 주소에서 다른 주소로 옮겨버린다.

 이렇게 개체가 GC에 의해 이동을 하거나 제거되어도 SetWinEventHook 함수는 unmanged code이기 때문에 대응하지 못하고 오류를 뱉어내게 된다.

 

 만일 기다리기가 힘들다면 Hook 코드를 등록하고 GC작업을 진행한 후, Hook 코드가 실행 될만한 행위를 진행하면 된다. 그럼 간헐적으로 이유 없이 발생하는 것 같던 오류가 곧바로 나올 것이다.

 

해결을 위한 변수

  문제 해결을 위해선 레퍼런스 카운트를 0이 되지 않도록 만들어주면 된다. 우리가 사용하려는 GetForeGroundWindow를 가리키는 관리되는 변수를 만들어주면 된다.

 

 GetForeGroundWindow는 WinEventProc delegate이다.

public delegate void WinEventProc(
        IntPtr hWinEventHook, 
        int iEvent, 
        IntPtr hWnd, 
        int idObject, 
        int idChild, 
        int dwEventThread, 
        int dwmsEventTime
);

 따라서, 이 delegate를 타입으로 하는 변수를 SetWinEventHook 함수가 실행된 후, UnHookWinEvent 함수로 바인딩 해제될 때까지 유지되는 곳에 저장해 관리해두기만 하면 해당 버그는 해결이 가능하다.

 달리 이야기하면 WinEventProc를 자료형으로 하는 변수의 라이프사이클을 상당히 긴 곳에 두면 웬만해선 이 문제가 발생하지 않는다는 것이다.

 


Reference.

 

.NET Framework: 133. CallbackOnCollectedDelegate was detected

.NET Framework: 133. CallbackOnCollectedDelegate was detected [링크 복사], [링크+제목 복사] 조회: 17522 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 부모글 보이기/감추기 CallbackOnCollectedDelegate was

www.sysnet.pe.kr

 

# index

728x90
반응형
Comments