Ввод-вывод, более сложные программы и чтение листингов
В этом разделе мы познакомимся с макросами ввода-вывода, разберем несколько более сложных примеров и посмотрим на листинги.
Структура ассемблерной программы
Посмотрим еще раз на программу из предыдущего раздела:
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-кодами.