Программирование на ассемблере под windows

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

Недостатки зачастую обусловлены лишь склонностью современного рынка к предпочтению количества качеству. Современные компьютеры способны легко справиться с нагромождением команд высокоуровневых функций, а если нелегко — будьте добры обновите аппаратную часть вашей машины! Таков закон коммерческого программирования. Если же речь идет о программировании для души, то компактная и шустрая программа, написанная на ассемблере, оставит намного более приятное впечатление, нежели высокоуровневая громадина, обремененная кучей лишних операций. Бытует мнение, что программировать на ассемблере могут только избранные. Это неправда. Конечно, талантливых программистов-ассемблерщиков можно пересчитать по пальцам, но ведь так обстоит дело практически в любой сфере человеческой деятельности. Не так уж много найдется водителей-асов, но научиться управлять автомобилем сумеет каждый — было бы желание. Ознакомившись с данным циклом статей, вы не станете крутым хакером. Однако вы получите общие сведения и научитесь простым способам программирования на ассемблере для Windows, используя ее встроенные функции и макроинструкции компилятора. Естественно, для того, чтобы освоить программирование для Windows, вам необходимо иметь навыки и опыт работы в Windows. Сначала вам будет многое непонятно, но не расстраивайтесь из- за этого и читайте дальше: со временем все встанет на свои места.

Итак, для того, чтобы начать программировать, нам как минимум понадобится компилятор. Компилятор — это программа, которая переводит исходный текст, написанный программистом, в исполняемый процессором машинный код. Основная масса учебников по ассемблеру делает упор на использование пакета MASM32 (Microsoft Macro Assembler). Но я в виде разнообразия и по ряду других причин буду знакомить вас с молодым стремительно набирающим популярность компилятором FASM (Flat Assembler). Этот компилятор достаточно прост в установке и использовании, отличается компактностью и быстротой работы, имеет богатый и емкий макросинтаксис, позволяющий автоматизировать множество рутинных задач. Его последнюю версию вы можете скачать по адресу: сайт выбрав flat assembler for Windows. Чтобы установить FASM, создайте папку, например, "D:FASM" и в нее распакуйте содержимое скачанного zip-архива. Запустите FASMW.EXE и закройте, ничего не изменяя. Кстати, если вы пользуетесь стандартным проводником, и у вас не отображается расширение файла (например, .EXE), рекомендую выполнить Сервис -> Свойства папки -> Вид и снять птичку с пункта Скрывать расширения для зарегистрированных типов файлов. После первого запуска компилятора в нашей папке должен появиться файл конфигурации — FASMW.INI. Откройте его при помощи стандартного блокнота и допишите в самом низу 3 строчки:
[Environment]
Fasminc=D:FASMINCLUDE
Include=D:FASMINCLUDE

Если вы распаковали FASM в другое место — замените "D:FASM" на свой путь. Сохраните и закройте FASMW.INI. Забегая вперед, вкратце объясню, как мы будем пользоваться компилятором:
1. Пишем текст программы, или открываем ранее написанный текст, сохраненный в файле .asm, или вставляем текст программы из буфера обмена комбинацией.
2. Жмем F9, чтобы скомпилировать и запустить программу, или Ctrl+F9, чтобы только скомпилировать. Если текст программы еще не сохранен — компилятор попросит сохранить его перед компиляцией.
3. Если программа запустилась, тестируем ее на правильность работы, если нет — ищем ошибки, на самые грубые из которых компилятор нам укажет или тонко намекнет.
Ну, а теперь мы можем приступить к долгожданной практике. Запускаем наш FASMW.EXE и набираем в нем код нашей первой программы:

.data
Caption db ‘Моя первая программа.’,0
Text db ‘Всем привет!’,0

.code
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0

