Лабораторная Работа №5

“Процедуры, прерывания, работа с командной строкой”

Задание:

Написать программу, которая бы принимала символы с клавиатуры и реализовывала следующий алгоритм:

·  Если пользователь ввёл название одного из регистров (например, ax), символ «=» и нажал Enter, программа выводит:

AX=<содержимое AX (шестнадцатеричный формат)>

·  Если пользователь вводит quit, программа корректно завершается

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

·  Пользователь может ввести, к примеру, строку «quitj231eiwjn», она будет обработана так же, как и «quit».

В каждом варианте обязательно предусмотреть только ввод одного из названий регистров, т. е. для третьего варианта программа реагирует на две команды – cx и quit, при вводе другой строки пользователю выводится сообщение об ошибке и предлагается повторить ввод.

Вариант

Регистр для вывода

1

AX

2

BX

3

CX

4

DX

5

BP

6

SP

7

SI

8

DI

9

DS

A

ES

Содержание отчета:

Титульный лист Цель работы Блок-схема алгоритма Текст программы с комментариями Результаты работы программы Описание основных команд, использованных в программе Выводы по работе программы

Теоретическая справка и пояснения:

В этой лабораторной работе мы познакомимся с интересным новым материалом – сдвигами, будем работать с командами обработки строк, а также активно использовать именно преимущества МАКРО ассемблера. Макроассемблер обладает специальными так называемыми директивами – такими, как Invoke, которые значительно облегчают написание сложных программ на нём. Мы уже сталкивались с директивами – например, PROC/ENDP, END, .MODEL и другими.

НЕ нашли? Не то? Что вы ищете?

1.  Сдвиги на ассемблере

Существуют различные команды сдвигов: ROR/ROL для циклических сдвигов, SHR/SHL для логических, SAR/SAL для арифметических. Нам в нашей работе понадобится команда логического сдвига (для вывода содержимого регистра по полубайтам). Её синтаксис имеет следующий вид:

SHR/SHL reg, imm8 ; reg – регистр общего назначения, imm8 – непосредственно заданное 8разрядное значение, например, 12h

SHR/SHL mem, imm8

SHR/SHL reg, cl

SHR/SHL mem, cl

Обратите внимание, что сдвиг на константу, отличную от единицы, работает только в более поздних по отношению к Intel 8086/8088 процессорах.

Приведем пример:

.model small

.code

main PROC

mov ax, 40h

shr ax, 5

main ENDP

end main

При попытке компиляции ассемблер выдаст ошибку:

error A2070: invalid instruction operands

Это обусловлено следующим. Если мы явно не указываем ассемблеру, для какого процессора компилируется программа, то он использует набор команд процессора Intel 8086, в котором сдвиг осуществляется либо на значение, заданное в регистре CL, либо на единицу. Добавим директиву.186 , которая означает, что программа пишется для процессора Intel 80186 и более старших моделей.

.model small

.186

.code

main PROC

mov ax, 40h

shr ax, 5

main ENDP

end main

2.  Передача параметров в стеке.

В прошлой лабораторной работе мы уже передавали аргументы процедуре в стек, используя его как удобную альтернативу регистровым параметрам. Однако это вынуждало нас использовать много рутинного кода и манипуляций с BP. Сейчас мы познакомимся с удобным механизмом передачи параметров функции через стек – директивами INVOKE, PROC и PROTO.

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

;---

printString PROC USES ax cx dx si,

ptrstring: WORD,

slength: WORD

; Выводит на экран строку, начало строки по адресу ptrstring, длина slength

;---

<код процедуры – мы можем работать с ptrstring и slength как с данными в памяти>

<например, mov si, ptrstring>

printString ENDP

printString – название процедуры, за ним идет ключевое слово PROC, а далее список аргументов. Первый аргумент у нас – ключевое слово USES, которое определяет, какие регистры нам необходимо сохранить перед началом выполнения процедуры, а затем восстановить в её конце. В начале и в конце процедуры, таким образом, будут автоматически подставлены следующие команды:

printString PROC

push ax ;Начинаем сохранение регистров

push cx

push dx

push si

<Код процедуры>

pop si ;Начинаем восстановление регистров

pop dx

pop cx

pop ax

printString ENDP

Не забудьте, что если ваша процедура возвращает какое-то значение в одном из регистров, этот регистр нельзя включать в директиву USES, иначе его значение уничтожится в процессе восстановления сохранённых регистров!

INVOKE имя_процедуры, аргумент_1, аргумент2, ….

Например, нашу процедуру printString из файла util. asm мы будем вызывать следующим образом:

INVOKE printString, OFFSET mystring, LENGTHOF mystring

По сути дела, компилятор преобразует это в следующую последовательность ассемблерных команд:

push OFFSET mystring

push LENGTHOF mystring

call getString

Если код процедуры находится до того, как мы её вызываем, то проблем не возникает – компилятор знает, какие параметры принимает эта процедура к моменту её вызова. Однако не всегда удобно описывать все процедуры в начале программы, более того – процедуры могут находиться в других файлах, и компилироваться не одновременно с нашим кодом. Для того, чтобы объявить процедуру, не расписывая её код, необходима директива PROTO.

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

- Скопируем директиву PROC со всеми параметрами

- Удалим директиву USES и список регистров за ней (если они есть)

- Заменим PROC на PROTO

Таким образом, наша процедура с описанием и вызовом примет следующий вид:

printString PROTO ptrstring: WORD,

slength: WORD

.data

mystring BYTE “Hey!”

.code

INVOKE printString, OFFSET mystring, LENGTHOF mystring

;---

printString PROC USES ax cx dx si,

ptrstring: WORD,

slength: WORD

