Hello, world на 32-битном ассемблере (Windows x86).

Преамбула

Обещал сделать другу-школьнику, пусть тут описание валяется, не пропадать же.

Листинг

.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'е не предполагается - этого хватит, описание функций выходит за рамки данного небольшого урока.

Вызов функции MessageBoxA

Описание функции есть в справочнике по 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

Под конец, о консольном HelloWorld

Его здесь не будет, потому что написание консольного приложения под 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

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

Ваш адрес email не будет опубликован.