Шаблон оконного приложения
В четвертой статье мы создали первый шаблон приложения, возможности которого потом постепенно наращивали. Сегодня важный момент: мы подошли к очередному рубежу. Наконец-то мы сможем создать полноценное работоспособное окно. Мы построим законченное приложение и сохраним его в качестве общего шаблона ("generic"), а затем будем проводить с ним эксперименты.
Для "доведения до ума" предыдущего приложения осталось уже совсем немного. Нужно реализовать возможность завершения цикла сообщений, обеспечить обработку сообщений по умолчанию и, наконец, дать возможность приложению закончить работу при закрытии окна.
Цикл сообщений завершается очень просто. На самом деле, функция GetMessageA возвращает значение 0, если получено сообщение с кодом 12h (WM_QUIT), и ненулевое значение для любого другого сообщения. Для отправки сообщения с кодом 12h служит функция PostQuitMessage (из модуля User32.dll), которая принимает единственный аргумент - код завершения приложения (при нормальном завершении обычно 0). Эту функцию можно вызвать при обработке другого сообщения от главного окна приложения - 2 (WM_DESTROY). В результате при закрытии окна приложение будет автоматически завершаться (на самом деле, просто выходить из цикла сообщений; но за ним традиционно стоит вызов функции ExitProcess. Любители кошмариков могут, конечно, вставить сюда переход в начало программы, чтобы создать "вечное" приложение, которое никак невозможно завершить).
Итак, в файле rdata.txt нужно восстановить функцию DefWindowProc, а также импортировать одну новую - PostQuitMessage. Но сначала изменим общую схему нашего приложения. Оставим больше места для последующих экспериментов; кроме того, минимизируем необходимость внесения в файлы правок в будущем и разместим структуры импорта "рыхлым" образом, с промежутками между ними (чтобы не править каждый раз смещения строк и таблиц). Размеры заголовка и секций в файлах сделаем по 400h (в памяти по-прежнему оставим 1000h - пока этого достаточно). С прицелом на будущее добавим также одну новую секцию ".rsrc" - для ресурсов (придется внести изменения в таблицу секций PE-заголовка). Т.о., макет будет следующим: заголовок (400h в файле, 1000h в памяти), секция кода ".code" по смещению 1000h в памяти и 400h в файле, затем секция данных импорта ".rdata" (2000h и 800h), секция данных ".data" (3000h и C00h), секция ресурсов (4000h и 1000h).
Файл "rdata.txt" будет иметь теперь следующий вид:
n rdata.bin
r cx
f 2000 l 400 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db f0 21 0 0
; ExitProcess
db 4 22 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db 12 22 0 0
; GetMessageA
db 24 22 0 0
; DispatchMessageA
db 32 22 0 0
; TranslateMessage
db 46 22 0 0
; DefWindowProc
db 5a 22 0 0
; RegisterClassExA
db 6c 22 0 0
; PostQuitMessage
db 80 22 0 0
db 0 0 0 0
a 2088
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db f0 21 0 0
; ExitProcess
db 4 22 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db 12 22 0 0
; GetMessageA
db 24 22 0 0
; DispatchMessageA
db 32 22 0 0
; TranslateMessage
db 46 22 0 0
; DefWindowProcA
db 5a 22 0 0
; RegisterClassExA
db 6c 22 0 0
; PostQuitMessage
db 80 22 0 0
db 0 0 0 0
a 2110
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; смещение таблицы поиска
db 88 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "Kernel32.dll"
db a0 21 0 0
; смещение IAT(1)
db 0 20 0 0
; запись для User32.dll
; смещение таблицы поиска
db 94 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "User32.dll"
db ad 21 0 0
; смещение IAT(2)
db 0C 20 0 0
; завершение таблицы
a 21a0
; имена модулей
db "Kernel32.dll" 0
db "User32.dll" 0
a 21f0
; имена функций
db 0 0 "GetModuleHandleA" 0 0
db 0 0 "ExitProcess" 0
db 0 0 "CreateWindowExA" 0
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0 0
db 0 0 "TranslateMessage" 0 0
db 0 0 "DefWindowProcA" 0 0
db 0 0 "RegisterClassExA" 0 0
db 0 0 "PostQuitMessage" 0
m 2000 l 400 100
w
q
Обратите внимание на сделанные изменения. Размер файла увеличен до 400h. Оставлено свободное место между IAT и таблицами поиска; между таблицами поиска и таблицей импорта; оставлен резерв для расширения самой таблицы импорта, а также место между именами модулей и таблицей имен импортируемых функций. Все соответствующие смещения в таблицах изменены.
Необходимо изменить также смещение таблицы импорта в PE-заголовке. Кроме того, в файле "header.txt" нужно изменить число секций (теперь 4), размер загруженного файла в памяти (5000h) и общий размер заголовков (400h), а также добавить данные для четвертой секции в соответствующей таблице. Размер самого файла "header.txt" также увеличен до 400h. Вот новый заголовок полностью:
n Header.bin
r cx
f 0 l 400 0
e 0 'MZ'
e 3C 40
e 40 'PE'
e 44 4C 01
a 46
; Число секций
db 04 00
a 54
; Размер дополнительного заголовка
db e0 00
; Тип файла
db 0F 01
; "Магическое" значение
db 0B 01
a 68
; Смещение точки входа
; относительно адреса загрузки
db 00 10 00 00
a 74
; Начальный адрес загрузки
db 00 00 40 00
; Выравнивание секций
db 00 10 00 00
; Выравнивание в файле
db 00 02 00 00
; Старшая версия Windows
db 04 00
a 88
; Старшая версия подсистемы
db 04 00
a 90
; Размер загруженного файла в памяти
db 00 50 00 00
; Размер всех заголовков в файле
db 00 04 00 00
a 9C
; Подсистема: 2 - графическая, 03 - консольная
db 02 00
a A0
; Зарезервированный размер стека
db 00 00 10 00
; Выделенный размер стека
db 00 10 00 00
; Зарезервированный размер кучи
db 00 00 10 00
; Выделенный размер кучи
db 00 10 00 00
a B4
; Число элементов каталога смещений
db 10 00 00 00
;
; Каталог смещений/размеров
; смещение таблицы экспорта
db 0 0 0 0
; размер данных экспорта
db 0 0 0 0
; смещение таблицы импорта
db 10 21 0 0
; размер таблицы импорта
db 3c 0 0 0
; смещение таблицы ресурсов
; db 0 0 0 0
; размер таблицы ресурсов
; db 0 0 0 0
a 138
; Начало таблицы секций
;
; Первая секция (имя - 8 символов)
db '.code' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 10 0 0
; размер в файле
db 0 4 0 0
; смещение начала данных секции в файле
db 0 4 0 0
; Пропускаем 12 байтов
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 20 0 0 60
;
; Вторая секция
db '.rdata' 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 20 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 8 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 40
;
; Третья секция
db '.data' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 30 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 c 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 C0
;
; Четвертая секция
db '.rsrc' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 40 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 10 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 c0
m 0 l 400 100
w
q
Файл "code.txt" также претерпел изменения. В начале изменяется размер сохраняемого файла (тоже до 400h). Целесообразно "собирать" секцию в debug по "родному" смещению (1000h), чтобы при черновом проходе можно было сразу определить нужный адрес главной функции окна.
n code.bin
r cx
f 1000 l 400 0
a 1000
; параметр GetModuleHandleA = 0
db 6a 0
; вызов GetModuleHandleA (по адресу в IAT(1) 402000h)
db ff 15 0 20 40 0
; скопировать описатель из EAX по адресу 403054h
db a3 54 30 40 0
; параметр для RegisterClassExA -
; адрес WNDCLASSEX (403040h)
db 68 40 30 40 0
; вызов RegisterClassExA (в IAT(2) - 402020h)
db ff 15 20 20 40 0
; параметры для CreateWindowExA
; дополнительное число (0)
db 6a 0
; описатель модуля (сохранен по адресу 403054h)
db ff 35 54 30 40 0
; описатель меню (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
; вызов CreateWindowExA (по адресу в IAT(2) 40200Ch)
db ff 15 c 20 40 0
; цикл
; параметры GetMessageA
db 6a 0
db 6a 0
db 6a 0
; 4-й параметр - адрес структуры MSG (в секции данных - 403020h)
db 68 20 30 40 0
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0
После вызова GetMessageA нужно проверить возвращенное функцией значение. Если EAX не равен 0, выполняется обычная последовательность действий с вызовом TranslateMessage и DispatchMessageA и последующим возвратом на начало цикла. Если же возрвращен 0, нужно перескочить этот участок кода прямо на вызов функции ExitProcess. Для сравнения используется инструкция 100000sw с байтом Mod111R/M; напомним, что она содержит второй операнд в виде непосредственного значения в коде самой инструкции (в данном случае, это число 0). Мы сравниваем содержимое всего регистра EAX, поэтому w = 1; но непосредственное значение помещается в одном байте - потом оно будет расширено до 4 байтов, поэтому s тоже равен 1. Первый операнд находится в регистре (Mod = 11), код EAX - 000. Результирующая инструкция:
10000011 11111000 00000000, или 83 F8 00 (h)
Продолжим:
; EAX = 0?
db 83 f8 00
; да - перескочить 18h байтов вперед
db 74 18
; параметр TranslateMessage - адрес MSG (403020h)
db 68 20 30 40 0
; вызов TranslateMessage (по адресу в IAT(2) 402018h)
db ff 15 18 20 40 0
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0
; возврат на "цикл" (-46 байт)
db eb d2
Здесь в стек помещается параметр для функции ExitProcess. Раньше мы использовали в качестве кода выхода 0; но теперь мы завершаем цикл с помощью сообщения WM_QUIT, а оно само заносит в параметр wParam структуры MSG код завершения (тот самый, который был передан функции PostQuitMessage). Нам нужно передать функции ExitProcess именно это значение. Вспомним инструкцию помещения в стек значений, находящихся в памяти; используется байт ModR/M. Мы познакомились с этой командой в статье "Простейшее приложение"; напомним ее формат:
11111111 Mod 110 R/M
Для указания непосредственного адреса памяти используется комбинация Mod = 00 и R/M = 101, затем следуют 4 байта адреса. Структура MSG у нас располагается по адресу 403020h, а ее поле wParam - по адресу 403028h:
; параметр ExitProcess (код завершения = MSG.wParam)
db ff 35 28 30 40 0
; вызов ExitProcess (по адресу в IAT(1) 402004h)
db ff 15 4 20 40 0
Дальше начинается главная функция окна. Сначала, как обычно, создаем фрейм стека для обращения к параметрам:
;-----------------------
; Процедура окна
; создать фрейм стека:
db 55
db 89 e5
Затем проверяем код сообщения, переданный в качестве параметра. На этот раз мы будем пересылать системной процедуре окна по умолчанию (DefWindowProc) все сообщения, кроме одного с кодом 2. Как уже говорилось в начале, это сообщение посылается системой окну при его разрушении (т.е. когда пользователь закрывает окно). Если это так, вызываем PostQuitMessage с параметром 0 и завершаем функцию. Если нет, нужно перескочить этот участок кода:
; сравнить значение в [EBP+0Ch] и 2
db 83 7d c 2
; не равны = перескочить 0Ch байтов (на "по умолчанию")
db 75 c
; параметр PostQuitMessage (0)
db 6a 0
; вызов PostQuitMessage
db ff 15 24 20 40 0
; "выход"
; удалить фрейм стека
db c9
; возврат из процедуры с очисткой стека
db c2 10 0
; "по умолчанию"
Дальше следует код обработки сообщений по умолчанию. Он сводится к упаковке параметров для DefWindowProc (тех самых, которые были переданы нашей функции окна) и вызову ее. В нашем случае операнд находится в памяти (в стеке), адрес соответствующего фрейма стека находится в регистре EBP; причем дополнительно необходимо использовать смещение. Размер фрейма стека небольшой, поэтому для смещения достаточно использовать 1 байт (при этом Mod будет равен 01). Код R/M для передачи адреса через EBP - 101, откуда получаем результирующую инструкцию:
11111111 01110101 <1 байт смещения> (FFh 75h <...>)
Параметры должны передаваться в том же порядке, в котором они были переданы нашей функции. Поэтому первый параметр будет иметь наибольшее смещение относительно текущего значения EBP (14h), затем соответственно следуют 10h, 0Ch и 8:
; параметры DefWindowProc
db ff 75 14
db ff 75 10
db ff 75 c
db ff 75 8
; вызов DefWindowProc
db ff 15 1c 20 40 0
После этого - выход из нашей функции. Соответствующий код у нас уже имеется; поэтому просто поставим инструкцию безусловного перехода на него:
; на "выход" - назад -24 байта
db eb e8
m 1000 l 400 100
w
q
Файл кода составлен. Остался файл данных "data.txt". Здесь, помимо размера самого файла, необходимо изменить лишь одну вещь - адрес главной функции окна в соответствующем поле структуры WNDCLASSEX:
n data.bin
r cx
f 3000 l 400 0
a 3000
; имя класса окна
db "MYCLASS" 0
a 3010
; заголовок окна
db "Моё окно" 0
a 3020
; структура MSG (28 байт)
a 3040
; структура WNDCLASSEX:
; размер структуры
db 30 0 0 0
; стили класса окна
db 0 0 0 0
; (403048h) адрес главной функции окна
db 89 10 40 0
; дополнительные байты класса
db 0 0 0 0
; дополнительные байты окна
db 0 0 0 0
; (403054h) описатель экземпляра приложения
db 0 0 0 0
; описатель ресурса значка
db 0 0 0 0
; описатель ресурса курсора
db 0 0 0 0
; фон окна
db 6 0 0 0
; адрес имени меню
db 0 0 0 0
; адрес имени класса окна
db 0 30 40 0
; описатель ресурса мелкого значка
db 0 0 0 0
m 3000 l 400 100
w
q
У нас появилась также и новая секция - но пока для нее создадим лишь пустой шаблон (файл rsrc.txt):
n rsrc.bin
r cx
f 4000 l 400 0
m 4000 l 400 100
w
q
Осталось лишь слегка подправить файл сборки (make.bat):
@echo off
debug < header.txt > report.lst
debug < code.txt >> report.lst
debug < rdata.txt >> report.lst
debug < data.txt >> report.lst
debug < rsrc.txt >> report.lst
copy /b header.bin+code.bin+rdata.bin+data.bin+rsrc.bin generic.exe
Имя результирующего файла "generic.exe" должно находится на одной строке с командой copy - здесь нет разрыва строки. Вот и все. Опечатки можно проверить в файле "report.lst"; если их нет, можно запускать получившийся файл. Это уже настоящее оконное приложение Win32, правда, пока с ограниченными возможностями. В дальнейшем мы будем их наращивать. Поэтому имеет смысл сохранить все исходные файлы для этой заготовки в отдельной папке - и до новых встреч!