; Выводит на экран строку, начало строки по адресу ptrstring, длина slength

;---

<Код процедуры>

printString ENDP

А если у нас есть процедура, которая описана в одном файле, а вызывается кодом в другом? Нам достаточно написать прототипы этой процедуры в начало каждого файла, в котором мы нуждаемся в ней. Но что, если таких файлов десять или сто? На помощь нам приходит механизм вставок – мы можем включить один файл внутрь другого с помощью директивы INCLUDE

INCLUDE util. inc

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

Попробуем скомпилировать программу, в которой мы используем эти директивы. К сожалению, ассемблер выдаст ошибку – language type must be specified! Что происходит не так?

Всё дело в том, что существует несколько стандартов того, как параметры помещаются в стек. Компиляторы языков высокого уровня не имеют единого стандарта по этому поводу – в C параметры процедур помещаются в стек в порядке, обратном заданному (справа налево), а значение SP восстанавливается уже после выхода из процедуры. В компиляторе Pascal параметры процедур заносятся в стек слева направо, а восстановление SP происходит в самой процедуре. Мы будем использовать описатель языка stdcall.

Stdcall подразумевает следующие соглашения:

- Параметры заносятся в стек в обратном порядке (как в C)

- Чтобы восстановить SP используется команда ret с параметром (как и в Pascal)

Например, ret 8 в конце процедуры будет означать то же самое, что и

add sp, 8

ret

Добавим к директиве. model small параметр stdcall

.model small, stdcall

Теперь программа компилируется нормально.

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

3.  Прерывания

Обычно все прерывания используются одинаково. Каждое из прерываний даёт доступ к набору процедур. Чтобы выбрать одну из них, её номер заносится в AH. Затем в другие регистры заносятся необходимые параметры, в них же (или в AH) эти процедуры могут возвращать какие-то значения. Каждое прерывание вместе со своим набором функций подробно задокументировано.

Например, нам необходимо вывести символ «А» на консоль. Для этого можно использовать следующий программный код:

mov ah, 02h

mov dl, “A”

int 21h

Нам понадобятся два прерывания. Рассмотрим их поподробнее.

int 21h – DOS прерывание, предоставляет доступ к функциям DOS

AH=02h DL=ASCII код символа

Вывод на консоль

AH=4ch AL=Код возврата

В конце каждой программы должно быть прерывание 21h с такими аргументами. Это обеспечивает корректное завершение работы и возврат в DOS.

int 16h - функции для работы с клавиатурой.

AH=10h

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

Возвращает: AH=<скан-код нажатой клавиши> AL=<ASCII код нажатой клавиши>

4.  Перевод строки

В Dos перевод строки осуществляется последовательным выводом символов 10d и 13d

При нажатии «ENTER» в ответ на ожидание нажатия клавиатуры, в регистр AL заносится как раз таки значение 13d – учтите это при реализации ввода строки.

5.  Команды работы со строками.

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

LODSB/LODSW

Загружает значение по адресу DS:SI в регистр AL/AX и увеличивает/уменьшает значение SI (DF=0 или DF=1) на один/два

STOSB/STOSW

Загружает значение из регистра AL/AX в память по адресу ES:SI в и увеличивает/уменьшает (DF=0 или DF=1) значение SI на один/два

CMPSB/CMPSW

Производит сравнение каждой пары элементов строк DS:SI и ES:DI, изменяет значения SI и DI. Например, пусть нам необходимо сравнить две строки

.data

axstring BYTE “ax=” ; Нам надо проверить, эту ли строку ввел пользователь

buffer BYTE 16 DUP(0h) ; Буфер для строки, введённой пользователем

.code

cld ; Очистим флаг направления (SI/DI увеличиваются)

mov si, OFFSET axstring ; Загружаем адрес начала шаблона в SI

mov di, OFFSET buff ; Загружаем адрес начала пользовательской строки

mov cx, LENGTHOF axstring ; Мы будем проводить сравнение не больше раз, чем символов в меньшей из строк

repe cmpsb ; Сравниваем элементы из этих двух строк. repe означает повторение, пока CX не станет нулём или пока не будет сброшен флаг ZF.

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

je call_ax ;Соответственно, если шаблон совпал с началом строки полностью, то флаг ZF всё еще установлен, иначе они не равны. JE перейдёт по метке call_ax, где содержится код вызова процедуры, при условии, что ZF = 0.
Указания к выполнению

Предлагаемая структура лабораторной работы - три файла: файл с процедурой main, файл со вспомогательными процедурам и «заголовочный» файл.

util. asm – файл со вспомогательными процедурами.

Содержит следующие процедуры:

- Процедура reg_out - вывод одного из РОН в консоль

- Процедура printString - вывод строки в консоль (принимает адрес начала строки и её длину).

! Используется команда LODSB для последовательной загрузки символов строки в AL.

- Процедура newline, которая осуществляет перевод на новую строку

- Процедура execString, которая считывает строку из консоли и сравнивает её с одним из шаблонов (например, “quit”, “ax=”), и, в зависимости от результатов, выводит содержимое регистра, завершается или предлагает повторить ввод.

! Используются команды:

STOSB (для помещения введённых пользователем символов в строку)

CMPSB (для сравнения введённой пользователем строки с шаблонами)

- Процедура Terminate, которая обеспечивает корректный выход из программы и возврат программой определённого значения (0 в случае успешной работы, другое значение при ошибке). Это значение Terminate принимает в качестве аргумента.

util. inc – заголовочный файл. Включает в себя описания процедур, используемых в обоих файлах

main. asm – основной исполняемый файл, включает в себя процедуру main, которая печатает приглашение ввода (с помощью printString) и вызывает процедуру execString, после чего корректно завершает работу программы с кодом 0.