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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...
NoWriterDateCnt.TitleFile(s)
12935정성태1/22/20227810.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/20227373오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/20227922.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/20228373.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20226483개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/20227141개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/20226935개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
12928정성태1/18/20227066개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226840개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227335오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227412개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20228901.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227777개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20226857개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225794개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
12920정성태1/14/20226548오류 유형: 786. Camtasia - An error occurred with the camera: Failed to Add Video Sampler.
12919정성태1/13/20226379Windows: 199. Host Network Service (HNS)에 의해서 점유되는 포트
12918정성태1/13/20226616Linux: 47. WSL - shell script에서 설정한 환경 변수가 스크립트 실행 후 반영되지 않는 문제
12917정성태1/12/20225818오류 유형: 785. C# - The type or namespace name '...' could not be found (are you missing a using directive or an assembly reference?)
12916정성태1/12/20225580오류 유형: 784. TFS - One or more source control bindings for this solution are not valid and are listed below.
12915정성태1/11/20225869오류 유형: 783. Visual Studio - We didn't find any interpreters
12914정성태1/11/20227860VS.NET IDE: 172. 비주얼 스튜디오 2022의 파이선 개발 환경 지원
12913정성태1/11/20228351.NET Framework: 1133. C# - byte * (바이트 포인터)를 FileStream으로 쓰는 방법 [1]
12912정성태1/11/20228987개발 환경 구성: 623. ffmpeg.exe를 사용해 비디오 파일의 이미지를 PGM(Portable Gray Map) 파일 포맷으로 출력하는 방법 [1]
12911정성태1/11/20226277VS.NET IDE: 171. 비주얼 스튜디오 - 더 이상 만들 수 없는 "ASP.NET Core 3.1 Web Application (.NET Framework)" 프로젝트
12910정성태1/10/20226753제니퍼 .NET: 30. 제니퍼 닷넷 적용 사례 (8) - CPU high와 DB 쿼리 성능에 문제가 함께 있는 사이트
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...