Microsoft MVP성태의 닷넷 이야기
VC++: 118. Win32 HANDLE 자료형의 이모저모 [링크 복사], [링크+제목 복사]
조회: 14800
글쓴 사람
정성태 (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)
12593정성태4/10/20219285개발 환경 구성: 567. Docker Desktop for Windows - kubectl proxy 없이 k8s 대시보드 접근 방법
12592정성태4/10/20219102개발 환경 구성: 566. Docker Desktop for Windows - k8s dashboard의 Kubeconfig 로그인 및 Skip 방법
12591정성태4/9/202112310.NET Framework: 1034. C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법 [2]파일 다운로드1
12590정성태4/9/20218869.NET Framework: 1033. C# - .NET 4.0 이하에서 Console.IsInputRedirected 구현 [1]
12589정성태4/8/202110171.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법 [1]
12588정성태4/7/202110713개발 환경 구성: 565. PowerShell - New-SelfSignedCertificate를 사용해 CA 인증서 생성 및 인증서 서명 방법
12587정성태4/6/202111403개발 환경 구성: 564. Windows 10 - ClickOnce 배포처럼 사용할 수 있는 MSIX 설치 파일 [1]
12586정성태4/5/20219197오류 유형: 710. Windows - Restart-Computer / shutdown 명령어 수행 시 Access is denied(E_ACCESSDENIED)
12585정성태4/5/20218952개발 환경 구성: 563. 기본 생성된 kubeconfig 파일의 내용을 새롭게 생성한 인증서로 구성하는 방법
12584정성태4/1/20219664개발 환경 구성: 562. kubeconfig 파일 없이 kubectl 옵션만으로 실행하는 방법
12583정성태3/29/202111195개발 환경 구성: 561. kubectl 수행 시 다른 k8s 클러스터로 접속하는 방법
12582정성태3/29/20219854오류 유형: 709. Visual C++ - 컴파일 에러 error C2059: syntax error: '__stdcall'
12581정성태3/28/20219788.NET Framework: 1031. WinForm/WPF에서 Console 창을 띄워 출력하는 방법 (2) - Output 디버깅 출력을 AllocConsole로 우회 [2]
12580정성태3/28/20218582오류 유형: 708. SQL Server Management Studio - Execution Timeout Expired.
12579정성태3/28/20218595오류 유형: 707. 중첩 가상화(Nested Virtualization) - The virtual machine could not be started because this platform does not support nested virtualization.
12578정성태3/27/20218920개발 환경 구성: 560. Docker Desktop for Windows 기반의 Kubernetes 구성 (2) - WSL 2 인스턴스에 kind가 구성한 k8s 서비스 위치
12577정성태3/26/202110958개발 환경 구성: 559. Docker Desktop for Windows 기반의 Kubernetes 구성 - WSL 2 인스턴스에 kind 도구로 k8s 클러스터 구성
12576정성태3/25/20218756개발 환경 구성: 558. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성 (2) - k8s 서비스 위치
12575정성태3/24/20217896개발 환경 구성: 557. Docker Desktop for Windows에서 DockerDesktopVM 기반의 Kubernetes 구성
12574정성태3/23/202111761.NET Framework: 1030. C# Socket의 Close/Shutdown 동작 (동기 모드)
12573정성태3/22/20219641개발 환경 구성: 556. WSL 인스턴스 초기 설정 명령어 [1]
12572정성태3/22/20219174.NET Framework: 1029. C# - GC 호출로 인한 메모리 압축(Compaction)을 확인하는 방법파일 다운로드1
12571정성태3/21/20218340오류 유형: 706. WSL 2 기반으로 "Enable Kubernetes" 활성화 시 초기화 실패 [1]
12570정성태3/19/202112657개발 환경 구성: 555. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법
12569정성태3/18/202111529개발 환경 구성: 554. WSL 인스턴스 export/import 방법 및 단축 아이콘 설정 방법
12568정성태3/18/20217249오류 유형: 705. C# 빌드 - Couldn't process file ... due to its being in the Internet or Restricted zone or having the mark of the web on the file.
... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...