Обещал сделать другу-школьнику, пусть тут описание валяется, не пропадать же.
.386
.MODEL FLAT
extrn ExitProcess:proc
extrn MessageBoxA:proc
.DATA
MSG_TITLE DB 'Hello, world!',0
MSG_MESSAGE DB 'I am running!',0
MB_INFORMATION DD 40h
.CODE
Start:
push MB_INFORMATION ;Message box style (Icon - Information)
push offset MSG_TITLE ;Message box title
push offset MSG_MESSAGE ;Message box text
push 0 ;hwndOwner
call MessageBoxA
push 0
call ExitProcess
end Start
Начало
1. Прописываем директиву совместимого процессора .386
(больше и не надо), и модель памяти FLAT
, стандартную для x86 PE исполняемых файлов.
2. Далее, экспортируем 2 функции WinAPI - ExitProcess
, которая позволит программе корректно завершиться, и MessageBoxA
, - функция вызовет стандартное диалоговое окно. Эти функции находятся в библиотеке IMPORT32.LIB
(есть в комплекте TASM), так что они станут доступны программе на этапе линковки, а директива extrn показывает компилятору, что функции внешние, т.е. компилятор не будет ругаться, что не нашел их в исходнике при компиляции.
Примечание: Кроме функции WinAPI MessageBoxA
, есть функция MessageBoxW
, параметры у этой функции аналогичные, но используется она, если выводимый текст в кодировке UTF-16.
3. В секции данных (.DATA
) определяем константы: MSG_TITLE
и MSG_MESSAGE
, содержащие, соответственно, строку заголовка и строку, содержащую текст в диалоговом окне. Строки должны оканчиваться символом с кодом 0
(,0
)
Примечание: Не строковым символом "0
", а нулевым байтом.
4. Также определяем четырехбайтовую (DD
) константу, которая будет управлять поведением окна. В данном случае MB_INFORMATION
, которой укажем значение 40h, что дополнит окно иконкой "Информация". Полный список констант, управляющих поведением окна, можно увидеть в источнике [1].
5. В секции кода (.CODE
) ставим метку Start: (на самом деле, название может быть либо любым, либо зависеть от используемого компилятора, в TASM и MASM любое), это будет указывать компилятору на точку входа в нашу программу, т.е. говорить системе, откуда начинать выполнять код.
6. И ключевое слово end
с именем той же метки, между этими конструкциями будет находиться код нашей программы. Поскольку, дополнительных внутренних функций в нашем HelloWorld'е не предполагается - этого хватит, описание функций выходит за рамки данного небольшого урока.
Описание функции есть в справочнике по WinAPI, где оно дано в C-подобном стиле:
int MessageBox( [in, optional] HWND hWnd, [in, optional] LPCTSTR lpText, [in, optional] LPCTSTR lpCaption, [in] UINT uType );
И во всех современных компиляторах ассемблера под Windows есть всякие удобняшки, типа готовых макросов, которые ускоряют написание кода, позволяют не париться с параметрами, не писать простыни кода, но, не позволяют осознать, как оно все на самом деле работает. Это или макросы в MASM или режим IDEAL в TASM. Впрочем, все нормальные ассемблеры должны уметь работать и с удобняшками, и без них. А поскольку, пример у нас маленький, то стоит как раз все показать и объяснить, без всяких удобняшек.
Функции WinAPI работают по единому стандартизированному принципу - они достают входные параметры из стека, а результат (конкретное значение или адрес, по которому следует взять данные) пишут в регистр EAX
. Значение, возвращаемое функцией, нам в данном примере не понадобится, так что пока это опустим. Разберемся с параметрами.
Стек - это такой способ организации памяти, который работает по принципу "последний зашел, первый вышел". Т.е. стек можно представить, как стопку монеток (значения), которые находятся в баночке, чей диаметр соответствует размеру монетки, и туда можно за одну операцию или положить монетку, или достать только самую верхнюю. Т.е. последнюю положенную.
Запись в стек осуществляется командой push
, извлечение из стека - командой pop
.
Ясно, что человеку такой способ записи параметров интуитивно непонятен, потому в языках высокого уровня, сделали так, чтоб было удобно. Если же писать на чистом ассемблере, мы должны положить параметры в стек в обратном порядке:
push MB_INFORMATION ;Стиль Message box (Добавляем иконку "Информация")
push offset MSG_TITLE ;Заголовок Message box
push offset MSG_MESSAGE ;Текст в Message box
push 0 ;ID Вызывающего окна - его нет, устанавливаем в 0.
Далее вызываем саму функцию WinAPI:
call MessageBoxA
Теперь вызываем функцию, необходимую для корректного завершения программы. На вход она принимает только один параметр - код возврата. Мы ничего не делаем, кроме вывода MessageBox
'а, так что отдадим стандартный код нормального завершения - 0
.
push 0
call ExitProcess
Его здесь не будет, потому что написание консольного приложения под Win32, связано с тем, что всегда в определенный момент возникает в ассемблере - "много мелких, суетливых движений", как сказал классик по другому поводу. Написание консольного приложения под Windows усложнено, алгоритм там примерно такой:
1. Получить дескриптор стандартного устройства ввода-вывода
2. Проверить, доступен ли он программе.
3. Если недоступен, значит нас вызвали не из консоли, а из GUI, например, щелчком мыши.
4. Если 3 - неверно
5. Вывести текст на консоль
6. Если 3 - верно
7. Создать новую консоль, вывести текст, закрыть/освободить консоль.
MASM, в отличии от TASM умеет прописывать на этапе линковки флаг IMAGE_SUBSYSTEM_WINDOWS_CUI (3)
в заголовок PE-файла, это показывает ОС, что приложение расчитано на консольную подсистему, что, в свою очередь, избавляет программиста от необходимости вручную открывать консоль и устраивать дополнительные проверки. Система откроет консоль за нас. Но вернемся к этому в другой раз.
1. MessageBox function
2. Исходник и откомпилированная версия на GitHub