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

Структура ассемблерной программы

Посмотрим еще раз на программу из предыдущего раздела:

include console.inc

.code
Start:  mov ecx, 5

again:  outstrln 'Hello World'
        dec ecx
        jnz again

        exit
        end Start

Разберем структуру исходного текста этой программы:

  • Строка include console.inc подключает макросы, которые мы будем использовать в этом курсе. О макросах мы поговорим чуть ниже.

  • Строка .code открывает секцию кода.

  • Следующие четыре строки содержат собственно код нашей программы. Отдельно стоит упомянуть метку Start, которая соответствует точке входа в нашу программу — мы можем разместить ее и не в начале кода. outstrln — это вызов макроса для вывода строки на стандартный поток вывода. Этот макрос определен в файле console.inc.

  • exit — это также макрос из console.inc, который осуществляет системный вызов, завершающий работу программы.

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

Макросы

Для написания простейших программ на языке ассемблера нам потребуется не так много возможностей языка. Умения работать с командами пересылки данных, арифметическими вычислениями и переходами вполне достаточно. Помимо этого, потребуется представление о структуре ассемблерной программы и директивах резервирования памяти.

Кроме этого, не хватает только возможностей ввода-вывода. Поскольку для организации ввода-вывода (а также, например, для завершения работы программы) требуется обращаться к операционной системе, мы не можем (пока) сделать этого самостоятельно — нам потребуется материал, который в этом курсе разбирается позднее.

В качестве врéменной меры мы будем пользоваться ограниченным набором макросов, определенных в файле console.inc. Этот файл разработан для нашего курса, найти его можно в каталоге include внутри каталога MASM (например, C:\masm32\include).

Макросы будут подробно разбираться далее в конце этого курса, и в дальнейшем мы познакомимся с тем, как устроен console.inc. На текущем этапе нам нужно знать о них следующее: макросы раскрываются в некоторый код языка ассемблера (возможно, в 2-3 машинные команды, а возможно, и в десятки и даже сотни).

Макросы ввода-вывода

Кратко рассмотрим минимальный набор макросов, которыми будем пользоваться в ближайшее время:

  • inchar, outchar — ввод и вывод символов; единственный операнд — код символа, который может быть регистром или памятью размерности 8, а в случае вывода еще и непосредственным операндом.

  • inint, outint, outword — ввод и вывод целых чисел; outword выводит число как беззнаковое, outint — как знаковое. Размерность операнда может быть 8, 16 или 32.

  • outstr "text" можно использовать для вывода текста.

  • newline выводит символы возврата каретки и перевода строки.

  • flush очищает буфер ввода.

  • Наконец, макрос exit завершает работу программы.

Ниже приведена шпаргалка по макросам:

outchar op1     ; r/m/i 8           вывод символа
inchar op1      ; r/m 8             ввод символа

outint op1      ; r/m/i 8/16/32     вывод целого с/з
outword op1     ; r/m/i 8/16/32     вывод целого б/з
inint op1       ; r/m 8/16/32       ввод целого

outstr "text"   ;                   вывод строки

newline         ;                   вывод символов CR LF
flush           ;                   очистка буфера ввода

exit            ;                   завершение работы программы           

Пример программы: числа Фибоначчи

Попробуем написать простую, но законченную программу. Наша программа (назовем ее fib) будет запрашивать у пользователя номер n и вычислять n-е число Фибоначчи.

Наша программа не будет использовать никаких переменных в памяти, обходясь одними регистрами.

Запросим у пользователя число n и поместим его в регистр EDX:

	outstr "enter n: "
	inint edx	; F_n to calculate

Проинициализируем рабочие регистры. Текущее число Фиббоначчи Fk будем хранить на EAX, предыдущее Fk — на EBX. Само текущее k будем держать в ECX:

	mov eax, 1	; F_2=F_k
	mov ebx, eax	; F_1
	mov ecx, 2	; k=2

Если нас просят вычислить F1 или F2, ответ (единица) у нас уже готов, прыгнем вперед на метку result (команда jbe, jump if below or equal, переходит на указанную метку, если беззнаковое сравнение на меньше или равно было истинным, т. е. EDX ≤ 2):

	cmp edx, 2	; F_1 or F_2? we already have the answer
	jbe result

В основном цикле вычисления мы будем складывать два предыдущих члена последовательности до тех пор, пока не доберемся до искомого n-го члена. Если сложение вызовет перенос, прыгнем на метку overflow, чтобы сообщить об ошибке:

nx:	xchg ebx, eax	; ebx <- F_k
	add eax, ebx	; eax <- F_k+1
	jc overflow
	inc ecx		; k = k + 1
	cmp ecx, edx
	jb nx

Если всё хорошо, выведем результат (EAX):

result:	outstr "F_"
	outword edx
	outstr " = "
	outword eax
	newline
	jmp fin

