Віконне додаток на асемблері | Блог по Windows

Надумав я тут написати невелику утиліту .. і зрозумів, що писати-то я і не вмію. Сміялися всім селом !! 🙂 Якщо розглядати питання по суті, то сьогодні ми розкриємо деякі особливості синтаксису мови асемблер в світлі використання компілятора, і наведемо типовий шаблон віконного програми на асемблері, а так само виконаємо розбір структури для подальшого використання в якості базису в різного роду проектах. Бути може, коли-то стаття і стане ланкою в циклі по вивченню програмування на мові Асемблер під Windows, але на даний момент вона є відокремлений матеріал.

Не дивлячись на те, що дана публікація представляє з себе посібник для початківців, вивчення наведеної тут теорії вимагає хоча б базового рівня знань мови Асемблер.

Я спробував до певної міри деталізувати невеликий накопичений досвід, щоб читач будь-якого рівня підготовки зміг побачити весь діапазон напрямків, необхідних для більш глибокого вивчення особливостей мови Асемблера при розробці додатків під операційну систему Windows, якщо з’явиться бажання подальшого просування. Будуть розглянуті основні (базові) директиви асемблера FASM, які дозволяють істотно впливати на структуру виконуваного файлу програми. Деякі з наведених розділів цілком могли б дорости до розміру самостійної статті, проте поки подібна структура не створена, інформація буде приводитися тут. Темою даної статті стане створення найпростішого графічного віконного програми на асемблері для Windows, тому як дана категорія додатків є найбільш поширеною, відповідно і затребуваною.

Віконне додаток Windows – це клас додатків, що використовують для взаємодії з користувачем елементи графічного інтерфейсу, тобто об’єкти типу: вікна, кнопки, поля введення, елементи контролю і багато інших. За допомогою пристроїв введення (клавіатуру / миша / тачпад та інші), користувач має можливість взаємодіяти з об’єктами віконного програми: переміщати, активувати, прокручувати. Прикладом даного класу є класичні графічні додатки, що працюють з вікнами.

За основу для вивчення я взяв стандартний шаблон 32-бітного віконного програми на асемблері з ім’ям template.asm, що поставляється в складі пакета FASM і розміщується в піддиректорії \ EXAMPLES \ TEMPLATE \, і злегка модифікував його для деякої наочності. Для початку представимо вихідний код програми:

Assembly (x86)

format; Формат PE. Версія GUI 4.0.entry start; Точка входаinclude ‘% include% \ win32a.inc’; Робимо стандартне включення опісателей._style VISIBLEDLGFRAMESYSMENU; Стилі вікна. Еквіваленти повинні задаватися ДО основного коду; === сегмент коду ====================================== ====================== section ‘.text’readableexecutable start invoke GetModuleHandle; Отримаємо дескриптор додатки. mov hInstance; Збережемо дескриптор додатки в поле структури вікна (wc) invoke LoadIconASTERISK; Завантажуємо стандартну іконку IDI_ASTERISK mov hIcon; Збережемо дескриптор іконки в поле структури вікна (wc) invoke LoadCursorARROW; Завантажуємо стандартний курсор IDC_ARROW mov hCursor; Збережемо дескриптор курсора в поле структури вікна (wc) mov lpfnWndProcWindowProc; Задамо покажчик на нашу процедуру обробки вікна mov lpszClassNameclass; Задамо ім’я класу вікна mov hbrBackgroundCOLORWINDOW; Задамо колір кисті invoke RegisterClass; Зареєструємо наш клас вікна test; Перевіримо на помилку (eax = 0). jz error; Якщо 0, то помилка – стрибаємо на error. invoke CreateWindowExclasstitlestylehInstance; Створимо екземпляр вікна на основі зареєстрованого класу. в eax повертає дескриптор вікна. test; Перевіримо на помилку (eax = 0). jz error; Якщо 0, то помилка – стрибаємо на error. mov wHMain; збережемо дескриптор створеного вікна; — цикл обробки повідомлень ————————————— ——— msg_loop invoke GetMessage; Отримуємо повідомлення з черги повідомлень додатку or; Порівнює eax з 0 jz; Якщо 0 то прийшло повідомлення WM_QUIT – виходимо з циклу очікування повідомлень, якщо не 0 – продовжуємо обробляти чергу msg_loop_2 invoke TranslateMessage; Додаткова функція обробки повідомлення. Конвертує повідомлення клавіатури відправляє їх назад в чергу. invoke DispatchMessage; Пересилає повідомлення відповідним процедурам обробки повідомлень (WindowProc …). jmp short; Зациклюємось error invoke MessageBoxerrorICONERROR; Виводимо вікно з помилкою end_loop invoke ExitProcesswParam; Вихід з програми.; — процедура обробки повідомлень вікна (функція вікна, віконна процедура, віконна функція) WindowProc wParamlParam push; збережемо все регістри cmp DESTROY; Перевіримо на WM_DESTROY je wmdestroy; на обробник wmdestroy cmp CREATE; Перевіримо на WM_CREATE je wmcreate; на обробник wmcreatedefwndproc invoke DefWindowProcwParamlParam; Функція за умовчанням. Обробляє всі повідомлення, які обробляє наш цикл. jmp finishwmcreate xor jmp finishwmdestroy; Оброблювач повідомлення WM_DESTROY. Обов’язковий. invoke PostQuitMessage; Посилає повідомлення WM_QUIT в чергу повідомлень, що змушує GetMessage повернути 0. Надсилається для виходу з програми. Надсилається тільки основним вікном. xor; Якщо наша процедура вікна обробляє будь-яке повідомлення, то вона повинна повернути в eax 0. Інакше програма поведеться непредсказуемо.finish pop; відновимо все регістри ret; === сегмент даних ======================================== ================== section ‘.data’readablewriteable_class’ FASMWIN32 ‘; Назва власного класса._title ‘Win32 program template’; Текст в заголовку окна._error ‘Startup failed.’ ; Текст ошібкіwHMain; дескриптор вікна WNDCLASS; Структура вікна. Для функції RegisterClass; Структура системного повідомлення, яке система посилає нашій програмі.; === таблиця імпорту ================================== ======================= section ‘.idata’importreadablewriteablelibrary kernel32’KERNEL32.DLL’32’USER32.DLL’include’ api \ kernel32.inc’include ‘api \ user32.inc’

