C# - (핸들을 이용하여) 모든 열린 파일을 열람
아시는 것처럼, .NET Framework에서는 기본적으로 프로세스에 열려진 '핸들' 목록을 얻을 수 있는 방법이 없습니다. 사실, 저수준의 핸들이라는 개념 자체가 닷넷에서는 감춰지므로 이해가 됩니다.
대신, P/Invoke를 이용하면 C/C++에서 했던 것과 동일하게 구현을 할 수 있습니다. 마침, Codeplex에서 이와 관련된 소스 코드를 찾아냈는데요.
DetectOpenFiles.cs - CodePlex
; https://vmccontroller.svn.codeplex.com/svn/VmcController/VmcServices/DetectOpenFiles.cs
한 가지 아쉬운 점이라면, 해당 코드가 32비트에서만 동작할 뿐 64비트에서 실행하면 다음과 같은 예외를 발생시킵니다.
System.OverflowException was unhandled by user code
Message=Arithmetic operation resulted in an overflow.
Source=mscorlib
StackTrace:
at System.IntPtr.op_Explicit(IntPtr value)
at WebApplication1.DetectOpenFiles.OpenFiles.<GetEnumerator>d__0.MoveNext() in d:\...\WebApplication1\DetectOpenFiles.cs:line 273
at WebApplication1.WebForm1.Page_Load(Object sender, EventArgs e) in d:\...\WebApplication1\WebForm1.aspx.cs:line 19
at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e)
at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e)
at System.Web.UI.Control.OnLoad(EventArgs e)
at System.Web.UI.Control.LoadRecursive()
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
InnerException:
다행히 64비트 포팅이 어렵지는 않습니다. 우선, SYSTEM_HANDLE_ENTRY에서 포인터 값이 기존에는 int로 표현되어 있는 것을 IntPtr로 수정해야 합니다.
[StructLayout(LayoutKind.Sequential)]
private struct SYSTEM_HANDLE_ENTRY
{
public int OwnerPid;
public byte ObjectType;
public byte HandleFlags;
public short HandleValue;
public IntPtr ObjectPointer;
public int AccessMask;
}
그 외, handlCount를 읽어들이는 부분에서 4byte가 아닌 8byte를 읽어야 하고,
long handleCount = 0;
if (IntPtr.Size == 4)
{
handleCount = Marshal.ReadInt32(ptr);
}
else
{
handleCount = Marshal.ReadInt64(ptr);
}
포인터 처리에 대해서 "(int)ptr"와 같은 표현을 "ptr.ToInt64()"와 같이 바꿔야 합니다.
기타 결정적인 차이점이, "GetFileNameFromHandle" 메서드 안에서 발생하는데요.
private static bool GetFileNameFromHandle(IntPtr handle, out string fileName)
{
...[생략]...
NT_STATUS ret = NativeMethods.NtQueryObject(handle, OBJECT_INFORMATION_CLASS.ObjectNameInformation, ptr, length, out length);
if (ret == NT_STATUS.STATUS_BUFFER_OVERFLOW)
{
...[생략]...
ret = NativeMethods.NtQueryObject(handle, OBJECT_INFORMATION_CLASS.ObjectNameInformation, ptr, length, out length);
}
if (ret == NT_STATUS.STATUS_SUCCESS)
{
fileName = Marshal.PtrToStringUni((IntPtr)((int)ptr + 8), (length - 9) / 2);
return fileName.Length != 0;
}
...[생략]...
}
위에서 파일 이름을 가져오는 포인터 옵셋값(+8)이 64비트에서는 바뀐다는 점입니다. 정확한 위치를 알기 위해 64비트로 해당 프로그램을 실행시킨 후 ptr.ToInt64() 값을 알아내야 합니다. 디버거의 "Watch" 창을 이용하여 알아낸 값이 "0x00000000022b0930"라고 가정하고 진행했을 때, 이 값을 메모리 윈도우(Ctrl+D,Y) 창에서 확인해 봅니다. 제 경우에는 다음과 같이 나왔습니다.
보시는 것처럼, 일정 바이트 후에 "\Device\HarddiskVolume2\Windows\assembly\pubpol60.dat"라는 (유니코드) 문자열이 확인되는데, 시작 바이트가 16바이트 이후임을 계산해 낼 수 있습니다. 최종적으로 이를 반영하면 다음과 같은 코드가 나옵니다.
if (IntPtr.Size == 4)
{
fileName = Marshal.PtrToStringUni((IntPtr)((int)ptr + 8), (length - 9) / 2);
}
else
{
IntPtr namePtr = new IntPtr(ptr.ToInt64() + 16);
fileName = Marshal.PtrToStringUni(namePtr);
}
옵셋값은 테스트 결과 다행히 Windows 2003부터 Windows 8까지 전혀 변함없이 사용할 수 있었습니다. 하지만, 코드를 실행하는 권한에 따라서는 범위가 달라지므로 사용 시 이를 감안해야 합니다.
첨부한 프로젝트는 위의 코드를 테스트할 수 있는 간략한 윈폼 프로젝트입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]