Жмем Run -> Run, или F9 на клавиатуре. В окне сохранения указываем имя файла и папку для сохранения. Желательно привыкнуть сохранять каждую программу в отдельную папку, чтобы не путаться в будущем, когда при каждой программе может оказаться куча файлов: картинки, иконки, музыка и прочее. Если компилятор выдал ошибку, внимательно перепроверьте указанную им строку — может, запятую пропустили или пробел. Также необходимо знать, что компилятор чувствителен к регистру, поэтому .data и .Data воспринимаются как две разные инструкции. Если же вы все правильно сделали, то результатом будет простейший MessageBox (рис. 1). Теперь давайте разбираться, что же мы написали в тексте программы. В первой строке директивой include мы включили в нашу программу большой текст из нескольких файлов. Помните, при установке мы прописывали в фасмовский ини-файл 3 строчки? Теперь %fasminc% в тексте программы означает D:FASMINCLUDE или тот путь, который указали вы. Директива include как бы вставляет в указанное место текст из другого файла. Откройте файл WIN32AX.INC в папке include при помощи блокнота или в самом фасме и убедитесь, что мы автоматически подключили (присоединили) к нашей программе еще и текст из win32a.inc, macro/if.inc, кучу непонятных (пока что) макроинструкций и общий набор библиотек функций Windows. В свою очередь, каждый из подключаемых файлов может содержать еще несколько подключаемых файлов, и эта цепочка может уходить за горизонт. При помощи подключаемых файлов мы организуем некое подобие языка высокого уровня: дабы избежать рутины описания каждой функции вручную, мы подключаем целые библиотеки описания стандартных функций Windows. Неужели все это необходимо такой маленькой программе? Нет, это — что-то вроде "джентльменского набора на все случаи жизни". Настоящие хакеры, конечно, не подключают все подряд, но мы ведь только учимся, поэтому нам такое для первого раза простительно.

Далее у нас обозначена секция данных — .data. В этой секции мы объявляем две переменные — Caption и Text. Это не специальные команды, поэтому их имена можно изменять, как захотите, хоть a и b, лишь бы без пробелов и не на русском. Ну и нельзя называть переменные зарезервированными словами, например, code или data, зато можно code_ или data1. Команда db означает "определить байт" (define byte). Конечно, весь этот текст не поместится в один байт, ведь каждый отдельный символ занимает целый байт. Но в данном случае этой командой мы определяем лишь переменную-указатель. Она будет содержать адрес, в котором хранится первый символ строки. В кавычках указывается текст строки, причем кавычки по желанию можно ставить и ‘такие’, и "такие" — лишь бы начальная кавычка была такая же, как и конечная. Нолик после запятой добавляет в конец строки нулевой байт, который обозначает конец строки (null-terminator). Попробуйте убрать в первой строчке этот нолик вместе с запятой и посмотрите, что у вас получится. Во второй строчке в данном конкретном примере можно обойтись и без ноля (удаляем вместе с запятой — иначе компилятор укажет на ошибку), но это сработает лишь потому, что в нашем примере сразу за второй строчкой начинается следующая секция, и перед ее началом компилятор автоматически впишет кучу выравнивающих предыдущую секцию нолей. В общих случаях ноли в конце текстовых строк обязательны! Следующая секция — секция исполняемого кода программы — .code. В начале секции стоит метка start:. Она означает, что именно с этого места начнет исполняться наша программа. Первая команда — это макроинструкция invoke. Она вызывает встроенную в Windows API-функцию MessageBox. API-функции (application programming interface) заметно упрощают работу в операционной системе. Мы как бы просим операционную систему выполнить какое-то стандартное действие, а она выполняет и по окончании возвращает нам результат проделанной работы. После имени функции через запятую следуют ее параметры. У функции MessageBox параметры такие:

1-й параметр должен содержать хэндл окна-владельца. Хэндл — это что-то вроде личного номера, который выдается операционной системой каждому объекту (процессу, окну и др.). 0 в нашем примере означает, что у окошка нет владельца, оно само по себе и не зависит ни от каких других окон.
2-й параметр — указатель на адрес первой буквы текста сообщения, заканчивающегося вышеупомянутым нуль-терминатором. Чтобы наглядно понять, что это всего лишь адрес, сместим этот адрес на 2 байта прямо в вызове функции: invoke MessageBox,0,Text+2,Caption,MB_OK и убедимся, что теперь текст будет выводиться без первых двух букв.
3-й — указатель адреса первой буквы заголовка сообщения.
4-й — стиль сообщения. Со списком этих стилей вы можете ознакомиться, например, в INCLUDEEQUATES USER32.INC. Для этого вам лучше будет воспользоваться поиском в Блокноте, чтобы быстро найти MB_OK и остальные. Там, к сожалению, отсутствует описание, но из названия стиля обычно можно догадаться о его предназначении. Кстати, все эти стили можно заменить числом, означающим тот, иной, стиль или их совокупность, например: MB_OK + MB_ICONEXCLAMATION. В USER32.INC указаны шестнадцатеричные значения. Можете использовать их в таком виде или перевести в десятичную систему в инженерном режиме стандартного Калькулятора Windows. Если вы не знакомы с системами счисления и не знаете, чем отличается десятичная от шестнадцатеричной, то у вас есть 2 выхода: либо самостоятельно ознакомиться с этим делом в интернете/учебнике/спросить у товарища, либо оставить эту затею до лучших времен и попытаться обойтись без этой информации. Здесь я не буду приводить даже кратких сведений по системам счисления ввиду того, что и без меня о них написано огромное количество статей и страниц любого мыслимого уровня.

Вернемся к нашим баранам. Некоторые стили не могут использоваться одновременно — например, MB_OKCANCEL и MB_YESNO. Причина в том, что сумма их числовых значений (1+4=5) будет соответствовать значению другого стиля — MB_RETRYCANCEL. Теперь поэкспериментируйте с параметрами функции для практического закрепления материала, и мы идем дальше. Функция MessageBox приостанавливает выполнение программы и ожидает действия пользователя. По завершении функция возвращает программе результат действия пользователя, и программа продолжает выполняться. Вызов функции ExitProcess завершает процесс нашей программы. Эта функция имеет лишь один параметр — код завершения. Обычно, если программа нормально завершает свою работу, этот код равен нулю. Чтобы лучше понять последнюю строку нашего кода — .end start, — внимательно изучите эквивалентный код: format PE GUI 4.0

Читайте также:  Lan games как настроить

section ‘.data’ data readable writeable

Caption db ‘Наша первая программа.’,0
Text db ‘Ассемблер на FASM — это просто!’,0

section ‘.code’ code readable executable
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0

section ‘.idata’ import data readable writeable
library KERNEL32, ‘KERNEL32.DLL’,
USER32, ‘USER32.DLL’

import KERNEL32,
ExitProcess, ‘ExitProcess’

import USER32,
MessageBox, ‘MessageBoxA’

Для компилятора он практически идентичен предыдущему примеру, но для нас этот текст выглядит уже другой программой. Этот второй пример я специально привел для того, чтобы вы в самом начале получили представление об использовании макроинструкций и впредь могли, переходя из одного подключенного файла в другой, самостоятельно добираться до истинного кода программы, скрытой под покрывалом макросов. Попробуем разобраться в отличиях. Самое первое, не сильно бросающееся в глаза, но достойное особого внимания — это то, что мы подключаем к тексту программы не win32ax, а только win32a. Мы отказались от большого набора и ограничиваемся малым. Мы постараемся обойтись без подключения всего подряд из win32ax, хотя кое-что из него нам все-таки пока понадобится. Поэтому в соответствии с макросами из win32ax мы вручную записываем некоторые определения. Например, макрос из файла win32ax:
macro .data

во время компиляции автоматически заменяет .data на section ‘.data’ data readable writeable. Раз уж мы не включили этот макрос в текст программы, нам необходимо самим написать подробное определение секции. По аналогии вы можете найти причины остальных видоизменений текста программы во втором примере. Макросы помогают избежать рутины при написании больших программ. Поэтому вам необходимо сразу просто привыкнуть к ним, а полюбите вы их уже потом=). Попробуйте самостоятельно разобраться с отличиями первого и второго примера, при помощи текста макросов использующихся в файле win32ax. Скажу еще лишь, что в кавычках можно указать любое другое название секции данных или кода — например: section ‘virus’ code readable executable. Это просто название секции, и оно не является командой или оператором. Если вы все уяснили, то вы уже можете написать собственный вирус. Поверьте, это очень легко. Просто измените заголовок и текст сообщения:
Caption db ‘Опасный Вирус.’,0