Як Ви бачите, я включив в нього власні невеличкі коментарі, які, втім, будуть розгортатися по ходу викладу. Перед тим, як приступити до опису структури нашої програми, давайте кілька слів скажемо про архітектуру системи Windows.

Ідеологія програмування під Windows

Для взаємодії з користувачем (обміну даних), коду користувацької програми в операційній системі Windows не потрібно робити викликів будь-яких спеціалізованих функцій, які очікують введення (натискання клавіш клавіатури / миші, введення символів із дисплея) від користувача, як це практикувалося за часів MSDOS . Фактично в Windows відсутні функцій, які зчитують рядок символів з клавіатури або очікують введення будь-якого числового значення.

Додаток в Windows “пасивно”, оскільки в ході функціонування воно чекає коли операційна система приділить їй увагу.

Підхід до програмування в середовищі Windows змінився і тепер основна концепція програмування орієнтована на так звані події. Це означає, що ядро ​​системи стежить за апаратної / програмної активністю, і у відповідь генерує спеціальні повідомлення, які потім передає (через спеціальні системні механізми) додатків, які очікують настання цих подій. Іншими словами, можна стверджувати що додатки в Windows управляються подіями. З подіями асоціюється будь-яка призначена для користувача / системна активність: переміщення вікон, натискання клавіш клавіатури / миші, зміни стану буфера обміну, зміни в апаратурною конфігурації, зміни статусу енергоспоживання, зміна значень таймерів і інша. Тому, коли відбувається будь-яка подія (до яких відноситься і будь-які дії користувача), система сама “надає” для призначеного для користувача додатки вхідні дані, і робить вона це за допомогою передачі повідомлень. А прикладна програма, в свою чергу, складається з обробників вхідних повідомлень, які неналежним чином реагують на них.

Таким чином додаток в Windows має забезпечувати той чи інший функціонал при надходженні різного роду повідомлень.

Як тільки програма завершує обробку події, управління повертається ядру системи. Це кардинально змінює підхід до написання програм, оскільки в MSDOS програма сама контролювала власні дії / події, тепер же операційна система передає користувальницької програмі керуючі повідомлення, які активують ті чи інші функції обробки.

Програмування під Windows – це, в основі своїй, програмування обробників повідомлень.

Але крім обробки введення або реакції на інші входять системні повідомлення, прикладної програмі потрібно виконувати і інші дії над об’єктами операційної системи. З метою забезпечення доступу для користувача програм до всього спектру виконуваних компонентів Windows, надається так званий програмний інтерфейс додатків (API). Це означає, що весь функціонал операційної системи доступний через функції, і щоб програмісту щось зробити – треба викликати функцію відповідного призначення.

API (інтерфейс прикладного програмування, Application Programming Interface) – різноманіття системних функцій, за допомогою яких додаток (процес) може взаємодіяти з операційною системою Windows.

Заголовок

Розгляд вихідного коду почнемо ми з заголовка, або, якщо висловитися точніше – “так званого заголовка”. Я візьму на себе сміливість подібним чином іменувати область вихідного коду, що починається безпосередньо з першого символу і йде до директиви оголошення першої секції. Початок області не позначається спеціальними директивами, це просто початкова частина лістингу програми. У цьому місці (у нас рядок 1) може використовуватися директива format, яка призначається для вказівки формату результуючого виконуваного файлу, який отримуємо на виході після компіляції. Формати можна вказати наступні:

ім’я розшифровка опис
MZ ark bikowski формат 16-бітних виконуваних файлів з розширенням для ОС MSDOS
PE PE64 ortable xecutable формат 32/64-бітних виконуваних файлів з розширенням для ОС Windows
COFF MS COFF MS64 COFF ommon bject ile ormat формат об’єктного файлу, що містить проміжне представлення коду програми, призначений для об’єднання з іншими об’єктними файлами (проектами / ресурсами) з метою отримання готового виконуваного модуля.
ELF ELF64 xecutable and inkable ormat формат виконуваних файлів систем сімейства UNIX. Об’єктний файл (.obj) для компілятора gcc.
ARM dvanced ISC achine формат виконуваних файлів під архітектуру ARM (?)
Binary файли бінарної структури. Що задасте, то і збереться. Наприклад, виставивши зміщення 100h (org 100h) від початку, можна отримати старий-добрий-файл під MSDOS. Формат має ряд аналогічних застосувань для створення довільних бінарних додатків або файлів даних.

Другим параметром (після вказівки формату виконуваного файлу) директиви format може вказуватися тип підсистеми для створюваного додатка:

