Win32 в машинных кодах

         

Класс окна


До сих пор все создаваемые нами окна использовали стандартные системные классы окон. Однако практически каждое самостоятельное приложение создает и регистрирует свои собственные классы (хотя бы для главного окна), дающие возможность реализовать уникальные особенности именно этого приложения. Настало время и нам создать собственный класс окна.

Для регистрации класса окна необходимо заполнить специальную структуру - WNDCLASSEX, в которой собираются вместе все необходимые для создания класса сведения. Структура имеет 12 полей, каждое размером 4 байта:

  1. размер данной структуры в байтах (30h);
  2. флаги, указывающие стили класса;
  3. адрес главной функции окна (по традиции ее называют также главной процедурой окна);
  4. количество дополнительных байтов класса (эти байты, если имеются, следуют непосредственно за данной структурой);
  5. количество дополнительных байтов окна (эти байты, если имеются, следуют после внутренней структуры окна);
  6. описатель экземпляра приложения. Это должно быть то приложение, в адресном пространстве которого размещена главная функция окна (поскольку одна и та же функция используется для всех окон данного класса);
  7. описатель ресурса значка, отображаемого в строке заголовка окна и на панели задач. Для получения описателя ресурс значка должен быть предварительно загружен;
  8. описатель ресурса курсора, ассоциированного с данным окном. При попадании указателя мыши в рабочую область окна система автоматически меняет форму курсора на данную. Ресурс курсора также должен быть предварительно загружен;
  9. описатель кисти, использующейся при создании фона окна. Объект кисти должен быть предварительно создан; либо должно использоваться число, соответствующее одному из предопределенных системных цветов;
  10. адрес строки с именем ресурса меню. Данное меню будет использоваться в создаваемом окне по умолчанию, если в функции CreateWindowExA не предусмотрено использование другого меню. Если это поле равно 0, у класса окна нет меню по умолчанию;
  11. адрес строки с именем класса. Это имя используется функцией CreateWindowExA при создании окна данного класса;
  12. описатель мелкого значка, связанного с данным окном. Если его нет, мелкий значок ищется через описатель ресурса значка.


Для начала в большинстве полей структуры можно оставить нулевое значение. В обязательном порядке должны быть заполнены лишь поля размера (всегда 30h), описателя приложения, имени класса и адреса главной процедуры окна. Мы будем постепенно знакомиться с полями данной структуры, проводя с ними эксперименты. Главная процедура окна сама по себе представляет собой отдельную большую тему, поскольку фактически вся функциональность нашего приложения определяется именно этой процедурой. Пока мы просто используем предоставляемую системой процедуру окна по умолчанию (DefWindowProcA), которую необходимо импортировать из модуля User32.dll. Там же расположена и другая нужная нам функция - RegisterClassExA, которая используется для регистрации заполненной нами структуры. Таким образом, нам придется добавить в созданный ранее файл rdata.txt две новые функции. С учетом всего этого, файл rdata.txt будет выглядеть следующим образом:

n rdata.bin r cx 200 f 2000 l 200 0 a 2000 ; 1-я IAT (для Kernel32.dll) ; GetModuleHandleA db a4 20 0 0 ; ExitProcess db b8 20 0 0 db 0 0 0 0 ; 2-я IAT (User32.dll) ; CreateWindowExA db c6 20 0 0 ; GetMessageA db d8 20 0 0 ; DispatchMessageA db e6 20 0 0 ; TranslateMessage db fa 20 0 0 ; DefWindowProc db e 21 0 0 ; RegisterClassExA db 20 21 0 0 db 0 0 0 0 ; таблица поиска для Kernel32.dll ; GetModuleHandleA db a4 20 0 0 ; ExitProcess db b8 20 0 0 db 0 0 0 0 ; таблица поиска для User32.dll ; CreateWindowExA db c6 20 0 0 ; GetMessageA db d8 20 0 0 ; DispatchMessageA db e6 20 0 0 ; TranslateMessage db fa 20 0 0 ; DefWindowProcA db e 21 0 0 ; RegisterClassExA db 20 21 0 0 db 0 0 0 0 ; Таблица импорта: 2 записи + завершающая (0) ; запись для Kernel32.dll ; смещение таблицы поиска db 28 20 0 0 db 0 0 0 0 0 0 0 0 ; смещение строки "Kernel32.dll" db 8c 20 0 0 ; смещение IAT(1) db 0 20 0 0 ; запись для User32.dll ; смещение таблицы поиска db 34 20 0 0 db 0 0 0 0 0 0 0 0 ; смещение строки "User32.dll" db 99 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 db 0 0 "DispatchMessageA" 0 0 db 0 0 "TranslateMessage" 0 0 db 0 0 "DefWindowProcA" 0 0 db 0 0 "RegisterClassExA" 0



m 2000 l 200 100 w q

Двойные нули после строк, как и ранее, предназначены для выравнивания по четным адресам. Для удобства выпишем здесь адреса полей IAT, в которых после загрузки приложения на исполнение будут находиться адреса соответствующих функций (это не адреса самих функций - это адреса, по которым будут размещены адреса этих функций!)

  • GetModuleHandle - 402000h ExitProcess - 402004h;
  • CreateWindowExA - 40200Ch GetMessageA - 402010h;
  • DispatchMessageA - 402014h;
  • TranslateMessage - 402018h;
  • DefWindowProcA - 40201Ch;
  • RegisterClassExA - 402020h.


