
200.
Директивы
Ассемблер
имеет ряд операторов, которые
позволяютуправлять процессом
ассемблирования и формирования
листинга. Эти операторы
называются псевдокомандами
илидирективами. Они
действуют только в процессе ассемблирования
программы и не генерируют машинных
кодов. Директивы
формирования листинга не очень интересны,
так как вывести на печать текст программы
не составляет труда для любого современного
редактора.
Директивы ассемблирования
буду изучать и по ходу дела добавлять
их в блог. К сожалению, стройного и
полного описания директив ассемблирования
и примеров, разъясняющих их назначение
и разницу между их параметрами, нет ни
в одной из имеющихся у меня книг.
Более-менее они описаны опять же у
старичка Абеля (но книжка все таки
старовата) и не плохое есть описание
есть у Юрова, но что тот, что другой не
приводят ни каких примеров, которые бы
пояснили разницу между параметрами
директивы SEGMENT и т.д. Так что придется
все исследовать самостоятельно.
Директива
SEGMENT
Любые
ассемблерные программы содержат, по
крайней мере, один сегмент - сегмент
кода. В некоторых программах используется
сегмент для стековой памяти и сегмент
данных для определения данных.
Еще
раз вспомним, что физически сегмент
представляет собой область памяти,
занятую командами и (или) данными, адреса
которых вычисляются относительно
значения в соответствующем сегментном
регистре.
Стандартные
Директивы Сегментации
Синтаксическое
описание сегмента на ассемблере
представляет собой конструкцию,
изображенную на рисунке ниже:
Важно
отметить, что функциональное назначение
сегмента несколько шире, чем простое
разбиение программы на блоки кода,
данных и стека. Сегментация является
частью более общего механизма, связанного
с концепцией модульного программирования.
Она предполагает унификацию оформления
объектных модулей, создаваемых
компилятором, в том числе с разных языков
программирования. Это позволяет
объединять программы, написанные на
разных языках. Именно для реализации
различных вариантов такого объединения
и предназначены операнды в директиве
SEGMENT.
Рассмотрим их подробнее.
Атрибут выравнивания сегмента (тип выравнивания) сообщает компоновщику о том, что нужно обеспечить размещение начала сегмента на заданной границе. Это важно, поскольку при правильном выравнивании доступ к данным в процессорах i80х86 выполняется быстрее. Допустимые значения этого атрибута следующие:
BYTE — выравнивание не выполняется. Сегмент может начинаться с любого адреса памяти;
WORD — сегмент начинается по адресу, кратному двум, то есть последний (младший) значащий бит физического адреса равен 0 (выравнивание на границу слова);
DWORD — сегмент начинается по адресу, кратному четырем, то есть два последних (младших) значащих бита равны 0 (выравнивание на границу двойного слова);
PARA — сегмент начинается по адресу, кратному 16, то есть последняя шестнадцатеричная цифра адреса должна быть 0h (выравнивание на границу параграфа);
PAGE — сегмент начинается по адресу, кратному 256, то есть две последние шестнадцатеричные цифры должны быть 00h (выравнивание на границу 256-байтной страницы);
MEMPAGE — сегмент начинается по адресу, кратному 4 Кбайт, то есть три последние шестнадцатеричные цифры должны быть 000h (адрес следующей 4-Кбайтной страницы памяти).
По умолчанию тип выравнивания имеет значение para.
Атрибут комбинирования сегментов (комбинаторный тип) сообщает компоновщику, как нужно комбинировать сегменты различных модулей, имеющие одно и то же имя. Значениями атрибута комбинирования сегмента могут быть:
PRIVATE — сегмент не будет объединяться с другими сегментами с тем же именем вне данного модуля;
PUBLIC — заставляет компоновщик соединить все сегменты с одинаковыми именами. Новый объединенный сегмент будет целым и непрерывным. Все адреса (смещения) объектов, а это могут быть, в зависимости от типа сегмента, команды и данные, будут вычисляться относительно начала этого нового сегмента;
COMMON — располагает все сегменты с одним и тем же именем по одному адресу. Все сегменты с данным именем будут перекрываться и совместно использовать память. Размер полученного в результате сегмента будет равен размеру самого большого сегмента;
AT xxxx — располагает сегмент по абсолютному адресу параграфа (параграф — объем памяти, кратный 16; поэтому последняя шестнадцатеричная цифра адреса параграфа равна 0). Абсолютный адрес параграфа задается выражением xxx. Компоновщик располагает сегмент по заданному адресу памяти (это можно использовать, например, для доступа к видеопамяти или области ПЗУ), учитывая атрибут комбинирования. Физически это означает, что сегмент при загрузке в память будет расположен, начиная с этого абсолютного адреса параграфа, но для доступа к нему в соответствующий сегментный регистр должно быть загружено заданное в атрибуте значение. Все метки и адреса в определенном таким образом сегменте отсчитываются относительно заданного абсолютного адреса;
STACK — определение сегмента стека. Заставляет компоновщик соединить все одноименные сегменты и вычислять адреса в этих сегментах относительно регистра ss. Комбинированный тип STACK (стек) аналогичен комбинированному типу PUBLIC, за исключением того, что регистр ss является стандартным сегментным регистром для сегментов стека. Регистр sp устанавливается на конец объединенного сегмента стека. Если не указано ни одного сегмента стека, компоновщик выдаст предупреждение, что стековый сегмент не найден. Если сегмент стека создан, а комбинированный тип STACK не используется, программист должен явно загрузить в регистр ss адрес сегмента (подобно тому, как это делается для регистра ds), а также установить регистр SP в правильное значение.
По умолчанию атрибут комбинирования принимает значениеPrivate.
Атрибут класса сегмента (тип класса) — это заключенная в кавычки строка, помогающая компоновщику определить соответствующий порядок следования сегментов при собирании программы из сегментов нескольких модулей. Компоновщик объединяет вместе в памяти все сегменты с одним и тем же именем класса (имя класса, в общем случае, может быть любым, но лучше, если оно будет отражать функциональное назначение сегмента). Типичным примером использования имени класса является объединение в группу всех сегментов кода программы (обычно для этого используется класс 'code'). С помощью механизма типизации класса можно группировать также сегменты инициализированных и не инициализированных данных.
Атрибут размера сегмента. Для процессоров i80386 и выше сегменты могут быть 16 или 32-разрядными. Это влияет, прежде всего, на размер сегмента и порядок формирования физического адреса внутри него. Атрибут может принимать следующие значения:
USE16 — это означает, что сегмент допускает 16-разрядную адресацию. При формировании физического адреса может использоваться только 16-разрядное смещение. Соответственно, такой сегмент может содержать до 64 Кбайт кода или данных;
USE32 — сегмент будет 32-разрядным. При формирования физического адреса может использоваться 32-разрядное смещение. Поэтому такой сегмент может содержать до 4 Гбайт кода или данных.
Директива ENDS обозначает конец сегмента. Обе директивы SEGMENT и ENDS должны иметь одинаковые имена. Упрощенные Директивы Сегментации Стандартные директивы сегментации изначально использовались для оформления программы в трансляторах MASM и TASM. Поэтому их называют стандартными директивами сегментации. Для простых программ, содержащих по одному сегменту для кода, данных и стека, хотелось бы упростить ее описание. Для этого в трансляторы MASM и TASM ввели возможность использования упрощенных директив сегментации. Но здесь возникла проблема, связанная с тем, что необходимо было как-то компенсировать невозможность напрямую управлять размещением и комбинированием сегментов. Для этого совместно с упрощенными директивами сегментации стали использовать директиву указания модели памяти MODEL, которая частично стала управлять размещением сегментов и выполнять функции директивы ASSUME (поэтому при использовании упрощенных директив сегментации директиву ASSUME можно не использовать).Директива MODEL связывает сегменты, которые в случае использования упрощенных директив сегментации имеют предопределенные имена, с сегментными регистрами (хотя явно инициализировать ds все равно придется). Пример программы с использованием упрощенных директив сегментации:
;---------------------------------------- masm ;режим работы TASM: ideal или masm model small ;модель памяти .data ;сегмент данных message db 'Введите две шестнадцатеричные цифры,$' .stack ;сегмент стека db 256 dup ('?') ;сегмент стека .code ;сегмент кода main proc ;начало процедуры main mov ax,@data ;заносим адрес сегмента ;данных в регистр ax mov ds,ax ;ax в ds ;далее текст программы (см. сегмента кода в листинге 3.1 книги) mov ax,4c00h ;пересылка 4c00h в регистр ax int 21h ;вызов прерывания с номером 21h main endp ;конец процедуры main end main ;конец программы с точкой входа main
Синтаксис
директивы MODEL:
Модификатор модели
памяти
Значение модификатора |
Назначение |
use16 |
Сегменты выбранной модели используются как 16-битные (если соответствующей директивой указан процессор i80386 или i80486) |
use32 |
Сегменты выбранной модели используются как 32-битные (если соответствующей директивой указан процессор i80386 или i80486) |
dos |
Программа будет работать в MS-DOS |
Модель памяти является обязательным параметром директивыMODEL. Этот параметр определяет модель сегментации памяти для программного модуля. Модели памяти
Модель |
Тип кода |
Тип данных |
Назначение модели |
TINY |
near |
near |
Код, данные и стек объединены в одну группу с именем DGROUP и размером до 64Кб. Используется для создания программ формата .com. Некоторые языки эту модель не поддерживают. СS=DS=SS=DGROUP |
SMALL |
near |
near |
Код занимает один сегмент, данные и стек объединены в одну группу с именем DGROUP (хотя для описания могут использоваться разные сегменты). Эту модель обычно используют для большинства программ на ассемблере. CS=_text DS=SS=DGROUP |
MEDIUM |
far |
near |
Код занимает несколько сегментов, по одному на каждый объединяемый программный модуль. Все ссылки на передачу управления — типа far (вызов подпрограмм). Данные и стек объединены в одной группе DGROUP; все ссылки на них — типа near (для доступа к данным испльзуется только смещение). CS=<модуль>_text DS=SS=DGROUP |
COMPACT |
near |
far |
Код находится в одном сегменте, данные и стек в группе DGROUP и могут занимать несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение ( ссылка на данные — типа far). CS=_text DS=SS=DGROUP |
LARGE |
far |
far |
Код может занимать несколько сегментов, по одному на каждый объединяемый программный модуль. Стек и данные находятся в группе DGROUP. Для ссылки на данные используются дальние указатели -far. CS=<модуль>_text DS=SS=DGROUP |
HUGE |
far |
far |
Тоже что и модель LARGE, что касается TurboAssebmler. |
FLAT |
far |
far |
Тоже, что и TINY, но используются 32-битная адресация, так что максимальный размер сегмента, содержащего и данные, и код, и стек - 4Гб. |
Предполагается, что программный модуль может иметь только определенные типы сегментов, которые определяются упомянутыми нами ранее упрощенными директивами описания сегментов. Эти директивы приведены в таблице ниже. Упрощенные директивы определения сегмента
Формат директивы (режим MASM) |
Формат директивы (режим IDEAL) |
Назначение |
.CODE [имя] |
CODESEG[имя] |
Начало или продолжение сегмента кода |
.DATA |
DATASEG |
Начало или продолжение сегмента инициализированных данных. Также используется для определения данных типа near |
.CONST |
CONST |
Начало или продолжение сегмента постоянных данных (констант) модуля |
.DATA? |
UDATASEG |
Начало или продолжение сегмента неинициализированных данных. Также используется для определения данных типа near |
.STACK [размер] |
STACK [размер] |
Начало или продолжение сегмента стека модуля. Параметр [размер] задает размер стека |
.FARDATA [имя] |
FARDATA [имя] |
Начало или продолжение сегмента инициализированных данных типа far |
.FARDATA? [имя] |
UFARDATA [имя] |
Начало или продолжение сегмента неинициализированных данных типа far |
Наличие в некоторых директивах параметра [имя] говорит о том, что возможно определение нескольких сегментов этого типа. С другой стороны, наличие нескольких видов сегментов данных обусловлено требованием обеспечить совместимость с некоторыми компиляторами языков высокого уровня, которые создают разные сегменты данных для инициализированных и неинициализированных данных, а также констант. При использовании директивы MODEL транслятор делает доступными несколько идентификаторов, к которым можно обращаться во время работы программы, с тем, чтобы получить информацию о тех или иных характеристиках данной модели памяти. Идентификаторы, создаваемые директивой MODEL
Имя идентификатора |
Значение переменной |
@code |
Физический адрес сегмента кода |
@data |
Физический адрес сегмента данных типа near |
@fardata |
Физический адрес сегмента данных типа far |
@fardata? |
Физический адрес сегмента неинициализированных данных типа far |
@curseg |
Физический адрес сегмента неинициализированных данных типа far |
@stack |
Физический адрес сегмента стека |
Если вы посмотрите на текст примера, то увидите пример использования одного из этих идентификаторов. Это @data – с его помощью мы получили значение физического адреса сегмента данных нашей программы. Необязательные параметры язык и модификатор языка определяют некоторые особенности вызова процедур. Необходимость в использовании этих параметров появляется при написании и связывании программ на различных языках программирования. Язык — необязательный операнд, принимающий значения C, PASCAL, BASIC, FORTRAN, SYSCALL и STDCALL. Если он указан, подразумевается, что процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня, следовательно, если указан язык C, все имена ассемблерных процедур, объявленных как PUBLIC, будут изменены так, чтобы начинаться с символа подчеркивания, как это принято в C. Модификатор — необязательный операнд, принимающий значенияNEARSTACK (по умолчанию) или FARSTACK. Во втором случае сегмент стека не будет объединяться в одну группу с сегментами данных. После того как модель памяти установлена, вступают в силу упрощенные директивы определения сегментов, объединяющие действия директив SEGMENT и ASSUME. Кроме того, сегменты, объявленные упрощенными директивами, не требуется закрывать директивой ENDS — они закрываются автоматически, как только ассемблер обнаруживает новую директиву определения сегмента или конец программы. Директива .CODE описывает основной сегмент кода .code имя_сегмента эквивалентно: _TEXT segment word public ’CODE’ для моделей TINY, SMALL и COMPACT name_TEXT segment word public ’CODE’ для моделей MEDIUM, HUGE и LARGE (name — имя модуля, в котором описан данный сегмент). В этих моделях директива .CODE также допускает необязательный операнд — имя определяемого сегмента, но все сегменты кода, описанные так в одном и том же модуле, объединяются в один сегмент с именем NAME_TEXT. Директива .STACK описывает сегмент стека .stack размер эквивалентно: STACK segment para public ’stack’ Необязательный параметр указывает размер стека. По умолчанию он равен 1 Кб. Директива .DATA описывает обычный сегмент данных .data эквивалентно: _DATA segment word public ’DATA’ Директива .DATA? описывает сегмент неинициализированных данных .data? Эквивалентно: _BSS segment word public ’BSS’ Этот сегмент обычно не включается в программу, а располагается за концом памяти, так что все описанные в нем переменные на момент загрузки программы имеют неопределенные значения. Директива .CONST описывает сегмент неизменяемых данных .const Эквивалентно: CONST segment word public ’CONST’ В некоторых операционных системах этот сегмент будет загружен так, что попытка записи в него может привести к ошибке. Директива .FARDATA описывает сегмент дальних данных .fardata имя_сегмента эквивалентно: имя_сегмента segment para private ’FAR_DATA’ Доступ к данным, описанным в этом сегменте, потребует загрузки сегментного регистра. Если не указан операнд, в качестве имени сегмента используется FAR_DATA. Директива .FARDATA? описывает сегмент дальних неинициализированных данных .fardata? имя_сегмента эквивалентно: имя_сегмента segment para private ’FAR_BSS’ Как и в случае с FARDATA, доступ к данным из этого сегмента потребует загрузки сегментного регистра. Если имя сегмента не указано, используется FAR_BSS. Во всех моделях памяти сегменты, представленные директивами .DATA, .DATA?, .CONST, .FARDATA и .FARDATA?, а также сегмент, описанный директивой .STACK, если не был указан модификатор FARSTACK, и сегмент .CODE в модели TINY автоматически объединяются в группу с именем FLAT — для модели памяти FLATили DGROUP — для всех остальных моделей. При этом сегментный регистр DS (и SS, если не было FARSTACK, и CS в модели TINY) настраивается на всю эту группу, как если бы была выполнена команда ASSUME. Порядок загрузки сегментов При использовании стандартных директив сегментациисегменты загружаются в память в том порядке, в котором они описываются в тексте программы. При использовании упрощенных директив сегментации (по умолчанию) устанавливается порядок загрузки сегментов, существующий в MS DOS и часто требуемый для взаимодействия программ на ассемблере с программами на языках высокого уровня. Порядок загрузки сегментов: 1. Все сегменты класса 'CODE'. 2. Все сегменты, не принадлежащие группе DGROUP и классу 'CODE'. 3. Группа сегментов DGROUP: 3.1. Все сегменты класса 'BEGDATA'. 3.2. Все сегменты, кроме классов 'BEGDATA', 'BSS' и 'STACK'. 3.3. Все сегменты класса 'BSS'. 3.4. Все сегменты класса 'STACK'. Знание порядка загрузки сегментов необходимо, например, для вычисления длины программы или адреса ее конца. Для этого надо знать, какой сегмент будет загружен последним, и смещение последнего байта в нем.
201.
Программа на ассемблере представляет собой совокупность блоков памяти, называемых сегментами памяти. Программа может состоять из одного или нескольких таких блоков-сегментов. Каждый сегмент содержит совокупность предложений языка, каждое из которых занимает отдельную строку кода программы.
Предложения ассемблера бывают четырех типов.
Команды или инструкции, представляющие собой символические аналоги машинных команд.
В процессе трансляции инструкции ассемблера преобразуются в соответствующие команды системы команд микропроцессора. Одна команда Ассемблера, как правило, соответствует одной команде микропроцессора, что, вообще говоря, является характерным для низкоуровневых языков.
Приведем пример инструкции, которая осуществляет увеличение двоичного числа, хранящегося в регисре eax, на единицу:
inc eax
– макрокоманды – оформляемые определенным образом предложения текста программы, замещаемые во время трансляции другими предложениями.
Примером макрокоманды может служить следующий макрос конца программы:
exit macro
mov ax,4c00h
int 21h
endm
– директивы, являющиеся указанием транслятору ассемблера на выполнение некоторых действий.
У директив нет аналогов в машинном представлении; В качестве примера приведем директиву TITLE, которая задает заголовок файла листинга: %TITLE «Листинг 1»
– строки комментариев, содержащие любые символы, в том числе и буквы русского алфавита. Комментарии игнорируются транслятором. Пример:
; эта строчка является комментарием
202-203.
Стеком
называют область программы для временного
хранения произвольных данных. Разумеется,
данные можно сохранять и в сегменте
данных, однако в этом случае для каждого
сохраняемого на время данного надо
заводить отдельную именованную ячейку
памяти, что увеличивает размер программы
и количество используемых имен. Удобство
стека заключается в том, что его область
используется многократно, причем
сохранение в стеке данных и выборка их
оттуда выполняется с помощью эффективных
команд push и pop без
указания каких-либо имен.
Стек
традиционно используется, например,
для сохранения содержимого регистров,
используемых программой, перед вызовом
подпрограммы, которая, в свою очередь,
будет использовать регистры процессора
"в своих личных целях". Исходное
содержимое регистров извлекается из
стека после возврата из подпрограммы.
Другой распространенный прием - передача
подпрограмме требуемых ею параметров
через стек. Подпрограмма, зная, в каком
порядке помещены в стек параметры, может
забрать их оттуда и использовать при
своем выполнении.
Отличительной
особенностью стека является своеобразный
порядок выборки содержащихся в нем
данных: в
любой момент времени в стеке доступен
только верхний элемент, т.е. элемент,
загруженный в стек последним. Выгрузка
из стека верхнего элемента делает
доступным следующий элемент.
Элементы
стека располагаются в области памяти,
отведенной под стек, начиная со дна
стека (т.е. с его максимального адреса)
по последовательно уменьшающимся
адресам. Адрес верхнего, доступного
элемента хранится в регистре-указателе
стека SP. Как и любая другая область
памяти программы, стек должен входить
в какой-то сегмент или образовывать
отдельный сегмент. В любом случае
сегментный адрес этого сегмента
помещается в сегментный регистр стека
SS. Таким образом, пара регистров SS:SP
описывают адрес доступной ячейки стека:
в SS хранится сегментный адрес стека, а
в SP - смещение последнего сохраненного
в стеке данного (смотрим рисунок). Обратите
внимание на то, что в исходном состоянии
указатель стека SP указывает на ячейку,
лежащую под дном стека и не входящую в
него.
а
- исходное состояние, б - после загрузки
одного элемента (в данном примере -
содержимого регистра АХ), в - после
загрузки второго элемента (содержимого
регистра DS), г - после выгрузки одного
элемента, д - после выгрузки двух элементов
и возврата в исходное состояние.
Загрузка
в стек осуществляется специальной
командой работы со стеком push (протолкнуть).
Эта команда сначала уменьшает на 2
содержимое указателя стека (SP), а затем
помещает операнд по адресу в SP. Если,
например, мы хотим временно сохранить
в стеке содержимое регистра АХ, следует
выполнить команду
push АХ
Стек
переходит в состояние, показанное на
рис. б. Видно, что указатель стека
смещается на два байта вверх (в сторону
меньших адресов) и по этому адресу
записывается указанный в команде
проталкивания операнд. Следующая команда
загрузки в стек, например,
push
DS
переведет стек в состояние,
показанное на рис. в. В стеке будут теперь
храниться два элемента, причем доступным
будет только верхний, на который указывает
указатель стека SP. Если спустя какое-то
время нам понадобилось восстановить
исходное содержимое сохраненных в стеке
регистров, мы должны выполнить команды
выгрузки из стека pop (вытолкнуть):
pop
DS
pop AX
Состояние стека после
выполнения первой команды показано на
рис. г, а после второй - на рис. д. Для
правильного восстановления содержимого
регистров выгрузка из стека должна
выполняться в порядке, строго
противоположном загрузке - сначала
выгружается элемент, загруженный
последним, затем предыдущий элемент и
т.д.
Совсем не обязательно при
восстановлении данных помещать их туда,
где они были перед сохранением. Например,
можно поместить в стек содержимое DS, а
извлечь его оттуда в другой сегментный
регистр - ES;
push DS
pop ES ;
Теперь ES=DS, а стек пуст
Это
распространенный прием для перенесения
содержимого одного регистра в другой,
особенно, если второй регистр -
сегментный.
Обратите внимание (см.
рисунок) на то, что после выгрузки
сохраненных в стеке данных они физически
не стерлись, а остались в области стека
на своих местах. Правда,
при "стандартной" работе со стеком
они оказываются недоступными.
Действительно, поскольку указатель
стека SP указывает под дно стека, стек
считается пустым; очередная команда
push поместит новое данное на место
сохраненного ранее содержимого АХ,
затерев его. Однако пока стек физически
не затерт, сохраненными и уже выбранными
из него данными можно пользоваться,
если помнить, в каком порядке они
расположены в стеке. Этот прием часто
используется при работе с
подпрограммами.
Какого размера должен
быть стек? Это зависит от того, насколько
интенсивно он используется в программе.
Если, например, планируется хранить в
стеке массив объемом 10 000 байт, то стек
должен быть не меньше этого размера.
При этом надо иметь в виду, что в ряде
случаев стек автоматически используется
системой, в частности, при выполнении
команды прерывания int 21h. По этой команде
сначала процессор помещает в стек адрес
возврата, а затем DOS отправляет туда же
содержимое регистров и другую информацию,
относящуюся к прерванной программе.
Поэтому, даже если программа совсем не
использует стек, он все же должен
присутствовать в программе и иметь
размер не менее нескольких десятков
слов.
Что произойдет, если программист
по ошибке или умышленно не опишет стек
в своей программе? Изменим нашу мега
программу, удалив из нее описание
стека.
В
регистр DX помещен код двух знаков @.
Это сделано для наглядности примера.
Запустим
программу под отладчиком.
Видим,
что в SS находится тот
же адрес, что и в CS;
от сюда можно сделать вывод, что сегменты
команд и стека совпадают. Однако
содержимое SP равно нулю. Первая же
команда push уменьшит содержимое SP на 2,
то есть поместит в SP число -2, значит ли
это, что стек будет расти как ему и
положено, ввех, но не внутри сегмента
команд, а над ним, по адресам -2, -4, -6 и
т.д. относительно верхней границы
сегмента команд. Оказывается это не
так.
Если взять 16-разрядный двоичный
счетчик, в котором записн 0, и послать в
него два вычитающих импульса, то после
первого в нем окажется число FFFFh, а после
второго – FFFEh.
При желании мы можем рассматривать
число FFFEh как -2 (что имеет место при
работе со знаковыми числами, о которых
поговорим чуть позже), однако процессор
при вычислении адресов рассматривает
содержимое регистров как целые числа
без знака и число FFFEh
оказывается эквивалентным не -2, а 65534.
В результате первая же команда занесения
данного в стек (в нашем случае push dx)
поместит это данное не над сегментом
команд, а в самый его конец, в последнее
слово по адресу CS:FFFEh
(что, в данном примере, эквивалентно
SS:FFFEh). При дальнейшем использовании
стека его указатель будет смещаться в
сторону меньших адресов, проходя значения
FFFCh, FFFAh и т.д.
Таким образом, если в
программе отсутствует явное определение
стека, система сама создает стек по
умолчанию в конце сегмента команд или,
точнее, по адресу FFFEh относительно начала
сегмента команд (см. рисунок).
Выполним
программу дальше и убедимся в этом. По
ходу выполнения программы с первой
команды до седьмой, мы сможем видеть,
что в стек помещаются какие-то значения,
но при этом регистр SP не изменяется
(предположительно, все это самодеятельность
отладчика), и только после выполнения
седьмой команды – push
dx, SP принимает
значение FFFEh,
и на дне стека мы видим два
символа @.
Рассмотренное
явление, когда при уменьшении адреса
после адреса 0 у
нас получается адрес FFFEh,
т.е. от начала сегмента мы прыгнули сразу
в его конец, носит
название циклического
возврата или оборачивания
адреса. С этим явлением приходится
сталкиваться довольно часто.
Расположение
стека в конце сегмента команд не приводит
к каким-либо неприятностям, пока размер
программы далек от граничной величины
64Кбайт. В этом случае начало сегмента
команд занимают коды команд, конец –
стек, а между ними располагаются данные
(если сегмент данных описан в программе
после сегмента команд). Если, однако,
размер сегментов команд или данных
приближается к 64Кбайт, то фактически
стек будет наложен на тот или иной
сегмент программы и возникнет опасность
затирания данных или кода. Очевидно,
что этого нельзя допускать. В
тоже время, система не проверяет, что
происходит со стеком и ни как не реагирует
на затирание команд или данных. Таким
образом, оценка размеров собственно
программы, данных и стека является
важным этапом разработки программы.
204.
Средства, сокращающие размер программы: процедуры; макроподстановки (макроассемблер); генерация и обработка программных прерываний.
Процедура – группа команд для решения конкретной подзадачи.
Синтаксис процедуры:
имя_процедуры PROC [[модификатор_языка ] язык] [расстояние ]
команды
[имя_процедуры ] ENDP
Пример
model small .stack 100h .data .code my_proc procnear ret my_proc endp start: end start
процедуру можно распологать в конце программы либо вкладывать в другую. Во втором случае неоходимо предусмотреть обход тела процедуры, ограниченного директивами PROC и ENDP, с помощью JMP/
Команда CALL осуществляет вызов процедуры (подпрограммы). Синтаксис команды: call [модификатор] имя_процедуры Подобно команде JMP команда CALL передает управление по адресу с символическим именем имя_процедуры, но при этом в стеке сохраняется адрес возврата (то есть адрес команды, следующей после команды CALL). Команда RET считывает адрес возврата из стека и загружает его в регистры CS и EIP/IP, тем самым возвращая управление на команду, следующую в программе за командой CALL Синтаксис команды: ret [число] Необязательный параметр [число] обозначает количество элементов, удаляемых из стека при возврате из процедуры.
Как и в случае команды JMP, вызов процедуры командой CALL может быть внутрисегментным и межсегментным.
С директивой PROC используются еще несколько директив: ARG, RETURNS, LOCAL, USES. Их назначение — помочь программисту выполнить некоторые рутинные действия при вызове и возврате из процедуры (заодно и повысив надежность кода). Директивы ARG и RETURNS назначают входным и выходным параметрам процедуры, передаваемым через стек, символические имена. Директива USES в качестве параметров содержит имена используемых в процедуре регистров.
Процессор поддерживает 18 команд условного перехода,
Условие обычно выясняется с помощью команды сравнения: cmp операнд_1,операнд_2
Флаги, устанавливаемые командой СМР, можно анализировать специальными командами условного перехода. Команды перехода начинаются с j, дальше идут такие сокращения
Е Equal N Not G Greater 1 Less аAbove b Below
например je перейти если равно jne если не равно jbe если меньше или равно
и так далее
пример (фрагмент обнуляет pole_m) .data n equ 50 pole_m db .code n dup (?) xor bx.bx ;bx=0 m1: mov mem[bx],0 inc bx cmp bx,n jne m1 exit
Так как команды условного перехода не изменяют флагов, то после одной команды СМР вполне могут следовать несколько команд условного перехода.
Часть команд перехода анализирует массив флагов, который в процессоре, и прыгает в зависимости от их состояния, а именно:
JC прыгает, если CF=1
JP : PF=1 JZ: ZF=1 JS: SF=1 JO: OF=1 JNC: CF = 0 JNP: PF = 0 JNZ: ZF = 0 Jns: Sf=0 JNO: OF = 0
Циклы можно организовать, просто переходя по меткам, но есть и специальная команда. Команда LOOP позволяет организовать циклы (loops), подобные циклам for в языках высокого уровня с автоматическим уменьшением счетчика цикла. Синтаксис команды: loop метка_перехода Команда реализует описанные далее действия. 1. Декремент регистра ЕСХ/СХ. 2. Сравнение регистра ЕСХ/СХ с нулем: если (ЕСХ/СХ) > 0, то управление передается на метку перехода; если (ЕСХ/СХ) = 0, то управление передается на следующую после LOOP команду.
205.
В программировании рекурсивной называется процедура, которая прямо или косвенно вызывает саму себя. В этой статье мы рассмотрим классический пример рекурсии — вычисление факториала.
Что
такое факториал? Это математическая
функция, определённая для целых
неотрицательных чисел.
Обозначается
восклицательным знаком (n! —
факториал числа n).
Факториал натурального числа равен
произведению всех натуральных чисел
до него. Факториал нуля равен 1. Подробнее
можете прочестьздесь.
У факториала есть такое свойство: для натурального числа он равен произведению этого числа на факториал предыдущего натурального числа. Иными словами, факториал может быть определен формулой:
Теперь должно быть всё понятно, можно писать код Наша задача — создать процедуру, которая будет возвращать 1, если параметр равен 0, иначе уменьшать его на 1 и вызывать саму себя!
Поскольку факториал определён для неотрицательных чисел, параметр процедуры и возвращаемое значение будут 16-битными целыми без знака. Параметр будет передаваться через стек, а результат возвращаться в регистре AX. Вот что у меня получилось:
; Рекурсивная процедура вычисления факториала
; вход: CX - число без знака
; выход: AX - результат
factorial:
push bp ;Сохранение BP
mov bp,sp ;BP=SP
mov ax,[bp+4] ;AX=параметр
test ax,ax ;Проверка AX
jz f_ret1 ;Если 0, вернуть 1
dec ax ;Декремент AX
push ax ;Помещение параметра в стек
call factorial ;Вызов процедуры для предыдущего числа
mul word[bp+4] ;Умножение результата на параметр процедуры
jmp f_ret ;Переход к возврату из процедуры
f_ret1:
inc ax
f_ret:
pop bp ;Восстановление BP
ret 2 ;Возврат из процедуры
Факториал — очень быстро возрастающая функция, поэтому мы не сможем этой процедурой вычислять его для чисел больше 8 (результат просто не влезет в 16 бит). Если вам нужно серьёзно вычислять факториал — используйте числа с плавающей точкой (хотя результат будет не совсем точным) или многобайтные целые (потребуется хитрая процедура умножения).
Для
наглядности я добавил в программу цикл
и процедуры для вывода чисел в десятичном
виде (из части
22 учебного курса).
В результате получилась такая программа
(вроде считает правильно
):
Скачать полный исходный код примера можно здесь: factorial.asm.