Главная функция окна
Сегодня мы впервые будем создавать собственную функцию, поэтому освежим в памяти особенности функций.
Функция - это некий код, который находится где-то по другому адресу от того места, из которого вызывается. Поэтому адрес возврата сохраняется в стеке. Кроме того, мы уже работали с передаваемыми функции данными - ее параметрами - и знаем, что они тоже помещаются в стек. А значит, при возвращении из функции мы должны компенсировать изменения в стеке, иначе он будет несбалансированным.
Процедура окна - это функция, которая вызывается самой системой; поэтому она должна удовлетворять соглашениям вызова функций 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 байт смещения. В результате осуществляется переход на исполнение команд в другом месте. Если условие не выполняется - ничего не происходит, выполнение продолжается со следующей инструкции (как будто команды условного перехода не было).
Как видим, эта инструкция действует подобно короткой инструкции безусловного перехода, за тем исключением, что "работает" она избирательно - лишь при заданном состоянии определенных флагов. Коды состояния флагов приведены в следующей таблице:
000 | OF=1 | Переполнение |
001 | CF=1 | Перенос |
010 | ZF=1 | Нулевой результат |
011 | CF=1 или ZF=1 | Нулевой результат или перенос |
100 | SF=1 | Отрицательный результат |
101 | PF=1 | Четный паритет в младших 8 битах |
110 | SF != OF | Сравнение со знаком: меньше |
111 | ZF=0 или SF != OF | Сравнение со знаком: меньше или равно |