SiteSearch
logo

Документация и статьи по Ассемблеру, Reversing:

 

Теория по языку Ассемблер

Оглавление

Введение

1. Архитектура процессора 8086
1.1. Регистры
1.2. Сегменты, принцип сегментации
1.3. Стек
1.4. Прерывания
1.5. Режимы адресации

2. Загрузка и выполнение программ в DOS
2.1. EXE- и COM-программы
2.2. Выход из программы

3. Ассемблер, макроассемблер, редактор связей

4. Язык Ассемблера. Начальные сведения
4.1. Идентификаторы, переменные, метки, имена, ключевые слова
4.2. Типы данных
4.3. Предложения
4.4. Выражения
4.5. Приоритеты операций
4.6. Ссылки вперед
4.7. Директивы определения данных
4.7.1. Скалярные данные
4.7.2. Записи
4.7.3. Структуры
4.8. Директива эквивалентности
4.9. Структура программы на языке Ассемблера
4.9.1. Директива ASSUME
4.9.2. Директива INCLUDE
4.9.3. Структура EXE- и COM- программы
4.10. Модификация адресов
4.11. Сегментные регистры по умолчанию

5. Команды пересылки
5.1. Команда MOV
5.2. Команда обмена данных XCHG
5.3. Команды загрузки полного указателя LDS и LES
5.4. Команда перекодировки XLAT
5.5. Команды работы со стеком
5.6. Команды ввода-вывода

6. Арифметические команды
6.1. Команды арифметического сложения ADD и ADC
6.2. Команды арифметического вычитания SUB и SBB
6.3. Команда смены знака NEG
6.4. Команды инкремента INC и декремента DEC
6.5. Команды умножения MUL и IMUL
6.6. Команды деления DIV и IDIV

7. Команды побитовой обработки
7.1. Команды, выполняющие логические операции
7.2. Команды, выполняющие операции сдвигов

8. Команды сравнения и передачи управления

9. Подпрограммы и прерывания

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

11. Команды управления процессором

12. Структуры данных
12.1. Массивы
12.2. Связанные списки

13. Условное ассемблирование

14. Макросредства
14.1. Макродирективы

15. Языки высокого уровня и Turbo Assembler
15.1. Основные принципы взаимодействия Turbo Assembler и Borland Pascal

Курс практических работ по программированию на языке Ассемблера
Практическая работа №1. Архитектуры процессора Intel 8086.
Практическая работа №2. Структура EXE- и COM- программы. Вывод на экран.
Практическая работа №3. Циклы. Ввод с клавиатуры.
Практическая работа №4. Ввод чисел. Перевод чисел в различные системы счисления.
Практическая работа №5. Подпрограммы. Работа с файлами через описатели.
Практическая работа №6. Работа с файлами, используя DTA.
Практическая работа №7.Работа с прерываниями: перехват и восстановление.
Практическая работа №8.Порты ввода-вывода. Обмен данными с внешним устройством.

Список литературы





Введение

Не смотря на то, что язык программирования Ассемблера, в чистом виде, относительно редко используется на практике, его изучение является необходимой частью подготовки профессиональных программистов, поскольку позволяет шире понять принципы работы ЭВМ, операционных систем и трансляторов с языков высокого уровня.
Настоящее учебное пособие содержит описание архитектуры и системы команд процессора Intel 8086, что является базой для изучения программирования других, более современных, процессоров Intel. Кроме того, в учебном пособии уделено внимание интеграции языков высокого уровня и языка Ассемблера, что в настоящее время наиболее актуально.
В качестве дополнения учебное пособие включает курс практических работ, изучение которых позволит получить навык в программировании на языке Ассемблера.

1. Архитектура процессора 8086

Системный блок персонального компьютера содержит: блок питания; системную (материнскую) плату; адаптеры внешних устройств; накопители на жестких магнитных (НЖМД) и гибких (НГМД) дисках, а также ряд других устройств. Для нас наибольший интерес представляет системная плата, на которой размещаются постоянное запоминающее устройство ПЗУ (ROM - read only memory), оперативное запоминающее устройство ОЗУ (RAM - random access memory), процессор и логика управления, связанные между собой шинами.
Физически и ОЗУ и ПЗУ выполнены в виде микросхем. Характерным для персонального компьютера является тот факт, что при выключении электропитания содержимое ОЗУ утрачивается (энергозависимая память), а ПЗУ – нет (энергонезависимая память).
Одна из основных задач ПЗУ обеспечить процедуру старта персонального компьютера. В ПЗУ хранятся базовая система ввода/вывода BIOS а также некоторые служебные программы и таблицы, например, начальный загрузчик, программа тестирования POST и т.п.
Оперативная память ОЗУ предназначена для временного хранения программ и данных, которыми они манипулируют. Логически оперативную память можно представить в виде последовательности ячеек, каждая из которых имеет свой номер, называемый адресом.
Центральный процессор (ЦП) в современных персональных компьютерах выполнен в виде одной сверхбольшой интегральной микросхемы (СБИС). ЦП выполняет машинные команды, выбирая их в заданной последовательности из оперативной памяти. Работа всех электронных устройств компьютера координируется сигналами управления, вырабатываемыми ЦП и некоторыми другими СБИС, сигналами тактового генератора, с помощью которых синхронизируются действия всех сигналов.
Возможности компьютера в большей степени зависят от типа установленного процессора и его тактовой частоты. Семейство процессоров 80х86 корпорации Intel включает в себя микросхемы: 8086, 80186, 80286, 80386, 80486, Pentium, Pentium II, Pentium III и т.д. Совместимые с 80х86 микросхемы выпускают также фирны AMD, IBM, Cyrix. Особенностью этих процессоров является преемственность на уровне машинных команд: программы, написанные для младших моделей процессоров, без каких-либо изменений могут быть выполнены на более старших моделях. При этом базой является система команд процессора 8086, знание которой является необходимой предпосылкой для изучения остальных процессоров.

Структуру центрального процессора Intel 8086 можно разделить на два логических блока (рис.1.1):

- блок исполнения (EU:Execution Unit);

- блок интерфейса шин (BIU:Bus Interface Unit).

(Интерфейс (interface) - это совокупность средств, обеспечивающих сопряжение устройств и программных модулей как на физическом, так и на логическом уровнях).

В состав EU входят: арифметическо-логическое устройство ALU, устройство управления CU и десять регистров. Устройства блока EU обеспечивают обработку команд, выполнение арифметических и логических операций.

Три части блока BIU - устройство управления шинами, блок очереди команд и регистры сегментов – предназначены для выполнения следующих функций:

- управление обменом данными с EU, памятью и внешними устройствами ввода/вывода;

- адресация памяти;

- выборка команд (осуществляется с помощью блока очереди команд Queue, который позволяет выбирать команды с упреждением).

С точки зрения программиста, процессор 8086 состоит из 8 регистров общего назначения, 4 сегментных регистров, регистра адреса команд (счетчика команд) и регистра флагов. Процессор выставляет на шину адреса адрес выбираемых из памяти команд (или данных), которые поступают в шестибайтный буфер (очередь команд), а затем исполняются.
Адресную шину можно представить в виде 20 проводников, в каждом из которых может либо протекать напряжение заданного уровня (сигнал 1), либо отсутствовать (сигнал 0). Таким образом, микропроцессор оперирует с двоичной системой счисления (двоичной системой представления данных). Символьная информация кодируется в соответствии с кодом ASCII (Американский стандартный код для обмена информацией). Числовые данные кодируются в соответствии с двоичной арифметикой. Отрицательные числа представляются в дополнительном коде.
Минимальная единица информации, соответствующая двоичному разряду, называется бит (Bit). Группа из восьми битов называется байтом (Byte) и представляет собой наименьшую адресуемую единицу – ячейку памяти. Биты в байте нумеруют справа налево цифрами 0...7.

Двухбайтовое поле образует шестнадцатиразрядное машинное слово (Word), биты в котором нумеруются от 0 до 15 справа налево. Байт с меньшим адресом считается младшим.

Четырехбайтовое поле образует двойное слово (Double Word), а шестнадцатибайтовое – параграф (Paragraph).

Таким образом, с помощью 16-разрядной шины данных можно передавать числа от 0 (во всех проводниках сигнал 0) до 65535 (во всех проводниках сигнал 1). Несмотря на то, что двоичная система обладает высокой наглядностью, она имеет существенный недостаток – числа, записанные в двоичной системе, слишком громоздки. С другой стороны, привычная для нас десятичная система слишком сложна для перевода чисел в двоичную систему счисления и обратно. Поэтому наибольшее распространение в практике программирования получила шестнадцатеричная система счисления.
При написании программ принято двоичные числа сопровождать латинской буквой B или b, (например, 101B), а шестнадцатеричные – буквой H или h на конце. Если число начинается с буквы, то обязательной является постановка нуля впереди, например, 0BA8H.

1.1. Регистры

Процессор 8086 имеет 14 шестнадцатиразрядных регистров, которые используются для управления исполнением команд, адресации и выполнения арифметических операций. Регистр, содержащий одно слово, адресуется по имени.

Регистры общего назначения. К ним относятся 16-разрядные регистры АХ, ВХ, СХ, DX, каждый из которых разделен на 2 части по 8 разрядов:

АХ состоит из АН (старшая часть) и AL (младшая часть);

ВХ состоит из ВH и BL;

СХ состоит из СН и CL;

DX состоит из DH и DL;

В общем случае функция, выполняемая тем или иным регист­ром, определяется командами, в которых он используется. При этом с каждым регистром связано некоторое стандартное его зна­чение. Ниже перечисляются наиболее характерные функции каж­дого регистра:

- регистр АХ служит для временного хранения данных (регистр аккумулятор); часто используется при выполнении операций сло­жения, вычитания, сравнения и других арифметических и логиче­ских операции;

- регистр ВХ служит для хранения адреса некоторой области па­мяти (базовый регистр), а также используется как вычислитель­ный регистр;

- регистр СХ иногда используется для временного хранения дан­ных, но в основном служит счетчиком; в нем хранится число повто­рений одной команды или фрагмента программы;

- регистр DX используется главным образом для временного хра­нения данных; часто служит средством пересылки данных между разными программными системами, в качестве расширителя акку­мулятора для вычислений повышенной точности, а также при умножении и делении.

Регистры для адресации. В микропроцессоре существуют четыре 16-битовых (2 байта или 1 слово) регистра, которые могут принимать участие в адресации операндов. Один из них одновременно является и регистром общего назначения — это регистр ВХ, или базовый регистр. Три другие регистра — это указатель базы ВР, индекс источника SI и индекс результата DI. Отдельные байты этих трех регистров недоступны.
Любой из названных выше 4 регистров может использоваться для хранения адреса памяти, а команды, работающие с данными из памяти, могут обращаться за ними к этим регистрам. При адресации памяти базовые и индексные регистры могут быть использованы в различных комбинациях. Разнообразные способы сочетания в командах этих регистров и других величин называются способами или режимами адресации.

Регистры сегментов. Имеются четыре регистра сегментов, с помощью которых память можно организовать в виде совокупности четырех различных сегментов. Этими регистрами являются:

- CS — регистр программного сегмента (сегмента кода) определяет местоположение части памяти, содержащей программу, т. е. выполняемые процессором команды;

- DS — регистр информационного сегмента (сегмента данных) идентифицирует часть памяти, предназначенной для хранения данных;

- SS — регистр стекового сегмента (сегмента стека) определяет часть памяти, используемой как системный стек;

- ES — регистр расширенного сегмента (дополнительного сегмента) указывает дополнительную область памяти, используемую для хранения данных.

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

Регистр указателя стека. Указатель стека SP – это 16-битовый регистр, который определяет смещение текущей вершины стека. Указатель стека SP вместе с сегментным регистром стека SS используются микропроцессором для формирования физического адреса стека. Стек всегда растет в направлении меньших адресов памяти, т.е. когда слово помещается в стек, содержимое SP уменьшается на 2, когда слово извлекается из стека, микропроцессор увеличивает содержимое регистра SP на 2.

Регистр указателя команд IP. Регистр указателя команд IP, иначе называемый регистром счетчика команд, имеет размер 16 бит и хранит адрес некоторой ячейки памяти – начало следующей команды. Микропроцессор использует регистр IP совместно с регистром CS для формирования 20-битового физического адреса очередной выполняемой команды, при этом регистр CS задает сегмент выполняемой программы, а IР – смещение от начала сегмента. По мере того, как микропроцессор загружает команду из памяти и выполняет ее, регистр IP увеличивается на число байт в команде. Для непосредственного изменения содержимого регистра IP служат команды перехода.

Регистр флагов. Флаги – это отдельные биты, принимающие значение 0 или 1. Регистр флагов (признаков) содержит девять активных битов (из 16). Каждый бит данного регистра имеет особое значение, некоторые из этих бит содержат код условия, установленный последней выполненной командой. Другие биты показывают текущее состояние микропроцессора.

Биты регистра флагов имеют следующее назначение:

OF (признак переполнения) – равен 1, если возникает арифметическое переполнение, то есть когда объем результата превышает размер ячейки назначения;

DF (признак направления) – устанавливается в 1 для автоматического декремента в командах обработки строк, и в 0 – для инкремента;

IF (признак разрешения прерывания) – прерывания разрешены, если IF=1. Если IF=0, то распознаются лишь немаскированные прерывания;

TF (признаков трассировки) - если TF=1, то процессор переходит в состояние прерывания INT 3 после выполнения каждой команды;

SF (признак знака) – SF=1, когда старший бит результата равен 1. Иными словами, SF=0 для положительных чисел, и SF=1 для отрицательных чисел;

ZF (признак нулевого результата) – ZF=1, если результат равен нулю;

AF (признак дополнительного переноса) – этот флаг устанавливается в 1 во время выполнения команд десятичного сложения и вычитания при возникновении переноса или заема между полубайтами;

PF (признак четности) – этот признак устанавливается в 1, если результат имеет четное число единиц;

CF (признак переноса) – этот флаг устанавливается в 1, если имеет место перенос или заем из старшего бита результата; он полезен для произведения операций над числами длиной в несколько слов, которые сопряжены с переносами и заемами из слова в слово;

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

1.2. Сегменты, принцип сегментации

Числа, устанавливаемые процессором на адресной шине, являются адресами, то есть номерами ячеек оперативной памяти (ОП). Размер ячейки ОП составляет 8 разрядов, т.е. 1 байт. Поскольку для адресации памяти процессор использует 16-разрядные адресные регистры, то это обеспечивает ему доступ к 65536 (FFFFh) байт или 64К (1К = 1024 байт = 210 байт) основной памяти. Такой блок непосредственно адресуемой памяти называется сегментом. Любой адрес формируется из адреса сегмента (всегда кратен 16, т.е. начинается с границы параграфа) и адреса ячейки внутри сегмента (этот адрес называется смещением). Для адресации большего объема памяти в процессоре 8086 используется специальная процедура пересчета адресов, называемая вычислением абсолютного (эффективного) адреса.
Когда процессор выбирает очередную команду на исполнение, в качестве ее адреса используется содержимое, регистра IP. Этот адрес называется исполнительным. Поскольку регистр IP шестнадцатиразрядный, исполнительный адрес тоже содержит 16 двоичных разрядов. Однако адресная шина, соединяющая процессор и память имеет 20 линий связи.
Чтобы получить 20-битовый адрес, дополнительные 4 бита адресной информации извлекаются из сегментных регистров. Сами сегментные регистры имеют размер в 16 разрядов, а содержащиеся в этих регистрах (CS, DS, SS или ES) 16-битовые значения называются базовым адресом сегмента. Микропроцессор объединяет 16-битовый исполнительный адрес и 16-битовый базовый адрес следующим образом: он расширяет содержимое сегментного регистра (базовый адрес) 4 нулевыми битами (в младших разрядах), делая его 20-битовым (полный адрес сегмента) и прибавляет смещение (исполнительный адрес). При этом 20-битовый результат является физическим или абсолютным адресом ячейки памяти.

