Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
이번에는 다음의 글로 시작하는 시리즈 6개를 제 맘대로 정리해 봤습니다. ^^
The evolution of dialog templates – Introduction
; https://devblogs.microsoft.com/oldnewthing/20040617-00/?p=38833
비주얼 스튜디오로 기본 생성한 C/C++ 윈도우 프로젝트를 보면 텍스트 형식의 RC 파일이 하나 들어 있고, 그 안에 IDD_ABOUTBOX 대화창으로 식별되는 텍스트가 추가돼 있습니다.
IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Project1"
FONT 8, "MS Shell Dlg"
BEGIN
ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20
LTEXT "Project1, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX
LTEXT "Copyright (c) 2023",IDC_STATIC,42,26,114,8
DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP
END
C# 세대에게 설명하자면, "폼 디자이너"의 초기 모델이라고 보면 될 듯합니다. ^^ 실제로 Visual studio는 위의 대화창 템플릿을 읽어들여 다음과 같이 디자인 창을 띄워 사용자가 컨트롤을 쉽게 추가/삭제/배치할 수 있도록 돕습니다.
단지 C# Windows Forms 프로젝트의 경우에는 디자인 창의 내용을 C# 코드로 직렬화하는 반면, 위의 dialog template 파일은 Win32 리소스 문법에 따라 직렬화한다는 차이가 있는 정도입니다.
실제로 여러분은 Resource Compiler(rc.exe)를 이용해 위의 dialog template만을 담은 파일을 컴파일할 수 있습니다. 이를 위해 필요한 include와 상수를 정의하고,
#include "C:\Program Files (x86)\Windows Kits\10\Include\10.0.22000.0\um\Windows.h"
#define IDD_ABOUTBOX 103
#define IDR_MAINFRAME 128
#ifndef IDC_STATIC
#define IDC_STATIC -1
#endif
IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Project1"
FONT 8, "MS Shell Dlg"
BEGIN
ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20
LTEXT "Project1, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX
LTEXT "Copyright (c) 2023",IDC_STATIC,42,26,114,8
DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP
END
Visual Studio 명령행 창에서 다음과 같이 명령을 내리면 됩니다.
c:\temp> rc test.rc
Microsoft (R) Windows (R) Resource Compiler Version 10.0.10011.16384
Copyright (C) Microsoft Corporation. All rights reserved.
그럼, 결과 파일로 res 확장자를 가진 이진 파일이 생성됩니다.
00 00 00 00 20 00 00 00 FF FF 00 00 FF FF 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
34 01 00 00 20 00 00 00 FF FF 05 00 FF FF 67 00
00 00 00 00 30 10 09 04 00 00 00 00 00 00 00 00
01 00 FF FF 00 00 00 00 00 00 00 00 C8 00 C8 80
04 00 00 00 00 00 AA 00 3E 00 00 00 00 00 41 00
62 00 6F 00 75 00 74 00 20 00 50 00 72 00 6F 00
6A 00 65 00 63 00 74 00 31 00 00 00 08 00 00 00
...[생략]...
29 00 20 00 32 00 30 00 32 00 33 00 00 00 00 00
00 00 00 00 00 00 00 00 01 00 03 50 71 00 29 00
32 00 0E 00 01 00 00 00 FF FF 80 00 4F 00 4B 00
00 00 00 00
비주얼 스튜디오로 C/C++ Windows 응용 프로그램을 빌드하면 저 res 2진 파일을 EXE/DLL 바이너리에 추가합니다. 그리고 다시 코드에서는 저 내용 중 원하는 대화상자, 이번 글에서는 IDD_ABOUTBOX 식별자로 구분되는 리소스의 내용을 이렇게 추출할 수 있습니다.
HRSRC hrsrc = ::FindResource(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), RT_DIALOG);
HGLOBAL hglobal = ::LoadResource(hInst, hrsrc);
LPVOID lpv = ::LockResource(hglobal);
DWORD resSize = SizeofResource(hInst, hrsrc);
BYTE* pBuffer = (BYTE*)lpv;
wchar_t buffer[1024] = { 0 };
for (int i = 1; i <= resSize; i++)
{
wsprintf(buffer, L"%02X ", pBuffer[i - 1]);
OutputDebugString(buffer);
if (i % 16 == 0)
{
OutputDebugString(L"\n");
}
}
::FreeResource(hglobal);
출력 내용은 우리가 컴파일했던 test.res의 5번째 줄부터 해당하는 바이트가 동일하게 나옵니다.
01 00 FF FF 00 00 00 00 00 00 00 00 C8 00 C8 80
04 00 00 00 00 00 AA 00 3E 00 00 00 00 00 41 00
62 00 6F 00 75 00 74 00 20 00 50 00 72 00 6F 00
6A 00 65 00 63 00 74 00 31 00 00 00 08 00 00 00
00 01 4D 00 53 00 20 00 53 00 68 00 65 00 6C 00
6C 00 20 00 44 00 6C 00 67 00 00 00 00 00 00 00
00 00 00 00 03 00 00 50 0E 00 0E 00 15 00 14 00
FF FF FF FF FF FF 82 00 FF FF 80 00 00 00 00 00
00 00 00 00 00 00 00 00 80 00 02 50 2A 00 0E 00
72 00 08 00 FF FF FF FF FF FF 82 00 50 00 72 00
6F 00 6A 00 65 00 63 00 74 00 31 00 2C 00 20 00
56 00 65 00 72 00 73 00 69 00 6F 00 6E 00 20 00
31 00 2E 00 30 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 02 50 2A 00 1A 00 72 00 08 00
FF FF FF FF FF FF 82 00 43 00 6F 00 70 00 79 00
72 00 69 00 67 00 68 00 74 00 20 00 28 00 63 00
29 00 20 00 32 00 30 00 32 00 33 00 00 00 00 00
00 00 00 00 00 00 00 00 01 00 03 50 71 00 29 00
32 00 0E 00 01 00 00 00 FF FF 80 00 4F 00 4B 00
00 00 00 00
그리고 위의 2진 파일의 내용을 해석하는 방법을 시리즈의 2~5번째 글에서 소개합니다.
The evolution of dialog templates – 16-bit Classic Templates
; https://devblogs.microsoft.com/oldnewthing/20040618-00/?p=38803
The evolution of dialog templates – 16-bit Extended Templates
; https://devblogs.microsoft.com/oldnewthing/20040622-00/?p=38773
The evolution of dialog templates – 32-bit Classic Templates
; https://devblogs.microsoft.com/oldnewthing/20040621-00/?p=38793
The evolution of dialog templates – 32-bit Extended Templates
; https://devblogs.microsoft.com/oldnewthing/20040623-00/?p=38753
버전별로 4개의 dialog template이 제공되는데 이 관계를 아래의 그림에서 잘 나타내고 있습니다.
16비트 포맷에 해당하는 DIALOG는 Windows 1.0 시절에, 16비트 DIALOGEX는 Windows 95와 후속 버전만 지원합니다. 그리고 각각의 버전이 다시 32비트 포맷으로 포팅됐습니다. 아주 오래된 프로그램이 아니라면 현재 32-bit DIALOGEX 이외의 형식은 구경조차 힘들 것입니다.
따라서, 예전 버전의 해석 방식은 각각 "
The evolution of dialog templates – 16-bit Classic Templates", "
The evolution of dialog templates – 16-bit Extended Templates", "
The evolution of dialog templates – 32-bit Classic Templates" 글에서 설명하고 있으니 참고하시고, 이번 글에서는 위의 예제로 나온 바이너리를 해석해야 하므로 "
32-bit Extended Templates"을 기준으로 설명합니다.
대화창의 헤더(32-bit extended header)는 다음과 같은 구조체로 시작한다고 합니다.
WORD wDlgVer; // version number - always 1
WORD wSignature; // always 0xFFFF
DWORD dwHelpID; // help ID
DWORD dwExStyle; // window extended style
DWORD dwStyle; // dialog style
WORD cItems; // number of controls in this dialog, 즉 대화창 1개에 65,535개까지의 컨트롤을 담을 수 있음
WORD x; // x-coordinate
WORD y; // y-coordinate
WORD cx; // width
WORD cy; // height
이 글의 예제에서 사용한 리소스의 경우에는 각각 다음과 같은 정보로 연결될 수 있고,
wDlgVer == 1
wSignature == 0xffff
dwHelpID == 0
dwExStyle == 0
dwStyle == DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
== 2160591048 == 80 C8 00 C8
cItems == BEGIN/END 사이의 컨트롤 수 == ICON/LTEXT/LTEXT/DEFPUSHBUTTON 총 4개
x == 0
y == 0
cx == 170 (0xaa)
cy == 62 (0x3e)
실제로 위의 바이트를 LockResource로 구한 바이트 배열에서 찾을 수 있습니다.
01 00 FF FF 00 00 00 00 00 00 00 00 C8 00 C8 80
04 00 : 컨트롤 수
00 00 : x
00 00 : y
AA 00 : cx
3E 00 : cy
그다음은 3개의 문자열 정보가 나오는데,
00 00: menu name 문자열 또는 ordinal
00 00: class name 문자열
41 00 62 00 6F 00 75 00 74 00 20 00 50 00 72 00 6F 00 6A 00 65 00 63 00 74 00 31 00 00 00: CAPTION 문자열 "About Project1"
해당 값이 문자열(위의 경우 "About Project1")인 경우라면 UNICODE 값과 함께 끝을 알리는 null 문자(\0\0)가 나옵니다. 반면 ordinal로 표현된다면 0xff, 0x00이 먼저 나오고 이어서 ordinal 2바이트가 나온다고 합니다. 예를 들어 위에서 menu name의 ordinal 값이 "2A 00"이라면 다음의 4바이트가 기록되는 것입니다.
FF 00 2A 00
어쨌든 위의 예제에서는 menu와 class name에 할당된 문자열이 없기 때문에 널 종료 문자(\0\0)만이 있습니다. (참고로
대화창의 class name 지정은 지난 예제에서 설명한 적이 있습니다.)
그다음은 가변 항목이 나오는데요, 대화창의 STYLE에,
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About Project1"
FONT 8, "MS Shell Dlg"
위와 같이 DS_SETFONT가 설정돼 있다면 함께 명시한 FONT 정보가 나온다고 합니다.
WORD wPoint; // point size
WORD wWeight; // font weight
BYTE bItalic; // 1 if italic, 0 if not
BYTE bCharSet; // character set
WCHAR szFontName[]; // variable-length
08 00: point size
00 00: font weight (0x0000 == FW_DONTCARE)
00: 1 if italic, 0 if not
01: character set, 0x01 = DEFAULT_CHARSET
4D 00 53 00 20 00 53 00 68 00 65 00 6C 00 6C 00 20 00 44 00 6C 00 67 00 00 00: "MS Shell Dlg" (널 종료 문자 포함)
자, 그다음부터는 이제 cItems에 지정한 컨트롤의 수만큼 다음의 정보들이 DWORD(4바이트) 정렬로 나열됩니다.
DWORD dwHelpID; // help identifier
DWORD dwExStyle; // window extended style
DWORD dwStyle; // window style
WORD x; // x-coordinate (DLUs)
WORD y; // y-coordinate (DLUs)
WORD cx; // width (DLUs)
WORD cy; // height (DLUs)
DWORD dwID; // control ID, 32비트 값이긴 하지만 WM_COMMAND 등의 메시지에서는 여전히 16비트 값으로 다루기 때문에 사실상 16비트만 사용 가능
WCHAR szClassName[];// variable-length (possibly ordinal)
WCHAR szText[]; // variable-length (possibly ordinal)
WORD cbExtra; // amount of extra data
BYTE rgbExtra[cbExtra]; // extra data follows (usually none)
szClassName, szText, rgbExtra 때문에 크기가 가변적이므로 고정 레코드로 읽어들일 수 없다는 점이 귀찮군요. 그래도 4개밖에 없으니 ^^ 아래와 같이 정리해 봤습니다.
00 00 00 00: dwHelpID
00 00 00 00: dwExStyle
03 00 00 50: window style, 0x50000003, 아마도 WS_CHILD(0x40000000) | WS_VISIBLE(0x10000000) | SS_ICON (0x03)
0E 00: x (14)
0E 00: y (14)
15 00: cx (21)
14 00: cy (20)
FF FF FF FF: control ID (-1)
FF FF 82 00: szClass == ordinal 0x0082 == static
FF FF 80 00: szText == ordinal 0x0080
00 00: cbExtra
00 00: 위의 cbExtra가 0이므로 이후 rgbExtra 바이트 배열이 아님. 4바이트 정렬이므로 padding 바이트
00 00 00 00: dwHelpID
00 00 00 00: dwExStyle
80 00 02 50: window style, 0x50020080, 아마도 WS_GROUP(0x020000) | WS_CHILD(0x40000000) | WS_VISIBLE(0x10000000) | SS_NOPREFIX(0x80)
2A 00: x (42)
0E 00: y (14)
72 00: cx (114)
08 00: cy (8)
FF FF FF FF: control ID (-1)
FF FF 82 00: szClass == ordinal 0x0082 == static
50 00 72 00 6F 00 6A 00 65 00 63 00 74 00 31 00 2C 00 20 00 56 00 65 00 72 00 73 00 69 00 6F 00 6E 00 20 00 31 00 2E 00 30 00 00 00: "Project1, Version 1.0" 문자열
00 00: cbExtra
00 00: 위의 cbExtra가 0이므로 이후 rgbExtra 바이트 배열이 아님. 4바이트 정렬이므로 padding 바이트
00 00 00 00: dwHelpID
00 00 00 00: dwExStyle
00 00 02 50: window style, 0x50020000, 아마도 WS_GROUP(0x020000) | WS_CHILD(0x40000000) | WS_VISIBLE(0x10000000)
2A 00: x (42)
1A 00: y (26)
72 00: cx (114)
08 00: cy (8)
FF FF FF FF: control ID (-1)
FF FF 82 00: szClass == ordinal 0x0082 == static
43 00 6F 00 70 00 79 00 72 00 69 00 67 00 68 00 74 00 20 00 28 00 63 00 29 00 20 00 32 00 30 00 32 00 33 00 00 00: "Copyright (c) 2023"
00 00: cbExtra
00 00 00 00: dwHelpID
00 00 00 00: dwExStyle
01 00 03 50: window style, 0x50030001, 아마도 WS_GROUP(0x020000) | WS_TABSTOP(0x010000) | WS_CHILD(0x40000000) | WS_VISIBLE(0x10000000) | BS_DEFPUSHBUTTON(0x01)
71 00: x (113)
29 00: y (41)
32 00: cx (50)
0E 00: cy (14)
01 00 00 00: control ID == IDOK(0x01)
FF FF 80 00: szClass == ordinal 0x0080 == button
4F 00 4B 00: "OK"
00 00: cbExtra
00 00: 위의 cbExtra가 0이므로 이후 rgbExtra 바이트 배열이 아님. 4바이트 정렬이므로 padding 바이트
그러니까 결국,
DialogBox 등의 함수들은 res로 컴파일된 바이너리에서 리소스 ID에 해당하는 대화창 정보를 위와 같은 포맷에 따라 해석해 CreateWindow로 보여주는 것입니다. 좀 ^^ 대단하지 않나요?
게다가... 이런 작업들이 Windows 1.0 시절부터 Win32 API와 그에 따른 Visual C++ 초기 IDE 도구가 협업해 대화창에 대한 Form 디자이너를 제공하고 있었다는 것은 매우 혁신적인 아이디어가 아니었을까 싶습니다. 제가 리눅스는 잘 몰라서 확실히 언급할 수는 없지만 아마 제대로 된 폼 디자이너를 아직도 갖추지 못한 것에 비하면 1985년에 이런 생각을 했다는 것에 놀랍기까지 합니다.
이렇게 해서 olenewthing의 6개 글 중 5개를 정리했습니다. 나머지 하나는,
The evolution of dialog templates – Summary
; https://devblogs.microsoft.com/oldnewthing/20040624-00/?p=38733
총정리를 다음의 표 하나로 제시하고 있습니다.
즉, 4개의 DIALOG Template 버전이 있지만 16비트/32비트에 대한 차이와 약간의 필드 추가 등을 제외하면 거의 비슷합니다.
약간의 첨언을 하자면.
대화창에 포함할 수 있는 컨트롤의 수가 WORD 2바이트로 제한이 되는데요, 사실 이건 제약이라고 볼 수 없습니다. 왜냐하면,
Why is the limit of window handles per process 10,000?
; https://devblogs.microsoft.com/oldnewthing/20070718-00/?p=25963
프로세스 하나가 생성할 수 있는 User Object가 10,000개로 제한이 되기 때문에 Dialog Template의 제약은 오히려 현재로서는 가능성이 없는 수준입니다.
또한, 위에서 설명한 dialog 포맷에 해당하는 구조체가 winuser.h 헤더 파일에 정의돼 있는데,
DLGTEMPLATE structure (winuser.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgtemplate
/*
* original NT 32 bit dialog template:
*/
typedef struct {
DWORD style;
DWORD dwExtendedStyle;
WORD cdit;
short x;
short y;
short cx;
short cy;
} DLGTEMPLATE;
DLGITEMTEMPLATE structure (winuser.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate
/*
* 32 bit Dialog item template.
*/
typedef struct {
DWORD style;
DWORD dwExtendedStyle;
short x;
short y;
short cx;
short cy;
WORD id;
} DLGITEMTEMPLATE;
아쉽게도 이 글에서 설명한 "32-bit Extended Templates" 유형이 아닌 "32-bit Classic Templates"에 해당하니 유의할 필요가 있습니다.
마지막으로, 본문에서 컨트롤들의 클래스에 대한 ordinal id를 설명 없이 언급했지만, 기본적인 컨트롤은 다음의 내용으로
문서에 나와 있습니다.
0x0080 Button
0x0081 Edit
0x0082 Static
0x0083 List box
0x0084 Scroll bar
0x0085 Combo box
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]