Microsoft MVP성태의 닷넷 이야기
VC++: 118. Win32 HANDLE 자료형의 이모저모 [링크 복사], [링크+제목 복사]
조회: 14861
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

Win32 HANDLE 자료형의 이모저모

Windows 응용 프로그램을 개발하다 보면 필연적으로 HANDLE이라는 자료형을 접하게 됩니다. 헤더 파일에 보면,

// winnt.h

typedef void *HANDLE;

PVOID 타입으로 정의되어 있어 포인터라고 생각할 수 있는데 엄밀히 "메모리의 주소"값을 가리키는 포인터는 아닙니다. 그보다는, 운영체제가 프로세스(EXE)마다 커널 주소 공간에 마련한 "핸들 테이블"의 요소(entry)를 가리키는 인덱스 값으로 HANDLE을 사용합니다.

테이블에 대한 인덱스이긴 하지만, 0번부터 시작하지는 않습니다. 왜냐하면 nullptr와 구분이 안된다는 이유로 핸들 테이블의 0번 인덱스 자리는 사용되지 않고 1번부터 값을 담고 있기 때문입니다. 게다가, 인덱스로 사용된다는 HANDLE의 숫자 값도 일반적이지 않습니다. 가령, 핸들 테이블의 1번 값을 가리키는 HANDLE은 4, 2번 값을 가리키는 HANDLE은 8, ... 로 (32비트/64비트 OS 상관없이) 4의 배수로 값이 늘어납니다. 또한, 핸들 테이블에 있는 1개의 항목은 32비트 운영체제에서 8바이트, 64비트 운영체제에서 16바이트로 이뤄집니다.

따라서, 32비트에서는 "HANDLE / 4 * 8", 64비트에서는 "HANDLE / 4 * 16" 공식으로 HANDLE 값에 대한 테이블의 entry 위치를 알 수 있습니다. 이와 같은 사실은 windbg를 통해서 직접 확인하는 것이 가능합니다.

Windbg - 윈도우 핸들 테이블
; https://www.sysnet.pe.kr/2/0/935




열릴 수 있는 핸들의 최대 수치에 대해서는 다음의 글에 잘 나와 있습니다.

Pushing the Limits of Windows: Handles
; https://blogs.technet.microsoft.com/markrussinovich/2009/09/29/pushing-the-limits-of-windows-handles/

윈도우 실행부(Executive)는 이 값을 32비트/64비트 상관없이 "16,777,216 (16*1024*1024)"로 정의한다고 합니다. 그런데 여기서 한 가지 궁금한 점이 생깁니다. 16,777,216이면 4의 배수로 늘어나는 것을 감안해도 최대 67,108,864이기 때문에 int 영역으로 충분히 표현할 수 있습니다. 그런데 왜? 플랫폼에 따라 32비트/64비트로 바뀌는 "void *"로 HANDLE 타입을 정했냐는 것입니다. 즉, 64비트에서 굳이 8바이트로 표현할 일이 없기 때문에 HANDLE을 단순히 int 범위로 한정했어도 충분했으며, 실제로도 다음과 같이 밝히고 있습니다.

Interprocess Communication Between 32-bit and 64-bit Applications
; https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication

64-bit versions of Windows use 32-bit handles for interoperability. When sharing a handle between 32-bit and 64-bit applications, only the lower 32 bits are significant, so it is safe to truncate the handle (when passing it from 64-bit to 32-bit) or sign-extend the handle (when passing it from 32-bit to 64-bit). Handles that can be shared include handles to user objects such as windows (HWND), handles to GDI objects such as pens and brushes (HBRUSH and HPEN), and handles to named objects such as mutexes, semaphores, and file handles.


결국, 64비트의 핸들 값도 상위 8바이트는 사용하고 있지 않기 때문에 32비트로 형 변환해서 사용해도 무방하다는 것입니다. (덕분에 하위 호환성이 지켜진 것은 덤이고!)

어쨌든 핸들의 총 개수가 16,777,216이기 때문에 프로세스(EXE)마다 할당 가능한 핸들 테이블의 최대 용량도 계산할 수 있습니다.

[32비트]
16,777,216 * 8바이트 = 134,217,728 (128MB)

[64비트]
16,777,216 * 16바이트 == 268,435,456 (256MB)




그런데, 궁금한 점이 하나 또 생깁니다. 왜 하필 핸들 값은 4의 배수로 늘어나는 것일까요?

