15. Язык Ассемблера
15.1. Что такое Ассемблер.
Ассемблер — программа для перевода мнемоники команд в машинный код. Дословно "Assembler" переводится как "сборщик", т.е. Ассемблер собирает коды. Язык, на котором записываются инструкции для программы Ассемблер, называется языком Ассемблера. Поэтому не совсем корректно говорить: "программа на Ассемблере". Правильно: "программа на языке Ассемблера".
С мини-Ассемблером нам пришлось работать, когда мы создавали программы в отладчике Turbo Debugger. Напомним, какие при этом возникали основные проблемы:
• Нам приходилось самостоятельно распределять память для данных и кода. При этом в памяти возникали "дыры", т.к. мы опасались перекрытия данных и кода при внесении изменений в код программы;
• Приходилось корректировать команды перехода "вперед", т.к. мы не знали числового значения метки в момент ввода команды;
• Весьма трудоемким оказывался процесс внесения изменений в программу, особенно когда в "середину" программы приходилось добавлять новые команды.
Теперь мы возложим все эти задачи на программы, которые входят в состав пакетов для разработки программ на языке Ассемблера. Эти пакеты включают не только Ассемблеры, но и компоновщики, отладчики и прочие утилиты.
Таких пакетов в настоящее время существует несколько. Наибольшей популярностью пользуются MASM (Macro Assembler) фирмы Microsoft и TASM (Turbo Assembler) фирмы Borland. Мы будем составлять программы так, чтобы они проходили трансляцию и компоновку средствами обоих пакетов. На различия будем указывать особо. Основное внимание будет уделено TASM.
В чем заключается основное отличие Ассемблера от мини-Ассемблера, встроенного в отладчик? Ассемблер обрабатывает текстовый файл, содержащий программу. Эта программа содержит не только мнемоники команд, но и директивы, т.е. указания Ассемблеру для генерации кода и распределения данных.
15.2. Программа на языке Ассемблера.
В любом текстовом редакторе наберем текст программы, которая выводит на экран строку "Hello!". Если мы набираем эту программу в Microsoft Word, то ее нужно сохранять как текст, а не как документ.
файл first.asm
.MODEL small ; Малая модель памяти
.STACK 100h ; Для стека выделяется 100h байтов
.DATA ; Начало секции (сегмента) данных
msg DB "Hello!",0Dh,0Ah,$ ; Строка для вывода
.CODE ; Начало секции (сегмента) кода
start: mov ax,@data ; Загрузить регистр DS адресом DGROUP,
mov ds,ax ; в который входят сегменты данных и стека.
mov ah,9h ; Вывод на экран
mov dx, OFFSET msg ; строки msg
int 21h ; с помощью 9-й функции 21-го прерывания.
mov ax,4C00h ; Завершение работы программы
int 21h ; с помощью функции 4Ch.
END start ; Завершение текста программы
Прежде всего обратим внимание, что в программе имеются директивы, т.е. указания для Ассемблера, которые он будет использовать на этапе компиляции программы. Директивы рекомендуется набирать прописными буквами, чтобы читателю было легче отличить их от команд, которые будут исполняться при запуске программы на выполнение.
Директива .MODEL small указывает, что программная секция данных (она начинается с директивы .DATA) и программная секция кода (начинается с директивы .CODE) будут занимать не более одного сегмента ОЗУ. Напомним, что размер сегмента — 64 Кбайт. Для стекового сегмента явно указан его размер: 256 байт.
В программе имеются метки: в секции данных метка msg, отделяемая пробелом от уже знакомой нам директивы определения данных DB. В секции кода первая команда снабжена меткой start. Метка отделена от команды двоеточием. Метка могла располагаться и в отдельной строке. Текст программы заканчивается директивой END, после которой указывается метка команды, с которой должно начаться выполнение программы.
В строке msg байты 0Dh и 0Ah — это коды возврата каретки и перевода строки. За меткой start следуют две команды, которые обеспечивают загрузку сегментного регистра DS адресом секции данных (более точно, адресом группы с именем DGROUP, в которую входят секции данных и стека). Этот адрес хранится во встроенной переменной @data. Команда mov ds,@data недопустима, поэтому приходится использовать промежуточный регистр AX. Операция OFFSET msg вычисляет смещение msg относительно начала секции данных. Так как это операция времени ассемблирования, набираем ее прописными буквами.
15.3. Трансляция и компоновка
Трансляция программы происходит по команде
с: asmin asm first.asm
На диске появляется файл first.obj. Объектный файл имеет весьма сложную структуру. В частности, там хранятся машинные коды команд. Компоновка происходит по команде
с: asmin link first.obj
На диске появляется файл first.exe, который можно запустить на выполнение. Одновременно создается карта загрузки first.map.
Запустим файл first.exe на выполнение.
first.exe
Hello!
(Поучительно проследить, как изменяется на разных этапах подготовки программы число, обозначенное как @data.)
Транслятор tasm и компоновщик tlink имеют ключи, которые задают режимы создания объектного и исполнительного файлов. Чтобы получить листинг при трансляции введите команду: с: asmin asm/l first. Обратите внимание, что расширение файла можно и не указывать.
Если вы хотите включить в загрузочный файл отладочную информацию, а затем изучать программу в отладчике, то нужно ввести команды:
с: asmin asm/zi first
с: asmin link/v first
с: asmin d first
Ключи можно комбинировать, например:
с: asmin asm/zi/l first
15.4. Анализ листинга программы
Листинг программы first.lst состоит из трех частей. Проанализируем каждую часть отдельно. Для наглядности из текста программы перед трансляцией были удалены комментарии.
Turbo Assembler Version 4.1 19/04/02 00:33:35 Page 1
first.asm
1 0000 .MODEL small
2 0000 .STACK 100h
3 0000 .DATA
4 0000 48 65 6C 6C 6F 21 0D+ msg DB "Hello!",0Dh,0Ah,$
5 0A 24
6 0009 .CODE
7 0000 B8 0000s start: mov ax,@data
8 0003 8E D8 mov ds,ax
9 0005 B4 09 mov ah,9h
10 0007 BA 0000r mov dx, OFFSET msg
11 000A CD 21 int 21h
12 000C B8 4C00 mov ax,4C00h
13 000F CD 21 int 21h
14 END start
В первой части листинга информация расположена в четырех колонках. В первой колонке — порядковые номера строк листинга. Вторая колонка — текущее значение счетчика адреса (не путать с регистром IP — счетчиком команд). В начале секции данных (.DATA) и секции кода (.CODE) счетчик адреса сбрасывается, а затем возрастает с появлением в листинге новых данных и команд. В нашем случае, когда программа размещается только в одном файле, значение счетчика адреса станет в дальнейшем значением смещения (offset). В третьей колонке показаны сгенерированные коды. В секции данных мы видим ASCII коды для строки "Неllo!" и управляющих символов. Коды не уместились на одной строке (символ переноса — знак +). По значению счетчика адреса 0009 можно заключить, что строка занимает 9 байтов.
В секции кода в третьей колонке размещены машинные коды команд. Обращает на себя внимание строка листинга
7 0000 B8 0000s start: mov ax,@data
Суффикс s (s — segment) в коде команды указывает, что непосредственный операнд связан с сегментным адресом. Когда мы будем изучать код программы в отладчике, то первый байт команды останется тем же — B8, а второй и третий байты изменятся, т.е. не будут соответствовать листингу. Об этом и предупреждает суффикс s.
В строке
10 0007 BA 0000r mov dx, OFFSET msg
суффикс r (r — relocatable — перемещаемый) сообщает, что смещение msg может измениться. В отладчике мы увидим код команды неизменным. Но если бы на этапе компоновки к нашему файлу был пристыкован другой файл с секцией данных, то смещение msg от начала сегмента изменилось бы. На такую возможность и указывает r.
Вторая часть листинга содержит таблицу символов. Приведем фрагмент этой таблицы.
Turbo Assembler Version 4.1 19/04/02 00:33:35 Page 2
Symbol Table
Symbol Name Type Value
??DATE Text "19/04/02"
??FILENAME Text "first "
??TIME Text "00:33:35"
…
@DATA Text DGROUP
…
MSG Byte DGROUP:0000
START Near _TEXT:0000
В первой колонке перечислены символические имена (Symbol Name), во второй колонке их тип (Type): текст, число. Для метки msg указано, что ее тип — байтовый, для метки start — что она "близкая" (это определяется выбранной моделью памяти small). В третьей колонке приведено значение (Value) символического имени. Все имена записаны прописными буквами. Нами в программе определены только два имени: msg и start (Ассемблер перевел их в верхний регистр, т.е. заменил все буквы прописными). Остальные имена являются встроенными.
В третьей, заключительной части листинга перечислены программные сегменты (секции) и группы, в которые они объединены.
Groups & Segments Bit Size Align Combine Class
DGROUP Group
STACK 16 0100 Para Stack STACK
_DATA 16 0009 Word Public DATA
_TEXT 16 0011 Word Public CODE
Сейчас мы не будем подробно анализировать эту часть листинга. Уместно сделать это позже, когда мы будем размещать текст программы в нескольких файлах. Сейчас отметим следующее. Секция кода (CODE) получила имя _TEXT, ее размер (Size) составляет 11h байтов. Секции стека с именем STACK и секция данных с именем _DATA объединены в группу DGROUP (группа размещается в одном сегменте оперативной памяти). Теперь ясно, почему в таблице символов введенные нами символические имена msg и start имеют сегментную часть адреса DGROUP и _TEXT. Конкретные числовые значения станут известны только на этапе загрузки .exe-файла в оперативную память.
15.5. Сообщения TASM об ошибках
При вызове tasm для трансляции first.asm выдаются следующие сообщения.
D: >tasm first.asm
Turbo Assembler Version 4.1 Copyright (c) 1988, 1996 Borland International
Assembling file: first.asm
Error messages: None
Warning messages: None
Passes: 1
Remaining memory: 388k
Сначала выводится номер версии Ассемблера. В пакет TASM 5.0 входит ассемблер tasm.exe версии 4.1. Далее сообщается, какой файл ассемблируется (позднее мы увидим, что за один вызов tasm можно ассемблировать несколько файлов, поэтому вовсе не лишней является информация, какой именно файл обрабатывается). Для этого файла в следующих двух строках выдается информация, что ошибок и предупреждений нет. Далее сообщается, что по тексту программы сделан один проход. Последнее сообщение информирует, сколько оперативной памяти осталось свободной при ассемблировании.
Перечислим возможные типы ошибок и предупреждений. Для этого намеренно будем вносить в файл ошибки.
1. Удалим директиву END start (проще всего закомментировать ее, поставив перед ней точку с запятой).
Assembling file: first.asm
**Fatal** first.asm(15) Unexpected end of file encountered
Error messages: 1
Выдается тип ошибки (Fatal — грубая), в скобках указан номер строки файла, где встретилась ошибка (15), далее — диагностическое сообщение: "Неожиданно встречен конец файла". При этом не создается объектный файл и не создается файл с листингом (при наличии ключа /l).
Восстановим директиву END start.
2. Удалим запятую, разделяющую операнды, в команде mov ah,9h
**Error** first.asm(8) Undefined symbol: AH9H
*Warning* first.asm(8) Argument needs type override
Error messages: 1
Warning messages: 1
На этот раз файл листинга есть, но объектный файл по-прежнему отсутствует. Сообщение об ошибке: "Неопределенный символ AH9H". Как видите, сообщение не проясняет происхождение ошибки. К этой же строке делается предупреждение: аргумент требует переопределения типа. Это даже комментировать не будем. Восстановите запятую.
3. Добавим перед директивой END start строку mov ax,[200h].
*Warning* first.asm(13) [Constant] assumed to mean immediate constant
Error messages: None
Warning messages: 1
На этот раз создается и файл с листингом и объектный файл. Но к предупреждениям нужно относиться внимательно и настороженно. В отладчике мы увидим строку:
1AB7:0011 B80002 mov ax,0200
Это вовсе не соответствует нашему замыслу: поместить в регистр AX содержимое ячейки со смещением 200. Предупреждение на этот раз было точным: Константа, заключенная в квадратные скобки, предполагается константой — непосредственным операндом. Как исправить эту ошибку? Нужно использовать префикс замены сегмента: mov ax, ds:[200h]. Тогда мы увидим в отладчике:
1AB7:0011 A10002 MOV AX,[0200]
Префикс для DS: в коде, конечно, отсутствует.
15.6. Карта памяти
Файл с картой памяти возникает при вызове компоновщика.
Start Stop Length Name Class
00000H 00010H 00011H _TEXT CODE
00020H 00028H 00009H _DATA DATA
00030H 0012FH 00100H STACK STACK
Program entry point at 0000:0000
Для каждой программной секции определен начальный адрес (Start), конечный адрес (Stop), длина в байтах (Length), имя (Name) и класс (Class). Указан стартовый адрес (Program entry point). Адреса, указанные в Start, будут приплюсованы к адресу загрузки программы. Этот адрес задает загрузчик — компонента операционной системы программа command.com
15.7. Сообщения TLINK об ошибках
Так же как и TASM компоновщик выдает сообщения об ошибках трех типов: Fatal, Error, Warning. Приведем два примера.
Изменим в программе first.asm последнюю строку:
END ; start
Трансляция пройдет без замечаний. А компоновщик выдаст сообщение об ошибке:
Turbo Link Version 7.1.30.1. Copyright (c) 1987, 1996 Borland International
Fatal: No program entry point (в программе нет стартового адреса)
Восстановим последнюю строку (END start) и закомментируем строку .STACK 100h. На этот раз получим предупреждение:
Warning: No stack
15.8. Запуск программы на выполнение
При запуске exe-файла на выполнение в ОЗУ образуется два блока памяти. В первом блоке расположены переменные окружения (enviroment). Например, там находятся строки PATH из autoexec.bat.
Второй блок (в котором и находится программа) состоит из двух частей. Сначала в памяти расположен так называемый префикс программного сегмента (PSP — program segment prefix). Его размер составляет ровно 256 = 100h байт. Далее располагается код и данные самой программы.
PSP в свою очередь состоит из двух частей. В его первой половине система размещает некоторую служебную информацию, к которой можно обращаться из самой программы. Например, по определенному смещению внутри PSP находится адрес блока окружения. Вторая половина PSP начинается со смещения 80h (именно половина: 80h + 80h = 100h). Она содержит хвост командной строки. Речь об этом пойдет ниже.
После загрузки программы в ОЗУ сегментные регистры содержат фиксированные адреса. В CS находится адрес сегмента кода программы, в SS — адрес сегмента стека программы. Они уже настроены нужным образом. А вот DS и ES содержат сегментный адрес PSP. Именно поэтому первые две выполняемые команды в exe-программе такие:
start: mov ax, @data
mov ds, ax
После этого DS содержит сегментный адрес данных (более точно: сегментный адрес группы, включающей сегмент данных и сегмент стека). ES продолжает показывать на PSP.
15.9. Директивы определения данных.
Определение байтов, слов и двойных слов. Для начала мы изучим три директивы: DB (define byte), DW (define word), DD (define doubleword). Первые две нам уже знакомы по мини-Ассемблеру. Например,
DB 15, –3, 7 (в отличие от debug можно вводить и отрицательные числа)
Метки. Метка — это символическое имя адреса с данными или командой. Метка может состоять из симолов: A-Z, a-z, _, @, $, ?, 0-9. Метка не должна начинаться символами 0-9. Длина метки может составлять 255 символов, но Ассемблер различает только первые 32 символа. От директивы объявления данных метка отделяется пробелами, от команды — двоеточием. Метка с двоеточием может стоять в отдельной строке.
Использование меток. Рассмотрим пример.
num DW 12
……
mov ax, num ; поместить в регистр AX число 12
Проанализируем синтаксис команды mov ax, num. Числовое значение num — это адрес. Допустим, адрес (смещение) равен 4. Но в AX загружается не адрес num, а содержимое слова по этому адресу! В отладчике мы увидим команду mov ax, [0004]. Поэтому хотелось бы записать на языке Ассемблера эту команду иначе: mov ax, [num]. И MASM и TASM транслируют такую команду правильно. Более того, в TASM есть режим IDEAL, который требует именно такую форму записи команды: mov ax, [num]. Встретив команду mov ax, num в режиме IDEAL ассемблер TASM выдает предупреждение: Pointer expression needs brackets (Ссылка на адрес в памяти не заключена в квадратные скобки). Но сложилась традиция опускать квадратные скобки. Будем ей следовать.
А как загрузить адрес? Это можно сделать двумя способами. Один вам уже знаком: lea si, num. Второй способ — новый: mov si, OFFSET num. OFFSET — это функция времени ассемблирования. На этапе ассемблирования вычисляется смещение num и формируется код команды. В отладчике (на этапе выполнения) мы увидим: mov si, 0004. Директивы и функции Ассемблера рекомендуется набирать прописными буквами. Ассемблеру это безразлично, а читателю делает текст программы это позволяет сразу видеть, что делается на стадии ассемблирования, а что — на стадии выполнения.
Цепочка символов. Определить цепочку символов можно двояко:
DB Hello
DB "Hello"
т.е. в качестве ограничителей строки могут быть либо одинарные, либо двойные кавычки. Но недопустимо эти кавычки смешивать: DB Hello".
К цепочке изображаемых символов можно добавить управляющие символы. Добавим в конце строки символы "возврат каретки" и "перевод строки":
DB "Hello", 0Dh, 0Ah, $
Если вместо 0Dh поместить Dh, то Ассемблер выдал бы сообщение об ошибке: Illegal use of register (неверное использование регистра). Dh было бы воспринято как имя регистра DH. Число должно предваряться десятичной цифрой.
Резервирование памяти. Если начальное значение выделяемой области памяти несущественно, то можно вместо конкретных чисел поместить вопросительные знаки:
DW ?, 6, ?, ? ; Зарезервировано четыре слова, определено второе.
В программе, работающей под управлением отладчика, эти вопросительные знаки заменяются нулями. Но если программа запускается автономно, то содержимое этих ячеек памяти может оказаться любым.
Оператор дублирования. Если некоторая последовательность данных многократно повторяется, то для сокращения записи можно воспользоваться оператором дублирования DUP (duplicate — дублировать). Примеры:
DB 6 DUP (1) ; эквивалентно DB 1,1,1,1,1,1
DW 2 DUP (1,2,?) ; эквивалентно DW 1,2,?,1,2,?
Итак, перед оператором DUP ставится коэффициент повторения, отделенный от DUP не менее, чем одним пробелом. В скобках указывается последовательность, подлежащая дублированию. Возможны вложенные DUP:
DB 2 DUP( 5, 3 DUP(1,0),2) ; эквивалентно DB 5,1,0,1,0,1,0,2,5,1,0,1,0,1,0,2
Атрибутные операторы. Как вы помните, отладчик "толковал по своем усмотрению" команду mov [300],5. Неясно, число 5 записывается в байт по адресу 300 или в слово по адресу 300? Разница существенная: байт с адресом 301 сохранит свое значение или будет обнулен? Поэтому для уточнения действия команды нужно было использовать атрибутный оператор: mov word ptr [300],5. Теперь рассмотрим пример на языке Ассемблера.
num DW 0FFFFh
……
mov num,5
Здесь атрибутный оператор не нужен. Ассемблер знает, что num — адрес слова, поэтому генерирует правильный код команды. После выполнения команды в байте с адресом num будет храниться число 5, а в байте с адресом num + 1 — нуль. Но как поступить, если нужно обратиться к байту слова? Вот здесь понадобится атрибутный оператор: mov byte ptr num+1,5. Адрес num + 1 вычисляется на этапе ассемблирования. В результате: num DW 5FFh.
В этом примере можно было обойтись без атрибутного оператора, но тогда слову num надо было дать альтернативное имя:
numb LABEL BYTE ; LABEL — метка
num DW 0FFFFh
……
mov numb+1,5
Еще об атрибутном операторе. В отладчике была допустимой команда mov ah, [400]. Операнд-приемник — регистр, имеющий размер байта. Поэтому по адресу 400 выбирается байт. Теперь введем команду: mov ah, num. Последует сообщение об ошибке: Operand types do not match (Несоответствие типов операндов). Попытаемся исправить ошибку:
mov byte ptr ah, num.
Вновь последует то же самое сообщение об ошибке. Правильно так:
mov ah, byte ptr num.
15.10. Подпрограммы.
Подпрограммы в программах на языке Ассемблера имеют формат
имя PROC
<тело подпрограммы>
ret
имя ENDP
Вызов подпрограммы осуществляется командой call имя.
В зависимости от используемой модели памяти генерируются дальние или ближние вызовы и возвраты. Пока мы используем только малую модель памяти. Так как для этой модели программный код заключен в одном сегменте, то генерируются ближние вызовы и возвраты.
Пример. Строка заканчивается символом $. Подпрограмма преобразует прописные буквы строки в строчные. Входной параметр подпрограммы — адрес строки.
.MODEL small
.STACK 100h
.DATA
msg DB "Dogs and Cats are Friends", 0Dh, 0Ah, $
.CODE
;-----------------------------------------------------------------------------------------------
; Подпрограмма преобразует прописные буквы строки в строчные.
; Строка должна завершаться символом $.
; Вход: SI — адрес строки;
; Выход: SI — адрес терминатора строки;
; Используемые регистры: AL.
;------------------------------------------------------------------------------------------------
ToLower PROC
next: mov al, [si] ; Поместить очередной символ строки в AL
cmp al, $ ; Если это терминатор строки,
je fin ; то завершить обработку.
cmp al, A ; Если символ лежит в диапазоне A - Z
jb cont
cmp al, Z
ja cont
add al, a - A ; то превратить его в соответствующую
; строчную букву.
mov [si], al ; и поместить символ обратно в строку
cont: inc si ; Переместить указатель на следующий символ
jmp next
fin: ret
ToLower ENDP
start:mov ax,@data
mov ds,ax
; Вывод исходной строки
mov ah,9
mov dx,OFFSET msg
int 21h
; Преобразование строки
mov si, OFFSET msg
call ToLower
; Вывод новой строки
mov ah,9
mov dx,OFFSET msg
int 21h
; Завершение программы
mov ax, 4C00h
int 21h
END start
Вместо je fin можно было сразу поместить команду ret.
Задача. Написать подпрограмму, которая удаляет из строки все цифры.
15.11. Командная строка.
Обычно мы запускаем программу на выполнение, набирая ее имя в командной строке и заканчивая ввод нажатием клавиши Enter. Но в командной строке можно вводить и дополнительную информацию, которая составляет так называемый "хвост командной строки". Пример: tasm.exe prim.asm. Здесь в хвосте командной строки мы указываем, какую программу нужно ассемблировать.
Хвост командной строки располагается в PSP, начиная со смещения 80h.
Пример. Пусть в командной строке введено:
D:>prim.exe ab c
Посмотрим в отладчике, как выглядит хвост командной строки в PSP
Для этого вызовем отладчик так:
td prim.exe ab c
Перейдем в окно CPU (F10 → View → CPU). Перейдем в панель данных (Shift+Tab). Отобразим данные, начиная со смещения 80h (Alt+F10 → Goto → 80). Мы увидим:
ds:0080 05 20 61 62 20 63 0D 00
Сегментная часть адреса находится в DS, т.к. выполнение программы еще не началось. 05 — количество символов в командной строке: пробел, a, b, пробел, c. Строка заканчивается управляющим символом 0D (в количество символов он не входит). Теперь из программы можно обратиться к этой области памяти и извлечь из нее необходимую информацию.
15.12. Пример выполнения задания D1.
15.12.1. Формулировка задания.
Вариант 0. Текст для шифрования вводится в командной строке. В тексте — только прописные латинские буквы и пробелы. Удалить пробелы и зашифровать текст. Каждый символ преобразуется по формуле 3*код+5 (mod 26). При этом считается, что код буквы A равен 0, код буквы B равен 1 и т.д. В программе должны быть одна или две подпрограммы для шифрования символа (или группы символов).
15.12.2. Текст программы.
файл D1v0.asm
COMMENT &
0. Текст для шифрования вводится в командной строке. В тексте — только прописные латинские буквы и пробелы. Удалить пробелы и зашифровать текст. Каждый символ преобразуется по формуле 3*код+5 (mod 26). При этом считается, что код буквы A равен 0, код буквы B равен 1 и т.д.
&
.MODEL small
.STACK 100h
.DATA
text DB 83 DUP(?)
msg1 DB "В командной строке нет текста", 0Dh, 0Ah, $
msg2 DB "В тексте недопустимые символы", 0Dh, 0Ah, $
.CODE
;---------------------------------------------------
; Подпрограмма шифровки символа
; Вход: AL - код символа
; Выход: AL - код зашифрованного символа
;----------------------------------------------------
encode PROC
cmp al,A ; Если код символа не ниже кода A
jnae cf1
cmp al,Z ; и не выше кода Z,
jnbe cf1
sub al,A ; Получить код символа, начиная с 0
;
; Вычисление нового кода по формуле 3*код+5 (mod 26)
mov bl,3
mul bl
add ax,5
mov bl,26
idiv bl
add ah,A ; Вернуться к коду ASCII
mov al,ah
clc ; нормальное завершение
ret
cf1: stc ; на входе символ, отличный от прописной латинской буквы
ret
encode ENDP
start: mov ax, @data
mov ds, ax
; Копирование цифр из хвоста командной строки в строку для обработки
mov cx, 0
mov cl, es:[80h] ; Длина хвоста командной строки в CX
jcxz empty ; Если хвост пустой – на empty
dec cx ; Не нужно учитывать начальный пробел
mov si, 82h ; Смещение хвоста – в SI
mov di, OFFSET text ; Указатель для новой строки – в DI
; Цикл обработки элементов строки
n: mov al, es:[si] ; Очередной символ хвоста – в AL
cmp al, ; Если встретился пробел,
je cont ; то пропустить его,
call encode ; а символ - зашифровать
jc no_A_Z ; Переход, если встретился недопустимый символ
mov [di],al ; Поместить зашифрованный символ в строку-приемник
inc di ; Переместить указатель в строке-приемнике
cont: inc si ; Переместить указатель в исходной строке
loop n
;
; Поместить в строку-приемник символы ВК и ПС, а также терминатор
mov word ptr [di], 0A0Dh
inc di
inc di
mov byte ptr [di], $
; Вывод итоговой строки на экран
mov dx, OFFSET text
mov ah, 9h
int 21h
jmp short fin
; Вывод сообщений об ошибках
empty: mov dx, OFFSET msg1 ; строка пуста
mov ah, 9h
int 21h
jmp short fin
no_A_Z: mov dx, OFFSET msg2 ; в строке недопустимые символы
mov ah, 9h
int 21h
; Завершение работы программы
fin: mov ax, 4C00h
int 21h
END start
Распечатка этого файла составляет содержание отчета. Текст программы обязательно должен включать подробные комментарии.
Отметим некоторые особенности в тексте программы.
1. Программа открывается блоком комментариев, начинающихся с директивы COMMENT. Сами комментарии должны ограничиваться парой одинаковых символов. В нашем случае выбран &, так как этот символ не встречается внутри самих комментариев.
2. Директивы Ассемблера (PROC, DB и т.д.) и операции времени трансляции (OFFSET) выделяем прописными буквами для лучшей читабельности текста программы.
3. Вместо числовых кодов букв (например, 41h) используем их символьные обозначения (A).
15.12.3. Трансляция, компоновка и выполнение программы.
Трансляция программы
tasm/zi/l d1v0
(ключ /zi — включение отладочной информации, /l — получение файла листинга). На выходе файлы a6v0.obj и a6v0.lst.
Компоновка
tlink /v d1v0
(ключ /v — включение отладочной информации). На выходе файлы a6v0.exe и a6v0.map.
Запуск на выполнение
D:>d1v0
В командной строке нет текста
D:>d1v0 Aa1
В тексте недопустимые символы
D:>d1v0 NEW LINE
SRTMDSR
15.12.4. Отладка
D:>td d1v0.exe NEW LINE
(расширение .exe нужно обязательно указывать).
Закроем окна Module (Alt+F3) и Watch (Alt+F3). Создадим окно CPU (F10 → View → CPU). «Распахнем» его (F5). Последовательными нажатиями Ctrl+M добьемся, чтобы в панели кода отображались и строки исходного текста программы и сгенерированный код:
#d1v0#start: start: mov ax, @data
cs:001E B82A57 mov ax,572A
Мы видим, что встроенная переменная Ассемблера @data на этапе выполнения получила значение 572Ah.
Перейдем в панель данных (Shift+Tab). Отобразим хвост командной строки (Alt+F10 → Goto → 80).
Уменьшим размер окна CPU и отобразим окно данных (F10 → View → Dump), расположив его ниже окна CPU. В окне Dump перейдем к адресу text (Alt+F10 → Goto → text). Обратите внимание, теперь мы набираем не конкретный адрес, а его символическое обозначение.
Переходим в окно CPU (F6). Выполняем программу по шагам, чтобы проследить правильность ее выполнения. (Никогда не запускайте программу на языке Ассемблера сразу на выполнение! Наверняка в ней есть ошибки.) Для этого нажимаем F7, если нужно войти в подпрограмму, или F8, если подпрограмму нужно выполнить как одну команду.
Проследите, как изменяется содержимое стека при вызове подпрограммы и возврате в главную программу.
Посмотреть вывод на экран зашифрованной строки можно, нажав комбинацию Alt+F5. "Вернуться обратно" — также Alt+F5.
Если мы хотим испытать работу программы с другими исходными данными, нет необходимости выходить из отладчика и заново вызывать его. В главном меню: F10 → Run → Arguments. Появляется диалоговое окно Enter command line arguments. Введем, например, WEB (начальный пробел вводить не нужно, он появится автоматически). Отладчик задает вопрос: Reload program so arguments take effect? (Заново загрузить программу, чтобы аргументы возымели эффект?). Разумеется, отвечаем Yes (нажимаем Enter). Чтобы в панели кода перейти к первой команде нашей программы, набираем Ctrl+G → start (опять-таки обратите внимание: мы набираем не адрес, а его символическое имя).
Выход из отладчика — Alt+X.
Для проверки правильности самих вычислений нам поможет отладчик. Вычислим с помощью отладчика, в какой символ перейдет буква N. Для этого откроем окно Watches (F10 → View → Watches) и введем в него выражение:
((N–A)*3+5) mod 26d+A,c
Здесь после запятой указан символ форматирования c, являющийся сокращением от слова character — символ. В окне появится выражение и результат его вычисления:
((T–A)*3+5) mod 26d+A,c S
Обратите внимание на суффикс d у числа 26. При отладке программы на языке Ассемблера числа по умолчанию рассматриваются как 16-ричные.
Для выполнения таких же вычислений с буквой E выделите уже введенное выражение и в контекстном меню выберите пункт Edit (редактирование).
15.13. Командный файл для трансляции, компоновки и отладки.
Для того чтобы не набирать каждый раз похожие команды, полезно создать командный файл. Его текст может быть, например таким.
1.bat
c: asmin asm /l/zi %1
IF ERRORLEVEL 1 GOTO exit
c: asmin link /v %1
pause
c: asmin d %1.exe
:exit
Для программы d1v0.asm вызов командного файла имеет вид
1 d1v0
Расширение файла (asm) не указывается. Если вызвать командный файл так:
1 d1v0.asm
то будет сгенерирована команда
c: asmin link /v d1v0.asm
вместо правильной
c: asmin link /v d1v0.obj
Но расширение файла при использовании tasm и tlink можно не указывать. А вот для Turbo Debugger это расширение (exe) нужно указать обязательно.
В системную переменную ERRORLEVEL записывается код возврата программы tasm. Вторую строку командного файла следует читать так:
IF ERRORLEVEL >= 1 GOTO exit
но знак >= опускается. Если tasm не выдал сообщения об ошибках, то его код возврата равен нулю и происходит вызов компоновщика. Если же в процессе трансляции обнаружены ошибки, то код возврата равен 1. Происходит досрочный выход из командного файла (переход на метку exit).
Зачем в файле 1.bat размещена команда pause? Если при трансляции будут предупреждения (warnings), то прочитать их мы не успеем, так как Turbo Debugger “заслонит” экран с сообщениями. Сначала мы изучаем сообщения tasm и tlink и нажимаем на любую клавишу, чтобы вызвать Turbo Debugger.
15.14. Средства языка Ассемблера.
15.14.1. Переменные и константы времени ассемблирования.
В программе на языке Ассемблера можно использовать переменные времени ассемблирования. Память под них не выделяется. Они используются в вычислениях на этапе ассемблирования, и в исполняемом файле никак не фигурируют.
Вот простой пример. Нам нужно задать четыре массива слов, заполненных нулями. Размер первого массива равен N, размер второго — N + 2, размер третьего — 2N – 1, четвертого — остаток от деления N на 3. Решение при N = 5:
.DATA
m1 DW 5 DUP(0)
m2 DW 7 DUP(0)
m3 DW 9 DUP(0)
m4 DW 2 DUP(0)
…
Если возникнет необходимость изменить число N, то размеры массивов придется заново вычислять вручную. Хотелось бы в программе задавать число N, а размеры массивов вычислять автоматически. Решение:
.DATA
N = 5
m1 DW N DUP(0)
m2 DW N+2 DUP(0)
m3 DW 2*N-1 DUP(0)
m4 DW N MOD 3 DUP(0)
…
Если в программе будут часто использоваться эти размеры, то им можно дать свои имена:
.DATA
N1 = 5
N2 = N1+2
N3 = 2*N1-1
N4 = N1 MOD 3
m1 DW N1 DUP(0)
m2 DW N2 DUP(0)
m3 DW N3 DUP(0)
m4 DW N4 DUP(0)
…
Переменные времени ассемблирования являются 16-разрядными. Такие переменные могут участвовать в арифметических выражениях. Возможны следующие операции: + (сложение), – (вычитание), * (умножение), / (деление нацело), MOD (взятие остатка по модулю).
Константы задаются с помощью директивы EQU. Например, часто используемому сочетанию символов 0Dh,0Ah,$ можно дать имя, которое Ассемблер будет заменять его определением.
CRLFT EQU 0Dh,0Ah,$
Далее в программе изменять это определение нельзя.
15.14.2. Счетчик адреса.
Когда начинается новая программная секция, специальная встроенная переменная Ассемблера — счетчик адреса — сбрасывается в нуль. По мере выделения новых участков памяти для переменных или команд счетчик адреса увеличивается. Посмотрим это на примере. Вот часть листинга небольшой программы.
1 0000 .MODEL small
2 0000 .STACK 100h
3 0000 .DATA
4 0000 01 02 a DB 1,2
5 0002 0003 0004 0005 b DW 3,4,5
6 0008 48 65 6C 6C 6F 24 c DB Hello$
7 000E 0000000C d DD 12
8 0012 .CODE
9 0000 B8 0000s s: mov ax,@data
10 0003 8E D8 mov ds,ax
11 0005 8B 1E 0006r mov bx,b+4
12 0009 B8 4C00 mov ax,4C00h
13 000C CD 21 int 21h
14 END s
Во второй колонке — значение счетчика адреса. Проследите его изменение в зависимости от количества и типа данных (в секции .DATA) и в зависимости от размера команд (секция .CODE). Величина b+4 вычисляется на этапе трансляции (мы видим слева 0006r).
А вот что мы увидим в отладчике.
-CPU Pentium-----------------------ds:0006 = 0005
cs:0000 B83919 mov ax,1939
cs:0003 8ED8 mov ds,ax
cs:0005►8B1E0600 mov bx,[0006]
cs:0009 B8004C mov ax,4C00
cs:000C CD21 int 21
Справа вверху в панели кода отображается адрес и содержимое ячейки, на которую ссылается текущая команда (команда, адрес которой в IP).
Счетчик адреса имеет символическое имя $ и это имя можно использовать в программе.
15.14.3. Примеры программ.
Перепишем программы a3 и a4, которые ранее мы создавали с помощью отладчика.
файл a3v0.asm
.MODEL small
.STACK 100h
.DATA
d DB "68",0Dh,0Ah,$
.CODE
start: mov ax,@data
mov ds,ax
mov dx,OFFSET d
mov ah, 9h
int 21h
mov al,d[0] ; Первая цифра — в AL.
mov ah,d[1] ; Вторая цифра — в AH.
add al,ah ; Сумма кодов цифр — в AL.
cmp al,2*0+ 10 ; Сравниваем сумму кодов с числом 10
jng m ; Если сумма больше 10,
xchg ah,d[0] ; то меняем местами
mov d[1],ah ; цифры,
jmp fin
m: sub al,ah ; иначе восстанавливаем код первой цифры.
cmp al,3 ; Будет ли в AL код цифры после вычитания 3?
jnge fin ; Если ДА,
sub al,3 ; то вычитаем из кода цифры тройку mov d[0],al ; и возвращаем цифру в память
fin: mov dx,OFFSET d
mov ah, 9h
int 21h
mov ax,4C00h
int 21h
END start
Здесь команду mov al,d[0] можно заменить на mov al,d, но в приведенном варианте выборка первой цифры выглядит как обращение к элементу массива. Команду mov ah,d[1] можно было заменить на mov al,d+1. Обратите внимание, что текст программы приобрел наглядность за счет использования средств Ассемблера: например, команда cmp al,33h заменена на cmp al,3.
В отладчике мы увидим строки:
#a3v0#18: jmp fin
193C:0021 EB09 jmp #a3v0#fin (002C)
193C:0023 90 nop
Команда nop появилась как следствие "технологии обратных поправок". Ассемблер во время трансляции команды jmp fin "не знает" числового значения метки fin, поэтому резервирует для команды перехода три байта, на случай, если метка fin расположена дальше, чем 127 байтов. Далее оказывается, что достаточно двухбайтовой команды перехода и лишний байт заполняется командой nop.
Избавиться от "паразитной" команды nop можно двумя способами. Во-первых, можно в программе указать команду короткого перехода: jmp short fin. Во-вторых, можно дать транслятору возможность совершить несколько проходов по тексту программы. Для этого указывается ключ /m. Тогда все лишние команды nop будут вычищены.
Перепишем программу для задания A4. Код, который не претерпел изменений, обозначим многоточием.
файл a4v0.asm
.MODEL small
.STACK 100h
.DATA
A DB 2,1,2, 13 DUP(1)
DimA = $ - A
B DB DimA DUP(0)
C DW DimA DUP(-1)
.CODE
start: mov ax, @data
mov ds,ax
mov si,OFFSET A
mov di,OFFSET B
mov bx,OFFSET C
sub dx,dx
mov cx,DimA
n: mov al,[si]
. . .
loop n
mov ax,4C00h
int 21h
END start
Поясним некоторые решения. В директиве A DB 2,1,2, 13 DUP(1) использована конструкция повторения DUP (DUPlicate — дублировать): тринадцать раз в память будет записана единица. В следующей строке введена переменная времени ассемблирования DimA (в исполняемом файле мы с помощью отладчика увидим только ее конкретное значение в команде mov cx,DimA). значение DimA вычисляется как разность текущего значения счетчика адреса (встроенная переменная $) и адреса массива A. Эта разность в точности равна количеству элементов массива A. В двух нижеследующих директивах переменная DimA используется как коэффициент повторения в DUP. Если мы захотим изменить в программе количество элементов в массиве A, то размер двух других массивов будет изменен автоматически. Также автоматически изменится непосредственный операнд в команде mov cx,DimA. В строке mov bx,OFFSET C на этапе ассемблирования вычисляется начальный адрес массива C, который ранее мы скрупулезно вычисляли вручную.
Запуск программы по-прежнему надо осуществлять под управлением отладчика, так как в программе не предусмотрен вывод результатов.
15. Язык Ассемблера 15.1. Что такое Ассемблер
Лекции по предмету «Информатика»