Существуют три основных типа сегментов:

- сегмент кода – содержит машинные команды, Адресуется регистром CS;

- сегмент данных – содержит данные, то есть константы и рабочие области, необходимые программе. Адресуется регистром DS;

- сегмент стека – содержит адреса возврата в точку вызова подпрограмм. Адресуется регистром SS.

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

<адрес сегмента>:<смещение>

или

<сегментный регистр>:<адресное выражение>

1.3. Стек



Во многих случаях программе требуется временно запомнить некоторую информацию. Эта проблема в персональном компьютере решена посредством реализации стека LIFO ("последний пришел - первый ушел"), называемого также стеком включения/извлечения (stack). Стек – это область памяти для временного хранения данных, в которую по специальным командам можно записывать отдельные слова (но не байты); при этом для запоминания данных достаточно выполнить лишь одну команду и не нужно беспокоиться о выборе соответствующего адреса: процессор автоматически выделяет для них свободное место в области временного хранения. Наиболее важное использование стека связано с подпрограммами, в этом случае стек содержит адрес возврата из подпрограммы, а иногда и передаваемые в/из подпрограмму данные. Стек обычно рассчитан на косвенную адресацию через регистр указатель стека. При включении элементов в стек производится автоматический декремент указателя стека, а при извлечении – инкремент, то есть стек всегда «растет» в сторону меньших адресов памяти. Адрес последнего включенного в стек элемента называется вершиной стека (TOS), а адрес сегмента стека – базой стека.

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

Часто возникают ситуации, когда необходимо выполнить одну из набора специальных процедур, если в системе или в программе возникают определенные условия, например, нажата клавиша на клавиатуре или произошло деление на ноль. Действие, стимулирующее выполнение одной из таких процедур, называется прерыванием, поскольку основной процесс при этом приостанавливается на время выполнения этой процедуры. Существует два общих класса прерываний: внутренние и внешние. Первые инициируются состоянием ЦП или командой, а вторые – сигналом, подаваемым от других компонентов системы. Типичные внутренние прерывания: деление на нуль, переполнение и т.п., а типичные внешние – это запрос на обслуживание со стороны какого-либо устройства ввода/вывода.
Переход к процедуре прерывания осуществляется из любой программы, а после выполнения процедуры прерывания обязательно происходит возврат в прерванную программу. Перед обращением к процедуре прерывания должно быть сохранено состояние всех регистров и флагов, используемых процедурой прерывания, после окончания прерывания эти регистры должны быть восстановлены.
Некоторыми видами прерываний управляют флаги IF и TF, которые для восприятия прерываний должны быть правильно установлены. Если условия для прерывания удовлетворяются и необходимые флаги установлены, то микропроцессор завершает текущую команду, а затем реализует последовательность прерывания:

- текущее значение регистра флагов включается в стек;

- текущее значение кодового сегмента включается в стек;

- текущее значение счетчика команд включается в стек;

- сбрасываются флаги IF и TF.

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

- указателя стека;

- сегмента кода;

- регистра флагов.

Двойное слово, в котором находится новое содержимое счетчика команд и сегмента кода, называется указателем прерывания, или вектором прерывания. Каждому типу прерывания назначено число из диапазона 0...255, и адрес вектора прерывания находится путем умножения номера типа на четыре (размер вектора прерывания). Таким образом, все вектора прерываний образуют таблицу векторов прерываний, которая содержится в памяти по адресу 0000h:0000h и инициализируется при загрузке компьютера.

1.5. Режимы адресации

В зависимости от спецификаций и местоположения источников образования полного (абсолютного) адреса в языке ассемблера различают следующие способы адресации операндов:

- регистровая;

- прямая;

- непосредственная;

- косвенная;

- базовая;

- индексная;

- базово-индексная.

Регистровая адресация подразумевает использование в качестве операнда регистра процессора, например:

PUSH DS

MOV BP,SP

При прямой адресации один операнд представляет собой адрес памяти, второй – регистр:

MOV Data,AX

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

MOV AH,4CH

При использовании косвенной адресации абсолютный адрес формируется исходя из сегментного адреса в одном из сегментных регистров и смещения в регистрах BX, BP, SI или DI:

MOV AL,[BX] ;База – в DS, смещение – в BX

MOV AX,[BP] ;База – в SS, смещение – в BP

MOV AX,ES:[SI] ;База – в ES, смещение – в SI

В случае применения базовой адресации исполнительный адрес является суммой значения смещения и содержимого регистра BP или BX, например:

MOV AX,[BP+6] ;База – SS, смещение – BP+6

MOV DX,8[BX] ;База – DS, смещение – BX+8

При индексной адресации исполнительный адрес определяется как сумма значения указанного смещения и содержимого регистра SI или DI так же, как и при базовой адресации, например:

MOV DX,[SI+5] ;База – DS, смещение – SI+5

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

MOV BX,[BP][SI] ;База – SS, смещение – BP+SI

MOV ES:[BX+DI],AX ;База – ES, смещение – BX+DI

MOV AX,[BP+6+DI] ;База – SS, смещение - BP+6+DI

2. Загрузка и выполнение программ в DOS

При загрузке программ в оперативную память DOS (дисковая операционная система) инициализирует как минимум три сегментных регистра: CS, DS и SS. При этом совокупности байтов, представляющих команды процессора (код программы), и данные помещаются из файла на диске в оперативную память, а адреса этих сегментов записываются в CS и DS соответственно. Сегмент стека либо выделяется в области, указанной в программе, либо совпадает (если он явно в программе не описан) с самым первым сегментом программы. Адрес сегмента стека помещается в регистр SS. Программа может иметь несколько кодовых сегментов и сегментов данных и в процессе выполнения специальными командами выполнять переключения между ними.
Для того чтобы адресовать одновременно два сегмента данных, например, при выполнении операции пересылки из одной области памяти в другую, можно использовать регистр дополнительного сегмента ES. Кодовый сегмент и сегмент стека всегда определяются содержимым своих регистров (CS и SS), и поэтому в каждый момент выполнения программы всегда используется какой-то один кодовый сегмент и один сегмент стека. Причем если переключение кодового сегмента – довольно простая операция, то переключать сегмент стека можно только при условии четкого представления логики работы программы со стеком, иначе это может привести к зависанию системы.
Все сегменты могут использовать различные области памяти, а могут частично или полностью перекрываться..

Кодовый сегмент должен обязательно описываться в программе, все остальные сегменты могут отсутствовать. В этом случае DOS при загрузке программы в оперативную память инициирует регистры DS и ES значением адреса префикса программного сегмента PSP (Program Segment Prefics) – специальной области оперативной памяти размером 256 (100h) байт. PSP может использоваться в программе для определения имен файлов и параметров из командной строки, введенной при запуске программы на выполнение, объема доступной памяти, переменных окружения системы и т.д. Регистр SS при этом инициализируется значением сегмента, находящегося сразу за PSP, т.е. первого сегмента программы. При этом необходимо учитывать, что стек «растет вниз» (при помещении в стек содержимое регистра SP, указывающего на вершину стека, уменьшается, а при считывании из стека – увеличивается). Таким образом, при помещении в стек каких-либо значений они могут затереть PSP и программы, находящиеся в младших адресах памяти, что может привести к непредсказуемым последствиям. Поэтому рекомендуется всегда явно описывать сегмент стека в тексте программы, задавая ему размер, достаточный для нормальной работы.

Рассмотрим распределение памяти на примере простейшей программы.

;Данные программы

DATA SEGMENT

MSG DB ‘Текст$’

DATA ENDS

;Код программы

CODE SEGMENT

ASSUME CS:CODE,DS:DATA

START: 

MOV AX,DATA

MOV DS,AX

MOV AH,09H ;Вывод сообщения

MOV DX,OFFSET MSG

INT 21H

MOV AH,4CH ;Завершение работы

INT 21H

CODE ENDS

END START

В этой программе явно описаны два сегмента – кода с именем CODE и данных с именем DATA. Директива ASSUME связывает имена этих сегментов, которые в общем случае могут быть произвольными, с сегментными регистрами CS и DS соответственно.

Как видно, сегмент стека в данном случае установлен на PSP, что при его интенсивном использовании может привести к неожиданным результатам. После инициализации в регистре IP находится смещение первой команды программы относительно начала кодового сегмента, адрес которого помещен в регистр CS. Процессор, считывая эту команду, начинает выполнение программы, постоянно изменяя содержимое регистра IP и при необходимости CS для получения кодов очередных команд до тех пор, пока не встретит команду завершения программы. DS после загрузки программы установлен на начало PSP, поэтому для его использования в первых двух командах программы выполняется загрузка DS значением сегмента данных.

2.1. EXE- и COM-программы

DOS может загружать и выполнять программные файлы двух типов – COM и EXE.
Ввиду сегментации адресного пространства процессора 8086 и того факта, что переходы (JMP) и вызовы (CALL) используют относительную адресацию, оба типа программ могут выполняться в любом месте памяти. Программы никогда не пишутся в предположении, что они будут загружаться с определенного адреса (за исключением некоторых самозагружающихся, защищенных от копирования программ).

Файл COM-формата – это двоичный образ кода и данных программы. Такой файл должен занимать менее 64K и не содержать перемещаемых адресов сегментов.

Файл EXE-формата содержит специальный заголовок, при помощи которого загрузчик выполняет настройку ссылок на сегменты в загруженном модуле.

Перед загрузкой COM- или EXE-программы DOS определяет сегментный адрес, называемый префиксом программного сегмента (PSP), как базовый для программы. Затем DOS выполняет следующие шаги:

- создает копию текущего окружения DOS (область памяти, содержащая ряд строк в формате ASCIIZ, которые могут использоваться приложениями для получения некоторой системной информации и для передачи данных между программами) для программы;

- помещает путь, откуда загружена программа, в конец окружения;

- заполняет поля PSP информацией, полезной для загружаемой программы (количество памяти, доступное программе; сегментный адрес окружения DOS; текущие векторы прерываний INT 22H INT 23H и INT 24H и т.д).

EXE-программы. EXE-программы содержат несколько программных сегментов, включая сегмент кода, данных и стека. EXE-файл загружается, начиная с адреса PSP:0100h. В процессе загрузки считывается информация заголовка EXE в начале файла и выполняется перемещение адресов сегментов. Это означает, что ссылки типа

mov ax,data_seg

mov ds,ax

и

call my_far_proc

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

После перемещения управление передается загрузочному модулю посредством инструкции далекого перехода (FAR JMP) к адресу CS:IP, извлеченному из заголовка EXE.

В момент получения управления программой EXE -формата:

- DS и ES указывают на начало PSP

- CS, IP, SS и SP инициализированы значениями, указанными в заголовке EXE

- поле PSP MemTop (вершина доступной памяти системы в параграфах) содержит значение, указанное в заголовке EXE. Обычно вся доступная память распределена программе.

COM-программы. COM-программы содержат единственный сегмент (или, во всяком случае, не содержат явных ссылок на другие сегменты). Образ COM-файла считывается с диска и помещается в память, начиная с PSP:0100h. В общем случае, COM-программа может использовать множественные сегменты, но она должна сама вычислять сегментные адреса, используя PSP как базу.

COM-программы предпочтительнее EXE-программ, когда дело касается небольших ассемблерных утилит. Они быстрее загружаются, ибо не требуется перемещения сегментов, и занимают меньше места на диске, поскольку заголовок EXE и сегмент стека отсутствуют в загрузочном модуле.

После загрузки двоичного образа COM-программы:

- CS, DS, ES и SS указывают на PSP;

- SP указывает на конец сегмента PSP (обычно 0FFFEH, но может быть и меньше, если полный 64K сегмент недоступен);

- слово по смещению 06H в PSP (доступные байты в программном сегменте) указывает, какая часть программного сегмента доступна;

- вся память системы за программным сегментом распределена программе;

- слово 00H помещено (PUSH) в стек.

- IP содержит 100H (первый байт модуля) в результате команды JMP PSP:100H.

2.2. Выход из программы

Завершить программу можно следующими способами:

- через функцию 4CH (EXIT) прерывания 21H в любой момент, независимо от значений регистров;

- через функцию 00H прерывания 21H или прерывание INT 20H, когда CS указывает на PSP.

Функция DOS 4CH позволяет возвращать родительскому процессу код выхода, который может быть проверен вызывающей программой или командой COMMAND.COM "IF ERRORLEVEL".
Можно также завершить программу и оставить ее постоянно резидентной (TSR), используя либо INT 27H , либо функцию 31H (KEEP) прерывания 21H. Последний способ имеет те преимущества, что резидентный код может быть длиннее 64K, и что в этом случае можно сформировать код выхода для родительского процесса.

3. Ассемблер, макроассемблер, редактор связей

Существует несколько версий программы ассемблер. Одним из наиболее часто используемых является пакет Turbo Assembler, водящий в состав комплекса программ Borland Pascal 7.0. Рассмотрим работу с этим пакетом более подробно.
Входной информацией для ассемблера (TASM.EXE) является исходный файл — текст программы на языке ассемблера в кодах ASCII. В результате работы ассемблера может получиться до 3-х выходных файлов:
1) объектный файл – представляет собой вариант исходной программы, записанный в машинных командах;
2) листинговый файл – является текстовым файлом в кодах ASCII, включающим как исходную информацию, так и результат работы программы ассемблера;
3) файл перекрестных ссылок – содержит информацию об использовании символов и меток в ассемблерной программе (перед использованием этого файла необходима его обработка программой CREF).
Существует много способов указывать ассемблеру имена файлов. Первый и самый простой способ — это вызов команды без аргументов. В этом случае ассемблер сам поочередно запрашивает имена файлов: входной (достаточно ввести имя файла без расширения ASM), объектный, листинговый и файл перекрестных ссылок. Для всех запросов имеются режимы, применяемые по умолчанию, если в ответ на запрос нажать клавишу Enter:

- объектному файлу ассемблер присваивает то же имя, что и у исходного, но с расширением OBJ;

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

Если ассемблер во время ассемблирования обнаруживает ошибки, он записывает сообщения о них в листинговый файл. Кроме того, он выводит их на экран дисплея.

Другой способ указать ассемблеру имена файлов — это задать их прямо в командной строке через запятую при вызове соответствующей программы, например:

TASM Test, Otest, Ltest, Ctest

При этом первым задается имя исходного файла, затем объектного, листингового и, наконец, файла перекрестных ссылок. Если какое-либо имя пропущено, то это служит указанием ассемблеру сгенерировать соответствующий файл по стандартному соглашению об именах.
Программа, полученная в результате ассемблирования (объектный файл), еще не готова к выполнению. Ее необходимо обработать командой редактирования связей TLINK, которая может связать несколько различных объектных модулей в одну программу и на основе объектного модуля формирует исполняемый загрузочный модуль.
Входной информацией для программы TLINK являются имена объектных модулей (файлы указываются без расширение OBJ). Если файлов больше одного, то их имена вводятся через разделитель «+». Модули связываются в том же порядке, в каком их имена передаются программе TLINK. Кроме того, TLINK требует указания имени выходного исполняемого модуля. По умолчанию ему присваивается имя первого из объектных модулей, но с расширением ЕХЕ. Вводя другое имя, можно изменять имя файла, но не расширение. Далее можно указать имя файла, для хранения карты связей (по умолчанию формирование карты не производится). Последнее, что указывается программе TLINK – это библиотеки программ, которые могут быть включены в полученный при связывании модуль. По умолчанию такие библиотеки отсутствуют.
Информацию обо всех этих файлах программа TLINK запрашивает у пользователя после ее вызова.