ім’я опис
GUI Графічне (віконне) додаток. Вихідний виконуваний файл, який має на увазі створення типових віконних додатків і ініціалізацію на початковій стадії всіх відповідних бібліотек Win32 API. Вихідний виконуваний файл, у якого в структурі PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значення поля Subsystem = 2 (воно ж IMAGE_SUBSYSTEM_WINDOWS_GUI).
console Консольний додаток. Вихідний виконуваний файл, що має на увазі виконання коду в консолі, без участі віконного інтерфейсу. Вихідний виконуваний файл, у якого в структурі PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значення поля Subsystem = 3 (воно ж IMAGE_SUBSYSTEM_WINDOWS_CUI).
native Рідне / нативное додаток. Вихідний виконуваний файл, у якого в структурі PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значення поля Subsystem = 1 (воно ж IMAGE_SUBSYSTEM_NATIVE). Подібне значення поля зазвичай характерно для драйверів, бібліотек і додатків режиму ядра, яким не потрібно ініціалізація підсистеми Win32 на стадії підготовки образу до виконання.
DLL Динамічна бібліотека. Особливий формат вихідного файлу, який призначається для експорту (надання) функцій стороннім додаткам, у якого в структурі PE-заголовка IMAGE_NT_HEADERS, подструктуре IMAGE_FILE_HEADER, в полі Characteristics включений прапор IMAGE_FILE_DLL (2000h).
WDM Системний драйвер, побудований на основі моделі WDM (Windows Driver Model).
EFI EFIboot EFIruntime UEFI-додаток. Вихідний виконуваний файл, у якого в структурі PE-заголовка IMAGE_NT_HEADERS, подструктуре OptionalHeader, значення поля Subsystem = 10 | 11 | 12 | 13 (воно ж IMAGE_SUBSYSTEM_EFI_APPLICATION, IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER, IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER, IMAGE_SUBSYSTEM_EFI_ROM). Подібне значення поля потрібно для створення UEFI-додатків різних стадії / типу: завантаження, виконання і драйвера.

Роль даного параметра досить велика, оскільки саме він визначає, яка саме підсистема буде викликатися для запуску виконуваного файлу, тобто фактично визначає програмне оточення при запуску процесу. Якщо використовується тип програми, необхідно уточнювати мінімальну версію системи (у нас: 4.0), під яку створюється наш виконуваний модуль.
Потім, в рядку під номером 2 у нашому вихідному коді розташовується директива з ім’ям entry, яка визначає точку входу в програму.

Як аргумент директиви entry вказується мітка в коді, з якої у нас почнеться виконання коду компільованою програми. Стає очевидним, що саме на основі цієї директиви компілятор формує значення відповідних полів результуючого виконуваного PE-файла. При запуску .exe-файлу, завантажувач образів (динамічний компонувальник) створить адресний простір процесу нашого застосування, довантажити і розбере виконуваний образ, зіставивши всі необхідні сегменти з регіонами пам’яті, сформувавши інші необхідні структури, передасть управління саме за адресою, де буде розташовуватися інструкція, описана в вихідному коді міткою, зазначеної в директиві entry. У нашому випадку точку входу визначає мітка start, розташовується в сегменті коду в рядку 11.
У рядку 3 ми виявляємо директиву include, за допомогою якої в вихідний код нашої програми (в позицію знаходження директиви) включається текст зовнішнього файлу, вказаного в ній в якості параметра.

Включення дозволяє підключати необхідні програмі структури даних із зовнішніх файлів. Основний файл у нас містить код програми, а специфічні зовнішні дані, такі як системні константи, змінні і визначення макросів, розміщуються в окремих заголовних файлах. Заголовки FASM дещо відрізняються від звичних нам по мові C / C ++ тим, що не описують прототипів процедур / функцій.

У нашому випадку підключається файл% include% \ win32a.inc, який, в свою чергу, містить посилання на інші підключаються файли, що містять визначення ключових структур, необхідних для компіляції нашої програми: макросів, типів даних, констант, системних структур. Без включення цього файлу у нас просто не пройде процес компіляції нашого вихідного коду, тобто виконуваний файл не буде створено (не збере).

Як Ви вже напевно помітили, в параметрі директиви include використовується змінна шляху% include%. У моєму випадку я створив її для зручності вказівки шляху до піддиректорії \ INCLUDE основний директорії FASM. Ви теж можете задати повний шлях до каталогу дистрибутива в налаштуваннях операційної системи, через розділи Змінні середовища користувача / Системні змінні настройки Змінні середовища.

Безпосередньо за підключенням зовнішнього файлу, в рядку 5 у нас розташовується оголошення внутрішньої константи _style, яка використовується в нашому коді і приймає значення WS_VISIBLE + WS_DLGFRAME + WS_SYSMENU, що визначає зовнішній вигляд вікна. Ключові слова WS_VISIBLE, WS_DLGFRAME, WS_SYSMENU є не чим іншим, як символічними іменами глобальних констант, або бітових прапорів (містяться в зовнішніх файлах включень, що підключаються на етапі компіляції), визначених у системі Windows і інакше іменованих стилями вікна.

Зауважте, що константи об’єднуються операцією + (Логічне / побітовое АБО, or), З метою отримати суму значень декількох властивостей, тобто застосувати їх сукупність.

З можливими варіантами стилів можна ознайомитися на відповідній сторінці, яка описує.

секції

Безпосередньо за визначальними заголовок директивами, слід вихідний код, розділений на області, звані секціями. Придивіться до наведеного вихідного коду і ви побачите що весь лістинг фактично розділений на своєрідні логічні блоки, що починаються з директиви section і іменовані секціями. Поряд із заголовком, секції є невід’ємними складовими частинами як файлу вихідного коду, так і виходить на виході у компілятора виконуваного PE-файла.

Секція – область (блок) в структурі виконуваного PE-файла, що служить для поділу всього масиву даних програми на логічні частини, які передбачають зберігання коду / даних, об’єднаних єдиним призначенням / методом доступу. У виконуваному файлі кожна секція характеризується власним ім’ям, зміщенням в виконуваному файлі, віртуальним адресою для копіювання вмісту, розміром і атрибутами. Всі ці параметри визначають спосіб завантаження секції, спосіб формування сторінок віртуальної пам’яті і управління іншими параметрами даних на стадії завантаження образу.