Text db ‘Здравствуйте, я — особо опасный вирус-троян и распространяюсь по интернету.’,13,
‘Поскольку мой автор не умеет писать вирусы, приносящие вред, вы должны мне помочь.’,13,
‘Сделайте, пожалуйста, следующее:’,13,
‘1.Сотрите у себя на диске каталоги C:Windows и C:Program files’,13,
‘2.Отправьте этот файл всем своим знакомым’,13,
‘Заранее благодарен.’,0

Число 13 — это код символа "возврат каретки" в майкрософтовских системах. Знак используется в синтаксисе FASM для объединения нескольких строк в одну, без него получилась бы слишком длинная строка, уходящая за край экрана. К примеру, мы можем написать start:, а можем — и st
ar
t:

Компилятор не заметит разницы между первым и вторым вариантом.
Ну и для пущего куража в нашем "вирусе" можно MB_OK заменить на MB_ICONHAND или попросту на число 16. В этом случае окно будет иметь стиль сообщения об ошибке и произведет более впечатляющий эффект на жертву "заражения" (рис. 2).

Вот и все на сегодня. Желаю вам успехов и до новых встреч!
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт

WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE OR WS_EX_CLIENTEDGE

WS_OVERLAPPEDWINDOW = WS_OVERLAPPED OR

PROCTYPE ptGetModuleHandle stdcall

PROCTYPE ptLoadIcon stdcall

PROCTYPE ptLoadCursor stdcall

PROCTYPE ptLoadMenu stdcall

PROCTYPE ptRegister > stdcall

PROCTYPE ptCreateWindowEx stdcall

PROCTYPE ptShowWindow stdcall

PROCTYPE ptUpdateWindow stdcall

PROCTYPE ptGetMessage stdcall

PROCTYPE ptTranslateMessage stdcall

PROCTYPE ptDispatchMessage stdcall

PROCTYPE ptSetMenu stdcall

PROCTYPE ptPostQuitMessage stdcall

PROCTYPE ptDefWindowProc stdcall

PROCTYPE ptSendMessage stdcall

PROCTYPE ptMessageBox stdcall

PROCTYPE ptExitProcess stdcall

extrn GetModuleHandleA :ptGetModuleHandle

extrn LoadIconA :ptLoadIcon

extrn LoadCursorA :ptLoadCursor

extrn Register > :ptRegisterClassEx

extrn LoadMenuA :ptLoadMenu

extrn CreateWindowExA :ptCreateWindowEx

extrn ShowWindow :ptShowWindow

extrn UpdateWindow :ptUpdateWindow

extrn GetMessageA :ptGetMessage

extrn TranslateMessage :ptTranslateMessage

extrn DispatchMessageA :ptDispatchMessage

extrn SetMenu :ptSetMenu

extrn PostQuitMessage :ptPostQuitMessage

extrn DefWindowProcA :ptDefWindowProc

extrn SendMessageA :ptSendMessage

extrn MessageBoxA :ptMessageBox

extrn ExitProcess :ptExitProcess

wndTitle db ‘Demo program’, 0

msg_open_txt db ‘You selected open’, 0

msg_open_tlt db ‘Open box’, 0

msg_save_txt db ‘You selected save’, 0

msg_save_tlt db ‘Save box’, 0

Start: call GetModuleHandleA, 0 ; не обязательно, но желательно

sub esp,SIZE Wnd > ; отведём место в стеке под структуру

mov [(WndClassEx esp).cbSize],SIZE WndClassEx

mov [(WndClassEx esp).style],CS_HREDRAW or CS_VREDRAW