4. Язык Ассемблера. Начальные сведения

Все ассемблерные программы состоят из одного или более предложений и комментариев. Предложение и комментарии представляют собой комбинацию знаков, входящих в алфавит языка, а также чисел и идентификаторов, которые тоже формируются из знаков алфавита. Алфавит языка составляют цифры, строчные и прописные буквы латинского алфавита, а также следующие символы:

? @ _ $ : . [ ] ( ) < > { } + / * & % ! ' ~ | \ = # ^ ; , ` "

4.1. Идентификаторы, переменные, метки, имена, ключевые слова

Конструкции языка ассемблера формируются из идентификаторов и ограничителей. Идентификатор представляет собой набор букв, цифр и символов «_», «.», «?», «$» или «@» (символ «.» может быть только первым символом идентификатора), не начинающийся с цифры. Идентификатор должен полностью размещаться на одной строке и может содержать от 1 до 31 символа (точнее, значимым является только первый 31 символ идентификатора, остальные игнорируются). Друг от друга идентификаторы отделяются пробелом или ограничителем, которым считается любой недопустимый в идентификаторе символ. Посредством идентификаторов представляются следующие объекты программы:

- переменные;

- метки;

- имена.

Переменные идентифицируют хранящиеся в памяти данные. Все переменные имеют три атрибута:

1) СЕГМЕНТ, соответствующий тому сегменту, который ассемблировался, когда была определена переменная;
2) СМЕЩЕНИЕ, являющееся смещением данного поля памяти относительно начала сегмента;
3) ТИП, определяющий число байтов, подвергающихся манипуляциям при работе с переменной.

Метка является частным случаем переменной, когда известно, что определяемая ею память содержит машинный код. На нее можно ссылаться посредством переходов или вызовов. Метка имеет два атрибута: СЕГМЕНТ и СМЕЩЕНИЕ.
Именами считаются символы, определенные директивой EQU и имеющие значением символ или число. Значения имен не фиксированы в процессе ассемблирования, но при выполнении программы именам соответствуют константы.
Некоторые идентификаторы, называемые ключевыми словами, имеют фиксированный смысл и должны употребляться только в соответствии с этим. Ключевыми словами являются:

- директивы ассемблера;

- инструкции процессора;

- имена регистров;

- операторы выражений.

В идентификаторах одноименные строчные и заглавные буквы считаются эквивалентными. Например, идентификаторы AbS и abS считаются совпадающими.

4.2. Типы данных

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

Целые числа имеют следующий синтаксис (xxxx – цифры):

[+|-]xxxx

[+|-]xxxxB

[+|-]xxxxQ

[+|-]xxxxO

[+|-]xxxxD

[+|-]xxxxH

Латинский символ (в конце числа), который может кодироваться на обоих регистрах, задает основание системы счисления числа: B – двоичное, Q и O – восьмеричное, D – десятичное, H – шестнадцатеричное. Шестнадцатеричные числа не должны начинаться с буквенных цифр (например, вместо некорректного ABh следует употреблять 0ABh). Шестнадцатеричные цифры от A до F могут кодироваться на обоих регистрах. Первая форма целого числа использует умалчиваемое основание (обычно десятичное).

Символьные и строковые константы имеют следующий синтаксис:

'символы'

"символы"

Символьная константа состоит из одного символа алфавита языка. Строковая константа включает в себя 2 или более символа. В отличие от других компонент языка, строковые константы чувствительны к регистру. Символы «'» и «"» в теле константы должны кодироваться дважды.
Кроме целых и символьных типов ассемблер содержит еще ряд типов (например, вещественные числа, двоично-десятичные числа), однако их рассмотрение выходит за рамки данного пособия.

4.3. Предложения

Программа на языке Ассемблера – это последовательность предложений, каждое из которых записывается в отдельной строке:

<Предложение>

<Предложение>

...

<Предложение>

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

- пробел обязателен между рядом стоящими идентификаторами и/или числами (чтобы отделить их друг от друга);

- внутри идентификаторов и чисел пробелы недопустимы;

- в остальных местах пробелы можно ставить или не ставить;

- там, где допустим один пробел, можно ставить любое число пробелов.

Все предложения языка ассемблера делятся на директивы ассемблера и инструкции (команды) процессора.

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

Инструкции процессора представляют собой мнемоническую форму записи машинных команд, непосредственно выполняемых микропроцессором. Все инструкции в соответствии с выполняемыми ими функциями делятся на 5 групп:

1) инструкции пересылки данных;
2) арифметические, логические и операции сдвига;
3) операции со строками;
4) инструкции передачи управления;
5) инструкции управления процессором.

4.4. Выражения

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

Арифметические операторы.

выражение_1 * выражение_2

выражение_1 / выражение_2

выражение_1 MOD выражение_2

выражение_1 + выражение_2

выражение_1 – выражение_2

+ выражение

– выражение

Эти операторы обеспечивают выполнение основных арифметических действий (здесь MOD - остаток от деления выражения_1 на выражение_2, а знаком / обозначается деление нацело). Результатом арифметического оператора является абсолютное значение.

Операторы сдвига.

выражение SHR счетчик

выражение SHL счетчик

Операторы SHR и SHL сдвигают значение выражения соответственно вправо и влево на число разрядов, определяемое счетчиком. Биты, выдвигаемые за пределы выражения, теряются. Замечание: не следует путать операторы SHR и SHL с одноименными инструкциями процессора.

Операторы отношений.

выражение_1 EQ выражение_2

выражение_1 NE выражение_2

выражение_1 LT выражение_2

выражение_1 LE выражение_2

выражение_1 GT выражение_2

выражение_1 GE выражение_2

Мнемонические коды отношений расшифровываются следующим образом:

EQ – равно;

NE – не равно;

LT – меньше;

LE – меньше или равно;

GT – больше;

GE – больше или равно.

Операторы отношений формируют значение 0FFFFh при выполнении условия и 0000h в противном случае. Выражения должны иметь абсолютные значения. Операторы отношений обычно используются в директивах условного ассемблирования и инструкциях условного перехода.

Операции с битами.

NOT выражение

выражение_1 AND выражение-2

выражение_1 OR выражение-2

выражение_1 XOR выражение-2

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

NOT – инверсия;

AND – логическое И;

OR – логическое ИЛИ;

XOR – исключающее логическое ИЛИ.

Операции выполняются над каждыми соответствующими битами выражений. Выражения должны иметь абсолютные значения.

Оператор индекса.

[[выражение_1]] [выражение_2]

Оператор индекса [] складывает указанные выражения подобно тому, как это делает оператор +, с той разницей, что первое выражение необязательно, при его отсутствии предполагается 0 (двойные квадратные скобки указывают на то, что операнд не обязателен).

Оператор PTR

тип PTR выражение

При помощи оператора PTR переменная или метка, задаваемая выражением, может трактоваться как переменная или метка указанного типа.
Оператор PTR обычно используется для точного определения размера, или расстояния, ссылки. Если PTR не используется, ассемблер подразумевает умалчиваемый тип ссылки. Кроме того, оператор PTR используется для организации доступа к объекту, который при другом способе вызвал бы генерацию сообщения об ошибке (например, для доступа к старшему байту переменной размера WORD).

Операторы HIGH и LOW

HIGH выражение

LOW выражение

Операторы HIGH и LOW вычисляют соответственно старшие и младшие 8 битов значения выражения. Выражение может иметь любое значение.

Оператор SEG

SEG выражение

Этот оператор вычисляет значение атрибута СЕГМЕНТ выражения. Выражение может быть меткой, переменной, именем сегмента, именем группы или другим символом.

Оператор OFFSET

OFFSET выражение

Этот оператор вычисляет значение атрибута СМЕЩЕНИЕ выражения. Выражение может быть меткой, переменной, именем сегмента или другим символом. Для имени сегмента вычисляется смещение от начала этого сегмента до последнего сгенерированного в этом сегменте байта.

Оператор SIZE

SIZE переменная

Оператор SIZE определяет число байтов памяти, выделенных переменной.

4.5. Приоритеты операций

При вычислении значения выражения операции выполняются в соответствии со следующим списком приоритетов (в порядке убывания):

1) LENGTH, SIZE, WIDTH, MASK, (), [], <>.
2) Оператор имени поля структуры (.).
3) Оператор переключения сегмента (:).
4) PTR, OFFSET, SEG, TYPE, THIS.
5) HIGH, LOW.
6) Унарные + и -.
7) *, /, MOD, SHR, SHL.
8) Бинарные + и -.
9) EQ, NE, LT, LE, GT, GE.
10) NOT.
11) AND.
12) OR, XOR.
13) SHORT, .TYPE.

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

4.6. Ссылки вперед

Хотя ассемблер и допускает ссылки вперед (т.е. к еще необъявленным объектам программы), такие ссылки могут при неправильном использовании приводить к ошибкам. Пример ссылки вперед:

JMP MET

...

MET: ...

Всякий раз, когда ассемблер обнаруживает неопределенное имя на 1-м проходе, он предполагает, что это ссылка вперед. Если указано только имя, ассемблер делает предположения о его типе и используемом регистре сегмента, в соответствии с которыми и генерируется код. В приведенном выше примере предполагается, что MET – метка типа NEAR и для ее адресации используется регистр CS, в результате чего генерируется инструкция JMP, занимающая 3 байта. Если бы, скажем, в действительности тип ссылки оказался FAR, ассемблеру нужно было бы генерировать 5-байтовую инструкцию, что уже невозможно, и формировалось бы сообщение об ошибке. Во избежание подобных ситуаций рекомендуется придерживаться следующих правил:

1) Если ссылка вперед является переменной, вычисляемой относительно регистров ES, SS или CS, следует использовать оператор переключения сегмента. Если он не использован, делается попытка вычисления адреса относительно регистра DS.
2) Если ссылка вперед является меткой инструкции в команде JMP и отстоит не далее, чем на 128 байтов, можно использовать оператор SHORT. Если этого не делать, метке будет присвоен тип FAR, что не вызовет ошибки, но на 2-м проходе ассемблер для сокращения размера содержащей ссылку инструкции вынужден будет вставить дополнительную и ненужную инструкцию NOP.
3) Если ссылка вперед является меткой инструкции в командах CALL или JMP, следует использовать оператор PTR для определения типа метки. Иначе ассемблер устанавливает тип NEAR, и, если в действительности тип метки окажется FAR, будет выдано сообщение об ошибке.

4.7. Директивы определения данных

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

Директивы определения данных могут задавать:

- скалярные данные, представляющие собой единичное значение или набор единичных значений;

- записи, позволяющие манипулировать с данными на уровне бит;

- структуры, отражающие некоторую логическую структуру данных.

4.7.1. Скалярные данные

Директива DB обеспечивает распределение и инициализацию 1 байта памяти для каждого из указанных значений. В качестве значения может кодироваться целое число, строковая константа, оператор DUP (см. ниже), абсолютное выражение или знак «?». Знак «?» обозначает неопределенное значение. Значения, если их несколько, должны разделяться запятыми. Если директива имеет имя, создается переменная типа BYTE с соответствующим данному значению указателя позиции смещением.
Если в одной директиве определения памяти заданы несколько значений, им распределяются последовательные байты памяти. В этом случае, имя, указанное в начале директивы, именует только первый из этих байтов, остальные остаются безымянными. Для ссылок на них используется выражение вида имя+k, где k – целое число.
Строковая константа может содержать столько символов, сколько помещается на одной строке. Символы строки хранятся в памяти в порядке их следования, т.е. 1-й символ имеет самый младший адрес, последний - самый старший.
Во всех директивах определения памяти в качестве одного из значений может быть задан оператор DUP. Он имеет следующий формат:

счетчик DUP (значение,...)

Указанный в скобках список значений повторяется многократно в соответствии со значением счетчика. Каждое значение в скобках может быть любым выражением, имеющим значением целое число, символьную константу или другой оператор DUP (допускается до 17 уровней вложенности операторов DUP). Значения, если их несколько, должны разделяться запятыми.

Оператор DUP может использоваться не только при определении памяти, но и в других директивах.

Синтаксис директив DW, DD, DQ и DT идентичен синтаксису директивы DB.

Примеры директив определения скалярных данных:

integer1 DB 16

string1 DB 'abCDf'

empty1 DB ?

contan2 DW 4*3

string3 DD 'ab'

high4 DQ 18446744073709551615

high5 DT 1208925819614629174706175d

db6 DB 5 DUP(5 DUP(5 DUP(10)))

dw6 DW DUP(1,2,3,4,5)

4.7.2. Записи

Запись представляет собой набор полей бит, объединенных одним именем. Каждое поле записи имеет собственную длину, исчисляемую в битах, и не обязано занимает целое число байтов. Объявление записи в программе на языке ассемблера включает в себя 2 действия:

- объявление шаблона или типа записи директивой RECORD;

- объявление собственно записи.

Формат директивы RECORD:

имя_записи RECORD имя_поля:длина[[=выражение]],...

Директива RECORD определяет вид 8- или 16-битовой записи, содержащей одно или несколько полей. Имя_записи представляет собой имя типа записи, которое будет использоваться при объявлении записи. Имя_поля и длина (в битах) описывает конкретное поле записи. Выражение, если оно указано задает начальное (умалчиваемое) значение поля. Описания полей записи в директиве RECORD, если их несколько, должны разделяться запятыми. Для одной записи может быть задано любое число полей, но их суммарная длина не должна превышать 16 бит.
Длина каждого поля задается константой в пределах от 1 до 16. Если общая длина полей превышает 8 бит, ассемблер выделяет под запись 2 байта, в противном случае – 1 байт. Если задано выражение, оно определяет начальное значение поля. Если длина поля не меньше 7 бит, в качестве выражения может быть использован символ в коде ASCII. Выражение не должно содержать ссылок вперед. Пример:

item RECORD char:7='Q',weight:4=2

При обработке директивы RECORD формируется шаблон записи, а сами данные создаются при объявлении записи, которое имеет следующий вид:

[[имя]] имя_записи <[[значение,...]]>

По такому объявлению создается переменная типа записи с 8- или 16-битовым значением и структурой полей, соответствующей шаблону, заданному директивой RECORD с именем имя_записи. Имя задает имя переменной типа записи. Если имя опущено, ассемблер распределяет память, но не создает переменную, которую можно было бы использовать для доступа к записи.
В скобках <> указывается список значений полей записи. Значения в списке, если их несколько, должны разделяться запятыми. Каждое значение может быть целым числом, строковой константой или выражением и должно соответствовать длине данного поля. Для каждого поля может быть задано одно значение. Скобки <> обязательны, даже если начальные значения не заданы. Пример:

table item 10 DUP(<'A',2>)

Если для описания шаблона записи использовалась директива RECORD из предыдущего примера, то по этому объявлению создается 10 записей, объединенных именем table.

4.7.3. Структуры