아마도 이것은 초창기 윈도우 시절로 거슬러 올라가야만 답변할 수 있을 것 같습니다.

How are window manager handles determined in 16-bit Windows and Windows 95?
; https://devblogs.microsoft.com/oldnewthing/20070716-00/?p=26003

저 시절(Windows 95/98/ME)에는 열릴 수 있는 핸들의 최대 수가 16,384개였으며 한 개의 entry에 소요되는 용량은 4바이트여서 핸들 테이블의 최대 용량이 65,536(64KB)로 딱 한 개의 data segment로 담을 수 있는 값입니다. 그리고 다음의 글을 보면,

The heap manager did this by pre-allocating a 64KB block of memory and allocating its handles out of that memory block, using the offset into the block as the handle.


이때까지만 해도 (void *) HANDLE에 담긴 값이 정확히 핸들 테이블의 주소(세그먼트 레지스터를 기준으로 오프셋 포인터)로 사용되었고, 이로 인해 4의 배수로 핸들 값이 설정된 것입니다.

하지만, Windows NT 계열로 오면서 한 개의 핸들에 대한 entry 용량이 8바이트로 늘어났는데 이번에도 역시 하위 호환성을 지키기 위해 핸들의 값은 4의 배수로 동일하게 유지하면서 용도를 (포인터가 아닌) 테이블에 대한 인덱스를 가리키는 값으로 바뀌었다고 추측됩니다. 마이크로소프트의 자료에 따르면,

Why are kernel HANDLEs always a multiple of four?
; https://devblogs.microsoft.com/oldnewthing/20050121-00/?p=36633

4의 배수가 된 이유를 OBJ_HANDLE_TAGBITS 마스크 값이 있다는 정도로 설명하고 있습니다.

// Low order two bits of a handle are ignored by the system and available
// for use by application code as tag bits.  The remaining bits are opaque
// and used to store a serial number and table index.

#define OBJ_HANDLE_TAGBITS  0x00000003L

That at least the bottom bit of kernel HANDLEs is always zero is implied by the GetQueuedCompletionStatus function, which indicates that you can set the bottom bit of the event handle to suppress completion port notification. In order for this to work, the bottom bit must normally be zero.


하지만 GetQueuedCompletionStatus의 최소 지원 운영체제가,

GetQueuedCompletionStatus function
; https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus

CreateIoCompletionPort function
; https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-createiocompletionport

Windows XP/2003인 걸로 봐서는 그전부터 유지되었던 4의 배수 규칙을 설명하기는 어렵습니다. 오히려, 하위 호환성을 위해 HANDLE 값을 4의 배수로 인덱스 개념을 적용할 수밖에 없었는데 우연찮게 IOCP를 구현하면서 쓸모없었던 하위 2비트의 값을 재활용할 수 있었던 것이 아닌가 싶습니다.

여기서 재미있는 사실이 하나 있는데, 운영체제는 하위 2비트의 값을 무시한다고 하므로 다음과 같이 코딩을 해도 WriteFile 및 CloseHandle은 정상적으로 동작한다는 것입니다.

#include "stdafx.h"
#include <windows.h>