Теперь можем заняться секцией данных. Как и ранее, там будут две строки - имя класса окна и заголовок окна, за ними - место для структуры MSG, а за ней расположим структуру WNDCLASSEX. Обратите внимание, что необязательно располагать данные вплотную друг к другу (в данном случае) - нам просто нужно следить, чтобы они не перекрывали друг друга, и знать их адреса в памяти (в т.ч. и адреса отдельных полей структур). Поэтому может оказаться удобным и здесь использовать "два прохода" в режиме ассемблирования, чтобы нужные смещения были вычислены автоматически. Я приведу уже готовый файл data.txt (нужные адреса указаны в скобках):

n data.bin r cx 200 f 3000 l 200 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 0 0 0 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 200 100 w q

Не забудьте, что пустые строки должны быть на своих местах - их нельзя удалять. Значения для размера структуры, адреса имени класса, а также фона окна уже указаны в секции данных. Однако, значения для еще двух обязательных полей - описатель экземпляра приложения и адрес главной процедуры окна - будут известны лишь после загрузки программы на исполнение, и мы должны предусмотреть инициализацию соответствующих полей (по адресам в памяти 403048h и 403054h) в секции кода.



Как получить описатель приложения, мы уже знаем - он будет возвращен в регистре EAX после успешного вызова GetModuleHandle. Это значение нужно скопировать из EAX по адресу памяти 403054h. Вспомним, что для копирования данных между регистром EAX и памятью используется инструкция с опкодом 101000dw и четырьмя байтами адреса памяти (это мы рассматривали в самой первой статье). В данном случае направление копирования - из EAX в память (d = 1), используется полный регистр (w = 1); вся инструкция будет иметь вид: A3 54 30 40 00.

Обратите внимание на особенность главной процедуры окна: хотя в данном конкретном случае мы используем импортируемую функцию, ее будет вызывать сама система, а не мы. Поэтому передавать ей параметры нам не придется; нужно лишь указать ее адрес в соответствующем поле структуры WNDCLASSEX (по адресу 403048h). Получить же этот адрес (импортированный) можно из второй IAT по адресу 40201Ch. (Если бы мы сами написали эту процедуру, мы знали бы ее адрес непосредственно). Таким образом, нам нужно скопировать значение из одного места в памяти (по адресу 40201Ch) в другое (по адресу 403048h). Поскольку нет непосредственной инструкции, производящей эту операцию, придется воспользоваться посредником в виде того же регистра EAX, и использовать две последовательные команды: копируем значение из [40201Ch] в EAX (A1 1C 20 40 00), затем из EAX в [403048h] (A3 48 30 40 00).

Теперь структура WNDCLASSEX инициализирована, и можно передать ее адрес функции RegisterClassExA (этот аргумент у функции единственный): 68 40 30 40 00 (поместить адрес в стек); FF 15 20 20 40 00 (вызвать функцию по адресу в IAT). После успешного завершения этой функции новый класс окна зарегистрирован, и мы можем использовать его имя ("MYCLASS" - можно было использовать и другое имя) в вызове CreateWindowExA. Дальнейший код нам уже известен.

Единственный момент - описатель экземпляра приложения теперь находится в памяти, а не в EAX, поэтому нужно использовать инструкцию косвенного помещения значения в стек (по адресу памяти). Ее мы рассматривали в статье "Простейшее приложение": 11111111 Mod 110 R/M. Используем непосредственный 4-байтный адрес после байта ModR/M (R/M = 101 при Mod = 0): FF 35 54 30 40 00. Полностью файл code.txt будет выглядеть следующим образом:



n code.bin r cx 200 f 1000 l 200 0 a 1000 ; параметр GetModuleHandleA = 0 db 6a 0 ; вызов GetModuleHanldeA (по адресу в IAT(1) 402000h) db ff 15 0 20 40 0 ; скопировать описатель из EAX по адресу 403054h db a3 54 30 40 0 ; скопировать в EAX адрес функции DefWindowProcA ; (из поля IAT(2) с адресом 40201Ch) db a1 1c 20 40 0 ; скопировать адрес функции из EAX ; в поле структуры WNDCLASSEX с адресом 403048h db a3 48 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 ; параметр 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 ; параметр ExitProcess (код завершения = 0) db 6a 0 ; вызов ExitProcess (по адресу в IAT(1) 402004h) db ff 15 4 20 40 0

m 1000 l 200 100 w q

В файле "header.txt" необходимо изменить лишь значение смещения таблицы импорта:

; Здесь начинается первый элемент каталога: ; смещение таблицы экспорта (4 байта) db 0 0 0 0 ; размер таблицы экспорта (4 байта) db 0 0 0 0 ; Второй элемент каталога: ; смещение таблицы импорта (4 байта) db 50 20 0 0 ; размер таблицы импорта (4 байта) db 3c 0 0 0

Имя исполняемого файла можно изменить, скажем, на "class.exe" (в файле make.bat). Строим приложение и запускаем его.

Мы получили почти нормальное окно. Поэкспериментируйте с ним сами. "Почти нормальное" потому, что при его закрытии приложение все равно не завершается - его опять нужно "прибивать" из менеджера задач. Чтобы программа завершалась при закрытии его главного окна, нам придется написать собственную главную функцию окна и реализовать в ней эту возможность. Но этим мы займемся уже в другой раз.


Содержание раздела