Структура представляет собой набор полей байтов, объединенных одним именем. Объявление структуры, аналогично объявлению записи, включает в себя 2 действия:

- объявление шаблона или типа структуры;

- объявление собственно структуры.

Формат объявления типа структуры:

имя STRUC

описания_полей

имя ENDS

Директивы STRUC и ENDS обозначают соответственно начало и конец описания шаблона (типа) структуры. Описание типа структуры задает имя типа структуры и число, типы и начальные значения полей структуры. Описания_полей определяют поля структуры и могут быть заданы аналогично описанию скалярных типов. Пример:

table STRUC

count DB 10

value DW 10 DUP(?)

tname DB 'font'

table ENDS

При обработке директив STRUC и ENDS формируется шаблон структуры, а сами данные создаются при объявлении структуры, которое имеет следующий вид:

[[имя]] имя_структуры <[[значение,...]]>

Значения полей в объявлении структуры аналогично значению полей при объявлении записи.

4.8. Директива эквивалентности

Константы в языке Ассемблера описываются с помощью директивы экви­валентности EQU, имеющей следующий синтаксис:

<имя> EQU <операнд>

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

4.9. Структура программы на языке Ассемблера

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

Инструкция процессора на языке ассемблера состоит не более чем из четырех поле и имеет следующий формат:

[[метка:]] мнемоника [[операнды]] [[;комментарии]]

Единственное обязательное поле – поле кода операции (мнемоника), определяющее инструкцию, которую должен выполнить микропроцессор. Поле операндов определяется кодом операции и содержит дополнительную информацию о команде. Каждому коду операции соответствует определенное число операндов. Метка служит для обозначения какого-то определенного места в памяти, т. е. содержит в символическом виде адрес, по которому храниться инструкция. Преобразование символических имен в действительные адреса осуществляется программой ассемблера. Часть строки исходного текста после символа «;» (если он не является элементом знаковой константы или строки знаков) считается комментарием и ассемблером игнорируется. Комментарии вносятся в программу как поясняющий текст и могут содержать любые знаки до ближайшего символа конца строки. Для создания комментариев, занимающих несколько строк, может быть использована директива COMMENT. Пример:

Метка Код операции Операнды ;Комментарий

MET: MOVE AX, BX ;Пересылка

Структура директивы аналогична структуре инструкции. Второе поле – код псевдооперации определяет смысловое содержание директивы. Как и у инструкции, у директивы есть операнды, причем их может быть один или несколько и они отделяются друг от друга запятыми. Допустимое число операндов в директиве определяется кодом псевдооперации. Пример:

ARRAY DB 0, 0, 0, 0, 0

END START

Директива может быть помечена символическим именем и содержать поле комментария. Символическое имя, стоящее в начале директивы распределения памяти, называется переменной. В отличие от метки команды после символического имени директивы двоеточие не ставиться. Комментарий записывается также как и в случае команды и может занимать целую строку.
Программа на языке ассемблера состоит из программных модулей, содержащихся в различных файлах. Каждый модуль, в свою очередь, состоит из инструкций процессора или директив ассемблера и заканчивается директивой END. Метка, стоящая после кода псевдооперации END, определяет адрес, с которого должно начаться выполнение программы и называется точкой входа в программу.
Каждый модуль также разбивается на отдельные части директивами сегментации, определяющими начало и конец сегмента. Каждый сегмент начинается директивой начала сегмента – SEGMENT и заканчивается директивой конца сегмента – ENDS. В начале директив ставится имя сегмента. Оператор SEGMENT может указывать выравнивание сегмента в памяти, способ, которым он объединяется с другими сегментами, а также имя класса. Существует два типа выравнивания сегмента: тип PARA, когда сегмент будет расположен начиная с границы параграфа, и тип BYTE, когда сегмент расположен начиная с любого адреса. Различные виды взаимной связи сегментов определяют параметры сборки, например, при модульном программировании. Объявление PUBLIC вызывает объединение всех сегментов с одним и тем же именем в виде одного большого сегмента. Объявление AT с адресным выражением располагает сегмент по заданному абсолютному адресу.
Каждый сегмент может быть также разбит на части. В общем случае информационные сегменты SS, ES и DS состоят из определений данных, а программный сегмент CS – из инструкций и директив, группирующих инструкции в блоки. Программный сегмент может разбиваться на части директивами определения процедур – некоторых выделенных блоков программы. Как и для определения сегмента, имеются две директивы определения процедуры (подпрограммы) – директива начала PROC и директива конца ENDP. Процедура имеет имя, которое должно включаться в обе директивы. В сегменте процедуры могут располагаться последовательно одна за другой или могут быть вложенными одна в другую.

4.9.1. Директива ASSUME

С помощью директивы ASSUME ассемблеру сообщается информация о соответствии между сегментными регистрами, и программными сегментами. Директива имеет следующий формат:

ASSUME <пара>[[, <пара>]]

ASSUME NOTHING

где <пара> - это <сегментный регистр> :<имя сегмента>

либо <сегментный регистр> :NOTHING 

Например, директива

ASSUME ES:A, DS:B, CS:C

сообщает ассемблеру, что для сегментирования адресов из сегмента А выбирается регистр ES, для адресов из сегмента В – регистр DS, а для адресов из сегмента С – регистр CS.

Таким образом, директива ASSUME дает право не ука­зывать в командах (по крайней мере, в большинстве из них) префиксы – опущенные префиксы будет самостоятельно восстанавливать ассемблер.
В качестве особенностей директивы прежде всего следует отметить, что директива ASSUME не загружает в сегментные ре­гистры начальные адреса сегментов. Этой директивой автор программы лишь сообщает, что в программе будет сделана такая, загрузка. Директиву ASSUME можно размешать в любом месте программы, но обычно ее указывают в начале сегмента команд, так как информация из нее нужна только при трансляции инструкций. При этом в директиве обязательно должно быть указано соответствие между регистром CS и данным сегментом кода, иначе при появлении первой же метки ассемблер зафиксирует ошибку. 
Если в директиве ASSUME указано несколько пар с одним и тем же сегментным регистром, то последняя из них «отменяет» предыдущие, т. к. каждому сегментному регистру, можно поставить в соответствие только один сегмент. В то же время на один и тот же сегмент могут указывать разные сегментные регистры. Если в директиве ASSUME в качестве второго элемента пары задано служебное слово NOTHING (ничего), например, ASSUME ES: NOTHING, то это означает, что с данного момента сегментный регистр не указывает ни на какой сегмент, что ассемблер не должен использовать этот регистр при трансляции команд.
В связи с тем, что директивой ASSUME автор программы лишь сообщает, что все имена из таких-то программных сегментов должны сегментироваться по таким-то сегментным регистрам (в начале выполнения программы в этих регистрах ничего нет), то выполнение программы необходимо начинать с команд, которые загружают в сегментные регистры адреса соответствующих сегментов памяти.
Загрузка производится следующим образом. Пусть регистр DS необходимо установить на начало сегмента В. Для загрузки регистра необходимо выполнить присваивание вида DS:=B. Однако сделать это командой MOV DS,B нельзя, поскольку имя сегмента – это константное выражение, т. е. непосредственный операнд, а по команде MOV запрещена пересылка непосредственного операнда в сегментный регистр (см. ниже). Поэтому такую пересылку следует делать через другой, несегментный регистр, например, через АХ:

MOV АХ,В

MOV DS,AX ;DS:=B

Аналогичным образом загружается и регистр ES.

Регистра CS загружать нет необходимости, так как к началу выполнения программы этот регистр уже будет указывать, на начало сегмента кода. Такую загрузку выполняет операционная система, прежде чем передает управление программе.

Загрузить регистр SS можно двояко. Во-первых, его можно загрузить в самой программе так же, как DS или ES. Во-вторых, такую загрузку можно поручить операционной системе. Для этого в директиве SEGMENT, открывающей описание сегмента стека, надо указать специальный параметр STACK, например:

S SEGMENT STACK

...

S ENDS

В таком случае загрузка S в регистр SS будет выполнена автоматически до начала выполнения программы.

4.9.2. Директива INCLUDE

Встречая директиву INCLUDE, ассемблер весь текст, хранящийся в указанном файле, подставит в программу вместо этой директивы. В общем случае обращение к директиве INCLUDE (включить) имеет следующий вид:

INCLUDE <имя файла>

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

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

4.9.3. Структура EXE- и COM- программы

Следует отметить, что какой-либо фиксированной структуры программы на языке Ассемблера нет, но для небольших EXE-программ с трехсегментной структурой типична следующая структура:

;Определение сегмента стека

STAK SEGMENT STACK

DB 256 DUP (?)

STAK ENDS

;Определение сегмента данных

DATA SEGMENT

SYMB DB '#' ;Описание переменной с именем SYMB

;типа Byte и со значением «#»

. . . ;Определение других переменных

DATA ENDS

;Определение сегмента кода

CODE SEGMENT

ASSUME CS:CODE,DS:DATA,SS:STAK

;Определение подпрограммы

PROC1 PROC

. . . ;Текст подпрограммы

PROC1 ENDP

START: ;Точка входа в программу START

XOR AX,AX 

MOV BX,data ;Обязательная инициализация

MOV DS,BX ;регистра DS в начале программы

CALL PROC1 ;Пример вызова подпрограммы

. . . ;Текст программы

MOV AH,4CH ;Операторы завершения программы

INT 21H

CODE ENDS

END START

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

Сегмент стека в приведенной структуре описан с параметром STACK, поэтому в самой программе нет необходимости загружать сегментный регистр SS. Сегментный регистр CS тоже нет необходимости загружать, как уже отмечалось ранее. В связи с этим в начале программы загружается лишь регистр DS.
Относительно сегмента стека нужно сделать следующее замечание. Даже если сама программа не использует стек, описывать в программе сегмент стека все равно надо. Дело в том, что стек программы использует операционная система при обработке прерываний.
Необходимо также заметить, что все предложения, по которым ассемблер заносит что-либо в формируемую программу (инструкции, директивы определения данных и т.д.) обязательно должны входить в какой-либо программный сегмент, размещать их вне программных сегментов нельзя. Исключение составляют директивы информационного характера, например, директивы EQU, директивы описания типов структур и записей. Кроме того, не рекомендуется размещать в сегменте данных инструкции, а в сегменте кода – описание переменных из-за возникающих в этом случае проблем с сегментированием.
Типичная структура COM-программы аналогична структуре EXE-программы, с той лишь разницей, что, как уже отмечалось выше, COM-программа содержит лишь один сегмент – сегмент кода, который включает в себя инструкции процессора, директивы и описания переменных.

;Определение сегмента кода

CODE SEGMENT

ASSUME CS:CODE,DS:CODE,SS:CODE

ORG 100H ;Начало необходимое для COM-программы

;Определение подпрограммы

PROC1 PROC

. . . ;Текст подпрограммы

PROC1 ENDP

START:

. . . ;Текст программы

MOV AH,4CH ;Операторы завершения программы

INT 21H

;===== Data =====

BUF DB 6 ;Определение переменной типа Byte

. . . ;Определение других переменных

CODE ENDS

END START

4.10. Модификация адресов

Чаще всего в инструкциях процессора указываются не только точные адреса (имена) ячеек памяти, но и некоторые регистры в квадратных скобках, например MOV AX,A[BX]. В этом случае команда работает с, так называемым, исполнительным адресом который вычисляется по формуле AИСП=(A+[BX]) mod 216, где [BX] обозначает содержимое регистра BX. То есть процессор, прежде чем выполнить команду, прибавит к адресу А, указанному в команде (см. пример), текущее содержимое BX, получит некоторый новый адрес и из ячейки с этим адресом возьмет второй операнд. Если в результате суммирования получилась сумма большая 65535, то от нее берутся только последние 16 бит (на это указывает mod в приведенной формуле).
Подобная замена адреса из команды на исполнительный адрес называется модификацией адреса, а регистр, участвующий в модификации – регистром-модификатором. В качестве регистра-модификатора можно использовать не любой регистр, а лишь один из следующих: BX, BP, SI и DI.
Модификация адреса очень широко используется в ассемблерных программах для реализации переменных с индексами (массивов).

4.11. Сегментные регистры по умолчанию

Анализ реальных программ на языке Ассемблера, позволяет сделать вывод, что в них, в большинстве случаев, указываются адреса лишь из трех областей памяти – сегмента кода, сегмента данных и сегмента стека. Например, в командах перехода всегда указываются адреса других команд, т.е. ссылки на сегмент кода. В командах работающих со стеком указываются адреса из сегмента стека. В остальных же командах (пересылках, арифметических и т.д.) указываются, как правило, адреса из сегмента данных. С учетом этой особенности реальных программ принят ряд соглашений, которые позволяют во многих командах не указывать явно сегментные регистры, а подразумевать их по умолчанию. Для этого необходимо, чтобы начальные адреса сегментов памяти находились в определенных регистрах, а именно: регистр CS должен указывать на начало сегмента кода, регистр DS – на начало сегмента данных, а регистр SS – на начало сегмента стека. Если эти правила соблюдены, то справедливо следующее:

1) адреса переходов всегда сегментируются по регистру CS;
2) во всех остальных инструкциях: если адрес в команде не модифицируется или если он модифицируется, но среди модификаторов нет регистра BP, то этот адрес считается ссылкой в сегмент данных и сегментируется по регистру DS; если адрес модифицируется по регистру BP, то он считается ссылкой в сегмент стека и поэтому по умолчанию сегментируется по регистру SS.

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

5. Команды пересылки

5.1. Команда MOV

Команда MOV – основная команда пересылки данных, которая пересылает один байт или слово данных из памяти в регистр, из регистра в память или из регистра в регистр. Команда MOV может также занести число (непосредственный операнд) в регистр или память. В действительности команда MOV это целое семейство машинных команд микропроцессора. На приведенном ниже рисунке представлены различные способы, которыми в микропроцессоре можно пересылать данные из одного места в другое. Каждый прямоугольник означает здесь регистр или ячейку памяти. Стрелки показывают пути пересылки данных, которые допускает микропроцессор. Необходимо также помнить, что все команды микропроцессора могут указывать только один операнд памяти.
Из рисунка видно, что запрещены пересылки из одной ячейки памяти в другую, из одного сегментного регистра в другой, запись непосредственного операнда в память. Это обусловлено тем, что в персональном компьютере отсутствуют соответствующие машинные команды. Если по алгоритму необходимо произвести одно из таких действий, то оно обычно реализуется в две команды, пересылкой через какой-нибудь несегментный регистр. Кроме того, командой MOV нельзя менять содержимое сегментного регистра CS. Это связано с тем, что регистровая пара CS:IP определяет адрес следующей выполняемой команды, поэтому изменение любого из этих регистров есть ничто иное, как операция перехода. Команда же MOV не реализует переход.

Примеры использования команды пересылки:

MOV Data,DI

MOV BX,CX

MOV DI,Index

MOV Start_Seg,DS

MOV ES,Buffer

MOV Days,356

MOV DI,0

5.2. Команда обмена данных XCHG

Команда XCHG меняет местами содержимое двух операндов. Порядок следования операндов не имеет значения. В качестве операндов могут выступать регистры (кроме сегментных) и ячейки памяти.

Примеры использования команды XCHG:

XCHG BL,BH

XCHG DH,Char

XCHG AX,BX

5.3. Команды загрузки полного указателя LDS и LES

Эти команды загружают полный указатель из памяти и записывают его в выбранную пару «сегментный регистр : регистр». При этом первое слово из адресуемой памяти загружается в регистр первого операнда, второе в регистр DS, если выполняется команда LDS, или в регистр ES если выполняется команда LES.

