2007 г.
Win32 в машинных кодах
Рустэм Галеев aka Roustem
Назад Содержание Вперёд
7. Сообщения Windows
Windows является многозадачной средой - в ней одновременно (параллельно) могут быть запущены сразу несколько приложений. Если в однозадачной среде приложение само отслеживает действия, происходящие с устройствами ввода и вывода, то в многозадачной среде эту функцию полностью берет на себя операционная система. Она осуществляет это централизованно для всех выполняющихся в данное время задач. Изменения в состоянии аппаратных средств, самой операционной системе, других исполняющихся в данный момент приложениях, которые могут оказать воздействие на ход выполнения задачи, называются событиями. Приложения извещаются о событиях через сообщения, которые им посылает Windows.
Вся система Windows построена на передаче, приеме и обработке сообщений. Вместо непосредственного контроля за состоянием устройств ввода-вывода приложение должно ожидать поступления соответствующего сообщения от операционной системы. Значительная часть программирования в среде Windows заключается в выборе сообщений, которые будут обрабатываться, и написании соответствующего кода для их обработки. Windows создает специальные очереди сообщений, в которые направляются сообщения для приложений. Приложения, в свою очередь, должны выбирать сообщения из этой очереди и обрабатывать их соответствующим образом; кроме того, они сами могут при необходимости посылать сообщения операционной системе и другим выполняющимся в данный момент приложениям.
В прошлый раз мы построили приложение, которое создает окно и входит в бесконечный цикл. Но в этом цикле оно не делает ничего полезного; а система тем временем создает для приложения очередь сообщений и начинает посылать туда для созданного окна сообщения, которые не только не обрабатываются, но даже и не забираются из этой очереди. Именно поэтому указатель мыши в пределах нашего окна принимал форму ожидания - Windows "наивно" полагала, что сообщения не забираются из очереди, потому что приложение занято какой-то длительной операцией.
Настало время исправить это положение. Для получения сообщения из очереди существует функция GetMessageA из модуля User32.dll. Эта функция перемещает очередное сообщение из очереди в структуру типа MSG, адрес которой мы указываем в параметрах. Структура MSG имеет следующее строение:
Смещение | Размер, байт | Поле |
0 | 4 | Описатель окна, которому предназначено сообщение |
4 | 4 | Код сообщения |
8 | 4 | Параметр сообщения wParam |
0Ch | 4 | Параметр сообщения lParam |
10h | 4 | Время отправки сообщения |
14h | 4 | Координата x указателя мыши в момент посылки сообщения |
18h | 4 | Координата y указателя мыши в момент посылки сообщения |
Как указывалось в прошлой статье, основной рабочей единицей в Windows является окно, и сообщения посылаются именно окнам. Чтобы самому послать сообщение, нужно указать описатель окна, для которого сообщение предназначено. Эта информация записывается в первое поле структуры. Параметры wParam и lParam содержат дополнительную информацию, которая специфична для каждого кода сообщения. Система добавляет также к каждому сообщению информацию о времени и координатах курсора мыши в момент отправки сообщения.
Функция GetMessageA принимет 4 параметра, которые размещаются в стеке в следующем порядке:
- максимальный код сообщения, который принимает функция;
- минимальный код сообщения, который принимает функция;
- описатель окна, для которого нужно получить сообщение;
- адрес структуры MSG, куда должно быть скопировано сообщение.
Первые три параметра образуют фильтр - GetMessageA может отбирать лишь те сообщения из очереди, которые попадают в заданный диапазон или предназначены лишь указанному окну. Если параметры максимального и минимального кодов равны нулю, из очереди извлекаются все типы сообщений. Если равен нулю параметр описателя окна, извлекаются сообщения для всех окон. Обычно первые три параметра всегда равны нулю - фильтрация сообщений не используется.
Переделаем созданное в прошлый раз приложение так, чтобы включить в него вызов функции GetMessageA. Причем сделаем это с некоторым "запасом" - оставим место (в первую очередь, в структурах для импорта) еще для 2 функций, которые нам пригодятся в дальнейшем.
В секции данных нам потребуется лишь зарезервировать место для структуры MSG размером 28 байтов; но, поскольку мы ее разместим после строк по смещению 3020h, а после нее ничего нет, никаких изменений в файл data.txt вводить не потребуется - просто надо запомнить, что адрес структуры (после загрузки ее в память) будет 403020h. Сюда в процессе работы приложения будут копироваться сообщения системы.
Зато изменить потребуется файл rdata.txt. GetMessageA и другие функции, которые мы собираемся добавить потом, находятся в одном модуле User32.dll, поэтому вторую IAT и вторую таблицу поиска придется увеличить на 12 байтов, а таблицу импорта и следующие за ней строки сместить. Соответственно потребуется также изменить смещения строк во всех таблицах. Файл rdata.txt будет выглядеть следующим образом:
n rdata.bin
r cx
200
f 2000 l 200 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db 94 20 0 0
; ExitProcess
db a8 20 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db b6 20 0 0
; GetMessageA
db c8 20 0 0
; запас для 2-й ф-ции
db 0 0 0 0
; запас для 3-й ф-ции
db 0 0 0 0
db 0 0 0 0
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db 94 20 0 0
; ExitProcess
db a8 20 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db b6 20 0 0
; GetMessageA
db c8 20 0 0
; запас для 2-й ф-ции
db 0 0 0 0
; запас для 3-й ф-ции
db 0 0 0 0
db 0 0 0 0
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; адрес таблицы поиска
db 20 20 0 0
db 0 0 0 0 0 0 0 0
; адрес строки "Kernel32.dll"
db 7c 20 0 0
; адрес IAT(1)
db 0 20 0 0
; запись для User32.dll
; адрес таблицы поиска
db 2c 20 0 0
db 0 0 0 0 0 0 0 0
; адрес строки "User32.dll"
db 89 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
db 0 0 "GetMessageA" 0
m 2000 l 200 100
w
q
В секции кода после вызова функции CreateWindowExA добавляется вызов функции GetMessageA (предварительно в стек помещаются 4 параметра - три нулевых и адрес структуры MSG в секции данных). Цикл также меняется: теперь внутри него находится функция GetMessageA, и соответствующая инструкция должна передать управление команде помещения в стек первого параметра для этой функции. Теперь это уже не просто цикл, а цикл обработки сообщений - один из главных элементов приложения Windows (хотя сама обработка пока не совсем полноценная).
Рассмотрим подробнее инструкцию безусловного перехода, которая на ассемблере обозначается мнемоникой JMP. Опкод можно представить следующим образом:
1110 10 s 1 <байт(ы) смещения>
"Буквально" эта инструкция добавляет с учетом знака непосредственное значение (следующее за опкодом) к текущему значению регистра EIP. Обратите внимание на наличие бита знакового расширения s. Напомним, что если он равен 1, за опкодом следует всего один байт данных, который расширяется со знаком (т.е. старший бит этого байта заполняет 3 оставшихся старших байта). Если s = 0, за опкодом следуют 4 байта.
Как вы уже знаете, в регистре EIP находится адрес начала следующей инструкции. Таким образом, данная инструкция изменяет адрес следующей инструкции и тем самым осуществляет безусловный переход на исполнение кода где-то в другом месте. Указанный операнд является смещением этого "другого места" относительно начала следующей инструкции. Если смещение положительно, переход осуществляется "вперед"; если отрицательно (старший бит = 1), переход "назад". Сама инструкция перехода в короткой форме занимает 2 байта, поэтому смещение -2 означает переход на себя (отсчет ведется от следующей инструкции!); переведем это отрицательное число в бинарную форму: 2 = 00000010b; двоичное дополнение = 11111101b, добавив единицу, получим: -2 = 11111110b. Полная инструкция будет (s = 1):
11101011 11111110, или EB FE
- то, что мы уже использовали в прошлый раз.
На этот раз нам нужно подсчитать число байтов, которые следует "перескочить" - это будет прыжок "назад", как вы уже поняли (и смещение соответственно отрицательное). В файле code.txt находим строку "параметр ExitProcess (код завершения = 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
Подсчитаем число получившихся байтов: 17. Еще 2 займет сама команда перехода; поэтому, чтобы перейти на начало первой инструкции '6a 0', нужно добавить к значению EIP смещение -19 (11101101b = EDh). Сама же инструкция перехода будет иметь вид FEh EDh. Остаток файла должен выглядеть так:
; возврат на "цикл" (-19 байтов)
db eb ed
m 0 l 200 100
w
q
В файле 'header.txt' необходимо изменить лишь смещение таблицы импорта:
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 40 20 0 0
Размер таблицы остался тем же. Файл 'make.bat' также остался без изменений. Строим новый файл wnd.exe и запускаем его.
Отличий у данного приложения по сравнению с предыдущим немного - оно также не перемещается, не изменяет размеров и не отвечает на попытки закрыть окно. Тем не менее, одно отличие все же есть: обратите внимание на форму указателя мыши в пределах окна. Он больше не принимает ждущей формы - сообщения забираются из очереди. Но они лишь копируются в одно и то же место и никак не обрабатываются.
Чтобы от сообщений был какой-то толк, их нужно не просто забирать из очереди, но и направлять соответствующему окну. Для этой цели служит еще одна функция из модуля User32.dll - DispatchMessageA. Эта функция принимает лишь один аргумент - адрес структуры MSG, содержащей соответствующее сообщение. Добавим в наше приложение эту функцию.
В секции данных никаких изменений не будет. В секцию .rdata нужно добавить после имеющихся строк строку 'DispatchMessageA', а также указать ее смещение в IAT(2) и таблице поиска в специально оставленных для этого местах. Найдем строку:
; запас для 2-й ф-ции
db 0 0 0 0
- и заменим ее на следующую:
; DispatchMessageA
db d6 20 0 0
Это нужно сделать в двух местах - в IAT(2) и в таблице поиска для User32.dll. В конец файла нужно добавить строку 'DispatchMessageA':
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0
m 2000 l 200 100
w
q
Теперь изменим секцию кода. В файле 'code.txt' найдем строки:
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0
Теперь вставим после них инструкции помещения в стек параметра и вызова функции DispatchMessageA:
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0
Дальше следует безусловный переход на начало цикла. Однако, мы добавили 11 новых байтов, поэтому смещение будет теперь не -19, а -30 (или E2h). Поэтому конец файла должен выглядеть так:
; возврат на "цикл" (-30 байтов)
db eb e2
m 0 l 200 100
w
q
Файлы header.txt и make.bat не изменились. Строим очередную версию wnd.exe.
Вот это другое дело! Теперь окно выглядит так, как оно должно было выглядеть - это одна большая кнопка! Причем реагирует на щелчки мышью! А само окно можно перемещать, изменять его размеры, сворачивать, разворачивать и даже закрывать! Правда, в последнем случае не спешите радоваться - окно-то вы закрыли, но приложение осталось работать (оно все еще крутится в цикле обработки сообщений). Убедиться в этом несложно, заглянув в менеджер задач (нажав Ctrl-Alt-Del) - вы увидите выполняющуюся задачу 'wnd' (если файл не был переименован). Ее снова придется "прибивать" отсюда. Это происходит потому, что класс окна "BUTTON", как уже говорилось, не предназначен для создания самостоятельных окон. При закрытии главного окна приложения в цикл сообщений посылается соответствующее уведомление о завершении приложения; в нашем случае этого не происходит. К тому же наш цикл вообще не предусматривает возможности выхода из него - в дальнейшем мы реализуем и такую возможность.
Чтобы рассмотреть еще одну функцию, использующуюся в цикле сообщений, изменим класс нашего окна на "EDIT". Для этого в файле 'data.txt' просто поменяем строку "BUTTON" на "EDIT". И все! Снова строим wnd.exe, запустив make.bat.
И какая разительная перемена! Поэкспериментируйте с этим приложением сами. В качестве подсказки: воспользуйтесь контекстным меню, щелкнув в окне правой клавишей мыши; а для копирования текста в буфер можно воспользоваться Блокнотом.
Я думаю, вы уже заметили особенность нового окна: хотя можно пользоваться командами редактирования текста из всплывающего меню и двигать текстовый курсор с помощью стрелок, набрать текст на клавиатуре не удается. Это происходит потому, что окно получает лишь сообщения о нажатиях клавиш, но они не преобразуются в сообщения о поступлении соответствующих символов. Как раз для решения этой задачи и служит последняя функция - TranslateMessage, тоже из модуля User32.dll. Эта функция принимает один параметр - конечно же, опять все тот же адрес структуры MSG, и помещается в цикл обработки сообщений как раз перед вызовом функции DispatchMessageA.
Добавим эту функцию. Для этого в конце файла 'rdata.txt' добавляем соответствующую строку:
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0 0
db 0 0 "TranslateMessage" 0
m 2000 l 200 100
w
q
Второй 0 после "DispatchMessageA" нужен для выравнивания начала следующей строки по четному адресу. Необходимо также изменить (тоже в двух местах - для IAT(2) и таблицы поиска User32.dll) строки:
; запас для 3-й ф-ции
db 0 0 0 0
на строки:
; TranslateMessage
db ea 20 0 0
В файле code.txt очередные 11 байтов нужно добавить до вызова DispatchMessageA (не забыв изменить смещение в инструкции безусловного перехода на начало цикла), так что конец файла будет выглядеть так:
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0
; параметр 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
; возврат на "цикл" (-41 байт)
db eb d7
m 0 l 200 100
w
q
Вот теперь, построив очередную версию wnd.exe, мы получим почти полноценный текстовый редактор! Хотя пока он работает лишь с одной строкой; но зато создан полностью в машинных кодах!
На сегодня этого достаточно. Не забудьте "прибить" работающие приложения - закрытия окна недостаточно для их завершения (пока). В заключение хочу заметить, что подобный способ завершения приложений, конечно, не является нормальным, и система может начать (и, возможно, у вас уже не раз начинала) работать нестабильно, так что придется ее перезагружать. Поэтому перед началом таких экспериментов лучше закрыть все остальные приложения, особенно, если вы работаете в них с важными данными.
Назад Содержание Вперёд