Використання секцій регламентовано структурою формату виконуваних PE-файлів, що використовуються в системі Windows. Саме специфікація формату PE визначає вимоги до наявності певних структур в виконуваних файлах і наказує використання тих чи інших секції для поділу інформаційних блоків. Відразу після директиви section в одинарних лапках (апостроф) задається ім’я (назва) секції і ряд параметрів: тип секції, прапори (атрибути) секції.

Найменування секції може бути довільним (але не більше 8 символів) або бути відсутнім зовсім, це ніяк не позначається на процесі компіляції і виконання програми, оскільки ключовим для компілятора є тип секції. Винятком є, хіба що, секція ресурсів з ім’ям .rsrc.

Прапори можуть набувати наступних значень: code, data, readable, writeable, executable, shareable, discardable, notpageable, на додаток до них можуть використовуватися специфікатор секції даних, такі як export, import, resource, fixups, які визначають структуру (будова) секції. Типи секцій, прапори і їх комбінації я звів в таблицю:

Найменування позначення FASM опис
секція коду code Секція, в якій пропонується розміщувати виконуваний код програми. Зазвичай в цю секцію включається весь асемблерний код, фактично реалізує логіку роботи програми.
секція даних data У даній секції пропонується розміщувати всі динамічні (змінні) дані (локальні / глобальні змінні, рядки, структури і т.п.), які активно використовуються в коді програми.
секція імпорту import Поширена назва: Таблиця імпорту. У даній секції розміщуються рядкові літерали (найменування) бібліотек і таблиці підключаються (імпортованих) з цих бібліотек віртуальних функцій, які потрібні нашій програмі для роботи. Функції можуть імпортувати по найменуванню (символічне ім’я) або по ордіналов (числовий ідентифікатор).
секція ресурсів resource Дана секція містить дані, які перетворюються в виконуваному файлі в багаторівневе двоичное дерево (індексований масив), побудоване певним чином для прискорення доступу до даних. Ці дані називаються ресурсами, доступні з коду через спеціальні ідентифікатори, статичні, описують різні використовувані в програмі об’єкти: меню, діалоги, іконки, курсори, картинки, звуки та інше.
Таблиця переміщень (таблиця налаштувань адрес, релокації) fixups Релокації – набір таблиць (fixup blocks) із зсувами (Relevant Virtual Addresses, RVA) від базового адреси завантаження образу (фактично покажчиками на абсолютні адреси в коді), які завантажувач образу повинен скорегувати (виправити) в пам’яті процесу, якщо образ завантажується за адресою, відмінному від пріоритетного. Інакше (простіше) можна уявити як список елементів пам’яті, які потребують коригування при завантаженні образу в пам’яті процесу за довільним адресою. Таблиця переміщень застосовується тільки для фіксованих адрес в коді програми, тобто адрес тих інструкцій, які компілятор поставив в явному вигляді (наприклад: mov al, [01698745]).
Таблиця експорту export Секція описує експортовані нашою програмою функції. Зазвичай використовується при створенні бібліотек DLL.

Зазвичай тип секції наказує розміщувати всередині неї код / ​​дані необхідного призначення.

Однак на практиці ж це зовсім не суворе правило, оскільки як мінімум можна привести один виняток – розміщення даних в секції коду. Однак, подібне змішування коду та даних може привести до проблем з безпекою (порушення прав доступу якщо секція коди не маркована для запису), так само як і проблем кешування на рівні процесора, що може позначитися на зниженні швидкодії програми. Інших винятків із правила і прикладів я назвати не можу, оскільки просто не тестував. Щодо кількості однотипних секцій в вихідному коді можна сказати що всі вони збираються в єдину секцію на етапі компіляції вихідного коду.

Секція коду (code)

Мабуть, без перебільшення, дану секцію можна сміливо найбільш значущою, оскільки саме вона визначає всю логіку роботи створюваного нами програми. Саме в секції code міститься опис того, як саме працює і що робить наше віконне додаток на асемблері, іншими словами саме секцією коду визначається алгоритм роботи програми. Як Ви вже зрозуміли, що вивчається нами тестовий приклад досить простий і всю його логіку можна описати таким невеликим списком:

  • Отримуємо дескриптор екземпляра пов’язаних з поточною діяльністю (в контексті якого і виконується наш код);
  • Реєструємо клас вікна. Реєстрація власного класу потрібно у всіх випадках за винятком тих, коли Ви використовуєте стандартні (зумовлені, що надаються системою) типи вікон;
  • Створюємо вікно на основі тільки що зареєстрованого класу;
  • Відображаємо вікно на екрані (виклик додаткової функції, яка в нашому випадку не використовується. Це зовсім не означає, що вікно з нашого прикладу не відображається на екрані, просто воно відображається за допомогою основних функцій);
  • Оновлюємо клієнтську область вікна (в нашому випадку не використовується, тому що ми не займаємося перемальовуванням клієнтської області, наш приклад для цього занадто простий);
  • Входимо в нескінченний цикл обробки повідомлень для всіх вікон, що належать нашого процесу. В даному прикладі обробляються повідомлення тільки до одного основного вікна;
  • Повідомлення, що надходять для будь-якого з контрольованих нами вікон обробляються спеціальною функцією;
  • Виходимо з програми після натискання користувачем кнопки Закрити (X) або комбінації клавіш Alt+F4;

Візуальним результатом роботи нашої програми є висновок на робочий стіл звичайного вікна з єдиною системною кнопкою (закрити) в правому верхньому куті.