Примеры использования команд:

LDS BX,[BP+4]

LES DI,TablePtr

5.4. Команда перекодировки XLAT

Команда XLAT заменяет содержимое регистра AL байтом из таблицы перекодировки (максимальная длинна – 256 байт), начальный адрес которой относительно сегмента DS находится в регистре BX.

Алгоритм выполнения команды XLAT состоит из двух этапов:

- содержимое регистра AL прибавляется к содержимому регистра BX;

- полученный результат рассматривается как смещение относительно регистра DS. По данному адресу выбирается байт и помещается в регистр AL.

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

Пример использования команды XLAT:

MOV BX,OFFSET Talbe

MOV AL,2

XLAT

...

Table DB ‘abcde’

5.5. Команды работы со стеком

Как уже было указано ранее, процессор адресует стек с помощью регистровой пары SS:SP. Помещение объектов в стек приводит к автоматическому декременту указателя стека, а извлечение – к инкременту, т.е. он «растет» в сторону меньших адресов памяти.
Для сохранения и восстановления различных 16-битовых данных в стеке используются команды PUSH (протолкнуть) и POP (вытолкнуть). За кодами операций PUSH и POP следует операнд, который необходимо поместить (извлечь) в (из) стек. В качестве операнда может выступать регистр или ячейка памяти, которую можно адресовать, используя известные способы адресации.

Замечание: Команда POP CS недопустима (восстановление из стека в регистр CS осуществляется по команде RET).

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

CODE SEGMENT

...

PROC1 PROC

MOV BP,SP ;загрузка в BP текущего адреса стека

MOV BX,[BP+4];выборка из стека 1 параметра (ca)

...

MOV BX,[BP+2];выборка из стека 2 параметра (ll)

...

RET 4 ;Возврат с удалением 4 слов из стека

PROC1 ENDP

START:

...

MOV AX,’ca’ ;Загрузка в AX символов

MOV CX,’ll’ ;Загрузка в CX символов

PUSH AX ;Сохранение AX в стек

PUSH CX ;Сохранение CX в стек

CALL PROC1 

...

CODE ENDS

5.6. Команды ввода-вывода

Все устройства ЭВМ принято делить на внутренние (центральный процессор ЦП, оперативная память ОП) и внешние (внешняя память, клавиатура, дисплей и т. д.). Под вводом-выводом понимается обмен информацией между ЦП и любым внешним устройством. В ЭВМ передача информации между ЦП и внешним устройством, как правило, осуществляется через порты. Порт – некоторый регистр размером в байт, находящийся вне ЦП (два соседних порта могут рассматриваться как порт размером в слово). Обращение к портам происходит по номерам. Все порты нумеруются от 0 до 0FFFFh. С каждым внешним устройством связан свой порт или несколько портов их адреса заранее известны.

Запись и чтение порта осуществляется при помощи следующих команд:

Чтение (ввод): IN AL, n или IN AX, n

Запись (вывод):OUT n, AL или OUT n, AX

Номер порта n в этих командах может быть задан либо непосредственно, либо регистром DX (IN AX,DX).

Сценарий ввода вывода через порты существенно зависит от специфики того внешнего устройства, с которым ведется обмен, но обычно ЦП связан с внешним устройством через два порта: первый – порт данных, второй – порт управления и достаточно типичной является следующая процедура обмена:

- ЦП записывает в порт управления соответствующую команду, а порт данных – выводимые данные;

- внешнее устройство, считав эту информацию, записывает в порт управления команду «занято» и начинает непосредственно вывод (например, печать);

- ЦП переходит либо в режим ожидания, опрашивая в цикле порт управления, либо занимается другой работой – до тех пор пока в порте управления не сменится сигнал «занято»;

- внешнее устройство заканчивает вывод и записывает в порт управления сигнал об успешном завершении или об ошибке;

- ЦП анализирует полученную информацию и продолжает свою работу.

6. Арифметические команды

Все арифметические команды устанавливают флаги CF, AF, SF, ZF, OF и PF в зависимости от результат операции.

Двоичные числа могут иметь длину 8 и 16 бит. Значение старшего (самого левого бита) задает знак числа: 0 – положительное, 1 – отрицательное. Отрицательные числа представляются в так называемом дополнительном коде, в котором для получения отрицательного числа необходимо инвертировать все биты положительного числа и прибавить к нему 1.
6.1. Команды арифметического сложения ADD и ADC



Команда ADD выполнят целочисленное сложение двух операндов, представленных в двоичном коде. Результат помещается на место первого операнда, второй операнд не изменяется. Команда корректирует регистр флагов в соответствии с результатом сложения. Существуют две формы сложения: 8-битовое и 16-битовое. В различных формах сложения принимают участие различные регистры. Компилятор следит за тем, чтобы операнды соответствовали друг другу. На следующих рисунках иллюстрируются различные варианты команды ADD.

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

Примеры использования команд ADD и ADC:

ADD AL,12h

ADD Count,1

ADC BX,4

ADD AX,BX

ADC Count,DI



6.2. Команды арифметического вычитания SUB и SBB



Команда вычитания SUB – идентична команде сложения, за исключением того, что она выполняет вычитание, а не сложение. Для нее верны предыдущие схемы, если в них поменять знак «+» на «–», т. е. она из первого операнда вычитает второй и помещает результат на место первого операнда. Команда вычитания также устанавливает флаги состояния в соответствии с результатом операции (флаг переноса здесь трактуется как заем). Команда вычитания с заемом SBB учитывает флаг заема CF, то есть значение заема вычитается из младшего бита результата.

Примеры использования команд SUB и SBB:

SUB AL,12h

SUB Count,1

SBB BX,4

SUB AX,BX

SBB Count,DI

6.3. Команда смены знака NEG

Команда отрицания NEG – оператор смены знака. Она меняет знак двоичного кода операнда – байта или слова.

6.4. Команды инкремента INC и декремента DEC

Команды инкремента и декремента изменяют значение своего единственного операнда на единицу. Команда INC прибавляет 1 к операнду, а команда DEC вычитает 1 из операнда. Обе команды могут работать с байтами или со словами. На флаги команды влияния не оказывают.

6.5. Команды умножения MUL и IMUL

Существуют две формы команды умножения. По команде MUL умножаются два целых числа без знака, при этом результат тоже не имеет знака. По команде IMUL умножаются целые числа со знаком. Обе команды работают с байтами и со словами, но для этих команд диапазон форм представления гораздо уже, чем для команд сложения и вычитания.
При умножении 8-битовых операндов результат всегда помещается в регистр AX. При умножении 16-битовых данных результат, который может быть длиною до 32 бит, помещается в пару регистров: в регистре DX содержатся старшие 16-бит, а в регистре AX – младшие 16-бит. Умножение не допускает непосредственного операнда.
Установка флагов командой умножения отличается от других арифметических команд. Единственно имеющие смысл флаги – это флаг переноса и переполнения.
Команда MUL устанавливает оба флага, если старшая половина результата не нулевая. Если умножаются два байта, установка флагов переполнения и переноса показывает, что результат умножения больше 255 и не может содержаться в одном байте. В случае умножения слов флаги устанавливаются, если результат больше 65535.
Команда IMUL устанавливает флаги по тому же принципу, т. е. если произведение не может быть представлено в младшей половине результата, но только в том случае если старшая часть результата не является расширением знака младшей. Это означает, что если результат положителен, флаг устанавливается как в случае команды MUL. Если результат отрицателен, то флаги устанавливаются в случае, если не все биты кроме старшего, равны 1. Например, умножение байт с отрицательным результатом устанавливает флаги, если результат меньше 128.

Примеры использования команд умножения:

MUL CX

IMUL Width

6.6. Команды деления DIV и IDIV

Как и в случае умножения, существуют две формы деления – одна для двоичных чисел без знака DIV, а вторая для чисел со знаком – IDIV. Любая форма деления может работать с байтами и словами. Один из операндов (делимое) всегда в два раза длиннее обычного операнда. Ниже приведены схемы, иллюстрирующие команды деления.
Байтовая команда делит 16-битовое делимое на 8-битовый делитель. В результате деления получается два числа: частное помещается в регистр AL, а остаток – в AH. Команда, работающая со словами, делит 32-битовое делимое на 16-битовый делитель. Делимое находится в паре регистров DX:AX, причем регистр DX содержит старшую значимую часть, а регистр AX – младшую. Команда деления помещает частное в регистр AX, а остаток в DX.
Ни один из флагов состояния не определен после команды деления. Однако, если частное больше того, что может быть помещено в регистр результата (255 для байтового деления и 65535 для деления слов), возникает ошибка значимости и выполняется программное прерывание уровня 0.

Примеры использования команд деления:

IDIV CX

DIV Count

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

Пример. Вычислить значение арифметического выражения. Все числа являются 16-битовыми целыми со знаком. Формула вычислений следующая:

Эту задачу решает приведенная ниже программа.

;Сегмент стека

SSEG SEGMENT STACK 

DB 256 DUP(?)

SSEG ENDS

;Сегмент данных

DATA SEGMENT 

X DW ? ;Память для переменных

A DW ? 

B DW ?

C DW ?

D DW ?

DATA ENDS

;Сегмент кода

CODE SEGMENT 

ASSUME CS:CODE, DS:DATA, SS:SSEG

START: 

MOV AX,Data ;Инициализация DS

MOV DS,AX

;Вычислительная часть

MOV AX,2 ;Загрузка константы

IMUL A ;dx:ax = a*2

MOV BX,DX 

MOV CX,AX ;bx:cx = a*2

MOV AX,B

IMUL C ;dx:ax = b*c

ADD AX,CX

ADC DX,BX ;dx:ax = a*2+b*c

MOV CX,D

SUB CX,3 ;cx = d-3

IDIV CX ;ax = (a*2+b*c)/(d-3)

NEG AX ;ax = -ax

INC AX ;ax = ax+1

MOV X,AX ;Сохранение результата

MOV AH,4CH

INT 21H

CODE ENDS 

END START

На первом этапе программа выполняет два умножения. Так как результат умножения всегда помещается в пару регистров DX:AX, то в примере результат первого умножения переносится в пару регистров BX:CX перед выполнением второго умножения. Затем программа выполняет сложение числителя. Поскольку умножение дает 32-битовые результаты, в программе требуется сложение повышенной точности (с учетом флага переноса). После сложения результат остается в DX:AX (числитель). Знаменатель вычисляется в регистре CX, а затем на него делится числитель. Частное записывается в регистр AX, затем его знак меняется на обратный и к полученному значению прибавляется 1. На последнем этапе программа записывает результат из регистра AX в переменную X. Остаток игнорируется.

7. Команды побитовой обработки

Эту группу команд можно разделить на две подгруппы: логические операции и операции сдвигов. Команды AND, TEST, OR, XOR и команды сдвигов изменяют значения флагов CF, OF, PF, SF, ZF (значение флага AF становится неопределенным). Команды циклических сдвигов изменяют только флаги OF и CF.

7.1. Команды, выполняющие логические операции

К командам, выполняющим логические операции, относятся AND, OR и XOR. Указанные команды выполняют соответственно операции «логическое умножение» (конъюнкцию), «логическое сложение» (дизъюнкцию) и «исключающее или» для двух операндов и помещают результат на место первого операнда. К группе логических команд также относится команда TEST, которая производит те же действия, что и команда AND, но не изменяет своих операндов, а лишь устанавливает соответствующие флаги.

В качестве операндов логических команд могут выступать те же операнды, что и у команд сложения и вычитания.

Примеры использования логических команд:

AND BL,100

TEST CX,DX

OR DX,Mask

XOR Flag,1000b

7.2. Команды, выполняющие операции сдвигов

Команды сдвига перемещает все биты в поле данных либо вправо, либо влево, работая либо с байтами, либо со словами. Каждая команда содержит два операнда: первый операнд – поле данных – может быть либо регистром, либо ячейкой памяти; второй операнд – счетчик сдвигов. Его значение может быть равным 1, или быть произвольным. В последнем случае это значение необходимо занести в регистр CL, который указывается в команде сдвига. Число в CL может быть в пределах 0-255, но его практически имеющие смысл значения лежат в пределах 0-16.

Общая черта всех команд сдвига – установка флага переноса. Бит, попадающий за пределы операнда, сохраняется во флаге переноса. Всего существует 8 команд сдвига: 4 команды обычного сдвига и 4 команды циклического сдвига. Команды циклического сдвига переносят появляющийся в конце операнда бит в другой конец, а в случае обычного сдвига этот бит пропадает. Значение, вдвигаемое в операнд, зависит от типа сдвига. При логическом сдвиге вдвигаемый бит всегда 0, арифметический сдвиг выбирает вдвигаемый бит таким образом, чтобы сохранить знак операнда. Команды циклического сдвига с переносом и без него отличаются трактовкой флага переноса. Первые рассматривают его как дополнительный 9-ый или 17-ый бит в операции сдвига, а вторые нет.

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

- команды логического сдвига вправо SHR и влево SHL;

- команды арифметического сдвига вправо SAR и влево SAL;

- команды циклического сдвига вправо ROR и влево ROL;

- команды циклического сдвига вправо RCR и влево RCL с переносом;

Примеры использования команд сдвига:

SHL CH,1

SHL [BP],CL

RCL Size,1

Приведенная ниже программа иллюстрирует использование команд побитовой обработки.

Пример. Вывести на экран шестнадцатеричное представление кода символа «Q».

;Сегмент стека

SSEG SEGMENT STACK

DB 256 DUP (?)

SSEG ENDS

;Сегмент данных

DSEG SEGMENT

SMP DB ‘Q’ ;Символ

TBL DB ‘0123456789ABCDEF’ ;Таблица 16-ричных цифр

DSEG ENDS

;Сегмент кода

CSEG SEGMENT

ASSUME CS:CSEG, DS:DSEG, SS:SSEG

START:

MOV AX,DSEG ;Инициализация DS

MOV DS,AX

MOV AH,2 ;В AH номер функции вывода

MOV BX,0

;Вывод на экран цифры соответствующей левой тетраде

MOV BL,Smp ;В BL символ

MOV CL,4 ;В CL величина сдвига

SHR BL,CL ;Сдвиг левой тетрады на место правой

MOV DL,Tbl[BX] ;Загрузка цифры из таблицы в DL

INT 21H ;Вывод на экран

;Вывод на экран цифры соответствующей правой тетраде

MOV BL,Smp ;В BL символ

AND BL,00001111B ;Обнуление левой тетрады

MOV DL,Tbl[BX] ;Загрузка цифры из таблицы в DL

INT 21H ;Вывод на экран

;Вывод на экран символа «h»

MOV DL,’h’ 

INT 21H

CSEG ENDS

END START

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

8. Команды сравнения и передачи управления

Команда сравнения CMP сравнивает два числа, вычитая второе из первого, также как и команда SUB. Отличие команд CMP и SUB состоит в том, что инструкция CMP не сохраняет результат, а лишь устанавливает в соответствии с результатом флаги состояния. Основное назначение команды CMP – это организация ветвлений (условных переходов) в ассемблерных программах

Безусловный переход – это переход, который передает управление без сохранения информации возврата всякий раз, когда выполняется. Ему соответствует команда JMP. Эта команда может осуществлять переход вплоть до 32768 байт. Если заранее известно, что переход вперед делается на место, лежащее в диапазоне 128 байт от текущего места, можно использовать команду JMP SHORT LABEL. Атрибут SHORT заставляет Ассемблер сформировать короткую форму команды перехода, даже если он еще не встретил метку LABEL.