int main()
{
    HANDLE hFile = CreateFile(L"test.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                    FILE_ATTRIBUTE_NORMAL, nullptr);
    
    if (hFile == INVALID_HANDLE_VALUE)
    {
        return 0;
    }

    hFile = (HANDLE)((int)hFile + 3); // 하위 2비트에 값을 설정

    char buf[20] = "Test is good.";
    DWORD dwWritten = 0;

    WriteFile(hFile, buf, strlen(buf), &dwWritten, nullptr); // 정상적으로 Write되고,

    CloseHandle(hFile); // 정상적으로 Close
    
    return 0;
}




마지막으로, "핸들"이라는 단어가 프로그래밍에서 워낙 일반적으로 사용되고 있기 때문에 주의할 점이 있습니다. 즉, "핸들"이라는 단어가 쓰였다고 해서 이 글의 설명이 적용되는 것은 아닙니다. 이 글에서 설명하는 핸들은 특별히 "커널 핸들"로 제한되는데, CloseHandle Win32 API로 닫을 수 있는 객체라고 보면 됩니다.

반면, 예를 들어 HTREEITEM의 경우 "handle of the tree item"으로,

struct _TREEITEM;
typedef struct _TREEITEM *HTREEITEM;

역시 "핸들"이라고 여겨질 수 있는데 이들은 "커널 핸들"이 아니기 때문에 이 글에서 설명한 내용이 적용되지 않습니다.




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







[최초 등록일: ]
[최종 수정일: 10/25/2023]

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

비밀번호

댓글 작성자
 




... [46]  47  48  49  50  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12476정성태1/5/202110282.NET Framework: 996. C# - 닷넷 코어에서 다른 스레드의 callstack을 구하는 방법파일 다운로드1
12475정성태1/5/202112863.NET Framework: 995. C# - Span<T>와 Memory<T> [1]파일 다운로드1
12474정성태1/4/202110395.NET Framework: 994. C# - (.NET Core 2.2부터 가능한) 프로세스 내부에서 CLR ETW 이벤트 수신 [1]파일 다운로드1
12473정성태1/4/20219208.NET Framework: 993. .NET 런타임에 따라 달라지는 정적 필드의 초기화 유무 [1]파일 다운로드1
12472정성태1/3/20219483디버깅 기술: 178. windbg - 디버그 시작 시 스크립트 실행
12471정성태1/1/20219977.NET Framework: 992. C# - .NET Core 3.0 이상부터 제공하는 runtimeOptions의 rollForward 옵션 [1]
12470정성태12/30/202010136.NET Framework: 991. .NET 5 응용 프로그램에서 WinRT API 호출 [1]파일 다운로드1
12469정성태12/30/202013725.NET Framework: 990. C# - SendInput Win32 API를 이용한 가상 키보드/마우스 [1]파일 다운로드1
12468정성태12/30/202010341Windows: 186. CMD Shell의 "Defaults"와 "Properties"에서 폰트 정보가 다른 문제 [1]
12467정성태12/29/202010242.NET Framework: 989. HttpContextAccessor를 통해 이해하는 AsyncLocal<T> [1]파일 다운로드1
12466정성태12/29/20208237.NET Framework: 988. C# - 지연 실행이 꼭 필요한 상황이 아니라면 singleton 패턴에서 DCLP보다는 static 초기화를 권장파일 다운로드1
12465정성태12/29/202011371.NET Framework: 987. .NET Profiler - FunctionID와 연관된 ClassID를 구할 수 없는 문제
12464정성태12/29/202010213.NET Framework: 986. pptfont.exe - PPT 파일에 숨겨진 폰트 설정을 일괄 삭제
12463정성태12/29/20209262개발 환경 구성: 520. RDP(mstsc.exe)의 다중 모니터 옵션 /multimon, /span
12462정성태12/27/202010892디버깅 기술: 177. windbg - (ASP.NET 환경에서 유용한) netext 확장
12461정성태12/21/202011716.NET Framework: 985. .NET 코드 리뷰 팁 [3]
12460정성태12/18/20209417기타: 78. 도서 소개 - C#으로 배우는 암호학
12459정성태12/16/20209818Linux: 35. C# - 리눅스 환경에서 클라이언트 소켓의 ephemeral port 재사용파일 다운로드1
12458정성태12/16/20209310오류 유형: 694. C# - Task.Start 메서드 호출 시 "System.InvalidOperationException: 'Start may not be called on a task that has completed.'" 예외 발생 [1]
12457정성태12/15/20208869Windows: 185. C# - Windows 10/2019부터 추가된 SIO_TCP_INFO파일 다운로드1
12456정성태12/15/20209143VS.NET IDE: 156. Visual Studio - "Migrate packages.config to PackageReference"
12455정성태12/15/20208665오류 유형: 693. DLL 로딩 시 0x800704ec - This Program is Blocked by Group Policy
12454정성태12/15/20209231Windows: 184. Windows - AppLocker의 "DLL Rules"를 이용해 임의 경로에 설치한 DLL의 로딩을 막는 방법 [1]
12453정성태12/14/202010215.NET Framework: 984. C# - bool / BOOL / VARIANT_BOOL에 대한 Interop [1]파일 다운로드1
12452정성태12/14/202010419Windows: 183. 설정은 가능하지만 구할 수는 없는 TcpTimedWaitDelay 값
12451정성태12/14/20209589Windows: 182. WMI Namespace를 열거하고, 그 안에 정의된 클래스를 열거하는 방법 [5]
... [46]  47  48  49  50  51  52  53  54  55  56  57  58  59  60  ...