Відповідно, вся логіка нашої програми укладається в створення вікна і обробку натискання в ньому однієї-єдиної кнопки: вихід. Так само, у вікні можна побачити обрану нами типову іконку (лівий верхній кут) і вікно має задані нами розміри.
Ну а тепер саме час розібратися з алгоритмом роботи. Насамперед ми отримуємо дескриптор (handle) нашого модуля за допомогою виклику функції GetModuleHandle. Трохи відірвемося від вивчення логіки і звернемо увагу на рядок 12 виклику даної функції, тут ми вперше зустрічаємося з ключовим словом invoke. Під з цього самого моменту для новачків починається знайомство з реаліями сучасного програмування під Windows на мові асемблер. Для людей, які розбираються з мовою навіть на початковому рівні, очевидно, що такої команди в асемблері немає, але це і не команда, це макрос. Макрос invoke міститься в файлах визначення макросів \ INCLUDE \ MACRO \ PROC32.INC і \ INCLUDE \ MACRO \ PROC64.INC пакета FASM і ось його оголошення:

macroinvoke; indirectly call STDCALL procedurecommon if reverse pushd common end call

З алгоритму макросу видно, що при наявності аргументів функції, час розміщення їх в стек у зворотному порядку, слідом вже викликається сама функція. І навіщо нам це все потрібно? Для полегшення процесу розробки програми! В принципі, Ви можете і не використовувати макрос invoke, але тоді від Вас буде потрібно маса додаткової роботи: Вам доведеться самостійно заносити вхідні аргументи в стек в порядку, визначеному для тієї чи іншої функції. Справа в тому, що порядок занесення визначається так званим угодою про виклики, яке повсюдно застосовується в програмах Windows і яке наказує заносити параметри в стек строго в необхідному порядку.
Повернемося назад до основного алгоритму. В офіційній документації сказано, що функція повертає дескриптор додатки. Якщо в якості вхідного параметра функції GetModuleHandle використовується значення 0, то функція повертає дескриптор для виконуваного образу, який бере участь у створенні викликає її процесу, тобто (простіше) для того процесу, з якого вона викликана. Повертається функцією дескриптор не глобальна і не успадковані, він актуальний в контексті тільки з поточною діяльністю.

Дескриптор (описувач, handle) – це абстрактний (віртуальний) покажчик на якийсь ресурс (адреса пам’яті, відкритий файл, канал тощо). У більшості випадків це всього-лише абстракція, яка приховує від розробника якийсь фізичний ресурс, дозволяючи ядру реорганізувати апаратні ресурси зручним йому чином, абсолютно прозоро для додатків. Фактично це своєрідний індекс (який використовується на вході / виході функцій), що перетворюється всередині ядра на основі спеціальних системних таблиць відповідності у внутрішнє представлення – покажчик на якийсь об’єкт. Тому дескриптор повинен розглядатися виключно як локального значення, що має сенс в контексті API в межах поточного додатка. Не застосовувати препарат подібний механізм ми не можемо, оскільки тоді втратимо зв’язування різних структур системи один з одним. Всі об’єкти (вікна, файли, процеси, потоки, події і т.д.) і системні ресурси в Windows описуються за допомогою дескрипторів.

Далі у нас йде блок коду, який відповідає за реєстрацію класу вікна. Тут у нас вперше в коді з’являється структура wc (буде детально описана в секції даних), яка описує всі необхідні параметри майбутнього вікна, тому всі члени (поля) цієї структури повинні бути попередньо ініціалізовані. Наприклад, функція GetModuleHandle повертає дескриптор додатки (процесу) в регістрі eax, і ми зберігаємо його в wc.hInstance (Рядок 13), тим самим ініціалізувавши член hInstance структури wc. Потім ми завантажуємо іконку за допомогою функції LoadIcon і инициализируем дескриптор іконки для майбутнього вікна wc.hIcon. Можна використовувати призначену для користувача іконку, певну в секції ресурсів, але зазвичай, для скорочення коду і спрощення логіки, використовують один з типових, саме так і зроблено в нашій програмі. Потім завантажуємо курсор за допомогою функції LoadCursor і инициализируем дескриптор курсора wc.hCursor майбутнього вікна. Знову ж таки, тут кожен може задавати власний користувальницький курсор, або використовувати стандартний зумовлений. Зверніть увагу на те, що в структурі wc є член lpfnWndProc, в який ми записуємо адресу початку процедури обробки подій вікна WindowProc, про яку буде розказано далі. Ще инициализируется дескриптор кисті фону вікна hbrBackground. Виклики всіх цих процедур нам необхідні для заповнення структури wc, яка використовується в подальшому для реєстрації класу нашого вікна за допомогою функції RegisterClass.

Клас вікна – набір властивостей і методів (специфікація), що визначає необхідні параметри (куpсоp, іконка, адреса функції обробки повідомлень вікна та інші), відповідно до яких будуть створюватися вікна в нашому додатку.

У нашому прикладі використовується власний (призначений для користувача) клас вікна. Однак, в операційній системі Windows для розробника пропонується і кілька визначених класів вікон, які можуть бути використані в додатку через певні ключові слова. Для чого взагалі потрібна реєстрація якогось класу вікна, чому не можна створити відразу безпосередньо саме вікно? Відповідь дає концепція об’єктно-орієнтованого програмування, і клас в рамках концепції існує для того, щоб задати всі параметри майбутнього вікна в одній точці коду, адже якщо Ви на основі одного-єдиного класу будете створювати безліч вікон, то подібна стратегія повністю себе виправдає. Звідси випливає висновок:

Операційна система виводить на екран і обслуговує вікна тільки зареєстрованих класів. Відповідно, що б система дізналася про вашому власному призначеному для користувача класі – його необхідно зареєструвати.

Після реєстрації класу вікна, за допомогою системної функції CreateWindowEx, ми створюємо екземпляр вікна зареєстрованого раніше класу. Як Ви вже помітили, в нашій програмі замість типової функції CreateWindow використовується розширена версія функції по імені CreateWindowEx, що відрізняється від типової підтримкою додаткових стилів вікна (параметр dwExStyle). В якості вхідних параметрів для функції Ви повинні вказувати деякий безліч параметрів:

Найменування Тип в ASM Тип в C опис
dwExStyle DD DWORD Розширений стиль опису створюваного вікна. Містить різноманітні прикрашення, які не входять в основний опис стилю dwStyle.Список можливих значень можна подивитися в Extended Window Styles.
lpClassName DD LPCTSTR Покажчик (адреса) на рядок з ім’ям класу вікна. У нашому випадку використовується рядок _class.
lpWindowName DD LPCTSTR Покажчик на рядок з ім’ям вікна, що відображаються в заголовку вікна. У нашому випадку використовується рядок _title.
dwStyle DD DWORD Константа, що визначає стиль вікна. У нашому випадку використовується константа _style, містить бітові прапори.
x DD int Горизонтальна координата (X) лівого верхнього кута вікна. У координатах екрану.
y DD int Вертикальна координата (Y) лівого верхнього кута вікна. У координатах екрану.
nWidth DD int Ширина вікна в пікселях.
nHeight DD HCURSOR Висота вікна в пікселях.
hWndParent DD HWND Дескриптор батьківського (що породжує) вікна.
hMenu DD HMENU Дескриптор меню, використовуваного вікном. У разі, якщо вікно засноване на зумовленому системному класі вікна, воно не може містити меню, тоді параметр використовується як ідентифікатор дочірнього елемента управління.
hInstance DD HINSTANCE Дескриптор додатки (модуля), що створює дане вікно.
lpParam DD LPVOID Опціональний покажчик на додаткову структуру даних (CREATESTRUCT), що посилаються вікна. Якщо параметр заданий, то в першому повідомленні WM_CREATE параметр lParam вказує на необхідну структуру. Або ж цей покажчик може приймати значення NULL, повідомляючи, що ніяких даних за допомогою функції CreateWindow не передається.

Безпосередньо після виклику функції CreateWindowEx наше вікно з’являється на екрані. Але ж сам по собі факт відтворення вікна нам мало що дає, вікно повинно функціонувати, іншими словами отримувати призначений для користувача / системний введення. Як нам це забезпечити? А забезпечити це ми можемо за допомогою черги повідомлень.

черга повідомлень

Давайте зробимо невеликий відступ і поговоримо про одну з фундаментальних основ більшості додатків операційної системи Windows: черги повідомлень.

Повідомлення – один з основних механізмів операційної системи Windows, що забезпечує взаємодію між різними процесами і об’єктами в межах операційної системи.

Як уже зазначалося, процеси в операційній системі обмінюються між собою повідомленнями, які вдають із себе зумовлені константи, однозначно характеризують подія, що відбулася. Кожен раз, коли для нашого процесу є вхідні дані (інформація, дії користувача: повідомлення від клавіатури / миші і ПРЧ.), Ядро передає їй ці дані у вигляді повідомлень, поміщаючи їх в чергу повідомлень того чи іншого потоку (найчастіше єдиного) в рамках процесу. Тобто ядро ​​передає події з додатком у формі повідомлення, поміщаючи їх в чергу того програмного потоку, якому належить вікно, над яким в даний момент проводяться ті чи інші дії, які є джерелами подій. Ядро може генерувати повідомлення не тільки у відповідь на якісь глобальні зміни в системі, ініційовані даними додатком (зміна пулу ресурсів системних шрифтів, зміна розміру одного зі своїх вікон і ПРЧ), але і по діям над об’єктами віконного інтерфейсу (вікна, дочірні елементи ), що належать процесу додатки. Щоб не було плутанини з розумінням типів і цілей повідомлень, треба усвідомити, що повідомлення бувають:

  • синхронні (Поставлені в чергу, queued): повідомлення, що надходять в основну чергу повідомлень потоку (якому належить вікно), а потім вже, в залежності від призначення, які обслуговуються в основний черзі, або діспетчерізіруемие в відповідну процедуру обробки повідомлень вікна (віконну процедуру);
  • асинхронні (Не поставленого в чергу, nonqueued): повідомлення, що надходять безпосередньо в процедуру WindowProc відповідного вікна, минаючи чергу повідомлень потоку;

Фактично, виклик (опосередковано або безпосередньо) кодом ядра системи віконної процедури відповідного вікна, називають знаменитим “зворотним викликом” або (по-англійськи) callback’ом.
Ядро є не єдиним джерелом повідомлень, оскільки повідомлення можуть створюватися і самим призначеним для користувача додатком. Віконне додаток може генерувати повідомлення безпосередньо до своїх власних або чужих вікон з метою виконання внутрішніх завдань або для забезпечення взаємодії з вікнами інших додатків.

Кожне зумовлене системне повідомлення має розмірність в 16 байт, власний унікальний ідентифікатор і відповідне символічне значення, яке визначає категорію і призначення повідомлення.

Наприклад, існує повідомлення WM_PAINT, яке наказує цільовим вікна отрисовать власне вміст. Як видно з назви WM_PAINT, символічне значення містить в своєму імені префікс (WM), Який вказує на його категорію. Символьні значення повідомлень (WM_CREATE, WM_DESTROY, WM_PAINT і ПРЧ.) визначені в файлах стандартних включень \ include \ equates \ user32.inc / \ include \ equates \ user64.inc пакета FASM. Це зроблено за аналогією зі стандартними файлах заголовків Windows (windows.h та інші), які включаються в програми C / C ++.
На початку циклу функція GetMessage перевіряє, чи є які-небудь повідомлення від операційної системи. Особливістю даної функції є те, що вона не повертає управління в зухвалу програму, поки не з’явиться яке-небудь повідомлення, потім витягує повідомлення з черги повідомлень потоку і поміщає його в структуру з ім’ям msg типу MSG. Як ми бачимо, параметр hWnd (Другий параметр) для функції встановлено в нуль (NULL), тому витягуються всі повідомлення, адресовані будь-якого вікна, асоційованого з поточним потоком, і будь-які повідомлення для поточного потоку, чиї hwnd дорівнюють нулю (NULL). Таким чином, якщо hWnd дорівнює нулю, і віконні повідомлення або потоку обробляються.