mov [(WndClassEx esp).lpfnWndProc],offset WndProc

mov [(WndClassEx esp).cbWndExtra],0

mov [(WndClassEx esp).cbClsExtra],0

mov [(WndClassEx esp).hInstance],eax

call LoadIconA, 0, IDI_APPLICATION

mov [(WndClassEx esp).hIcon],eax

call LoadCursorA, 0, IDC_ARROW

mov [(WndClassEx esp).hCursor],eax

mov [(WndClassEx esp).hbrBackground],COLOR_WINDOW

mov [(WndClassEx esp).lpszMenuName],MyMenu

mov [(WndClassEx esp).lpszMenuName],0

mov [(WndClassEx esp).lpszClassName],offset classTitle

mov [(WndClassEx esp).hIconSm],0

call Register > esp ; зарегистрируем класс окна

add esp,SIZE Wnd > ; восстановим стек

; и создадим окно

call CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, extended window style

offset classTitle, pointer to registered class name

offset wndTitle, pointer to window name

WS_OVERLAPPEDWINDOW, window style

CW_USEDEFAULT, horizontal position of window

CW_USEDEFAULT, vertical position of window

CW_USEDEFAULT, window width

CW_USEDEFAULT, window height

0, handle to parent or owner window

0, handle to menu, or child-window identifier

[hInst], handle to application instance

0 ; pointer to window-creation data

call LoadMenu, hInst, MyMenu

call CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, extended window style

offset classTitle, pointer to registered class name

offset wndTitle, pointer to window name

WS_OVERLAPPEDWINDOW, window style

CW_USEDEFAULT, horizontal position of window

CW_USEDEFAULT, vertical position of window

CW_USEDEFAULT, window width

CW_USEDEFAULT, window height

0, handle to parent or owner window

eax, handle to menu, or child-window identifier

[hInst], handle to application instance

0 ; pointer to window-creation data

call ShowWindow, eax, SW_SHOW ; show window

call UpdateWindow, [hWnd] ; redraw window

call LoadMenuA, [hInst], MyMenu

call SetMenu, [hWnd], eax

call GetMessageA, offset msg, 0, 0, 0

call TranslateMessage, offset msg

call DispatchMessageA, offset msg

exit: call ExitProcess, 0

public stdcall WndProc

proc WndProc stdcall

arg @@hwnd: dword, @@msg: dword, @@wPar: dword, @@lPar: dword

call PostQuitMessage, 0

call DefWindowProcA, [@@hwnd], [@@msg], [@@wPar], [@@lPar]

call SendMessageA, [@@hwnd], WM_CLOSE, 0, 0

@@open: mov eax, offset msg_open_txt

mov edx, offset msg_open_tlt

@@save: mov eax, offset msg_save_txt

mov edx, offset msg_save_tlt

@@mess: call MessageBoxA, 0, eax, edx, MB_OK

Комментарии к программе

Здесь мне хотелось в первую очередь продемонстрировать использование прототипов функций API Win32 . Конечно их (а также описание констант и структур из API Win32 ) следует вынести в отдельные подключаемые файлы, поскольку, скорее всего Вы будете использовать их и в других программах. Описание прототипов функций обеспечивает строгий контроль со стороны компилятора за количеством и типом параметров, передаваемых в функции. Это существенно облегчает жизнь программисту, позволяя избежать ошибок времени исполнения, тем более, что число параметров в некоторых функциях API Win32 весьма значительно.

Существо данной программы заключается в демонстрации вариантов работы с оконным меню. Программу можно откомпилировать в трёх вариантах (версиях), указывая компилятору ключи VER2 или VER3 (по умолчанию используется ключ VER1) . В первом варианте программы меню определяется на уровне класса окна и все окна данного класса будут иметь аналогичное меню. Во втором варианте, меню определяется при создании окна, как параметр функции CreateWindowEx . Класс окна не имеет меню и в данном случае, каждое окно этого класса может иметь своё собственное меню. Наконец, в третьем варианте, меню загружается после создания окна. Данный вариант показывает, как можно связать меню с уже созданным окном.

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