Условный переход проверяет текущее состояние машины (флагов или регистров), чтобы определить, передать управление или нет. Команды переходов по условию делятся на две группы:

- проверяющие результаты предыдущей арифметической или логической операции Jcc;

- управляющие итерациями фрагмента программы (организация циклов) LOOPcc.

Все условные переходы имеют однобайтовое смещение, то есть метка, на которую происходит переход должна находится в том же кодовом сегменте и на расстоянии, не превышающем –128 +127 байт от первого байта следующей команды. Если условный переход осуществляется на место, находящееся дальше 128 байт, то вместо недопустимой команды

JZ ZERO

необходимо использовать специальные конструкции типа:

JNZ CONTINUE

JMP ZERO

CONTINUE:

Первая группа команд Jcc (кроме JCXZ/JECXZ) проверяет текущее состояние регистра флагов (не изменяя его) и в случае соблюдения условия осуществляет переход на смещение, указанное в качестве операнда. Флаги, проверяемые командой, кодируются в ее мнемонике, например: JC – переход, если установлен CF. Сокращения «L» (less – меньше) и «G» (greater – больше) применяются для целых со знаком, а «A» (above – над) и «B» (below – под) для целых без знака. Ниже в таблице показаны команды условного перехода и проверяемые ими флаги.

Команды условного перехода можно разделить на три подгруппы:

1) Непосредственно проверяющие один из флагов на равенство 0 или 1.

2) Арифметические сравнения со знаком. Существуют 4 условия, которые могут быть проверены: меньше (JL), меньше или равно (JLE), больше (JG), больше или равно (JGE). Эти команды проверяют одновременно три флага: знака, переполнения и нуля.

3) Арифметические без знака. Здесь также существует 4 возможных соотношения между операндами. Учитываются только два флага. Флаг переноса показывает какое из двух чисел больше. Флаг нуля определяет равенство.

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

CSEG SEGMENT

ASSUME CS:CSEG, DS:DSEG, SS:SSEG

START:

...

MOV BH,X ;Загрузка в BH значения Х

MOV BL,Y ;Загрузка в BL значения Y

CMP BH,BL ;Сравнение BH и BL

JE MET1 ;Если BH=BL, то переход на MET1

JMP MET2 ;Иначе переход на MET2

MET1:

...

JMP MET3

MET2:

...

MET3:

MOV AH,4Ch

INT 21H

CSEG ENDS

END START

JCXZ отличается от других команд условного перехода тем, что она проверяет содержимое регистра CX, а не флагов. Эту команду лучше всего применять в начале условного цикла, чтобы предотвратить вхождение в цикл, если CX=0.

Вторая группа команд условного перехода LOOPcc служит для организации циклов в программах. Все команды цикла используют регистр CX в качестве счетчика цикла. Простейшая из них – команда LOOP. Она уменьшает содержимое CX на 1 и передает управление на указанную метку, если содержимое CX не равно 0. Если вычитание 1 из CX привело к нулевому результату, выполняется команда, следующая за LOOP.

Команда LOOPNE (цикл пока не равно) осуществляет выход из цикла, если установлен флаг нуля или если регистр CX достиг нуля. Команда LOOPE (цикл пока равно) выполняет обратную к описанной выше проверку флага нуля: в этом случае цикл завершается, если регистр CX достиг нуля или если не установлен флаг нуля.

Приведенный ниже фрагмент программы иллюстрирует использование команд организации циклов.

DSEG SEGMENT

BUF DB “0123406789”

DSEG ENDS

CSEG SEGMENT

ASSUME CS:CSEG,DS:DSEG,SS:SSEG

START:

...

MOV BX,OFFSET BUF;В BX – начало буферов

MOV CX,10 ;В CX – длинна буфера

MOV SI,0

M1: MOV DL,[BX+SI] ;В DL – символ из буфера

MOV AH,2 ;в AH номер функции-вывода

INT 21H ;Вывод на экран

INC SI ;Увеличение индекса на 1

LOOP M1 ;Оператор первого цикла

...

CSEG ENDS

END START

9. Подпрограммы и прерывания.

Все современные программы разрабатываются по модульному принципу – программа обычно состоит из одной или нескольких небольших частей, называемых подпрограммами или процедурами, и одной главной программы, которая вызывает эти процедуры на выполнение, передавая им управление процессором. После завершения работы процедуры возвращают управление главной программе и выполнение продолжается с команды, следующей за командой вызова подпрограммы.
Достоинством такого метода является возможность разработки программ значительно большего объема небольшими функционально законченными частями. Кроме того, эти подпрограммы можно использовать в других программах, не прибегая к переписыванию частей программного кода. В довершение ко всему, так как размер сегмента не может превышать 64К, то при разработке программ с объемом кода более 64К, просто не обойтись без модульного принципа.
Язык программирования Ассемблера поддерживает применение процедур двух типов – ближнего (near) и дальнего (far).
Процедуры ближнего типа должны находится в том же сегменте, что и вызывающая программа. Дальний тип процедуры означает, что к ней можно обращаться из любого другого кодового сегмента.

При вызове процедуры в стеке сохраняется адрес возврата в вызывающую программу:

- при вызове ближней процедуры – слово, содержащее смещение точки вызова относительно текущего кодового сегмента;

- при вызове дальней процедуры – слово, содержащее адрес сегмента, в котором расположена точка возврата, и слово, содержащее смещение точки возврата в этом сегменте.

В общем случае группу команд, образующих подпрограмму, можно никак не выделять в тексте программы. Для удобства восприятия в языке Ассемблера процедуры принято оформлять специальным образом. Описание процедуры имеет следующий синтаксис:

<имя_процедуры> PROC <параметр>

<тело_процедуры>

<имя_процедуры> ENDP

Следует обратить внимание, что в директиве PROC после имени не ставится двоеточие, хотя имя и считается меткой.

Параметр, указываемый после ключевого слова PROC, определяет тип процедуры: ближний (NEAR) или дальний (FAR). Если параметр отсутствует, то по умолчанию процедура считается ближней.
В общем случае, размещать подпрограмму в теле программы можно где угодно, но при этом следует помнить, что сама по себе подпрограмма выполняться не должна, а должна выполняться лишь при обращении к ней. Поэтому подпрограммы принято размещать либо в конце сегмента кода, после команд завершения программы, либо в самом начале сегмента кода, перед точкой входа в программу. В больших программах подпрограммы нередко размещают в отдельном кодовом сегменте.
Передавать фактические параметры процедуре можно несколькими способами. Простейший способ – передача параметров через регистры: основная программа записывает параметры в какие-либо регистры, а процедура по мере необходимости извлекает их из этих регистров и использует в своей работе. Такой способ имеет один основной недостаток: передавать параметры через регистры можно если их немного (если много, то просто не хватит регистров). Решить это проблему можно, передавая параметры через стек. В этом случае основная программа записывает параметры в стек и вызывает подпрограмму, подпрограмма работает с параметрами и, возвращая управление, очищает стек (см. пример в разделе «5.5. Команды работы со стеком»).
Для работы с подпрограммами в систему команд процессора включены специальные команды, это вызов подпрограммы CALL и возврат управления RET.
Все команды вызова CALL безусловны. Внутрисегментный вызов NEAR CALL используется для передачи управления процедуре, находящейся в том же сегменте. Он указывает новое значение регистра IP и сохраняет старое значение счетчика команд (IP) в стеке в качестве адреса возврата. Межсегментный вызов FAR CALL используется для передачи управления процедуре, находящейся в другом сегменте или даже программном модуле. Он задает новые значения сегмента CS и смещения IP для дальнейшего выполнения программы и сохраняет в стеке как регистр IP, так и регистр CS.
Все возвраты RET являются косвенными переходами, поскольку извлекают адрес перехода из вершины стека. Внутрисегментный возврат извлекает из стека одно слово и помещает его в регистр IP, а межсегментный возврат извлекает из стека два слова, помещая слова из меньшего адреса в регистр IP, а слово из большего адреса – в регистр CS. Команда RET может иметь операнд, который представляет собой значение, прибавляемое микропроцессором к содержимому указателя стека SP после извлечения адреса возврата (очистка стека) (см. пример в «5.5. Команды работы со стеком».
Другим видом процедур являются прерывания DOS и BIOS. Прерывания это обычные процедуры, написанные разработчиками операционной системы (прерывания DOS) или разработчиками аппаратных средств компьютера (прерывания BIOS). Поэтому к этим процедурам можно обращаться точно так же, как и к другим процедурам. Отличает их лишь форма вызова: вместо команды CALL ProcName используется команда типа INT Number, где Number – номер прерывания, например INT 10h, INT 21h и т.п. Эта команда вызывает прерывание с номером Number (0< Number <255), то есть после такой команды начинает работать подпрограмма обработки прерывания, начальный адрес которой записан в Number-ом элементе таблицы векторов прерываний. (Таблица векторов прерываний представляет собой массив из 256 элементов, расположенный по адресу 0000:0000 и имеющий длину 1024 байт. Каждый элемент таблицы векторов занимает 4 байта и представляет собой начальный адрес процедуры обработки прерывания.) Закончив свое выполнения, подпрограмма передает управление на команду расположенную непосредственно за командой INT.
В связи с тем, что в состав операционной системы входит много стандартных процедур и для них не хватает допустимых векторов прерываний, они (процедуры) объединяются в группы, вызываемые по прерыванию с одним и тем же номером. Подпрограммы одной группы называют функциями прерывания. Чтобы различать функции прерывания перед его вызовом в регистр AH заносится номер необходимой функции:

MOV AH,<номер функции>

INT <номер прерывания>

Для выполнения вызванной таким образом процедуры может потребоваться определенная информация (например, для функции вывода символа на экран – код символа). Такая информация передается через регистры микропроцессора.

Ниже приведен пример использования прерывания 21H функции 02 для вывода символа на экран.

...

MOV AH,02h ;AH – номер функции

MOV DL,’a’ ;DL – выводимый символ

INT 21h ;инициализация прерывания

...

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

Строкой в Ассемблере называют последовательность байтов или слов длинной от 1 до 65535 байт. Операции со строками обеспечивают пересылку, сравнение, сканирование строк по значению, а также пересылку строки в аккумулятор или из него. Каждая строковая операция представленная в процессоре двумя командами: одна предназначена для обработки строк состоящих из байт, другая – из слов (их мнемоника различается наличием буквы B (byte) или W (word)).
Если флаг направления DF перед выполнением команды строковой обработки установлен в 0 (выполнена команда CLD), значение в индексном регистре автоматически увеличивается, если в 1 (выполнена STD) – уменьшается. Индексные регистры уменьшаются или увеличиваются на 1, если команды работают с байтами, или на 2 – если со словами.
Команды строковой обработки чаще всего используются с однобайтными префиксами (префиксами повторения), которые обеспечивают многократное автоматическое повторение выполнения команды.

Команда сравнения строк CMPS (CMPSB, CMPSW).

Команда CMPS сравнивает значение элемента одной строки (DS:SI) со значением элемента второй строки (ES:DI) и настраивает значения регистров на следующие элементы строк в соответствии с флагом направления DF. Сравнение происходит как и по команде сравнения CMP. Результат – сформированные флаги.

Команда сканирования строки SCAS (SCASB, SCASW).

Команда SCAS производит сравнение содержимого регистра (AL или AX) с байтом памяти, абсолютный адрес которого определяется парой ES:DI, после чего регистр DI устанавливается на соседний элемент памяти (байт или слово) в соответствии с флагом DF. Команда SCAS используется обычно для поиска в строке (ES:DI) элемента заданного в регистре AL или AX.

Команда пересылки строки MOVS (MOVSB, MOVSW).

Команда MOVS пересылает поэлементно строку DS:SI в строку ES:DI и настраивает значения индексных регистров на следующий элемент строки.

Команда сохранения строки STOS (STOSB, STOSW).

Команда STOS заполняет строку, содержащуюся по адресу ES:DI, элементом из регистра AL или AX. На флаги команда не влияет

Команда загрузки строки LODS (LODSB, LODSW).

Команда LODS записывает в регистр AL или AX содержимое ячейки памяти, адрес которой задается регистрами DS:SI. Флаги не меняются.

Префиксы повторения.

В системе команд процессора имеется две команды без операндов, которые называются префиксами повторения:

1-ый префикс: REPE (повторять, пока равно)

REPZ (повторять, пока ноль)

REP (повторять)

2-ой префикс: REPNE (повторять, пока не равно)

REPNZ (повторять, пока не ноль)

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

REPE CMPB

Префикс использует регистр CX как счетчик циклов. На каждом этапе цикла выполняются следующие действия:

1) Проверка CX. Если он равен 0 – выход из цикла и переход к следующей команде.

2) Подтверждение любых возникающих прерываний.

3) Выполнение указанной строковой операции.

4) Уменьшение CX на единицу, флаги при этом не изменяются.

5) Проверка флага ZF, если выполняется строковая операция SCAS или CMPS. Если условие повторения цикла не выполняется – выход из цикла и переход к следующей команде. Выход из цикла, если префиксом является REPE и ZF=0 (последнее сравнение не совпало) или используется префикс REPNE и ZF=1 (последнее сравнение не совпало).

6) Изменение значения индексных регистров в соответствии со значением флага направления и переход на начало цикла.

Фрагмент программы, иллюстрирующий работу со строковыми данными, приведен ниже.

CLD ;DF=0

LEA SI,s1 ;DS:SI=начало s1

LEA DI,s2 ;ES:DI=начало s2

MOV CX,n ;CX=длинна строк

REPE CMPSB ;сравнение, пока элементы равны

JNE NoEq ;если s1<>s2 (ZF=0), то на NoEq

...

NoEq:

...

11. Команды управления процессором

К командам управления процессором чаще всего относят команды работы (установка и очистка) с флагами. Среди них наиболее часто приходится использовать следующие.

Команда CLC устанавливает значение флага переноса CF, равное нулю. Все остальные флаги и регистры остаются неизменными.

Команда CMC изменяет значение флага переноса CF на противоположное. Другие флаги остаются без изменений.

Команда STC устанавливает флаг переноса в единицу.

Команда CLD очищает флаг направления DF. Все остальные флаги и регистры остаются неизменными. После выполнения CLD используемые строковые операции будут увеличивать индексный регистр (SI или DI).

Команда STD устанавливает флаг направления DF в единицу, что заставляет все последующие строковые операции уменьшать при их выполнении индексные регистр (SI или DI).

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

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

12. Структуры данных

12.1. Массивы

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

Mas1 DB 10 DUP (?)

а двумерный массив слов:

Mas2 DW 10 DUP (10 DUP (?))

Легко заметить что, при описании массивов разработчик указывает имя массива, тип элементов и их количество, но не указывает как индексируются эти элементы. Поэтому, в общем случае, начальным индексом массива может быть любая величина k, например, для описанного выше одномерного массива, диапазон индексов будет следующим [k...9+k]. Тогда верно следующее соотношение:

Адрес(X[i])=X+(Type X)*[i-k]

где X – начальный адрес массива, (Type X ) – тип элементов массива, k – индекс первого элемента массива. Очевидно, что при k=0 эта зависимость становится наиболее простой:

Адрес(X[i])=X+(Type X)*i

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

Для многомерных массивов ситуация аналогична.

