C#에서 만든 COM 객체를 C/C++로 P/Invoke Interop 시 메모리 누수(Memory Leak) 발생
닷넷 객체를 COM 방식으로 접근할 수 있도록 다음과 같이 만들 수 있습니다.
[ComVisible(true)]
[Guid("34CF251B-EAED-428D-9686-C5A5711D3A3E")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMyUnk
{
void Test();
}
[ComVisible(true)]
[Guid("c9f685ea-3388-3ff5-958a-3234d08587c1")]
[ClassInterface(ClassInterfaceType.None)]
public class MyUnkObject : IMyUnk
{
public void Test()
{
// Console.WriteLine("Call: Test()");
}
}
그럼 C++에서 이런 식으로 함수를 정의하면,
extern "C"
{
interface IMyUnk : IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Test() = 0;
};
__declspec(dllexport) void __cdecl UseIUnk(IUnknown *pUnk, bool doRelease)
{
if (pUnk == nullptr)
{
return;
}
IID iid;
IIDFromString(L"{34CF251B-EAED-428D-9686-C5A5711D3A3E}", &iid);
IMyUnk* myPtr = nullptr;
if (pUnk->QueryInterface(iid, (LPVOID*)&myPtr) != S_OK)
{
return;
}
myPtr->Test();
myPtr->Release();
if (doRelease == true)
{
pUnk->Release();
}
}
}
C#에서 C++로 MyObject 객체를 다음과 같이 전달해 사용할 수 있습니다.
using DetourFunc.ClrType;
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
class Program
{
[DllImport("IUnkSample.dll")]
internal static unsafe extern void UseIUnk([MarshalAs(UnmanagedType.Interface)] object unk, bool doRelease);
static void Main(string[] args)
{
MyObject obj = new MyObject();
UseIUnk(obj, true);
}
}
}
문제는 저렇게 interop을 하는 경우 메모리 누수가 발생한다는 겁니다. 실제로 아래와 같이 실행해 보면,
static void Main(string[] args)
{
int i = 0;
while (true)
{
TestHandleLeak();
i++;
if (i > 1000)
{
GC.Collect();
GC.Collect();
i = 0;
}
}
}
private unsafe static void TestHandleLeak()
{
MyUnkObject obj = new MyUnkObject();
UseIUnk(obj, true);
}
메모리가 증가하는 것을 확인할 수 있습니다. 얼핏 보면 이번에도 이전에 설명했던 것들과 비슷한 상황인 듯한데,
C# - Marshal.GetNativeVariantForObject 사용 시 메모리 누수(Memory Leak) 발생 및 해결 방법
; https://www.sysnet.pe.kr/2/0/12157
C# - Marshal.GetIUnknownForObject/GetIDispatchForObject 사용 시 메모리 누수(Memory Leak) 발생
; https://www.sysnet.pe.kr/2/0/12158
근본적인 원인은 다른 것 같습니다. 왜냐하면, !gchandles의 "Ref Count Handles"가 증가하지 않고 순수하게 닷넷 객체가 해제되지 않아 쌓이기 때문입니다. 게다가 C++ 측에서 VARIANT로 받아 VariantClear를 호출하는 식의 방법도,
// C++
__declspec(dllexport) void __cdecl UseObjectToVariant(VARIANT variant)
{
VariantClear(&variant);
}
// C#
[DllImport("IUnkSample.dll")]
internal static unsafe extern void UseObjectToVariant(object variant);
private unsafe static void TestHandleLeak2()
{
// object obj = new object();
MyUnkObject obj = new MyUnkObject();
UseObjectToVariant(obj);
// UseObjectToVariant(5.5); // VARIANT의 데이터 필드에 저장할 수 있는 데이터는 메모리 누수가 없음
}
이번에는 해결책이 되지 못했습니다.
문제 유형은 다르지만, 그래도 해결 방식은 "
C# - Marshal.GetIUnknownForObject/GetIDispatchForObject 사용 시 메모리 누수(Memory Leak) 발생" 글과 마찬가지로 다룰 수 있습니다.
해당 글에서 만든 (ObjectInterface 타입을 이름 변경한) VariantMarshaller 타입을 정리해 DetourFunc 라이브러리에 넣어 두었으니 NuGet을 통해,
Install-Package DetourFunc -Version 1.1.1
// 소스 코드: github - https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc
참조 추가하면 다음과 같이 사용할 수 있습니다.
[DllImport("IUnkSample.dll")]
internal static unsafe extern void UseIntPtrToVariant(IntPtr variant);
private unsafe static void TestHandleWithoutLeak()
{
MyUnkObject obj = new MyUnkObject();
using (VariantMarshaller oi = new VariantMarshaller(obj))
{
UseIntPtrToVariant(oi.Variant);
}
}
그리고 대응하는 C++ 함수에서는 그냥 사용만 하면 됩니다. (이후 실행해 확인하면 메모리 누수가 발생하지 않습니다.)
__declspec(dllexport) void __cdecl UseIntPtrToVariant(VARIANT *variant)
{
// variant->punkVal // (IUnknown *)
}
그런데, 이런 식의 메모리 누수가 현실적으로 문제로 인식되는 경우는 많지 않습니다. 왜냐하면, .NET 객체를 COM 인터페이스로 Native에 넘기는 경우도 많지 않을 뿐더러, 그렇다고 해도 웬만큼 반복하지 않는 한 메모리 누수가 크게 부각되기까지는 꽤나 시간이 걸릴 것이기 때문입니다.
게다가 그 문제를 완화시키는 또 다른 사유로는, 이런 문제가 (Managed COM 객체가 아닌) Native COM 객체를 pinvoke로 전달할 때는 발생하지 않는다는 점입니다. 예를 들어, 아래의 코드는 메모리 누수가 발생하지 않습니다.
private unsafe static void TestHandleWithXmlDoc()
{
Type type = Type.GetTypeFromProgID("Microsoft.XMLDOM");
object xmlDoc = Activator.CreateInstance(type); // native COM 객체 생성
UseIUnk(xmlDoc, false);
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
참고로, pinvoke에서 object 인자의 마샬링 과정을 살펴보면 최초 UseIUnk 호출 시,
C:\ConsoleApp1\ConsoleApp1\Program.cs @ 47:
00007ffc`7cf40985 488b4df0 mov rcx,qword ptr [rbp-10h]
00007ffc`7cf40989 e802fbffff call 00007ffc`7cf40490 (ConsoleApp1.Program.UseIUnk(System.Object), mdToken: 0000000006000001)
00007ffc`7cf4098e 90 nop
마샬링을 위한
코드가 JIT 컴파일되는 과정을 동일하게 거쳐,
00007ffc`7cf40490 49ba005ae37cfc7f0000 mov r10,7FFC7CE35A00h
00007ffc`7cf4049a 40e96043545f jmp clr!ThePreStub (00007ffc`dc484800)
clr!ThePreStub:
00007ffc`dc484800 4157 push r15
00007ffc`dc484802 4156 push r14
00007ffc`dc484804 4155 push r13
00007ffc`dc484806 4154 push r12
00007ffc`dc484808 55 push rbp
00007ffc`dc484809 53 push rbx
00007ffc`dc48480a 56 push rsi
00007ffc`dc48480b 57 push rdi
00007ffc`dc48480c 4883ec68 sub rsp,68h
00007ffc`dc484810 48898c24b0000000 mov qword ptr [rsp+0B0h],rcx
00007ffc`dc484818 48899424b8000000 mov qword ptr [rsp+0B8h],rdx
00007ffc`dc484820 4c898424c0000000 mov qword ptr [rsp+0C0h],r8
00007ffc`dc484828 4c898c24c8000000 mov qword ptr [rsp+0C8h],r9
00007ffc`dc484830 660f7f442420 movdqa xmmword ptr [rsp+20h],xmm0
00007ffc`dc484836 660f7f4c2430 movdqa xmmword ptr [rsp+30h],xmm1
00007ffc`dc48483c 660f7f542440 movdqa xmmword ptr [rsp+40h],xmm2
00007ffc`dc484842 660f7f5c2450 movdqa xmmword ptr [rsp+50h],xmm3
00007ffc`dc484848 488d4c2468 lea rcx,[rsp+68h]
00007ffc`dc48484d 498bd2 mov rdx,r10
00007ffc`dc484850 e8eba30000 call clr!PreStubWorker (00007ffc`dc48ec40)
00007ffc`dc484855 660f6f442420 movdqa xmm0,xmmword ptr [rsp+20h]
00007ffc`dc48485b 660f6f4c2430 movdqa xmm1,xmmword ptr [rsp+30h]
00007ffc`dc484861 660f6f542440 movdqa xmm2,xmmword ptr [rsp+40h]
00007ffc`dc484867 660f6f5c2450 movdqa xmm3,xmmword ptr [rsp+50h]
00007ffc`dc48486d 488b8c24b0000000 mov rcx,qword ptr [rsp+0B0h]
00007ffc`dc484875 488b9424b8000000 mov rdx,qword ptr [rsp+0B8h]
00007ffc`dc48487d 4c8b8424c0000000 mov r8,qword ptr [rsp+0C0h]
00007ffc`dc484885 4c8b8c24c8000000 mov r9,qword ptr [rsp+0C8h]
00007ffc`dc48488d 4883c468 add rsp,68h
00007ffc`dc484891 5f pop rdi
00007ffc`dc484892 5e pop rsi
00007ffc`dc484893 5b pop rbx
00007ffc`dc484894 5d pop rbp
00007ffc`dc484895 415c pop r12
00007ffc`dc484897 415d pop r13
00007ffc`dc484899 415e pop r14
00007ffc`dc48489b 415f pop r15
00007ffc`dc48489d 48ffe0 jmp rax // JIT 컴파일 시켜 jmp 코드로 바뀐 원래의 호출 위치로 다시 JMP (rax == 00007ffc`7cf40490)
00007ffc`7cf40490 49ba005ae37cfc7f0000 mov r10,7FFC7CE35A00h
00007ffc`7cf4049a 40e910050000 jmp 00007ffc`7cf409b0 // Jit된 대상 메서드로 JMP
PInvoke 층의 메서드 내에서 InterfaceMarshaler__ConvertToNative 함수를 통해 "object" 인자를 native로 전달하기 위한 마샬링 작업을 수행합니다.
00007ffc`7cf409b0 55 push rbp
00007ffc`7cf409b1 4157 push r15
00007ffc`7cf409b3 4156 push r14
00007ffc`7cf409b5 4155 push r13
00007ffc`7cf409b7 4154 push r12
00007ffc`7cf409b9 57 push rdi
00007ffc`7cf409ba 56 push rsi
00007ffc`7cf409bb 53 push rbx
00007ffc`7cf409bc 4881ec98000000 sub rsp,98h
00007ffc`7cf409c3 488dac24d0000000 lea rbp,[rsp+0D0h]
00007ffc`7cf409cb 4c8955a0 mov qword ptr [rbp-60h],r10
00007ffc`7cf409cf 4889a550ffffff mov qword ptr [rbp-0B0h],rsp
00007ffc`7cf409d6 48894d10 mov qword ptr [rbp+10h],rcx
00007ffc`7cf409da 488d8d68ffffff lea rcx,[rbp-98h]
00007ffc`7cf409e1 498bd2 mov rdx,r10
00007ffc`7cf409e4 e85744545f call clr!JIT_InitPInvokeFrame (00007ffc`dc484e40)
00007ffc`7cf409e9 488945a8 mov qword ptr [rbp-58h],rax
00007ffc`7cf409ed 488bcc mov rcx,rsp
00007ffc`7cf409f0 48894d88 mov qword ptr [rbp-78h],rcx
00007ffc`7cf409f4 488bcd mov rcx,rbp
00007ffc`7cf409f7 48894d98 mov qword ptr [rbp-68h],rcx
00007ffc`7cf409fb 488b4da8 mov rcx,qword ptr [rbp-58h]
00007ffc`7cf409ff 488d8568ffffff lea rax,[rbp-98h]
00007ffc`7cf40a06 48894110 mov qword ptr [rcx+10h],rax
00007ffc`7cf40a0a 488b4da0 mov rcx,qword ptr [rbp-60h]
00007ffc`7cf40a0e e87d06685f call clr!StubHelpers::DemandPermission (00007ffc`dc5c1090)
00007ffc`7cf40a13 4533c0 xor r8d,r8d
00007ffc`7cf40a16 448945c4 mov dword ptr [rbp-3Ch],r8d
00007ffc`7cf40a1a 90 nop
00007ffc`7cf40a1b 4533c0 xor r8d,r8d
00007ffc`7cf40a1e 4d63c0 movsxd r8,r8d
00007ffc`7cf40a21 33d2 xor edx,edx
00007ffc`7cf40a23 4863d2 movsxd rdx,edx
00007ffc`7cf40a26 488b4d10 mov rcx,qword ptr [rbp+10h]
00007ffc`7cf40a2a 41b910000000 mov r9d,10h
00007ffc`7cf40a30 e85b8c625f call clr!StubHelpers::InterfaceMarshaler__ConvertToNative (00007ffc`dc569690)
00007ffc`7cf40a35 488945b8 mov qword ptr [rbp-48h],rax
00007ffc`7cf40a39 c745c401000000 mov dword ptr [rbp-3Ch],1
00007ffc`7cf40a40 90 nop
00007ffc`7cf40a41 488b4da0 mov rcx,qword ptr [rbp-60h]
00007ffc`7cf40a45 41bb20000000 mov r11d,20h
...[생략]...
InterfaceMarshaler__ConvertToNative 호출 시점의 rcx에는 object 인자의 GC Heap 주소가 담겨 있습니다. InterfaceMarshaler__ConvertToNative 함수가 기존의
Marshal.GetIUnknownForObject,
Marshal.GetNativeVariantForObject와 다른 점은 내부에서 "clr!MarshalObjectToInterface" 함수를 부른다는 정도입니다.
clr!StubHelpers::InterfaceMarshaler__ConvertToNative:
00007ffc`dc569690 44894c2420 mov dword ptr [rsp+20h],r9d ss:00000000`003ee7b8=02502df0
00007ffc`dc569695 4c89442418 mov qword ptr [rsp+18h],r8
00007ffc`dc56969a 4889542410 mov qword ptr [rsp+10h],rdx
00007ffc`dc56969f 53 push rbx
00007ffc`dc5696a0 56 push rsi
00007ffc`dc5696a1 57 push rdi
00007ffc`dc5696a2 4154 push r12
00007ffc`dc5696a4 4155 push r13
00007ffc`dc5696a6 4156 push r14
00007ffc`dc5696a8 4157 push r15
00007ffc`dc5696aa 4881ec60010000 sub rsp,160h
00007ffc`dc5696b1 48c7442458feffffff mov qword ptr [rsp+58h],0FFFFFFFFFFFFFFFEh
00007ffc`dc5696ba 458bf1 mov r14d,r9d
00007ffc`dc5696bd 4d8bf8 mov r15,r8
00007ffc`dc5696c0 4c8be2 mov r12,rdx
00007ffc`dc5696c3 488d35c6ffffff lea rsi,[clr!StubHelpers::InterfaceMarshaler__ConvertToNative (00007ffc`dc569690)]
00007ffc`dc5696ca 4889742438 mov qword ptr [rsp+38h],rsi
00007ffc`dc5696cf 4885c9 test rcx,rcx
00007ffc`dc5696d2 0f84d0000000 je clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x118 (00007ffc`dc5697a8)
00007ffc`dc5696d8 488364243000 and qword ptr [rsp+30h],0
00007ffc`dc5696de 48894c2428 mov qword ptr [rsp+28h],rcx
00007ffc`dc5696e3 c784249000000040000000 mov dword ptr [rsp+90h],40h
00007ffc`dc5696ee 4889b424a0000000 mov qword ptr [rsp+0A0h],rsi
00007ffc`dc5696f6 488d05fb886e00 lea rax,[clr!HelperMethodFrame_1OBJ::`vftable' (00007ffc`dcc51ff8)]
00007ffc`dc5696fd 4889442478 mov qword ptr [rsp+78h],rax
00007ffc`dc569702 488d442428 lea rax,[rsp+28h]
00007ffc`dc569707 4889842450010000 mov qword ptr [rsp+150h],rax
00007ffc`dc56970f 488d8c24a8000000 lea rcx,[rsp+0A8h]
00007ffc`dc569717 e8d4b7f1ff call clr!LazyMachStateCaptureState (00007ffc`dc484ef0)
00007ffc`dc56971c 488d4c2478 lea rcx,[rsp+78h]
00007ffc`dc569721 e80ab8f1ff call clr!HelperMethodFrame::Push (00007ffc`dc484f30)
00007ffc`dc569726 488b8c2498000000 mov rcx,qword ptr [rsp+98h]
00007ffc`dc56972e 33ff xor edi,edi
00007ffc`dc569730 4532ed xor r13b,r13b
00007ffc`dc569733 8a05ff989200 mov al,byte ptr [clr!g_StackProbingEnabled (00007ffc`dce93038)]
00007ffc`dc569739 84c0 test al,al
00007ffc`dc56973b 0f851b3e3000 jne clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x303ecc (00007ffc`dc86d55c)
00007ffc`dc569741 458bce mov r9d,r14d
00007ffc`dc569744 4d8bc7 mov r8,r15
00007ffc`dc569747 498bd4 mov rdx,r12
00007ffc`dc56974a 488d4c2428 lea rcx,[rsp+28h]
00007ffc`dc56974f e864000000 call clr!MarshalObjectToInterface (00007ffc`dc5697b8)
00007ffc`dc569754 488bd8 mov rbx,rax
00007ffc`dc569757 4889442430 mov qword ptr [rsp+30h],rax
00007ffc`dc56975c c644244800 mov byte ptr [rsp+48h],0
00007ffc`dc569761 803dd098920000 cmp byte ptr [clr!g_StackProbingEnabled (00007ffc`dce93038)],0
00007ffc`dc569768 0f85123e3000 jne clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x303ef0 (00007ffc`dc86d580)
00007ffc`dc56976e 4584ed test r13b,r13b
00007ffc`dc569771 7539 jne clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x11c (00007ffc`dc5697ac)
00007ffc`dc569773 488d4c2478 lea rcx,[rsp+78h]
00007ffc`dc569778 e8f3b7f1ff call clr!HelperMethodFrame::Pop (00007ffc`dc484f70)
00007ffc`dc56977d 488d8c24a8000000 lea rcx,[rsp+0A8h]
00007ffc`dc569785 e846acf1ff call clr!HelperMethodFrameRestoreState (00007ffc`dc4843d0)
00007ffc`dc56978a 85c0 test eax,eax
00007ffc`dc56978c 0f8551ffffff jne clr!StubHelpers::InterfaceMarshaler__ConvertToNative+0x53 (00007ffc`dc5696e3)
00007ffc`dc569792 488bc3 mov rax,rbx
00007ffc`dc569795 4881c460010000 add rsp,160h
00007ffc`dc56979c 415f pop r15
00007ffc`dc56979e 415e pop r14
00007ffc`dc5697a0 415d pop r13
00007ffc`dc5697a2 415c pop r12
00007ffc`dc5697a4 5f pop rdi
00007ffc`dc5697a5 5e pop rsi
00007ffc`dc5697a6 5b pop rbx
00007ffc`dc5697a7 c3 ret
실제로 rcx에 object의 [주소를 가리키는 주솟값]을 담아 MarshalObjectToInterface 호출하고 반환받은 rax값을 살펴보면,
00007ffc`dc56974f e864000000 call clr!MarshalObjectToInterface (00007ffc`dc5697b8)
// 결괏값: rax == 00000000`007c0018
0:000> dq @rax L1
00000000`007c0018 0000000000960938 // IDispatch
IDispatch 인터페이스를 가리키는 포인터가 담겨 있음을 알 수 있습니다.
0:000> dq 0000000000960938 L7
00000000`00960938 00007ffcdc5692f0 00007ffcdc4d7920 00007ffcdc568e50 00007ffcdc9dc1a0
00000000`00960958 00007ffcdc9dc370 00007ffcdc9dbf70 00007ffcdc9dc5b0
00007ffcdc5692f0부터 00007ffcdc9dc5b0까지의 7개 주소는 IDispatch의 정의 따라 각각 다음의 함수 진입점을 가리키고 있습니다.
clr!Unknown_QueryInterface:
clr!Unknown_AddRef:
clr!Unknown_Release:
clr!Dispatch_GetTypeInfoCount_Wrapper:
clr!Dispatch_GetTypeInfo_Wrapper:
clr!Dispatch_GetIDsOfNames_Wrapper:
clr!Dispatch_Invoke_Wrapper:
이는 IUnknown/IDispatch에서 정의한 함수의 순서와 정확히 일치합니다.
MIDL_INTERFACE("00000000-0000-0000-C000-000000000046")
IUnknown
{
public:
BEGIN_INTERFACE
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
END_INTERFACE
};
MIDL_INTERFACE("00020400-0000-0000-C000-000000000046")
IDispatch : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
/* [out] */ __RPC__out UINT *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/* [in] */ __RPC__in REFIID riid,
/* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
/* [range][in] */ __RPC__in_range(0,16384) UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
/* [annotation][in] */
_In_ DISPID dispIdMember,
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][in] */
_In_ LCID lcid,
/* [annotation][in] */
_In_ WORD wFlags,
/* [annotation][out][in] */
_In_ DISPPARAMS *pDispParams,
/* [annotation][out] */
_Out_opt_ VARIANT *pVarResult,
/* [annotation][out] */
_Out_opt_ EXCEPINFO *pExcepInfo,
/* [annotation][out] */
_Out_opt_ UINT *puArgErr) = 0;
};
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]