Представляет определённый интерес использование стековых фреймов и заполнение структур в стеке посредством регистра указателя стека (esp) . Именно это продемонстрировано при заполнении структуры WndClassEx . Выделение места в стеке (фрейма) делается простым перемещением esp :

sub esp,SIZE WndClassEx

Теперь мы можем обращаться к выделенной памяти используя всё тот же регистр указатель стека. При создании 16-битных приложений такой возможностью мы не обладали. Данный приём можно использовать внутри любой процедуры или даже произвольном месте программы. Накладные расходы на подобное выделение памяти минимальны, однако, следует учитывать, что размер стека ограничен и размещать большие объёмы данных в стеке вряд ли целесообразно. Для этих целей лучше использовать “кучи” (heap) или виртуальную память ( virtual memory ).

Остальная часть программы достаточно тривиальна и не требует каких-либо пояснений. Возможно более интересным покажется тема использования макроопределений.

Мне достаточно редко приходилось серьёзно заниматься разработкой макроопределений при программировании под DOS . В Win32 ситуация принципиально иная. Здесь грамотно написанные макроопределения способны не только облегчить чтение и восприятие программ, но и реально облегчить жизнь программистов. Дело в том, что в Win32 фрагменты кода часто повторяются, имея при этом не принципиальные отличия. Наиболее показательна, в этом смысле, оконная и/или диалоговая процедура. И в том и другом случае мы определяем вид сообщения и передаём управление тому участку кода, который отвечает за обработку полученного сообщения. Если в программе активно используются диалоговые окна, то аналогичные фрагменты кода сильно перегрузят программу, сделав её малопригодной для восприятия. Применение макроопределений в таких ситуациях более чем оправдано. В качестве основы для макроопределения, занимающегося диспетчеризацией поступающих сообщений на обработчиков, может послужить следующее описание.

Читайте также:  Packard bell intel atom n2600

macro MessageVector message1, message2:REST

dd offset @@&message1

@@VecCount = @@VecCount + 1

macro WndMessages VecName, message1, message2:REST

label @@&VecName dword

MessageVector message1, message2

@@&VecName&_1: dec ecx

cmp eax,[dword e cx * 8 + offset @@&VecName ]

jmp [dword e c x + offset @@&VecName + 4]

@@default: call DefWindowProcA, [@@hWnd], [@@msg], [@@wPar], [@@lPar]

@@ret_false: xor eax,eax

@@ret_true: mov eax,-1

Комментарии к макроопределениям

При написании процедуры окна Вы можете использовать макроопределение WndMessages , указав в списке параметров те сообщения, обработку которых намерены осуществить. Тогда процедура окна примет вид:

proc WndProc stdcall

arg @@hWnd: dword, @@msg: dword, @@wPar: dword, @@lPar: dword

WndMessages WndVector, WM_CREATE, WM_SIZE, WM_PAINT, WM_CLOSE, WM_DESTROY

; здесь обрабатываем сообщение WM_CREATE

; здесь обрабатываем сообщение WM_SIZE

; здесь обрабатываем сообщение WM_PAINT

; здесь обрабатываем сообщение WM_CLOSE

; здесь обрабатываем сообщение WM_DESTROY

Обработку каждого сообщения можно завершить тремя способами:

– вернуть значение TRUE , для этого необходимо использовать переход на метку @@ret_true;

– вернуть значение FALSE, для этого необходимо использовать переход на метку @@ret_false;

– перейти на обработку по умолчанию, для этого необходимо сделать переход на метку @@default.

Отметьте, что все перечисленные метки определены в макро WndMessages и Вам не следует определять их заново в теле процедуры.

