Окна Windows
Основной "действующей единицей" в ОС Windows является окно. Графически оно представляет собой прямоугольную область на экране, через который осуществляется весь ввод и вывод. Каждое окно имеет свой логический номер - описатель (handle), по которому операционная система отличает одно окно от другого. Любой ввод и вывод может осуществляться только через окно; более того, сообщения также адресуются именно окнам. На самом деле, окно является исходным элементом, на котором построена вся операционная система Windows.
Для пользователя окна выступают прежде всего областями экрана, занимаемого разными программами. В "классическом" окне выделяют различные области и зоны - строку заголовка со значком и кнопками свертывания, разворачивания и восстановления окна; строку меню; обрамление; различные дополнения в виде панелей инструментов, полос прокрутки, строки состояния и т.п. Однако окна отнюдь не ограничиваются подобным "классическим" типом. Любая более или менее функционирующая область экрана является, как правило, окном. Более того, сами элементы окна часто тоже являются, в свою очередь, дочерними окнами.
Давайте посмотрим, сколько окон можно насчитать, скажем, запустив наш любимый текстовый редактор "Word". Во-первых, это, конечно, главное окно программы, которое содержит в себе все остальные, со значком W и надписью "Microsoft Word". Во-вторых, многочисленные окна документов (для переключения между которыми имеется специальное меню "Окно"). Панели инструментов с кнопками наверху и строка состояния внизу - это тоже окна, но уже дочерние, принадлежащие главному окну. Это же относится к полосам прокрутки справа и внизу окна документа, а также горизонтальной и вертикальной линейке (если вы их включили). Особой разновидностью окна является и строка меню, а также всплывающие меню, появляющиеся при щелчке правой клавишей мыши. При выборе различных пунктов меню появляются диалоговые окна, содержащие, в свою очередь, большое количество собственных дочерних окон - вкладок, элементов управления и т.д. Даже область пользователя окна документа является, как это ни странно, самостоятельным окном! При использовании мастера приложений MS Visual C++ программа будет построена именно таким образом - основное окно является только обрамлением, а всю рабочую область занимает дочернее окно (представляющее собой закрывающий всю область пользователя главного окна белый прямоугольник), в которое и осуществляется вывод.
Однако, окно - это не только прямоугольная область экрана для ввода и вывода. Окно является основным системным объектом, обеспечивающим взаимодействие приложения с пользователем, другими приложениями и самой операционной системой. Как и другие объекты, окно принадлежит определенному классу, который называется классом окна, имеет свои свойства, не все из которых находят непосредственное графическое отражение на экране, а также специальную функцию, которая, называется главной процедурой окна.
Класс окна представляет собой набор атрибутов, которые система использует в качестве шаблона при создании окна. Сюда относятся такие внешние атрибуты окна, как значок, форма курсора, фон окна, меню окна по умолчанию, а также набор стилей, называемых стилями класса окна и определяющих наиболее общие особенности поведения и отображения окна. Все эти атрибуты хранятся в специальной структуре WNDCLASSEX.
Каждый класс окна имеет связанную с ним процедуру окна, которую разделяют все окна одного класса. Процедура окна обрабатывает сообщения для всех окон данного класса и тем самым контролирует их поведение и отображение.
Каждое окно имеет также свою внутреннюю структуру CREATESTRUCT, которая заполняется во время создания окна с помощью функции CreateWindowExA. Это такие атрибуты окна, как его расположение и размеры, название, меню, отношения владения-подчинения с другими окнами, а также набор индивидуальных стилей, определяющих конкретный тип окна.
Рассмотрим функцию CreateWindowExA подробнее. Эта функция из модуля User32.dll принимает аж целых 12 параметров, которые должны быть размещены в стеке в следующем порядке:
- адрес переменной, в которой находится дополнительное значение для передачи некоторым типам окон. Если окну не требуется дополнительное значение, этот параметр равен нулю;
- описатель экземпляра приложения, которому принадлежит окно. Это значение может быть получено с помощью функции GetModuleHandleA из модуля Kernel32.dll;
- в зависимости от стиля окна, этот параметр является либо идентификатором дочернего окна, либо описателем меню. Если создаваемое окно - дочернее, это идентификатор окна; если нет - описатель меню окна (при отсутствии меню параметр равен нулю);
- описатель родительского окна или окна-владельца (если окно самостоятельное, параметр равен нулю);
- высота окна в пикселах;
- ширина окна в пикселах;
- начальная вертикальная координата окна. Если окно дочернее, вертикальное положение отсчитывается от левого верхнего угла клиентской области родительского окна; если окно самостоятельное - от левого верхнего угла экрана;
- начальная горизонтальная координата окна. Аналогично вертикальной координате, за точку отсчета для дочерних окон принимается левый верхний угол клиентской области родительского окна, для самостоятельных окон - левый верхний угол экрана;
- флаги, указывающие стиль окна;
- адрес строки с именем окна;
- адрес строки с именем класса окна;
- флаги, указывающие расширенный стиль окна.
При успешном создании окна в регистре EAX возвращается его описатель. Если произошла ошибка, EAX будет содержать 0. Постепенно мы разберем каждый параметр этой функции более подробно. Сейчас же попробуем создать приложение с использованием этой функции.
Сначала с помощью функции GetModuleHandleA нужно получить значение описателя для экземпляра нашего приложения. GetModuleHandleA принимает всего один аргумент - адрес строки с именем модуля, для которого нужно возвратить описатель. Подразумевается, что модуль уже загружен в адресное пространство того процесса, который вызывает эту функцию. (Например, так можно получать описатели для модулей загруженных dll). Если параметр равен нулю, возвращается описатель для самого вызывающего приложения (как в нашем случае).
Многие параметры CreateWindowExA будут равны нулю, например, дополнительное значение окна, описатель меню, описатель родительского окна, а также параметр расширенных стилей. Поскольку вызов CreateWindowExA следует непосредственно за вызовом GetModuleHandleA, значение описателя экземпляра приложения можно поместить в стек прямо из регистра EAX. Начальные координаты и размеры окна можно выбрать произвольные; пусть будут, например, такие: высота - 100h, ширина - 150h, начальная координата y - 100h, x - 150h пикселей. Нужно указать также стиль окна. Подробнее разбираться со стилями мы будем в другой раз, а сейчас просто используем значение 10CF0000h.
Остались 2 параметра: адреса строк с именами окна и класса окна. Имя окна тоже может быть произвольным (например, просто "Моё окно") или даже вовсе отсутствовать (в этом случае параметр равен 0). А вот имя класса окна должно быть предварительно зарегистрировано в системе вместе с соответствующей структурой WNDCLASSEX. Этим мы займемся в следующий раз, а сейчас используем один из предопределенных в системе классов - "BUTTON". Правда, окна этого класса должны использоваться лишь в качестве дочерних в составе других окон; но мы ради эксперимента создадим самостоятельное окно и посмотрим, что из этого получится. Соответствующие строки должны находиться в секции данных, с создания которой мы и начнем конструирование нашего приложения.
"Макет" сделаем по нашему стандартному шаблону, т.е. первой будет секция кода .code со смещениями в памяти и файле 1000h и 200h соответственно; второй - секция данных импорта .rdata (2000h и 400h); третьей - секция данных .data (3000h и 600h). Создаем файл data.txt:
n data.bin r cx 200 f 0 l 200 0 e 0 "BUTTON" 0 e 10 "Моё окно" 0 m 0 l 200 100 w q
Переходим к секции .rdata. Нам нужно импортировать 2 функции из модуля Kernel32.dll (GetModuleHandleA и ExitProcess) и одну функцию (CreateWindowExA) из модуля User32.dll. Напомним, что данные для импортируемых из одного модуля функций должны располагаться в одних и тех же таблицах поиска и импортируемых адресов (IAT), а для функций из разных модулей нужно использовать разные таблицы. Поэтому у нас будут по две IAT и таблицы поиска, но одна общая таблица импорта с тремя записями (по одной на каждый модуль плюс завершающая, заполненная нулями). Создание вспомогательных таблиц не должно вызвать проблем (файл rdata.txt):
n rdata.bin r cx 200 f 2000 l 200 0 a 2000 ; 1-я IAT (для Kernel32.dll) ; GetModuleHandleA db 7C 20 0 0 ; ExitProcess db 90 20 0 0 db 0 0 0 0 ; 2-я IAT (User32.dll) ; CreateWindowExA db 9E 20 0 0 db 0 0 0 0 ; таблица поиска для Kernel32.dll ; GetModuleHandleA db 7C 20 0 0 ; ExitProcess db 90 20 0 0 db 0 0 0 0 ; таблица поиска для User32.dll ; CreateWindowExA db 9E 20 0 0 db 0 0 0 0 ; Таблица импорта: 2 записи + завершающая (0) ; запись для Kernel32.dll ; смещение таблицы поиска db 14 20 0 0 db 0 0 0 0 0 0 0 0 ; смещение строки "Kernel32.dll" db 64 20 0 0 ; смещение IAT(1) db 0 20 0 0 ; запись для User32.dll ; смещение таблицы поиска db 20 20 0 0 db 0 0 0 0 0 0 0 0 ; смещение строки "User32.dll" db 71 20 0 0 ; смещение IAT(2) db 0C 20 0 0 ; завершение таблицы db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 db "Kernel32.dll" 0 db "User32.dll" 0 db 0 0 "GetModuleHandleA" 0 0 db 0 0 "ExitProcess" 0 db 0 0 "CreateWindowExA" 0
m 2000 l 200 100 w q
Секция, как обычно, строится "в два прохода"; чтобы сразу получить нужные смещения в режиме ассемблирования, образ в debug собирается по тому же смещению, что и образ в памяти (2000h), а затем для записи переносится по смещению 100h. После строки "GetModuleHandleA" дополнительный 0 поставлен для выравнивания, т.к. строки в таблице имен должны начинаться по четному адресу. Теперь секция кода (файл code.txt):
n code.bin r cx 200 f 0 l 200 0 a 0 ; параметр GetModuleHandleA = 0 db 6a 0 ; вызов GetModuleHanldeA (по адресу в IAT(1) 402000h) db ff 15 0 20 40 0 ; параметры CreateWindowExA ; дополнительное число (0) db 6a 0 ; описатель модуля (в EAX) db 50 ; описатель меню (0) db 6a 0 ; описатель окна-владельца (0) db 6a 0 ; высота окна db 68 0 1 0 0 ; ширина окна db 68 50 1 0 0 ; координата y db 68 0 1 0 0 ; координата x db 68 50 1 0 0 ; стиль окна db 68 0 0 cf 10 ; адрес имени окна (в секции данных - 403010h) db 68 10 30 40 0 ; адрес имени класса (в секции данных - 403000h) db 68 0 30 40 0 ; расширенный стиль окна (0) db 6a 0 ; вызов CreateWindowEx (по адресу в IAT(2) 40200Ch) db ff 15 c 20 40 0 ; параметр ExitProcess (код завершения = 0) db 6a 0 ; вызов ExitProcess (по адресу в IAT(1) 402004h) db ff 15 4 20 40 0
m 0 l 200 100 w q
Осталось слегка подправить наш шаблон заголовка. Скопируем файл header.txt в рабочий каталог. Изменения требует лишь начало каталога смещений. Находим строку "Здесь начинается первый элемент каталога:" и вставляем такой кусок:
; смещение таблицы экспорта (4 байта) db 0 0 0 0 ; размер таблицы экспорта (4 байта) db 0 0 0 0 ; Второй элемент каталога: ; смещение таблицы импорта (4 байта) db 28 20 0 0 ; размер таблицы импорта (4 байта) db 3c 0 0 0
Файл "make.bat" традиционный:
@echo off debug < header.txt > report.lst debug < code.txt >> report.lst debug < rdata.txt >> report.lst debug < data.txt >> report.lst copy /b header.bin+code.bin+rdata.bin+data.bin wnd.exe
Проверив файл report. lst на наличие ошибок, можно запускать wnd.exe. Что-то мелькает? Хлопайте в ладоши! Это не ошибка - так и должно быть. Ошибка, если появится сообщение от Windows или вообще ничего не появится. Наше приложение создает окно, но завершается раньше, чем мы успеваем что-либо рассмотреть. Как "остановить" приложение? Один раз мы это уже делали, когда создавали самый первый PE-файл (в статье "Исполняемые файлы Windows"): нужно зациклить программу с помощью инструкции EB FE. Ее можно ввести вместо параметра функции ExitProcess (т.е. в файле code.txt вместо последнего 'db 6a 0' записать 'db eb fe'). Изменив файл code.txt, нужно снова запустить make.bat.
Теперь, запустив wnd.exe, можно полюбоваться на созданное окно (хотя на самом деле оно пока выглядит не так, как должно). Все, что надо, на месте - и три кнопки в правом верхнем углу, и строка с именем, и даже значок слева. Только вот сделать с ним ничего не удастся - ни сдвинуть, ни изменить размеры, ни даже закрыть. Обратите внимание - когда указатель мыши попадает в область нашего окна, курсор принимает ждущую форму. Если переключиться на другое окно, которое перекроет наше, оно исчезнет и больше не появится.
Все это происходит потому, что окно должно обрабатывать сообщения, которые система начинает посылать в очередь сообщений приложения, создавшего окно, сразу после его создания. Мы же обработку сообщений не предусмотрели. Но этим мы займемся уже в следующий раз. А пока можно поэкспериментировать с изменением значений тех параметров функции CreateWindowExA, которые можно пронаблюдать - т.е. размеров и расположения окна. Учтите, что данные в коде - 16-ричные. А чтобы завершить приложение, придется снова "прибивать" его из менеджера задач, нажав Ctrl-Alt-Del .