Главная функция окна
Сегодня мы впервые будем создавать собственную функцию, поэтому освежим в памяти особенности функций.
Функция - это некий код, который находится где-то по другому адресу от того места, из которого вызывается. Поэтому адрес возврата сохраняется в стеке. Кроме того, мы уже работали с передаваемыми функции данными - ее параметрами - и знаем, что они тоже помещаются в стек. А значит, при возвращении из функции мы должны компенсировать изменения в стеке, иначе он будет несбалансированным.
Процедура окна - это функция, которая вызывается самой системой; поэтому она должна удовлетворять соглашениям вызова функций API. Т.е. мы можем внутри функции свободно работать с регистрами EAX, ECX, EDX и изменять их значения; но если потребуется работа с регистрами EBX, ESI или EDI, нужно сначала сохранить их содержимое в стеке, а перед возвращением из функции - восстановить старые значения. Кроме того, в EAX должен быть помещен результат работы функции, если она возвращает какое-то значение.
Теперь разберем все эти детали подробнее. Нужно каким-то образом получить доступ к параметрам, помещенным в стек. Мы уже знаем, что указатель стека находится в ESP, и можно было бы воспользоваться косвенной адресацией через этот регистр. Кстати, современные оптимизирующие компиляторы используют именно этот способ. Но если функция сама будет активно использовать стек, например, сохранять и восстанавливать регистры EBX, ESI или EDI, смещение параметров относительно ESP будет все время меняться и работать с ним будет неудобно - особенно при "ручном" кодировании. Поэтому для работы с параметрами (и локальными переменными, с которыми мы познакомимся позже) создается т.н. фрейм стека: значение ESP в самом начале функции копируется в регистр EBP, который и используется в дальнейшем в качестве базового указателя при обращении к параметрам (и локальным переменным). Предварительно в стек заносится старое значение EBP, которое восстанавливается перед завершением функции.
Главная функция окна строится по единому шаблону. Эта функция получает в качестве параметров значения 4 из 7 полей, содержащихся в структуре MSG. Параметры помещаются в стек в следующем порядке:
- дополнительный параметр lParam, зависящий от типа сообщения;
- дополнительный параметр wParam, зависящий от типа сообщения;
- код сообщения (msg);
- описатель окна (hWnd), для которого предназначено сообщение.
На рисунке показаны состояния стека в различные моменты при вызове главной функции окна. На первой картинке показан момент непосредственно перед инструкцией вызова функции: все параметры уже помещены в стек. Вторая картинка изображает момент непосредственно после исполнения инструкции вызова функции: мы находимся "внутри" функции. В стек только что был помещен адрес возврата. Для создания фрейма стека помещаем в него содержимое регистра EBP, затем значение ESP копируем в EBP. В дальнейшем значение ESP может меняться, в EBP же "замораживается" то состояние стека, которое было в самом начале функции.
Этим и можно воспользоваться для получения доступа к параметрам. На следующем рисунке показан механизм этого процесса. Текущее значение в EBP является адресом, по которому в стеке сохранено старое значение EBP. По смещению +4 от этого адреса будет находиться адрес возврата из функции, а после него - параметры, которые были помещены в стек перед вызовом функции. Параметр, который мы помещали в стек первым, будет иметь наибольшее смещение (в данном случае, +14h).
Получив доступ к параметрам, мы можем использовать их для своих целей. Действия, которые совершает главная процедура окна, определяются кодом полученного сообщения. Поэтому нам нужно начать с анализа именно этого параметра (находящегося в стеке по адресу EBP+0Ch), и предпринимать те или иные действия в зависимости от его значения. А для этого придется изучить инструкции проверки условий и условных переходов.
На ассемблере группа инструкций для сравнения значений обозначается мнемоникой CMP. В данном случае код сообщения представляет собой обычное число, поэтому нам будет нужно использовать вариант инструкции, сравнивающий значение в памяти с непосредственным значением. Она имеет вид, приведенный на следующем рисунке.
Сразу обращаем внимание, что опкод содержит биты s и w. Бит w определяет размер сравниваемого операнда в памяти - 1 байт или 4. Поскольку сравниваемые операнды всегда должны иметь одинаковый размер, это определяет и длину непосредственного значения. Однако, в случае, когда s = 1 (и w = 1), в качестве непосредственного значения в инструкцию записывается лишь 1 байт, который затем расширяется с учетом знака до 4 байт. Непосредственное значение в инструкции всегда располагается в самом конце, после всех прочих полей.
После байта ModR/M, в зависимости от значения поля Mod, могут следовать 1 или 4 байта смещения, значение которых добавляется к значению закодированного в R/M регистра для формирования адреса памяти (где находится сравниваемое число). При Mod = 00 в соответствующем регистре содержится полный адрес; при Mod = 01 к значению регистра добавляется 1 байт смещения (с учетом знака); при Mod = 10 к значению регистра добавляются 4 байта смещения.
В нашем случае к адресу в EBP нужно добавить смещение 0Ch - для него достаточно одного байта, откуда имеем: Mod = 01, R/M = 101 (адрес в EBP). В стеке все хранящиеся значения являются 32-разрядными, поэтому w = 1. Значение кода сообщения будем сравнивать с 81h - это самое первое сообщение (WM_NCCREATE), которое получает окно при своем создании. 81h (десятичное 129) не укладывается в диапазон представимых в виде 1 байта знаковых значений (от -128 до +127), поэтому "сократить" его с 4 до 1 байта не удастся - бит s = 0, а непосредственное значение придется кодировать четырьмя байтами. С учетом всего этого получаем инструкцию:
10000001 01111101 00001100 10000001 00000000 00000000 00000000, или 81 7D 0C 81 00 00 00 (h).
Рассмотрим теперь механизм работы этой команды. Инструкция сравнения делает "пробное" вычитание второго операнда из первого. Значения операндов при этом не изменяются; зато, как и в случае многих других инструкций, меняются отдельные поля регистра флагов EFLAGS, о котором мы упоминали в самой первой статье. Настало время рассмотреть его подробнее.
Отдельные поля (биты) регистра EFLAGS служат в качетве своего рода переключателей, используемых другими инструкциями (в частности, инструкциями условных переходов) для запуска тех или иных действий. Широко используются т.н. флаги состояния, которые приведены на следующем рисунке.
Инструкция сравнения в зависимости от полученного результата операции изменяет все эти 6 флагов. В данном случае нас интересует состояние одного флага, а именно: флага нуля. Если сравниваемые величины равны, этот флаг будет установлен (1), если нет - сброшен (0).
Изменить ход выполнения программы в зависимости от значений флагов состояния позволяют инструкции условного перехода, имеющие следующий общий формат:
0111 ttt n <1 байт смещения>
Где ttt является кодом состояния определенных флагов (условием), а n указывает, нужно использовать само условие (при n = 0) или его отрицание (при n = 1). При выполнении условия (или его отрицания, если n = 1) к значению EIP прибавляется (с учетом знака) следующий за опкодом 1 байт смещения. В результате осуществляется переход на исполнение команд в другом месте. Если условие не выполняется - ничего не происходит, выполнение продолжается со следующей инструкции (как будто команды условного перехода не было).
Как видим, эта инструкция действует подобно короткой инструкции безусловного перехода, за тем исключением, что "работает" она избирательно - лишь при заданном состоянии определенных флагов. Коды состояния флагов приведены в следующей таблице:
КодФлагиУсловие 000OF=1Переполнение 001CF=1Перенос 010ZF=1Нулевой результат 011CF=1 или ZF=1Нулевой результат или перенос 100SF=1Отрицательный результат 101PF=1Четный паритет в младших 8 битах 110SF != OFСравнение со знаком: меньше 111ZF=0 или SF != OFСравнение со знаком: меньше или равно Таблица отражает работу команды на низком уровне. Использование состояния различных флагов, их комбинаций и создаваемые этим условия мы будем изучать по мере необходимости. Сейчас не будем углубляться в детали, рассмотрим лишь конкретный пример.
Допустим, два сравниваемых операнда равны. В результате вычитания второго операнда из первого будет установлен флаг нуля (ZF=1). Мы можем составить инструкцию, которая осуществляет переход при этом условии - код условия (ZF=1) 010, n = 0, получаем опкод 01110100 (74h), за которым следует 1 байт смещения. В то же время можно составить противоположную инструкцию - которая не делает перехода при этом условии, а делает его при ZF=0 (т.е. когда операнды не равны); для этого нужно всего лишь установить бит n = 1: 01110101 (75h).
У нас теперь есть необходимый теоретический минимум для модернизации нашего приложения путем добавления в него собственной процедуры окна. Пока она будет просто выводить MessageBox в ответ на сообщение Windows 81h (в самом начале создания окна). Скопируем созданные в прошлый раз файлы в новый рабочий каталог. Чтобы переделок было не слишком много, заменим в файле "rdata.txt" строку "DefWindowProc" на "MessageBoxA", дополнив ее нужным числом нулей (для сохранения длины прежнего названия) - благо что эта функция из того же модуля (User32.dll):
db 0 0 "TranslateMessage" 0 0 db 0 0 "MessageBoxA" 0 0 0 0 0 db 0 0 "RegisterClassExA" 0
Теперь именно ее адрес окажется в поле IAT 40201Ch. В файле "code.txt" найдите и удалите следующие строки:
; скопировать в EAX адрес функции DefWindowProcA ; (из поля IAT(2) с адресом 40201Ch) db a1 1c 20 40 0 ; скопировать адрес функции из EAX ; в поле структуры WNDCLASSEX с адресом 403048h db a3 48 30 40 0
У нас будет своя главная функция окна - копировать адрес импортированной функции не нужно. Саму же процедуру окна разместим в конце секции кода (чтобы узнать адрес процедуры, удобно использовать предварительный "черновой" проход в режиме ассемблирования). Итак, составляем функцию, добавляя код в конце файла code.txt после вызова ExitProcess:
; вызов ExitProcess (по адресу в IAT(1) 402004h) db ff 15 4 20 40 0 ;----------------------------------------------- ; Процедура окна
Сначала - на входе функции - сохраняем в стеке значение регистра EBP. Вспоминаем инструкцию копирования в стек значений регистров:
; создание фрейма стека db 55
Затем копируем ESP в EBP: инструкция с использованием байта ModR/M, причем Mod = 11 (оба операнда - регистры). В зависимости от состояния бита d, команду можно закодировать двумя способами; выберем этот:
db 89 e5
Теперь сравниваем параметр кода сообщения с числом 81h - эту инструкцию уже разбирали:
; сравнить значение в [EBP+0Ch] c 81h db 81 7d c 81 0 0 0
И сразу за ней должен следовать условный переход: если флаг нуля не установлен (операнды не равны, т.е. получено сообщение с кодом, отличным от 81h) - переход к завершению функции. А если это "наше" сообщение - выведем MessageBox; код нам уже знаком, причем используем в качестве заголовка название класса окна, а в качестве сообщения - заголовок самого окна (чтобы не возиться с лишними строками). Инструкция условного перехода (75h + смещение) должна "перепрыгнуть" именно этот участок кода.
; если не равно - перескочить 14h байтов db 75 14 ; параметры MessageBoxA db 6a 0 db 68 0 30 40 0 db 68 10 30 40 0 db 6a 0 ; вызов MessageBoxA db ff 15 1c 20 40 0
Перед выходом из функции следует восстановить значение регистра ESP, если он был изменен внутри функции (например, из-за создания в стеке пространства для локальных переменных) - для этого обычно просто копируют сохраненное в EBP значение обратно в ESP, а затем извлекают из стека старое значение EBP. Существует одна короткая инструкция для всех этих действий - C9, ее мы и используем.
; удаление фрейма стека db c9
Осталось вернуться из функции обратно по сохраненному в стеке адресу. При этом надо еще удалить из стека помещенные в него ранее параметры (в нашем случае - 4 параметра общим размером в 10h байт). Для этого просто увеличивают значение ESP на соответствующую величину. Здесь также существует специальная инструкция возврата, которая автоматически выравнивает стек:
11000010 <2 байта>
Значение следующих за опкодом 2 байтов при возвращении из функции добавляется к ESP. Нам нужно добавить 10h:
; возврат из процедуры с очисткой стека db c2 10 0 <пустая строка>
Конец файла обычный; после процедуры не забудьте оставить пустую строку.
m 1000 l 200 100 w q
После "чернового" прохода узнаем смещение начала нашей функции (1080h); полученный адрес нужно вставить в структуру WNDCLASSEX в файле data.txt:
; (403048h) адрес главной функции окна db 80 10 40 0
В заголовок никаких изменений вносить не требуется. Собираем файл и запускаем его. Если ошибок не было, сразу появится окно сообщения. А вот после щелчка на "OK" поведение системы станет странным: в определенной области экрана указатель мыши начнет принимать ждущую форму, но окно нашего приложения так и не появится. Мы как бы опять вернулись к одному из предыдущих зацикленных приложений, которое не обрабатывало сообщений. В данном случае наша главная функция окна обрабатывает лишь самое первое сообщение, игнорируя все последующие. В результате окно не создается - т.к. для его создания должны быть специальным образом обработаны некоторые сообщения, посылаемые окну при его создании. Если процедура окна не обрабатывает эти (и вообще любые другие) сообщения самостоятельно, она должна переправлять их системной процедуре окна по умолчанию (DefWindowProc), которую мы в этот раз не использовали.
Но это мы оставим уже на следующий раз. Сегодня было много нового материала; мы будем двигаться небольшими шажками. Это позволяет почувствовать вкус бесконечности.