Теперь давайте разберёмся, что происходит при вызове макроопределения WndMessages . Вначале производится обнуление счётчика параметров самого макроопределения (число этих параметров может быть произвольным). Теперь в сегменте данных создадим метку с тем именем, которое передано в макроопределение в качестве первого параметра. Имя метки формируется путём конкатенации символов @@ и названия вектора. Достигается это за счёт использования оператора & . Например, если передать имя TestLabel , то название метки примет вид: @@TestLabel . Сразу за объявлением метки вызывается другое макроопределение MessageVector , в которое передаются все остальные параметры, которые должны быть ничем иным, как списком сообщений, подлежащих обработке в процедуре окна. Структура макроопределения MessageVector проста и бесхитростна. Она извлекает первый параметр и в ячейку памяти формата dword заносит код сообщения. В следующую ячейку памяти формата dword записывается адрес метки обработчика, имя которой формируется по описанному выше правилу. Счётчик сообщений увеличивается на единицу. Далее следует рекурсивный вызов с передачей ещё не зарегистрированных сообщений, и так продолжается до тех пор, пока список сообщений не будет исчерпан.

Сейчас в макроопределении WndMessage можно начинать обработку. Теперь существо обработки, скорее всего, будет понятно без дополнительных пояснений.

Обработка сообщений в Windows не является линейной, а, как правило, представляет собой иерархию. Например, сообщение WM_COMMAND может заключать в себе множество сообщений поступающих от меню и/или других управляющих элементов. Следовательно, данную методику можно с успехом применить и для других уровней каскада и даже несколько упростить её. Действительно, не в наших силах исправить код сообщений, поступающих в процедуру окна или диалога, но выбор последовательности констант, назначаемых пунктам меню или управляющим элементам ( controls ) остаётся за нами. В этом случае нет нужды в дополнительном поле, которое сохраняет код сообщения. Тогда каждый элемент вектора будет содержать только адрес обработчика, а найти нужный элемент весьма просто. Из полученной константы, пришедшей в сообщении, вычитается идентификатор первого пункта меню или первого управляющего элемента, это и будет номер нужного элемента вектора. Остаётся только сделать переход на обработчик.

Вообще тема макроопределений весьма поучительна и обширна. Мне редко доводится видеть грамотное использование макросов и это досадно, поскольку с их помощью можно сделать работу в ассемблере значительно проще и приятнее.

Для того, чтобы писать полноценные приложения под Win32 требуется не так много:

– собственно компилятор и компоновщик ( я использую связку TASM32 и TLINK32 из пакета TASM 5.0) . Перед использованием рекомендую “наложить” patch , на данный пакет. Patch можно взять на site www.borland.com или на нашем ftp сервере ftp.uralmet.ru.

– редактор и компилятор ресурсов (я использую Developer Studio и brcc32.exe );

– выполнить перетрансляцию header файлов с описаниями процедур, структур и констант API Win32 из нотации принятой в языке Си, в нотацию выбранного режима ассемблера: Ideal или MASM.

В результате у Вас появится возможность писать лёгкие и изящные приложения под Win32 , с помощью которых Вы сможете создавать и визуальные формы, и работать с базами данных, и обслуживать коммуникации, и работать multimedia инструментами. Как и при написании программ под DOS, у Вас сохраняется возможность наиболее полного использования ресурсов процессора, но при этом сложность написания приложений значительно снижается за счёт более мощного сервиса операционной системы, использования более удобной системы адресации и весьма простого оформления программ.

Приложение 1. Файлы, необходимые для первого примера

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

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

За основу для изучения я взял стандартный шаблон 32-битного оконного приложения на ассемблере с именем template.asm , поставляемый в составе пакета FASM и размещающийся в поддиректории EXAMPLESTEMPLATE , и слегка модифицировал его для некоторой наглядности. Для начала представим исходный код программы:

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

Идеология программирования под Windows

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