Традиційно для Windows-функцій, результат виконання коду функції повертається в регістрі eax.

Тому в нашому коді (рядок 35) ми аналізуємо вміст даного регістра і якщо воно дорівнює 0, то це означає, що прийшло повідомлення WM_QUIT, в разі чого переходимо на мітку end_loop з подальшим виходом. У всіх інших випадках мається на увазі, що прийшло повідомлення, відмінне від WM_QUIT, і його потрібно обробити. Обробка починається з виклику допоміжної функції TranslateMessage (з аргументом у вигляді структури msg), яка призначена для доповнення (розширення) повідомлень клавіатури WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN і WM_SYSKEYUP повідомленнями WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, WM_SYSDEADCHAR, що містять ASCII-значення натиснутих клавіш. Погодьтеся, що мати справу з ASCII-значеннями простіше, ніж з scan-кодами. Якщо її виключити з циклу, то ймовірно ми не отримаємо символічних значень, а будемо задовольнятися лише скан-кодами натиснутих у вікні клавіш. Потім у нас викликається функція DispatchMessage (аргументом якої все так же є посилання на нашу структуру msg), яка відправляє повідомлення в процедуру вікна, оскільки головне її призначення розбирати повідомлення, витягнуті функцією GetMessage.

Функція DispatchMessage в коді обробки черги повідомлень потоку перевіряє, для якого саме класу вікна призначено повідомлення і викликає відповідну віконну процедуру.

Зверніть увагу, що тут виникає один тонкий момент: навіщо нам фактично дві логіки розбору черзі через GetMessage і через WindowProc, адже можна було обійтися однією, навіщо нам потрібно викликати ще окрему процедура обробки повідомлень вікна, коли можна обробити повідомлення в основному циклі? Так то воно так, але як ми вже згадували, повідомлення можуть бути синхронними і асинхронними. Синхронні повідомлення переносяться в чергу повідомлень потоку, відповідно витягуються і діспетчерізіруются вони в основному циклі обробки повідомлень: за допомогою GetMessage, а потім можуть бути відправлені в віконну процедуру через зв’язку DispatchMessage + WindowProc. Асинхронні повідомлення передаються безпосередньо вікна шляхом прямого виклику віконної процедури. Це як, невже ядро ​​щось безпосередньо викликає в призначеному для користувача коді? Я думаю, що тут все трохи інакше і “прямий” передачею (асинхронних повідомлень) займається виключно функція DispatchMessage, тому як дані повідомлення не витягуються з черги функцією GetMessage? У будь-якому випадку, віконна процедура приймає всі типи повідомлень, адресовані заданому вікна: синхронні і асинхронні. Саме тому цикл обробки повідомлень у нас виглядає так а не інакше.
Ну і, нарешті, вся ця логіка обробки повідомлень завершується в рядку 40 командою jmp, яка зациклює прийом і обробку повідомлень. Таким чином, якщо повідомлень на даний момент немає, то функція все одно чекає появи повідомлення в нескінченному циклі, виходом з якого є лише дія щодо закриття вікна.

Основна логіка роботи більшості Windows-програми з віконним інтерфейсом зводиться до роботи з повідомленнями вікна.

Код під міткою error, на яку здійснює перехід з декількох місць обробки помилкових ситуацій в нашій програмі, служить для обробки критичної ситуації, коли функції у нас по яких-небудь причин повертають помилку і подальше виконання коду програми стає безглуздим. В цьому випадку ми видаємо вікно з помилкою за допомогою функції MessageBox, а потім викликається функція ExitProcess з аргументом msg.wParam, який містить код виходу.

У разі [раптового] ​​завершення циклу обробки повідомлень, код виходу зберігається в члені wParam структури msg. Цей код виходу необхідно повернути ядру операційної системи за допомогою виклику функції ExitProcess з вхідним параметром, рівним значенню msg.wParam.

Тут виникає резонне питання, чому мітка error розташовується в основному циклі обробки повідомлень, адже логічніше було б її взагалі винести в інше місце коду. Так, це дійсно так, але тоді б нам довелося дублювати вихід з програми за допомогою функції ExitProcess, а так ми можемо використовувати вже існуючу точку виходу (використовувану в циклі), не вдаючись до дублювання коду. Виключно з цією метою логіка обробки помилки вбудована в цикл обробки повідомлень.

Процедура обробки повідомлень вікна

У вихідному коді нашого віконного програми на асемблері логіку по роботі з повідомленнями вікна забезпечує процедура WindowProc, яка ще називається віконної процедурою або віконної функцією.

Віконна (процедура) функція отримує і обробляє всі повідомлення, які стосуються вікна.

Фактично вона вдає із себе цикл обробки повідомлень, що посилаються вікна системою Windows. А вся справа в тому, що вікно це не просто якась область на екрані, за допомогою якої додаток може надати на загальний огляд свої дані, це ще і адресат (мета) подій і повідомлень в операційній системі Windows.

Віконна (процедура) функція – функція зворотного виклику. Ядро Windows саме посилає повідомлення вікна, що в свою чергу, ініціює виклик зіставленої віконної процедури.

