Microsoft MVP성태의 닷넷 이야기
VC++: 118. Win32 HANDLE 자료형의 이모저모 [링크 복사], [링크+제목 복사]
조회: 14798
글쓴 사람
정성태 (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)
12366정성태10/12/202011300.NET Framework: 950. C# 9.0 - (4) 원시 크기 정수(Native ints) [1]파일 다운로드1
12365정성태10/12/202010289.NET Framework: 949. C# 9.0 - (3) 람다 메서드의 매개 변수 무시(Lambda discard parameters)파일 다운로드1
12364정성태10/11/202011497.NET Framework: 948. C# 9.0 - (2) localsinit 플래그 내보내기 무시(Suppress emitting localsinit flag)파일 다운로드1
12363정성태10/11/202012380.NET Framework: 947. C# 9.0 - (1) 대상으로 형식화된 new 식(Target-typed new expressions) [2]파일 다운로드1
12362정성태10/11/20209157VS.NET IDE: 151. Visual Studio 2019에 .NET 5 rc/preview 적용하는 방법
12361정성태10/11/202010750.NET Framework: 946. C# 9.0을 위한 개발 환경 구성
12360정성태10/8/20208010오류 유형: 666. The type or namespace name '...' does not exist in the namespace 'Microsoft.VisualStudio.TestTools' (are you missing an assembly reference?)
12359정성태10/7/20209531오류 유형: 665. Windows - 재부팅 후 iSCSI 연결이 끊기는 문제
12358정성태10/7/20209518오류 유형: 664. Web Deploy 설치 시 "A newer version of Microsoft Web Deploy 3.6 was found on this machine." 오류 [3]
12357정성태10/7/20207631오류 유형: 663. 이벤트 로그 - The storage optimizer couldn't complete retrim on New Volume
12356정성태10/7/202022235오류 유형: 662. ASP.NET Core와 500.19, 500.21 오류 (0x8007000d)
12355정성태10/3/20207699오류 유형: 661. Hyper-V Linux VM의 Internal 유형의 가상 Switch에 대한 IP 연결이 되지 않는 경우
12354정성태10/2/202020478오류 유형: 660. Web Deploy (msdeploy.axd) 실행 시 오류 기록 [1]
12353정성태10/2/202010308개발 환경 구성: 518. 비주얼 스튜디오에서 IIS 웹 서버로 "Web Deploy"를 이용해 배포하는 방법
12352정성태10/2/202010820개발 환경 구성: 517. Hyper-V Internal 네트워크에 NAT을 이용한 인터넷 연결 제공
12351정성태10/2/202010320오류 유형: 659. Nox 실행이 안 되는 경우 - Unable to bind to the underlying transport for ...
12350정성태9/25/202013793Windows: 175. 윈도우 환경에서 클라이언트 소켓의 최대 접속 수 [2]파일 다운로드1
12349정성태9/25/20208810Linux: 32. Ubuntu 20.04 - docker를 위한 tcp 바인딩 추가
12348정성태9/25/20209505오류 유형: 658. 리눅스 docker - Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
12347정성태9/25/202023591Windows: 174. WSL 2의 네트워크 통신 방법 [4]
12346정성태9/25/20208765오류 유형: 657. IIS - http://localhost 방문 시 Service Unavailable 503 오류 발생
12345정성태9/25/20208486오류 유형: 656. iisreset 실행 시 "Restart attempt failed." 오류가 발생하지만 웹 서비스는 정상적인 경우파일 다운로드1
12344정성태9/25/20209643Windows: 173. 서비스 관리자에 "IIS Admin Service"가 등록되어 있지 않다면?
12343정성태9/24/202019086.NET Framework: 945. C# - 닷넷 응용 프로그램에서 메모리 누수가 발생할 수 있는 패턴 [5]
12342정성태9/24/202010467디버깅 기술: 171. windbg - 인스턴스가 살아 있어 메모리 누수가 발생하고 있는지 확인하는 방법
12341정성태9/23/20209672.NET Framework: 944. C# - 인스턴스가 살아 있어 메모리 누수가 발생하고 있는지 확인하는 방법파일 다운로드1
... 46  47  48  49  [50]  51  52  53  54  55  56  57  58  59  60  ...