Подход к программированию в среде Windows существенно изменился и теперь основная концепция программирования ориентирована на так называемые события. Это значит, что ядро системы следит за аппаратной/программной активностью, и в ответ генерирует специальные сообщения, которые затем передает (через специальные системные механизмы) приложениям, которые ожидают наступление этих событий. Иными словами, можно утверждать что приложения в Windows управляются событиями. С событиями ассоциируется любая пользовательская/системная активность: перемещение окон, нажатие клавиш клавиатуры/мыши, изменения состояния буфера обмена, изменения в аппаратурной конфигурации, изменение статуса энергопотребления, изменение значений таймеров и другая. Поэтому, когда происходит какое-либо событие (к коим относится и любые действия пользователя), система сама "предоставляет" для пользовательского приложения входные данные, и делает она это посредством передачи сообщений. А прикладная программа, в свою очередь, состоит из обработчиков этих самых поступающих в него сообщений, которые требуемым образом реагируют на них.

При завершении программой обработки события, управление возвращается ядру системы. Вы уже поняли, что все это коренным образом меняет подход к написанию программ, поскольку в MSDOS программа сама контролировала собственные действия/события, теперь же операционная система Windows сама проводит большинство работы, а пользовательской программе передает лишь управляющие сообщения, которые активизируют те или иные функции обработки в программе.

Но помимо обработки ввода или реакции на иные входящие системные сообщения, прикладной программе требуется выполнять и некоторые другие действия над объектами операционной системы. С целью обеспечения доступа пользовательских программ ко всему спектру исполняемых компонентов Windows, предоставляется так называемый программный интерфейс приложений (API). А это означает, что весь функционал операционной системы доступен через функции, и чтобы программисту что-либо сделать – надо вызвать функцию соответствующего назначения.

Заголовок

Рассмотрение исходного кода начнем мы с заголовка, или, если выразиться более точно – "так называемого заголовка". Я возьму на себя смелость подобным образом именовать область исходного кода, начинающуюся непосредственно с первого символа и идущую до директивы объявления первой секции. Начало области не обозначается специальными директивами, это просто начальная часть листинга программы. В этом месте (у нас строка 1 ) может использоваться директива format , которая предназначается для указания формата результирующего исполняемого файла, получаемого на выходе после компиляции. Форматы можно указать следующие:

Имя Расшифровка Описание
MZ M ark Z bikowski формат 16-битных исполняемых файлов с расширением .exe для ОС MSDOS
PE PE64 P ortable E xecutable формат 32/64-битных исполняемых файлов с расширением .exe для ОС Windows
COFF MS COFF MS64 COFF C ommon O bject F ile F ormat формат объектного файла, содержащий промежуточное представление кода программы, предназначенный для объединения с другими объектными файлами (проектами/ресурсами) с целью получения готового исполнимого модуля.
ELF ELF64 E xecutable and L inkable F ormat формат исполняемых файлов систем семейства UNIX. Объектный файл (.obj) для компилятора gcc.
ARM A dvanced R ISC M achine формат исполняемых файлов под архитектуру ARM (?)
Binary файлы бинарной структуры. Что зададите, то и соберется. Например, выставив смещение 100h (org 100h) от начала, можно получить старый-добрый .com -файл под 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-приложений различных стадии/типа: загрузки, выполнения и драйвера.

Роль данного параметра достаточно велика, поскольку именно он определяет, какая именно подсистема будет вызываться для запуска исполняемого файла, то есть фактически определяет программное окружение при запуске процесса. Если используется тип приложения GUI , необходимо уточнять минимальную версию системы (у нас: 4.0), под которую создается наш исполняемый модуль.
Затем, в строке под номером 2 в нашем исходном коде располагается директива с именем entry , которая определяет точку входа в программу.

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

В нашем случае подключается файл %fasminc%win32a.inc , который, в свою очередь, содержит ссылки на другие подключаемые файлы, содержащие определения ключевых структур, требуемых для компиляции нашей программы: макросов, типов данных, констант, системных структур. Без включения этого файла у нас попросту не пройдет процесс компиляции нашего исходного кода, то есть исполняемый файл не будет создан (не соберется).

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

С возможными вариантами стилей можно ознакомиться на соответствующей странице, описывающей стили окна.

Секции

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

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

Флаги могут принимать следующие значения: 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 содержится в файлах определения макросов INCLUDEMACROPROC32.INC и INCLUDEMACROPROC64.INC пакета FASM и вот его объявление:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock detector