І що таке функція зворотного виклику? Це функція, яка викликається ядром при настанні певних умов. Насправді система не може так от запросто взяти і викликати будь-яку довільну функцію вашої програми, натомість вона надає спеціальний механізм, за допомогою якого може викликати тільки заздалегідь визначену функцію в вашому призначеному для користувача коді. Ось саме ця функція зворотного виклику називається віконної (функцією) процедурою (зазвичай носить ім’я WndProc, у нашому випадку WindowProc) І асоціюється з усіма графічними вікнами процесу. Здається адреса функції зворотного виклику через спеціальний член структури класу вікна lpfnWndProc, на етапі реєстрації класу. Кожен раз, коли для будь-якого вікна нашого процесу або його дочірніх елементів (елементи меню, поля, кнопки, радіокнопки, елементи управління та інше) є вхідні дані (інформація, дії користувача: повідомлення від клавіатури / миші і ПРЧ.), Ядро опосередковано викликає відповідну віконну процедуру і передає їй ці дані у вигляді повідомлень, які надходять через вхідні параметри процедури, таким чином процедура призначається для обробки повідомлень, адресованих вікнам нашого процесу. На кожну подію (наприклад, набір користувачем символів з клавіатури, рух курсора миші в межах кордонів вікна, клацання по елементам управління (кнопка, скролл-бар і ПРЧ.)), Що відноситься до вікна, ядро ​​генерує певне повідомлення. У конкретному прикладі алгоритм процедури обробляє всього два повідомлення: WM_DESTROY і WM_CREATE. Процедура WindowProc передує у нас в коді якимось ключовим словом proc і отримує чотири вхідних параметра (hWnd, wMsg, wParam, lParam), але що це за proc? А це ні що інше як, знову ж таки, макрос, що налаштовує пролог / епілог викликається процедури. Давайте трохи відступимо від основної лінії оповіді і познайомимося з макросом proc ближче:

macro; define procedurecommon matchparamsdefineparamsprologueprologuedefmacroprologuedefprocnameparmbyteslocalbytesreglistlocal loclocalbytes parmbase localbaseparmbyteslocalbytes push mov iflocalbytes sub end end irpsreglistepilogueepiloguedefmacroepiloguedefprocnameparmbyteslocalbytesreglistreglistreverseparmbyteslocalbytes leave end10000b retn else retnparmbytes endclose

Структура даного макросу зайвий раз говорить за те, що в FASM реалізований дуже просунутий макромова. Адже даними макросом, компілятор фактично “налаштовує” будь-яку процедуру, описану за допомогою ключового слова proc. І настройка ця полягає в автоматичному створенні прологу і епілогу процедури, налаштування стекового фрейма (кадру), резервації місця в стеку під локальні змінні, відновленні стека в епілозі, збереженні / відновлення регістрів загального призначення. Всі ці підготовчі дії при вході в процедуру і виході з неї, вважаються типовими і використовуються вже давно в компіляторах мов різних рівнів. У відсутності даного макросу програмісту довелося б писати весь “обваження” процедури самостійно, витрачаючи на це дорогоцінний час, або витрачаючи його на те, щоб самостійно створювати подібні полегшують програмування макроси. Повірте, сукупність подібних (на вигляд незначних) автоматизацій серйозно полегшує роботу розробника. Тому, хочу окремо відзначити незаперечні переваги макромови FASM, оскільки саме з його допомогою Ви можете створювати воістину грандіозні конструкції, які можуть кардинально змінити синтаксис мови.
Віконна процедура займає в початковому тексті рядки з 48 по 66. На початку процедури перевіряємо ідентифікатор вхідного повідомлення wMsg на стандартну константу WM_DESTROY. Дане повідомлення надсилається вікна в разі його закриття.

WM_DESTROY – єдине повідомлення, яке неодмінно (завжди, в будь-якому випадку) має бути оброблено у вашій віконноїпроцедурі!

Якщо повідомлення прийшло, то переходимо на локальну мітку .wmdestroy, по якій у нас розташовується функція PostQuitMessage, фактично посилає в чергу повідомлень повідомлення WM_QUIT, яке потім обробляється вже в основному циклі функцією GetMessage і наказує їй повернути 0 (в регістрі eax), Що веде до виходу з програми (рядок 36).

У рядках 58 і 62 у нас присутня команда xor eax, eax, яка обнуляє регістр eax. Цікаво, для чого нам раптом знадобилося його обнуляти? Справа в тому, що це регламентується загальним правилом API: функція повинна повертати в регістрі eax або код завершення, або один з результатів своєї роботи. Відповідно, якщо віконна процедура WindowProc обробляє будь-яке повідомлення, то вона повинна повернути 0 в разі успішного завершення, або будь-яке інше значення в разі помилкового. Ось саме з цієї причини у нас тут і розташовується команда обнулення регістра, оскільки мається на увазі, що всі оброблювані нашій процедурою повідомлення обробляються успішно.

Потім в рядку 52 порівнюємо значення поля wMsg зі значенням WM_CREATE, фактично цим ми перевіряємо, чи не надійшло чи повідомлення про створення вікна? У нас ця гілка коду пустує, ми як би реагуємо на це повідомлення в черзі, але в дійсності нічого не робимо. Далі, для всіх повідомлень, які не обробляються нашої віконної процедурою, ми повинні викликати функцію DefWindowProc, як того наказує Microsoft. Фактично функція DefWindowProc є функцією обробки за замовчуванням і гарантує, що кожне надходить в чергу повідомлення (навіть те, яке Вас не цікавить), буде оброблено. Далі слід локальна мітка .finish, яка відновлює збережені на вході віконної процедури регістри і виходить з неї.

Секція даних (data)

Як вже було з опису секції, в даному розділі містяться дані, необхідні виконуваного коду, звідси походить і назва секції. Ключовими даними тут є дві структури: wc і msg, на яких варто зупинитися докладніше. Структура wc має прототип структури WNDCLASS. А вже сама структура WNDCLASS є стандартною для бібліотек Win32 API і описана в файлах \ INCLUDE \ EQUATES \ USER32.INC і \ INCLUDE \ EQUATES \ USER64.INC пакета FASM.

Ссылка на основную публикацию