А если EAX переполнился, напечатаем сообщение об ошибке:

overflow:
	outstr "The result exceeds 2^32"

fin:	exit
	end Start

Программа целиком

include console.inc

.code
Start:
	outstr "enter n: "
	inint edx	; F_n to calculate

	mov eax, 1	; F_2=F_k
	mov ebx, eax	; F_1
	mov ecx, 2	; k=2

	cmp edx, 2	; F_1 or F_2? we already have the answer
	jbe result

nx:	xchg ebx, eax	; ebx <- F_k
	add eax, ebx	; eax <- F_k+1
	jc overflow
	inc ecx		; k = k + 1
	cmp ecx, edx
	jb nx

result:	outstr "F_"
	outword edx
	outstr " = "
	outword eax
	newline
	jmp fin

overflow:
	outstr "The result exceeds 2^32"

fin:	exit
	end Start

Поместим текст программы в файл fib.asm в рабочем каталоге, после чего соберем и запустим ее при помощи mkr:

C:\work>mkr fib.asm
 Assembling: fib.asm
enter n: 10
F_10 = 55

Генерация листинга

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

Для генерации листинга используется аргумент /Fl (регистр символов важен). Получим листинг программы fib:

C:\work>ml /c /coff /Fl fib.asm
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: fib.asm

Файл листинга будет называться fib.lst. Откроем его в текстовом редакторе.

Листинг состоит из двух страниц — собственно листинга и таблиц макросов, процедур, структур и т. д. Остановимся подробнее на листинге:

				include console.inc
			      C .NOLIST
			      C .LIST
			      C 
			      C 
			      C ;include masm32.inc
			      C 
			      C    includelib masm32.lib
			      C    includelib user32.lib
			      C    includelib kernel32.lib
			      C    includelib msvcrt.lib
			      C    includelib io_proc.lib
			      C    
			      C 

 00000000			.code
 00000000			Start:
					outstr "enter n: "
					inint edx	; F_n to calculate

 0000003C  B8 00000001			mov eax, 1	; F_2=F_k
 00000041  8B D8			mov ebx, eax	; F_1
 00000043  B9 00000002			mov ecx, 2	; k=2

 00000048  83 FA 02			cmp edx, 2	; F_1 or F_2? we already have the answer
 0000004B  76 0E			jbe result

 0000004D  93			nx:	xchg ebx, eax	; ebx <- F_k
 0000004E  03 C3			add eax, ebx	; eax <- F_k+1
 00000050  0F 82 0000014D		jc overflow
 00000056  41				inc ecx		; k = k + 1
 00000057  3B CA			cmp ecx, edx
 00000059  72 F2			jb nx

 0000005B			result:	outstr "F_"
					outword edx
					outstr " = "
					outword eax
					newline
 000001A1  EB 33			jmp fin

 000001A3			overflow:
					outstr "The result exceeds 2^32"

 000001D6			fin:	exit
					end Start

Листинг состоит из трех колонок: первая содержит адреса (точнее, смещения в секции), вторая — сгенерированный машинный код и третья — соответствующие строчки исходного кода программы.

Примечание. Обратите внимание, что вызовам макросов никакого машинного кода в листинге не соответствует. Это происходит потому, что в console.inc генерация листинга для макросов подавлена. Ее можно снова включить директивой .LISTMACRO.

По листингу сразу видно, что команда mov формата «регистр — константа» занимает 5 байт (1 байт на код операции и обозначение формата и регистра-приемника, плюс 4 байта на сам непосредственный операнд), а команда формата «регистр — регистр» — только 2 байта.

Адреса в первой колонке «прыгают» от 0 к 3C потому, что макросы outstr и inint раскрываются в некоторое количество машинных команд, которые попросту не отражены в листинге.

Если обратить внимание на команду jbe result, то видно, что переход указан относительно, т. е. «вперед на 8 байт». Если прибавить 8 к адресу следующей команды, 4Dh, то получится 55h — адрес метки result.

Листинг для секции данных

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

Например, пусть секция данных выглядит вот так:

.data
B       db 15
W       dw 15
D       dd 15
B2      db -2
D2      dd -2
Pr      db "ABCD"
X       db 'A'
X2      db 41h

Тогда соответствующий фрагмент листинга будет таким:

 00000019			.data
 00000019 0F			B       db 15
 0000001A 000F			W       dw 15
 0000001C 0000000F		D       dd 15
 00000020 FE			B2      db -2
 00000021 FFFFFFFE		D2      dd -2
 00000025 41 42 43 44		Pr      db "ABCD"
 00000029 41			X       db 'A'
 0000002A 41			X2      db 41h

Из этого примера хорошо видно, к чему приводит указание одинаковых начальных значений для переменных разных размерностей, а также то, что символы представляются их ASCII-кодами.