Теперь рассмотрим каким образом организуется доступ к элементам массива. Основная трудность возникают в ситуации, когда массив обрабатываются в цикле, и необходимо организовать доступ к каждому элементу массива. В этом случае поступают следующим образом:

- адрес текущего элемента массива X+(Type X)*i разбивается на два слагаемых: постоянное слагаемое X (начальный адрес массива) и переменное слагаемое (Type X)*i (смещение текущего элемента относительно начала массива);

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

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

Ниже приведен фрагмент программы, иллюстрирующий нахождение суммы элементов одномерного массива байт, описанного выше.

MOV AX,0 ;начальное значение суммы

MOV CX,10 ;счетчик циклов (кол-во элементов)

MOV SI,0 ;начальное значение индекса

M: ADD AX,Mas1[SI] ;AX:=AX+Mas1[i]

INC SI ;увеличение индекса

LOOP M

12.2. Связанные списки

Списком (линейным, однонаправленным) называется последовательность звеньев, которые могут размещаться в произвольных местах памяти и в каждом из которых содержится элемент списка (Ei) и ссылка на следующее звено (изображена стрелкой):

В последнем звене размещается специальная «пустая» ссылка nil, указывающая на конец списка. Ссылка на первое звено списка хранится в некоторой переменной LIST, если список пуст, то ее значением является nil.
Возможность размещать звенья списков в любых местах памяти обуславливают плюсы и минусы списков. Достоинством списков является то, что их длина заранее не фиксируется, что под них надо отводить ровно столько места, сколько требуется в текущий момент, и что удаление и вставка элементов реализуются просто и быстро – заменой ссылок в одном-двух звеньях. К недостаткам же списков относится лишний расход памяти для хранения ссылок и то, что доступ к элементам списка осуществляется последовательно.
Для хранения звеньев списков обычно отводят специальную область памяти, называемую кучей (heap). Обычно на начало кучи постоянно указывает сегментный регистр ES. Если внутри кучи звено списка имеет адрес (смещение) А, то абсолютный адрес этого звена задается адресной парой ES:A. Ссылка на звено – это адрес данного звена. Для экономии памяти ссылкой на звено обычно считается лишь смещение А, то есть адрес этого звена, отсчитанный от начала кучи (два байта). Под каждое звено отводится несколько соседних байтов памяти, в первых из которых размещается соответствующий элемент списка ELEM, а в остальных ссылка на следующее звено NEXT. Размер звена зависит от размера элементов списка.

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

NODE STRUC ;тип звена списка

ELEM DW ? ;элемент списка

NEXT DW ? ;ссылка на следующее звено

NODE ENDS

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

ES:A.ELEM – поле с элементом

ES:A.NEXT – поле с адресом следующего звена

В качестве пустой ссылке nil обычно используется адрес 0, при этом удобно описать в программе этот адрес как константу

NIL ЕQU 0

и далее пользоваться именем NIL.

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

List DW ?

При работе со списком приходится последовательно просматривать его звенья. Поэтому необходимо знать адрес текущего звена. Для хранения этого адреса удобно использовать какой-нибудь регистр-модификатор, например, ВХ. Если текущим является i-e звено списка, то графически это можно представить так:

В этом случае доступ к полям текущего звена можно получить следующим образом ES:[BX].ELEM и ES:[BX].NEXT.

Анализ текущего элемента списка

Следующий фрагмент программы иллюстрирует анализ, равенства элемента из текущего звена значению некоторой переменной X, и, если условие истинно, переход на метку EQ:

MOV AX,ES:[BX].ELEM

CMP АХ,Х 

JE EQ

Переход к следующему звену списка

Фрагмент программы, иллюстрирующий эту операцию, выглядит следующим образом (ВХ – текущее звено списка):

MOV BX,ES:[BX].NEXT

Поиск элемента в списке

Нижеследующий фрагмент программы отыскивает в списке LIST значение X. Если X найден, то в регистр AL заносится 1 (входит) иначе 0.

MOV AL,0

MOV СХ,Х ;искомая величина

MOV BX,LIST ;bх - nil или адрес 1-го звена

L: СМР BX,NIL

JE NO ;конец списка

СМР ES:[BX].ELEM,CX

JE YES

MOV BX,ES:[BX].NEXT ;bх - адрес следующего звена

JMP L

YES: MOV AL,1

NO: ...

Вставка элемента в список

Для вставки нового элемента X в список LIST необходимо выполнить следующие действия:

1) отвести (выделить) место под новое звено (эти действия выполняет процедура NEW (см. ниже), она отыскивает в куче свободное место и возвращает его адрес через регистр DI);
2) заполнить новое звено, то есть в поле ELEM записать величину X, а в поле NEXT – ссылку на бывшее первое звено (она берется из LIST)
3) в LIST записать адрес нового звена (берется из DI):

CALL NEW ;new(di)

MOV AX,X

MOV ES:[DI].ELEM,AX ;di^.elem=x

MOV AX, LIST

MOV ES:[DI].NEXT,AX ;di^.next:=list

MOV LIST,DI ;list=di

Организация кучи

При программировании на языке ассемблера необходимо самому освобождать и выделять память под звенья списка (процедуры New и Dispose). Ниже приведен один из вариантов организации кучи.

Список свободной памяти

В процессе работы программы память постоянно освобождается и выделяется, то есть занятые и свободные ячейки в сегменте кучи разбросаны. Поэтому удобно все свободные ячейки кучи объединить в один список, который принято называть списком свободной памяти (ССП). Начало этого списка указывается в некоторой фиксированной ячейке, например, с именем HEAP_PTR (heap pointer, указатель кучи). Используется ССП следующим образом: когда программе нужно место под новое звено, то оно выделяется из этого ССП, а когда программа отказывается от какого-то звена, то это звено добавляется к ССП.
Поскольку в простейшем случае звенья имеют фиксированную длину, то имеет смысл объединять в ССП элементы именно этого размера (например, двойные слова, как в предыдущих примерах). Если при этом ссылку на следующее звено ССП хранить во втором слове звена, то получим обычный список.
Место для переменной HEAP_PTR лучше всего отвести в самом начале кучи – в ячейке с относительным адресом 0. В этом случае ни одно звено не будет размещаться в этой ячейке и не будет иметь адрес 0, т.е. этот адрес останется свободным и его можно использовать для представления пустой ссылки nil.

Описание сегмента кучи

Поскольку куча – это один из сегментов памяти, используемый программой, то его необходимо описать в программе (n – число звеньев в куче):

HEAP_SIZE EQU N

HEAP SEGMENT ;сегмент кучи

HEAP_PTR DW ? ;ячейка с начальным адресом ССП

DD HEAP_SIZE DUP(?)

HEAP ENDS

Инициализация кучи

Первоначально все ячейки кучи свободны, и не объединены в ССП. Поэтому в начале программы необходимо связать все ячейки (двойные слова) кучи в единый список и записать в HEAP_PTR ссылку на начало этого списка. Нижи приведен пример подпрограммы инициализации кучи:

INIT_HEAP PROC FAR

PUSH SI

PUSH BX

PUSH CX

;установка ES на начало сегмента кучи

MOV CX,HEAP

MOV ES,CX

;объединение всех двойных слов кучи в ССП

MOV CX,HEAP_SIZE ;число звеньев в ССП

MOV BX,NIL ;ссылка на построенную часть ССП

MOV SI,4*HEAP_SIZE-2 ;адрес нового звена ССП

INIT1:MOV ES:[SI].NEXT,BX ;добавить в начало

MOV BX,SI ;ССП новое звено

SUB SI,4 ;SI - на двойное слово "выше"

LOOP INIT1

MOV ES:HEAP_PTR,BX ;HEAP_PTR – на начало ССП

POP CX

POP BX

POP SI

RET

INIT_HEAP ENDP

Обратиться к этой процедуре необходимо в начале программы, после чего уже можно использовать процедуры NEW и DISPOSE.

Процедура Dispose

К этой процедуре программа обращается, когда она отказывается от некоторого звена. Адрес этого звена, например, передается через регистр DI. Звено становится свободным и его необходимо добавить в начало ССП:

;на входе: DI - адрес ненужного звена

DISPOSE PROC FAR

PUSH ES:HEAP_PTR

POP ES:[DI].NEXT ;di^.next:=heap_ptr

MOV ES:HEAP_PTR,DI ;heap_ptr:=di

RET

DISPOSE ENDP

Процедура New

К этой процедуре программа обращается, когда ей необходимо место для нового звена списка. Это место берется из ССП: от этого списка отщепляется одно из звеньев и отдается программе. Проще всего отделить первое звено. Адрес этого звена процедура должна вернуть через регистр DI.

;на выходе: DI - адрес свободного звена

NEW PROC FAR

MOV DI,ES:HEAP_PTR

СMР DI,NIL

JE EMPTY ;пустой ССП -> ЕНРТУ_НЕАР

PUSH ES:[DI].NEXT

POP ES:HEAP_PTR ;heap_ptr - на 2-е звено ССП

RET

EMPTY: ;реакция на пустую кучу

...

NEW ENDP

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

13. Условное ассемблирование

Язык ассемблера включает в себя директивы условного ассемблирования. Условное ассемблирование – удобно при многократных прогонах программы. Оно дает возможность в исходном тексте держать несколько вариантов одного и того же участка программы, и при каждом прогоне оставлять в окончательном тексте только один из них. Какой именно вариант будет оставлен, зависит от тех или иных условий, которые автор программы задает перед прогоном. Таким образом, автор программы не должен перед каждым ее прогоном вручную редактировать текст, а возлагает эту работу на макрогенератор.

Участок программы, затрагиваемый условным ассемблированием, должен записываться в виде так называемого IF-блока:

<IF-директива>

<фрагмент-1>

ELSE

<фрагмент-2>

ENDIF

или

<IF-директива>

<фрагмент-1>

ENDIF

Директивы ELSE и ENDIF обязательно должны записываться в отдельных строках. В каждом же фрагменте может быть любое число любых предложений, в частности в них снова могут быть IF-блоки, т. е. допускается вложенность IF-блоков.

В IF-дирсктиве указывается некоторое условие, которое проверяется макрогенератором. Если условие выполнено, то макрогенератор оставляет в окончательном тексте программы только фрагмент-1, а фрагмент-2 исключает, не переносит в окончательный текст. Если же условие не выполнено, тогда фрагмент-1 игнорируется, а в окончательную программу вставляется только фрагмент-2. Если части ELSE нет, то считается, что фрагмент-2 пуст, поэтому при невыполнении условия такой IF-блок ничего не «поставляет» в окончательный текст программы.
Поскольку условие в IF-директиве проверяется на этапе макрогенерации, то в нем не должно быть ссылок на величины, которые станут известными только при выполнении программы (например, в условии нельзя ссылаться на содержимое регистров или ячеек памяти). Более того, условие должно быть таким, чтобы макрогенератор мог вычислить его сразу, как только встретит (в нем не должно быть ссылок вперед).
В макроязыке довольно много IF-директив их удобно рассматривать парами, в каждой из которых директивы проверяют противоположные условия.

Директивы IF и IFE имеют следующий вид:

IF <константное выражение>

IFE <константное выражение>

Встречая любую из этих директив, макрогенератор вычисляет указанное в ней константное выражение. В директиве IF условие считается выполненным, если значение выражения отлично от 0, а директиве IFE - если значение равно 0.

В общем случае константное выражение, указываемое в директивах IF и IFE, может быть любым, но по смыслу оно, должно быть логическим выражением. В языке ассемблера имеется шесть операторов отношения:

<выражение> EQ <выражение>

<выражение> NE <выражение>

<выражение> LT <выражение>

<выражение> LE <выражение>

<выражение> GT <выражение>

<выражение> GE <выражение>

Названия этих операторов: EQ -равно, NE - не равно, LT - меньше, LE -меньше или равно, GT - больше, GE - больше или равно. Оба операнда должны быть либо константными выражениями, либо адресными выражениями, значениями которых обязаны быть адреса из одного и того же сегмента памяти. Если проверяемое отношение выполняется, то значением оператора является «истина», не если выполняется – «ложь».

Логические значения и отношения можно объединять в более сложные логические выражения с помощью следующих логических операторов:

NOT <константное выражение>

<константное выражение> AND <константное выражение>

<константное выражение> OR <константное выражение>

<константное выражение> XOR <константное выражение>

Эти операторы реализуют соответственно операции отрицания, конъюнкции, дизъюнкции и «исключающего ИЛИ». Их операндами могут быть любые константные выражения (но не адресные), значения которых трактуются как 16-битовые слова. Значением этих операторов также является 16-битовое слово, которое получается в результате поразрядного выполнения соответствующей операции.

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

Директивы IFIDN, IFDIF, IFB и IFNB имеют следующий вид:

IFIDN <tl>,<t2>

IFDIF <tl>,<t2>

где tl и t2 - любые последовательности символов (причем они обязательно должны быть заключены в угловые скобки), которые посимвольно сравниваются. В директиве IFIDN условие считается выполненным, если эти строки равны, а в директиве IFDIF - если они не равны. При сравнении этих текстов большие и малые буквы не отождествляются.

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

Директивы

IFB <t>

IFNB <t>

Фактически являются вариантами директив IFIDN и IFDIF, когда второй текст пуст. В директиве IFB условие считается выполненным, если текст t пуст, а в директиве IFNB - если текст t не пуст.

Эти директивы используются в макросах для проверки, задан ли фактический параметр или нет.

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

Предположим, что необходимо отладить программу и для этого в определенные места ее текста вставляются отладочные печати (ОП), т. е. печать промежуточных значений каких-то переменных. После окончания отладки ОП убираются из текста. Процесс отладки может повторяться несколько раз. Вставлять и убирать ОП можно, с помощью какого-либо текстового редактора, но чаще всего ОП разбросаны по всей программе, и это занимает не мало времени. В подобной ситуации удобно использовать возможности условного ассемблирования: в тексте программы постоянно сохраняются ОП, но перед каждой из них указывается условие, что команды ОП должны оставаться в окончательном тексте программы только при отладке.
Это можно осуществить следующим образом. Пусть режим прогона программы указывается с помощью константы DEBUG, которая описана в начале текста программы и которой присваивается значение 1 (отладка) или значение 0 (счет). Тогда, (см. пример) участок исходной программы с отладочной печатью (например, переменной X) должен быть записан так, как указано слева, в окончательном же тексте программы этот участок будет выглядеть так, как изображено справа (PRINT – макрос вывода на экран):



MOV Х,АХ

IF DEBUG

PRINT X

ENDIF

MOV BX,0





Debug<>0



MOV X,AX

PRINT X

MOV BX,0



Debug=0

MOV X,AX

MOV BX,0



При таком построении текста программы достаточно перед ее прогоном поменять лишь одну строчку – описание константы DEBUG, чтобы макрогенератор сформировал разный окончательный текст программы (с ОП или без них).

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

14. Макросредства

Макросредства языка ассемблера позволяют формировать в исходной программе блоки предложений (макроопределения), имеющие одно общее имя, и затем многократно использовать это имя для представления всего блока. В процессе ассемблирования компилятор автоматически замещает каждое распознаваемое макроимя (макрокоманду) последовательностью предложений в соответствии с макроопределением, в результате чего формируется макрорасширение.
Макрокоманда может встречаться в исходной программе столько раз, сколько это необходимо. Макроопределение в исходном файле должно предшествовать его первому использованию в макрокоманде. Макроопределение может как непосредственно находиться в тексте программы, так и подключаться из другого файла директивой INCLUDE. В макроопределение могут быть переданы параметры, которые будут управлять макроподстановкой или задавать фрагменты текста.
В программе на языке ассемблера макрокоманды выполняют в целом те же функции, что и процедуры, т.е. обеспечивают многократное и функционально законченное действие с параметрическим управлением. Различие заключается в следующем:

1) Процедура присутствует в исходной программе один раз, тогда как тело макроопределения дублируется столько раз, сколько соответствующих макрокоманд содержит исходный файл.
2) Текст процедуры статичен и неизменен в то время, как состав макрорасширения может зависеть от параметров макрокоманды.

Следует помнить, что параметры макрокоманды - это значения времени ассемблирования, а параметры процедуры принимают какие-то определенные значения лишь в процессе выполнения программы.

14.1. Макродирективы

Макроопределение представляет собой блок исходных предложений, начинающийся директивой MACRO и заканчивающийся директивой ENDM. Формат макроопределения:

имя MACRO [[формальный-параметр,...]]

предложения

ENDM

Именем макроопределения считается имя, указанное в директиве MACRO. Оно должно быть уникальным и может использоваться в исходном файле для вызова макроопределения. Формальные параметры представляют собой внутренние по отношению к данному макроопределению имена, которые используются для обозначения значений, передаваемых в макроопределение при его вызове. Может быть определено любое число формальных параметров, но все они должны помещаться на одной строке и разделяться запятыми.
В теле макроопределения допустимы любые предложения языка ассемблера, в том числе и другие макродирективы. Допустимо любое число предложений, а каждый формальный параметр может быть использован любое число раз в этих предложениях.
Макроопределения могут быть переопределены. При этом нет необходимости заботиться об удалении из исходного файла первого макроопределения, так как каждое следующее макроопределение автоматически замещает предыдущее макроопределение с тем же именем.
Макроопределение может быть вызвано в любой момент простым указанием его имени в исходном файле. Ассемблер при этом копирует предложения макроопределения в точку вызова, замещая в этих предложениях формальные параметры на фактические параметры, задаваемые при вызове.

Общий вид макровызова:

имя [[фактический-параметр,...]]

Имя должно быть именем ранее определенного в исходном файле макроопределения. Фактическим параметром может быть имя, число или другое значение. Допустимо любое число фактических параметров, но все они должны помещаться на одной строке. Элементы списка параметров должны разделяться запятыми, пробелами, или TAB-символами.

Компилятор замещает первый формальный параметр на первый фактический параметр, второй формальный параметр на второй фактический параметр и т.д. Если фактических параметров в макровызове больше, чем формальных параметров в макроопределении, лишние фактические параметры игнорируются. Если же фактических параметров меньше, чем формальных, формальные параметры, для которых не заданы фактические, замещаются пустыми строками (пробелами). Для определения того, заданы или не заданы соответствующие фактические параметры могут быть использованы директивы IFB и IFNB (см. «13. Условное ассемблирование»).

В макросредствах языка ассемблера имеется возможность передавать список значений в качестве одного параметра. Этот список должен быть заключен в одинарные угловые скобки < >, а сами элементы списка – разделяться запятыми, например:

allocb <1,2,3,4>

При написании макроопределений иногда возникает необходимость в задании меток инструкций или имен полей данных. Поскольку каждое макроопределение может использоваться многократно, возникают ситуации, когда несколько имен или меток определены многократно, что вызовет ошибку трансляции. Для обеспечения правильной обработки таких ситуаций в макроязыке предусмотрена директива LOCAL, позволяющая локализовать заданные имена исключительно в данном макрорасширении. Формат:

LOCAL формальное-имя,...

Формальное-имя может затем использоваться в данном макроопределении с уверенностью, что при каждом макровызове его значение будет уникальным. В директиве LOCAL, если она присутствует, должно быть задано хотя бы одно имя, а если их несколько, они должны разделяться запятыми. Для обеспечения уникальности определенных директивой LOCAL имен ассемблер для каждого такого имени при каждом макровызове порождает реальное имя следующего вида:

??номер

Номер представляет собой 16-ричное число в пределах от 0000 до FFFF. Для предотвращения повторного определения имен программисту не рекомендуется определять в своей программе имена этого типа.

Директива LOCAL может использоваться только в макроопределении, причем, там она должна предшествовать всем другим предложениям макроопределения, т.е. следовать непосредственно после MACRO.

Для удаления макроопределений служит директива PURGE. Формат:

PURGE имя-макроопределения,...

Директивой PURGE удаляются все текущие макроопределения с указанными именами. Последующий вызов одного из этих макроопределений будет приводить к ошибке. Если имя-макроопределения представляет мнемонику инструкции или директивы, восстанавливается первоначальный смысл мнемоники в соответствии со значением данного ключевого слова. Директива PURGE часто используется для удаления ненужных макроопределений из подключаемой директивой INCLUDE библиотеки макроопределений. Библиотека макроопределений представляет собой обычный последовательный файл, который, в общем случае, может содержать большое число макроопределений. Комбинация директив INCLUDE и PURGE позволяет выбрать из них только нужные для данной программы, что сократит размер исходного файла.

Выход из текущего макроопределения до достижения директивы ENDM обеспечивается директивой EXITM, имеющей следующий формат:

EXITM

Выход из макроопределения по директивам ENDM и EXITM заключается в прекращении генерации текущего макрорасширения и возврате в точку вызова текущего макроопределения в динамически внешнем макрорасширении или в исходной программе. Ниже приведен пример законченного макроопределения:

add MACRO param

IFB param

EXITM

ENDIF

ADD AX,param

ENDM

В этом мароопределении осуществляется добавление величины, определяемой формальным параметром param, к содержимому регистра AX. Блок условного ассемблирования IFB обеспечивает выход из макроопределния, если при вызове параметр не был задан.

15. Языки высокого уровня и Turbo Assembler

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

15.1. Основные принципы взаимодействия Turbo Assembler и Borland Pascal

Директива компилятора $L и внешние подпрограммы

Для того, чтобы подпрограммы, созданные с помощью Turbo Assembler, могли ис­пользоваться программами на Borland Pascal необходимо, чтобы, во-первых, Pascal-программа содержала соответствующую директиву компилятора $L и объявление подпрограммы с помощью директивы external, а во-вторых, чтобы эта подпрограмма на ассемблере использовала директивы PUBLIC и EXTRN.

Директива $L применяется для указания имени объектного файла, содержащего оттранслированную ассемблерную подпрограмму, например, {$L MYFILE} (если рас­ширение опущено, подразумевается, что файл имеет расширение .OBJ).

Для того чтобы компилятор получил информацию о процедуре или функции, ко­торую он должен искать в указанном объектном файле, вызывающая Pascal програм­ма должна содержать ее объявление, использующее директиву external, например:

procedure AsmProc(a:lnteger; b: Real); external;

function AsmFunc(c:word; d: Byte): Byte; external;

Объявления процедур и функций такого вида подразумевают, что в ассемблерной программе будет использовано объявление соответствующих процедур с помощью директивы PUBLIC, например:

CODE SEGMENT BYTE PUBLIC

AsmProc PROC NEAR

PUBLIC AsmProc

...

AsmProc ENDP

AsmFunc PROC FAR

PUBLIC AsmFunc

...

AsmFunc ENDP

CODE ENDS

Borland Pascal не проверяет соответствия типов процедур, объявленных директи­вой PROC (NEAR или FAR) типам процедур, объявленным директивой external. Таким образом, программист должен сам принимать необходимые меры, чтобы объявления процедур на ассемблере и Pascal соответствовали друг другу (фактически компилятор Borland Pascal не проверяет, является ли объявленный общедоступным идентификатор именем ассемблерной процедуры).

Особенности использования директив PUBLIC и EXTRN

Borland Pascal может получать доступ только к тем идентификаторам, которые объявлены общедоступными директивой PUBLIC, причем, как уже говорилось, это могут быть не только имена процедур. Таким образом, с точки зрения Borland Pascal, следующие объявления эквивалентны:

AsmLabel PROC FAR

PUBLIC AsmLabel

и

AsmLabel:

PUBLIC AsmLabel

Кроме того, Borland Pascal не распознает идентификаторов, объявленных как PUBLIC, если они находятся в сегменте данных, поэтому все конструкции, которые должны будут взаимодействовать с Pascal-процедурами, необходимо объявлять в кодовом сегменте.

Применение директивы EXTRN позволяет Turbo Assembler получать доступ к процедурам, функциям, переменным и типизированным константам Borland Pascal, которые объявлены на самом верхнем уровне модуля. Эти правила не распространя­ются только на метки и обычные константы Borland Pascal, к которым ассемблерные программы не могут обращаться.

Соглашения о передаче параметров, принятые в Borland Pascal

Borland Pascal выполняет передачу параметров, используя стек процессора или сопроцессора (для параметров типа Single, Double, Extended или Comp). Параметры всегда вычисляются и помещаются в стек в том порядке, в котором они использо­ваны в объявлении процедуры, т. е. слева направо.

Параметры-значения — это параметры, значение которых нельзя изменить под­программой, в которую они передаются. Так же, как и другие компиляторы, Borland Pascal не всегда передает эти параметры в подпрограмму только с использованием стека процессора, конкретный метод определяется типом этих параметров.

Параметры-значения скалярного типа (Boolean, Char, ShortInt, Byte, Integer, Word, LongInt и перечислимые типы) передаются в процедуру путем помещения их значения в стек. Если при этом размер параметра составляет 1 байт, то он помеща­ется в стек в виде целого слова, но информация, содержащаяся в старшем байте этого слова, не может быть использована. Параметры размером в 2 и 4 байта поме­щаются в стек в виде слова и двойного слова соответственно. По соглашениям, при­нятым для процессора 8086, первым в стек помещается старшее слово четырехбай­тных параметров.

Параметры-значения вещественного типа Real помещаются в стек в виде шести­байтного значения. Для всех остальных типов (Single, Double, Extended и Comp) выполняются стандартные для компиляторов Borland соглашения — они передаются в стек процессора вместе с другими параметрами командами вызова сопроцессора.

Параметры-значения указателей всех типов помещаются в стек в виде дальних указателей — сначала слово, содержащее сегмент, затем слово, содержащее смеще­ние. Таким образом, по соглашениям Intel, значение сегмента занимает старший ад­рес. Для загрузки значения указателя ассемблерные программы могут использовать команды LDS или LES.

Строковые параметры, независимо от их размера, никогда не помещаются не­посредственно в стек. Вместо этого Borland Pascal помещает в стек указатель на строку, имеющий тип FAR. Вызываемая подпрограмма не должна изменять значение строки, описываемое данным указателем, и при необходимости может работать с ко­пией этой строки.

Записи, массивы и объекты, имеющие размер 1, 2 или 4 байта, передаются не­посредственно через стек. Для всех других размеров (включая 3 байта) в стек поме­щается указатель, причем для модификации этих параметров в таком случае необхо­димо использовать их локальную копию.

Множества так же, как и строки, никогда не передаются через стек непосред­ственно, а только с помощью указателя, который адресует 32-байтовое представление множества. Первый бит младшего байта в таком представлении всегда соответствует базовому элементу множества с порядковым значением 0.

Параметры-переменные всегда передаются в процедуры одним и тем же спосо­бом — через указатель дальнего типа на их содержимое.

Работа со стеком

По соглашениям, принятым в Borland Pascal, вызываемая процедура должна пе­ред возвратом управления выполнить очистку стека от переданных в нее параметров.

Для этого можно использовать два способа. Первый из них заключается в при­менении команды RET n, где n — это размер переданных параметров в байтах. Вто­рым способом является сохранение адреса возврата в регистре или в памяти и после­довательное извлечение всех параметров процедуры из стека с помощью команды POP. Применение команд POP позволяет выполнить оптимизацию программы по скорости, а также уменьшает размер подпрограммы, поскольку каждая из них зани­мает всего один байт.

Осуществление доступа к параметрам из ассемблерных процедур

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

Для доступа к этим параметрам Turbo Assembler поддерживает три метода:

- с использованием регистра ВР;

- с использованием другого базового или индексного регистра;

- с помощью команд POP.

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

Представление результата функции в Borland Pascal

Форма представления результата функции в Borland Pascal зависит от типа данных, которые она возвращает.

Результат функций скалярных типов возвращается в регистрах процессора. Если размер результата составляет 1 байт, то значение возвращается в регистре AL, 2 байта — в регистре АХ, 4 байта — в регистрах DX:АХ (старшее слово — в DX, младшее — АХ).

Результат функций типа REAL возвращается в трех регистрах процессора. При этом старшее слово помещается в DX, среднее — в ВХ, младшее — в АХ.

Функции, возвращающие данные всех остальных вещественных типов, помещают их в регистр ST(0) (ST) сопроцессора.

Данные строкового типа возвращаются функциями в виде дальнего указателя на временную область, в которой они находятся. Этот указатель помещается в стек пе­ред вызовом функции автоматически и не входит в список параметров функции. После вызова функции этот указатель не должен удаляться в явном виде из стека – эту работу выполняет Borland Pascal.

Функции, возвращающие результат в виде указателя, всегда помещают его в ре­гистры DX:АХ (сегмент:смещение).

Распределение памяти под локальные данные

Подпрограммы на ассемблере, вызываемые из Borland Pascal, могут выделять память для собственных переменных – как постоянных, которые используются в нескольких подпрограммах, так и временных, которые создаются только в какой-то одной процедуре.

Распределение памяти для постоянных локальных переменных

Borland Pascal позволяет ассемблерным программам резервировать пространство для данных в сегменте глобальных данных (DATA или DSEG). В тексте ассемблер­ной программы такое выделение данных происходит с помощью директив типа DB, DW и т. п.).

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

Распределение памяти для временных локальных переменных

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

Ниже приведен пример использования ассемблерных процедур в Pascal-программах. Процедура Exchange выполняет обмен двух переменных, каждая из которых имеет тип Word.

Программа на Ассемблере, содержащая процедуру Exchange:

CODE SEGMENT

ASSUME CS:CODE, DS:NOTHING

var1 EQU DWORD PTR SS:[BP+6]

var2 EQU DWORD PTR SS:[BP+10]

Exchange PROC FAR

PUBLIC Exchange

MOV DX,DS

PUSH BP

MOV BP,SP

;Получение адресов

LDS SI,var1

LES DI,var2

;Обмен переменных

MOV AX,DS:[SI]

MOV BX,ES:[DI]

MOV DS:[SI],BX

MOV ES:[DI],AX

MOV DS,DX

POP BP

RET 8

Exchange ENDP

CODE ENDS

END 

Pascal-программа, использующая процедуру Exchange из модуля Exchange.obj имеет вид:

Var a,b:word;

procedure Exchange (Var a,b:word);far;external;

{$L Exchange.obj}

Begin

a:=10;

b:=20;

WriteLn('До обмена: a=',a,' b=',b);

Exchange(a,b);

WriteLn('После обмена: a=',a,' b=',b);

End.
 

 

[Главная] [Документация] [Теория] [Практика] [Рассылки] [Книги] [Исходники] [Reversing] [Друзья] [Архив] [Диски] [Новости]

Посещаемость сайта такова:
На деревню дедушке

Hosted by uCoz

Rambler's Top100
РЕГИСТРАТУРА.РУ: бесплатная автоматическая регистрация в каталогах ссылок и поисковых машинах, проведение рекламных кампаний в Интернете, привлечение на сайт целевых посетителей.

Hosted by uCoz