Microsoft MVP성태의 닷넷 이야기
VC++: 118. Win32 HANDLE 자료형의 이모저모 [링크 복사], [링크+제목 복사]
조회: 14825
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12721정성태7/20/20217645오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
12720정성태7/19/20216980Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/20216138오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
12718정성태7/19/20216755개발 환경 구성: 581. Windows에서 WSL로 파일 복사 시 root 소유권으로 적용되는 문제파일 다운로드1
12717정성태7/18/20216730Windows: 195. robocopy에서 파일의 ADS(Alternate Data Stream) 정보 복사를 제외하는 방법
12716정성태7/17/20217525개발 환경 구성: 580. msbuild의 Exec Task에 robocopy를 사용하는 방법파일 다운로드1
12715정성태7/17/20219242오류 유형: 736. Windows - MySQL zip 파일 버전의 "mysqld --skip-grant-tables" 실행 시 비정상 종료 [1]
12714정성태7/16/20217948오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/20218502.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/20217902개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/20218244개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202110018개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/20216683Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202110039Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202177254오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/20218562.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/20218681VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/20217831개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202113468개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/20218593오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218253.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20218639.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217465VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111446오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217335개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20217887VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...