Встроенный ассемблер


Ассемблером называется машинно-зависимый компилятор, преобразующий специальным образом составленные текстовые строки в машинные инструкции. Как и любой другой компилятор, ассемблер упрощает разработку программ за счет того, что предоставляет пользователю доступ к кодам машинных инструкций и операндам с помощью символьных имен.
В этой главе рассматриваются приемы программирования с помощью ассемблера, встроенного в компилятор Турбо Паскаля. Встроенный ассемблер имеется в версиях 6.0 и 7.0 Турбо Паскаля и в руках опытного программиста представляет собой мощный инструмент, позволяющий «выжать» из ПК все возможное.
 
Общее описание МП 8086/8088
Встроенный ассемблер (далее - просто ассемблер) дает возможность программировать на уровне отдельных машинных инструкций. Это - главное отличие ассемблера от Паскаля и в этом отличии сосредоточены все его достоинства и недостатки. Достоинство заключается в том, что, программируя на ассемблере, программист обычно выбирает последовательность машинных инструкций так, чтобы реализовать нужные вычисления с максимальной скоростью при минимальных затратах памяти, в то время как даже такой весьма совершенный компилятор, как компилятор Турбо Паскаля, неизбежно вносит в машинный код некоторую избыточность, уменьшающую скорость счета и увеличивающую затраты памяти. С другой стороны, программирование на уровне машинных инструкций - чрезвычайно хлопотное занятие и не может сравниться по скорости разработки программ с программированием на Паскале - в этом заключается главный недостаток ассемблера.
Чтобы использовать средства ассемблера, необходимо ясно представлять себе детали архитектуры микропроцессоров Intel 80x86. К этому семейству относятся микропроцессоры:
8086 - 16-разрядный микропроцессор, используемый в ПК IBM PC/XT;
8088 - аналог 8086, отличается от него только взаимодействием с памятью: 8086 может обмениваться с памятью как байтами, так и 16-разрядными словами, в то время как 8088 - только байтами;
80286 - улучшенный вариант 8086, используемый в ПК IBM AT; может работать в двух режимах: в реальном режиме, полностью эмулирующем работу МП 8086, и в защищенном режиме, в котором способен адресовать память до 16 Мбайт (в реальном - до 1 Мбайт);
80386 - 32-разрядный вариант 80286; способен адресовать до 4 Гбайт;
80486 - комбинация 80386/80387, т.е. имеет внутреннюю подсистему реализации операций с плавающей точкой;
80586 (Pentium) - имеет ряд усовершенствований, обеспечивающих ему увеличение производительности в 2...3 раза по сравнению с 80486, в том числе возможность обрабатывать 64-разрядные числа.
Микропроцессоры этого семейства наращивают свои возможности в перечисленном порядке, но строго совместимы от младших моделей к старшим: все, что может 8086/8088, реализует и Pentium, но не наоборот. Ниже обсуждается архитектура (внутреннее устройство, способы адресации и система команд) МП 8086/8088.
 
Регистры
В МП 8086/8088 имеется 14 регистров. В функциональном отношении они делятся на группы:

  • регистры общего назначения (АХ, ВХ, СХ, DX); предназначены для хранения операндов и выполнения основных команд; любой из них может использоваться как совокупность двух независящих друг от друга 8-разрядных регистров: старшего байта регистра (АН, ВН, СН, DH) и младшего байта (AL, BL, CL, DL); например, АХ состоит из АН и AL;
  • сегментные регистры (CS, DS, SS, ES); используются для указания сегмента при адресации памяти;
  • регистры-указатели (SP, BP, IP); используются для указания смещения при адресации памяти;
  • индексные регистры (SI, DI); применяются для индексной адресации;
  • регистр флагов; используется для хранения признаков состояния процессора.

Внутри одной и той же функциональной группы регистры используются различным образом. Ниже описывается специфика использования регистров.
Регистр АХ.
Является основным сумматором. Используется во всех арифметических операциях (сложить, умножить и т.п.). Только с помощью АХ и его полурегистров AHIAL возможен обмен данными с портами ввода/вывода.
Регистр ВХ.
Используется как сумматор в арифметических операциях, а также как базовый регистр при индексной адресации.
Регистр СХ.
В основном используется как счетчик при выполнении операций повторения и сдвига. Может также участвовать в арифметических операциях.
Регистр DX.
Используется как регистр данных в операциях ввода/вывода, а также как сумматор при обработке длинных целых чисел (32-разрядных).
Регистр CS.
Содержит номер сегмента памяти (сегмента кода), в котором располагается текущая машинная инструкция. Для получения полного адреса следующей команды его содержимое сдвигается влево на 4 разряда и складывается с регистром-указателем IP. Содержимое CS автоматически изменяется в командах дальнего (межсегментного) перехода и вызова процедур.
Регистр IP.
Определяет смещение относительно начала сегмента кода CS очередной исполняемой машинной инструкции. Содержимое IP автоматически изменяется в ходе исполнения инструкции, обеспечивая правильный порядок выборки команд из памяти.
Регистр DS.
Содержит номер сегмента памяти (сегмента данных), в котором располагаются данные (константы и переменные). Все глобальные переменные и типизированные константы программы Турбо Паскаля всегда располагаются в единственном сегменте, адресуемом этим регистром.
Регистр SS.
Содержит номер сегмента стека. Стек - это участок автоадресуемой памяти, предназначенный для временного хранения операндов. С помощью стека ТурбоПаскаль организует обмен данными между программой и процедурами, кроме того, в нем он размещает все локальные переменные (т.е. переменные, объявленные внутри процедуры). Память стека используется по правилу «последним пришел - первым ушел»: самый последний помещенный в стек операнд будет первым извлекаться из него.
Регистр SP.
Указывает на вершину стека, т.е. совместно с регистром 55 адресует ячейку памяти, куда будет помещаться операнд или откуда он будет извлекаться. Содержимое этого регистра автоматически уменьшается после размещения в стеке очередного операнда и увеличивается после извлечения операнда из стека.
Регистр ВР.
Так называемый указатель базы. Облегчает создание и использование локального стека (т.е. стека для использования внутри процедуры).
Регистр ES.
Дополнительный сегментный регистр ES используется для межсегментного обмена данными и в некоторых строковых операциях.
Регистр SI.
Определяет адрес источника информации при индексной адресации данных (например, при обработке массивов). Обычно используется в паре с регистром DS.
Регистр DI.
В паре с регистром £5 определяет приемник информации при межсегментном обмене данными.
Регистр флагов.
Отдельные разряды (биты) этого регистра имеют следующее назначение.
Флаг переноса CF.
Содержит 1, если произошел перенос единицы при сложении или заем единицы при вычитании. Используется также в циклических операциях и операциях сравнения.
Флаг четности PF.
Содержит 1, если в результате операции получено число с четным количеством значащих разрядов, т.е. дополняет результат до нечета - используется в операциях обмена для контроля данных.
Флаг внешнего переноса AF.
Контролирует перенос из 3-го бита данных. Полезен при операциях над упакованными десятичными числами.
Флаг нуля ZF.
Равен 1, если в результате операции получен ноль, и равен 0 в противном случае.
Флаг знака SF.
Равен 1, если в результате операции получено отрицательное число (с единицей в старшем разряде).
Флаг трассировки TF.
Равен 1, если программа исполняется по шагам, с передачей управления после каждой выполненной команды по прерыванию с вектором 1.
Флаг прерываний IF.
Содержит 1, если микропроцессору разрешена обработка прерываний.
Флаг направления DF.
Управляет направлением передачи данных: если он содержит 0, то после каждой индексной операции содержимое индексных регистров увеличивается на 1, в противном случае - уменьшается на 1.
Флаг переполнения OF.
Устанавливается в единицу, если в результате операции получено число, выходящее за разрядную сетку микропроцессора.
 
Адресация
В архитектуре МП 8086/8088 адрес любого байта задается двумя 16-битовыми словами - сегментом и смещением. При формировании 20-разрядного полного адреса, необходимого для адресации в пределах 1 Мбайт, сегмент сдвигается влево на 4 разряда (умножается на 16) и складывается со смещением. Поскольку емкость 16-разрядного смещения составляет 65536 значений, в пределах одного сегмента можно адресовать до 64 Кбайт.
Архитектура МП позволяет использовать семь различных способов адресации.
Регистровая 
Извлекает операнд из регистра или помещает его в регистр. Примеры:
mov ах,bх {Извлекаем из ВХ и помещаем в АХ} 
add cx,ax {Содержимое АХ прибавляем к СХ}
push ex {Заталкиваем в стек содержимое СХ}
Непосредственная
Операнд (8- или 16-разрядная константа) содержится непосредственно в теле команды. Примеры:
mov ax,100 {Загружаем в АХ значение 100}
add ax,5 {К содержимому АХ прибавляем 5} 
mov cx,$FFFF {Помещаем в СХ значение 65535}
Прямая
Смещение операнда задается в теле программы и складывается с регистром DS; например:
var
X: Word; В: Byte;
.......
mov ах,Х {Пересылаем значение переменной X регистр АХ} 
add ah,В {К содержимому регистра АН прибавляем значение переменной В}
mov X,ax {Пересылаем содержимое регистра АХ, в область памяти переменной X}
Косвенная регистровая
Исполнительный адрес операнда (точнее, его смещение) содержится в одном из регистров ВХ, ВР, SI или DI. Для указания косвенной адресации этот регистр должен заключаться в квадратные скобки, например:
mov ax,[bx] {Содержимое 16-разрядного слова, хранящегося в памяти по адресу DS:BX,пересылаем в регистр АХ};
Каждый из регистров BX...DI по умолчанию работает со своим сегментным регистром:
DS:BX, SS:BP, DS:SI, ES:DI
Допускается явное указание сегментного регистра, если он отличается от умалчиваемого, например:
mov ax,es:[bx]
Адресация по базе 
Базовый регистр ВХ (или ВР) содержит базу (адрес начала некоторого фрагмента памяти), относительно которой ассемблер вычисляет смещение, например:
mov ах,[Ьх]+10 {Загружаем в АХ 10-й по счету байт от начала базы памяти по адресу DS-.BX};
Индексная адресация
Один из индексных регистров SI или DI указывает положение элемента относительно начала некоторой области памяти. Пусть, например, АОВ - имя массива значений типа Byte. Тогда можно использовать такие фрагменты:
mov si,15 {Помещаем в SI константу 15}
mov ah,АОВ[si] {Пересылаем в АН 16-й по порядку байт от начала массива}
mov si,0
mov AOB[si],ah {Пересылаем полученное в самый первый элемент массива}
Адресация по базе с индексированием
Вариант индексной адресации для случая, когда индексируемая область памяти задается своей базой. Например:
mov ax,[bx][si]
Этот тип адресации удобен при обработке двумерных массивов. Если, например, АОВ есть массив из 10x10 байт вида
var
АОВ: array [0..9,0..9] of Byte;
то для доступа к элементу АОВ [2,3] можно использовать такой фрагмент
mov bx,20 {База строки 2}
mov si,2 {Номер 3-го элемента}
mov ax,AOB[bx] [si] {Доступ к элементу}
 
Система команд
В приводимых ниже таблицах указывается мнемоника всех допустимых инструкций для МП 8086/8088. Для удобства пользования все команды разбиты на 6 функциональных групп - пересылки данных, арифметические, битовые, строковые, передачи управления, прерываний. Внутри каждой группы команды объединяются в подгруппы по общим дополнительным признакам.
Детальный анализ всех команд МП 8086/8088 занял бы слишком много места, поэтому в идущих за таблицами пояснениях рассматриваются лишь наиболее популярные команды. Исчерпывающее описание всех команд Вы найдете в [1], [20].
Команды пересылки данных

Одна из наиболее часто используемых команд - МОV позволяет в защищенном режиме переслать байт или слово из регистра в регистр, из памяти в регистр или из регистра в память. Тип пересылаемых данных (байт или слово) определяется регистром, участвующим в пересылке. Ниже приводятся примеры использования команды:
mov ах,Table {Пересылка слова из памяти в АХ} 
mov Table,ah {Пересылка байта из АН в память}
mov ds,ax {Пересылка в сегмент данных}
mov es:[bx],ax {Пересылка слова в память: базовая адресация с заменой сегмента}
mov ch,-17 {Переслать константу в регистр} 
mov Table,$FF {Переслать константу в память}
С помощью MOV нельзя пересылать: 

  • из памяти в память, например, вместо

mov Mem1,Mem2
следует использовать
mov ax,Mem2 
mov Mem1,ax

  • константу или переменную в DS, например, нельзя

mov DS,Data_Seg
нужно:
mov ax,Data_Seg 
mov ds,ax

  • один сегментный регистр в другой, например, нельзя

mov es, ds 
но можно
mov ax,ds 
mov es,ax

  • в регистр CS; значение этого регистра (сегмента кода) автоматически меняется при выполнении дальних команд CALL и JMP; кроме того, он загружается из стека при выполнении команды RETF (выход из дальней процедуры).

Для временного сохранения регистров и данных, а также для обмена значениями между регистрами широко используются стековые команды PUSH и POP. Каждая из них работает со словом, т.е. в стек нельзя поместить или извлечь из него одиночный байт. При выполнении PUSH вначале уменьшается на 2 содержимое указателя SP, а затем операнд помещается по адресу SS: SP. При извлечении из стека сначала читается память по адресу SS: SP, а затем SP увеличивается на 2. Таким образом, при заполнении указатель вершины стека SP смещается к младшим адресам, а при освобождении -к старшим. При работе со стеком следует помнить о специфике использования стековой памяти («последним пришел - первым ушел»), а также о том, что эта память интенсивно используется при вызове процедур, т.е. состояние стека к моменту выхода из процедуры должно быть строго согласовано с дальнейшей работой программы. Первое условие определяет порядок извлечения данных из стека - он должен быть обратным порядку, в котором эти данные помещались в стек. Второе условие фактически означает, что после выхода из процедуры указатель SP должен содержать то же смещение, что и к моменту входа в нее. Иными словами, процедура не должна «забыть» в стеке лишнее слово или взять из него больше нужного.
Команда загрузки адреса LEA загружает в регистр адрес (смещение) нужного участка памяти. Этого же можно достичь с помощью зарезервированного слова OFFSET, стоящего перед именем переменной. Например:
var
X: Word;
..........
asm
mov ax, OFFSET X {Загружаем смещение X в АХ} 
lea ax,X {To же действие} 
end ;
Разница состоит в том, что в случае команды LEA разрешается использовать индексную адресацию, что особенно удобно при пересылке массивов данных.
Две другие команды адресной загрузки - LDS и LES загружают первое 16-разрядное слово из источника в регистр-приемник, а затем следующее слово - в регистр DS или ES, т.е. они рассчитаны на загрузку полного адреса операнда (сегмента и смещения).
Арифметические команды

При использовании арифметических команд следует помнить о том, что МП может обрабатывать знаковые числа, числа без знака, а также двоично-десятичные числа. В беззнаковых числах для представления значения используются все биты. т.е. они эквивалентны типам Byte и Word, в то время как знаковые числа в старшем разряде хранят знак числа и эквивалентны типам Shortlnt и Integer. Двоично-десятичные числа используют по 4 бита для каждого десятичного разряда и могут быть упакованными или неупакованными. В первом случае один байт хранит 2 десятичные цифры (старшая - в старшем полубайте), во втором - только одну (старший полубайт не используется). Основные арифметические команды МП (ADD, SUB, MUL, DIV) не учитывают двоично-десятичную форму представления чисел, поэтому в архитектуру МП включены команды коррекции результата.
Битовые команды

Битовые команды используются при исчислении логических выражений, а также в тех случаях, когда необходимо изменить отдельные разряды операнда. Логические команды AND, OR, XOR и NOT эквивалентны соответствующим операциям Турбо Паскаля в случае, когда операндами являются целочисленные выражения. Команда TEST выполняет целочисленную операцию поразрядного суммирования AND, но не изменяет значения операндов, а лишь устанавливает флаги в соответствии со значением результата сравнения: обнуляет CF и OF, изменяет PF, ZF, SF и не меняетAF (флаг ZF установится в 1 в том случае, когда оба операнда содержат по единице хотя бы в одном соответствующем разряде). Команды сдвига SHL/SHR эквивалентны одноименным операциям Турбо Паскаля и отличаются от команд циклического сдвига ROLIROR тем, что вытесненные в ходе их выполнения значащие разряды теряются, в то время как при циклическом сдвиге эти разряды появляются «с другой стороны». Например, если выполнить фрагмент
mov al,1 {Загружаем в AL единицу}
shr al,1 {Сдвигаем вправо ,на 1 разряд}
регистр AL будет содержать 0 (вытесненная вправо единица будет помещена в CF), в то время как после замены команды SHR на ROR в нем будет значение $80=128 (вытесненная единица будет помещена в старший бит регистра).
Заметим, что счетчиком в командах сдвига может быть цифра 1 или количество сдвигов, указываемое в регистре CL.

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

Команды безусловных переходов CALL, RET, JMP могут использовать дальнюю или ближнюю модель памяти, в то время как команды условных переходов - только малую (в пределах -128...+127 байтов). При дальней модели памяти (устанавливается опцией Options/Compiler/Force far calls среды Турбо Паскаля или директивой компилятора {F+}) осуществляется как внутрисегментная, так и межсегментная передача управления, при ближней - только внутрисегментная.
Инструкция CALL работает следующим образом. Вначале адрес следующей за CALL инструкции (адрес возврата) помещается в стек, затем в регистр IP (или в пару CS:IP) помещается адрес точки входа в процедуру, таким образом сразу за командой CALL будет исполняться уже первая команда процедуры. Оба адреса (точки входа и возврата) будут 16-битовыми смещениями для внутрисегментного вызова или 32-битовыми полными адресами - для межсегментного. Все процедуры (функции) Паскаля, оттранслированные в режиме {F+} или содержащие зарезервированное слово FAR в заголовке, должны вызываться как дальние. Для этого за инструкцией CALL следует указать модель памяти:
Procedure MyProc; Far;
.......
asm
call FAR MyProc {Вызов дальней процедуры}
.......
end;
Таким же способом должны вызываться все библиотечные подпрограммы (т.е. объявленные в интерфейсных частях модулей). При дальнем вызове в стек сначала заталкивается содержимое сегмента кода CS, а уже затем - смещение возврата.
При выходе из дальней процедуры команда RET извлекает из стека оба 16-разрядных слова и помещает первое в IP, а второе в CS, а при выходе из ближней извлекает из стека только смещение и помещает его в IP.
Команды условных переходов способны передавать управление на метку, расположенную в пределах ближайших плюс-минус 128 байт от самой команды. Если нужно передать управление на метку, расположенную дальше в том же сегменте, или на метку в другом сегменте, сразу за командой условной передачи располагают безусловную команду JMP или CAL, например:
стр ах,0 {Проверяем АХ} 
jne@NotZero {AX=0 ?} 
jmp IsZero {Да - переходим на дальнюю метку}
....... {Нет - продолжаем работу}
.......
В таблице термин «выше/ниже» используется применительно к сравнению беззнаковых операндов, а «больше/меньше» - знаковых.
Поскольку условные переходы реализуют ветвление программы на основе проверки флагов, обычно непосредственно перед ними располагаются команды, изменяющие эти флаги, чаще всего - команда сравнения СМР. Ниже показаны комбинации СМР - условный_переход для различных соотношений приемника и источника (первого и второго операнда) команды СМР:

Например:
сmр ах,5 {АХ>5 ?} 
ja @AboveS {Да, больше - переходим} 
стр bх,- 3 {ВХ<=-3 ?} 
jle @LessM3 {Да, меньше или равен}
Команды LOOP/LOOPE/LOOPNE служат для организации циклов. Все они используют содержимое регистра СХ как счетчик числа повторений. Команда LOOP уменьшает СХ на единицу и передает управление на метку начала цикла, если содержимое этого регистра отлично от нуля. Команды LOOPE/LOOPNE также уменьшают счетчик СХ, но передают управление в начало цикла при совместном условии установки (или сброса) флага ZF и неравенства нулю счетчика СХ.
Вот как, например, можно отыскать нулевой байт в массиве АОВ:
var
АОВ: array [1..1000] of Byte;
.......
asm
mov ex, It)00 {Инициируем счетчик СХ} 
lea bx,AOB {Помещаем адрес АОВ в ВХ} 
dec bx {Готовим цикл} 
{Здесь начало цикла проверки}
@@Test: inc bx {Адрес очередного байта}
cmp BYTE PTR [bx],0 {Проверяем байт} 
loopne ©Test {Замыкаем цикл} 
jnz ©NotZero {Если не найден нулевой байт}
....... {Нашли нулевой байт}
end;
Строковые команды

Строковые команды рассчитаны на обработку строк. Замечу, что термин «строка» здесь отнюдь не эквивалентен аналогичному термину Турбо Паскаля и означает произвольную цепочку байт или слов длиной до 64 Кбайт. Эти команды оперируют пятью примитивами, каждый из которых обрабатывает лишь один байт или одно слово за раз. Перед примитивом обычно указывается префикс повторения REP/REPE/REPNE, заставляющий выполняться примитив до тех пор, пока не обнулится счетчик повторений СХ или не будет нарушено соответствующее условие.
При использовании строковых команд важно помнить два обстоятельства. Во-первых, эти команды всегда берут адрес строки-источника из пары DS:SI, а строки-приемника - из пары ES:DI. Таким образом, перед исполнением строковой команды необходимо инициировать сегментные регистры нужным образом. Во-вторых, строковые команды используют индексную адресацию с автоматическим изменением смещения в SI/DI после однократного исполнения примитива. Содержимое этих регистров изменяется на 1 при обработке байтов и на 2 при обработке слов, причем наращивается, если флаг направления DF сброшен, и уменьшается, если он равен 1.
Вот как можно осуществить пересылку массива А в массив В:
var
А,В: array [1..250] of Integer;
.......
asm
lea si, A {Смещение А - в SI (источник)'} 
push ds pop es {Инициируем ES := DS} 
lea di,B {Смещение В - в DI (приемник)} 
mov ex,250 {Счетчик- переноса} 
сld {Направление переноса - наращивать}
rep movsw {Переносим 500 байт}
end;
В программе на Турбо Паскале регистр DS всегда содержит сегмент данных, поэтому инициировать его необязательно. Что касается регистра дополнительного сегмента ES, такого правила нет, и хотя в большинстве случаев он также ссылается на сегмент данных, рекомендуется проводить его инициацию перед использованием строковой команды (см. выше команды push ds, popes).
Команды прерываний

Выполнение прерываний во многом напоминает косвенный вызов дальней процедуры. По команде INT (INTO) в стек помещается регистр флагов, сегмент CS и указатель IP, а новые значения этих регистров берутся из 4-байтного вектора прерывания, соответствующего номеру прерывания в команде INT, или из вектора 4 -для команды INTO. Таким образом, единственным отличием от команды CALL является то, что в стек предварительно заносится регистр флагов. Следует, правда, оговориться: перед передачей управления программе обработки прерывания микропроцессор сбрасывает флаги трассировки TF и прерываний IF; сброс TF необходим для обеспечения нормальной работы отладчиков, использующих прерывание по вектору 1 или 4, сброс IF блокирует вмешательство других процессов в ход обработки прерывания.
Команда INTO представляет собой условное прерывание и выполняется, если в этот момент взведен флаг переполнения OF. Команда IRET реализует правильный выход из программы обработки прерывания: она считывает из стека 3 двухбайтные слова и помещает их в регистры IP, CS и регистр флагов.
Команды управления

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

WAIT заставляет МП выполнять холостой режим работы и каждые 5 тактов проверять уровень сигнала на входной шине: пока на этой шине нет сигнала

активности, процессор выполняет WAIT, но как только шина активизируется, он продолжит исполнение программы. Эта инструкция обычно используется для ожидания сигнала обслуживания (прерывания) высокоприоритетного устройства типа контроллера прямого доступа к памяти.
Команда ESC используется для передачи указанного в ней операнда на шину данных. Тем самым обеспечивается возможность передачи команд другим процессорам. Эта команда чаще всего используется для управления работой арифметического сопроцессора. В этом случае код представляет собой код команды сопроцессора, а источник - используемый в этой команде операнд.
Команда LOCK фактически представляет собой однобайтовый префикс, который можно использовать совместно с любой другой командой микропроцессора. По этой команде МП активизирует одноименный сигнал на своей шине, что исключает возможность использования этой шины любым другим внешним устройством (процессором).
 
Спецификация встроенного ассемблера
Приведенное выше общее описание архитектуры МП 8086/8088 является базовым для любого ассемблера, в том числе и для встроенного ассемблера Турбо Паскаля. Однако ассемблеры содержат массу дополнительных возможностей, облегчающих разработку готовых к работе программ. Эти возможности отражаются в директивах и макрокомандах ассемблера. Встроенный ассемблер не предназначен для написания законченных программ, поэтому в нем отсутствуют макрокоманды и директивы. Главной особенностью встроенного ассемблера является практически полное отсутствие в нем средств описания переменных и данных, т.к. эти объекты описываются средствами Турбо Паскаля.
 
Оператор ASM
Зарезервированное слово ASM открывает доступ к средствам встроенного ассемблера. Этот оператор может располагаться только внутри исполняемой части программы (подпрограммы). Область действия оператора ASM ограничивается ближайшим по тексту зарезервированным словом END. Таким образом, структура любого ассемблерного оператора такова:
asm
<Одна или несколько команд встроенного ассемблера> 
end;
С точки зрения Турбо Паскаля пара asm... end считается операторными скобками, ограничивающими единственный оператор Паскаля, например:
if X>10 then 
asm
.......
end 
else
.......;
for k :=1 to 5 do
asm
.......
end;
Тело ассемблерного оператора asm... end может быть пустым или содержать несколько ассемблерных команд. Каждая ассемблерная команда должна располагаться на отдельной строке или отделяться от следующей за ней команды символом «;». Ниже приводятся два разных способа написания одной и той же последовательности ассемблерных команд:
asm
mov ah,0; int $16; mov ChCode, al; mov ScCode, ah 
end; 
asm
mov ah , 0
int $16
mov ChCode, al
mov ScCode, ah 
end;
В конце строки, содержащей единственную ассемблерную команду, или между двумя командами, располагающимися на одной строке, разрешается вставлять комментарий, который должен оформляться по обычным правилам Турбо Паскаля, т.е. ограничиваться символами «{», «}» или «(*», «*)». Таким образом, комментарии разрешены между ассемблерными командами, но не внутри них. Например, такой оператор будет правильным: 
asm
{Инициируем регистры}
lea si,X; push ds;
pop es; {ES := DS}
lea di,Y; mov ex,100
cld {Перенос - вперед}
rep {Выполняем Y := X} 
movsw
{Здесь нет ошибки - комментарий можно вставлять между префиксом и командой}
end;
а такой - неправильным:
asm
{Готовим регистры}
lea si,X; push ds; 
pop {ES:=DS} es; 
{Ошибка! Комментарий разорвал мнемонику команды и ее операнд} 
lea di,Y; mov ex,100 {и направление} eld 
{Комментарий является разделителем команд, поэтому перед ним можно не ставить ";"}.
 rep movsw 
end;
В пределах ассемблерного оператора допускаются любые команды, но Турбо Паскаль требует выполнения следующего соглашения:
В начале ассемблерного оператора регистр DS содержит сегмент кода, SS - сегмент стека, ВР - текущий стек, SP указывает на вершину стека. Все эти регистры должны иметь точно такие же значения к моменту завершения работы ассемблерного оператора.
Программист не должен делать каких-либо предположений о содержимом остальных регистров, и эти регистры могут иметь произвольное значение после завершения работы ассемблерного оператора. Исключением является случай ассемблерной функции, которая должна использовать некоторые регистры для возврата своего значения (см. п. 12.2.3).
 
Синтаксис ассемблерных команд
Здесь и далее ассемблерными командами называются команды на языке встроенного ассемблера, вставляемые в тело ассемблерного оператора asm... end. Структура ассемблерной команды такова:
[Метка] [Префикс] [Код [Операнд [,Операнд]]] 
В квадратных скобках указываются необязательные элементы структуры.
Метки
Любой команде ассемблерного оператора может предшествовать одна или несколько меток. В ассемблере используется два типа меток: глобальные и локальные. Глобальные метки - это обычные метки Турбо Паскаля. Они объявляются в разделе описаний после зарезервированного слова Label. С помощью глобальной метки можно передать управление в тело ассемблерного оператора оператором GOTO. Например:
Label
AltEnt; 
begin 
Goto AltEnd; {Передаем управление внутрь ассемблерного опера тора}
.......
asm
.......
AltEnd: {Сюда можно передать управление извне}
.......
end;
Локальные метки объявляются непосредственно в теле ассемблерного оператора. Эти метки обязаны начинаться символом «@». Поскольку этот символ нельзя использовать в именах Турбо Паскаля, он позволяет отличить локальную метку от глобальной. Локальная метка не известна нигде вне ассемблерного оператора, поэтому на нее нельзя передать управление оператором GOTO. По этой же причине в разных ассемблерных операторах можно использовать одноименные локальные метки.
Префиксы 
Встроенный ассемблер поддерживает следующие префиксы команд:
LOCK             Захват шины
REP/REPE/REPNE    Повтор строковой команды
REPZ/REPNZ       Синоним REPE/REPNE
SEGCS            Перекрытие CS
SEGDS            Перекрытие DS
SEGSS            Перекрытие SS
SEGES            Перекрытие ES
Префиксы LOCK/REP/REPE/REPNE описаны в п. 12.1.3. Префиксы SEGxx определяют сегментный регистр, который должен использоваться вместо умалчиваемого, и распространяются только на следующие за ними ассемблерные команды.
Если префикс указан без кода инструкции, он распространяет свое действие на следующую ассемблерную команду.
Код инструкции очень редко имеет более одного префикса и никогда - более трех: допускается следующая последовательность
LOCK SEGxx REPxx
Замечу, что если при обработке строковой команды произошло аппаратное прерывание, МП 8086/8088 «забывает» префиксы LOCK и SEGxx, которые, возможно, определены в той же команде, так что использовать сложные префиксные конструкции не рекомендуется.
Коды инструкций
Встроенный ассемблер поддерживает мнемонику всех команд, перечисленных в п.12.1.3. Кроме того, в ассемблерных командах может использоваться мнемоника инструкций процессора 8087, а также команды процессоров 80286/80287. Замечу, что инструкции 8087 допустимы только при активном состоянии {SN+}, 80286 - при {$G+}, a 80287 - в случае {$G+,N+}.
Операнды
Операндами встроенного ассемблера могут быть выражения, состоящие из комбинации регистров, констант, имен и символов операций.
Регистры
Во встроенном ассемблере используется мнемоника регистров, указанная в п. 12.1.1, а также имя ST для ссылки на регистры арифметического сопроцессора.
Константы
Ассемблер поддерживает строковые и числовые константы.
Строковые константы заключаются в апострофы или кавычки. Если константа объявлена с помощью кавычек, внутри нее символ апостроф рассматривается наравне с другими символами, т.е. не считается ограничителем константы, точно так же внутри константы, обрамленной апострофами, не считается ограничителем символ кавычки. Если внутри константы необходимо указать ограничивающий ее символ, он удваивается. Примеры:
'Строковая константа'
"Это - тоже строковая константа"
'Символ '' не считается ограничителем'
'внутри строки, обрамленной кавычками "..."'
Числовые константы могут быть только целыми и их значение не может превосходить емкости двойного слова, т.е. должно быть внутри диапазона
- 2 147 483 648...+ 4 294 967 295.
По умолчанию при записи числовых констант используется десятичная нотация, но ассемблер поддерживает также двоичные, восьмеричные и шестнадцатеричные константы. Двоичная константа составляется как комбинация единиц и нулей, заканчивающаяся символом В (от Binary - двоичный); при записи восьмеричной константы используются символы 0...7, а в ее конце ставится символ О (Octal - восьмеричный); шестнадцатеричная константа записывается по правилам Турбо Паскаля (начинается с символа #) либо по правилам Турбо Ассемблера: начинается с цифры, в конце ставится символ H (от Hexadecimal - шестнадцатеричный).
Имена
Локальные метки - это единственные имена, которые разрешается определять внутри ассемблерного оператора. Имена остальных объектов программы - констант, переменных, подпрограмм - должны определяться только с помощью средств Турбо Паскаля.
Область определения имен подчиняется тем же правилам, что и в Турбо Паскале -имена должны быть «видны» в том месте, где они используются, и они локализуются в пределах блока, в котором описаны.
Во встроенном ассемблере могут использоваться три предопределенных имени:
@@Code - текущий сегмент кода
@Data - начальный сегмент данных
@Result - ссылка внутри функции на ее результат
Имена @Code и @Data могут использоваться только в сочетании с директивой SEG для ссылки на нужный сегмент. Например:
asm
mov ax, SEG ©Data
mov ds,ax 
end;
Имя @Result используется для присвоения результата функции. Например:
Function Min(X,Y: Integer): Integer; 
{Эта функция сравнивает два целых числа и возвращает наименьшее из них) 
begin 
asm
mov ax,X {Помещаем Х в АХ} 
cmp ax,Y {X<Y ?}
jl @ {Да - на выход}
mov ax,Y {Нет - помещаем Y в АХ} 
@: mov ©Result,ax {АХ содержит результат}
end 
end;
Для доступа к полям записей разрешается использование составных имен. Например:
type
Point = record X,Y: Integer 
end; 
Rect = record
A,B: Point 
end; 
var
P: Point; 
R: Rect; 
begin 
asm
mov ax,P.X 
add ax,P.Y
mov R.A.X,ax 
end 
end.
Идентификаторы типов можно применять к операндам для уточнения данных, о которых идет речь. Каждая из следующих команд реализует одно и то же действие: загружает в регистр АХ слово по адресу ES: [DI+4 ]:
mov ax,(Rect PTR es:[di]).В.Х
mov ax,Rect(es:[di]).В.Х
mov ax,esrRect[di].B.X
mov ax,Rect[es:di].B.X
mov ax,es:[di].Rect.B.X
Следующие имена не могут использоваться в операндах встроенного ассемблера:

  • стандартные процедуры и функции (например, WriteLn, Chr);
  • предопределенные массивы Mem, MemW, MemL, Port, PortW;
  • константы с плавающей точкой, строковые и множественного типа;
  • макросы (процедуры и функции, полностью реализуемые одним InLine-оператором);
  • символ
  • @Result вне функции.

Выражения
Встроенный ассемблер использует выражения трех классов: регистровые, ссылки на память и непосредственные.
Регистровое выражение - это выражение, состоящее из имени регистра. Все следующие команды содержат только регистровые выражения:
push ds 
pop es 
mov ah,bl 
add ex,ax
Непосредственные выражения - это нетипизированные константы и имена типов. Примеры непосредственных выражений:
const
dec =10;
.....
asm
mov ax, dec 
mov bx,0 
add cx,2*dec+l 
sub dh,- 5 
end;
Все остальные выражения относятся к ссылкам на память. Например:
const
dec: Word = 10; 
Step =12;
var
Х,Y: Byte;
asm
mov ax, dec
mov ex, [Step]
add ah,X mov Y,bl
mov ax,[bx] 
end;
Важным отличием ассемблерных выражений от выражений Турбо Паскаля является то обстоятельство, что они должны быть статическими, т.е. разрешены (вычислены) на этапе создания программы. Если выражение может быть полностью вычислено к моменту его трансляции, т.е. если оно состоит только из регистровых или непосредственных значений, такое выражение называется абсолютным, компилятор вычисляет его и использует для создания команды.
В ходе компиляции программы вырабатывается так называемый объектный код, который затем преобразуется компоновщиком в готовую к работе программу. При создании объектного кода компилятор не может вычислить значения выражений типа «ссылка на память», так как не знает окончательного положения в памяти меток, переменных, подпрограмм. В результате он создает так называемое перемещаемое выражение, которое затем компоновщиком преобразуется в нужную ссылку на память.
Встроенный ассемблер разрешает любую операцию над абсолютным значением (см. ниже), но ограничивает перемещаемые выражения до сложения или вычитания, одним из операндов которого должна быть константа.
Другое важное отличие ассемблерных выражений от выражений Турбо Паскаля заключается в способе интерпретации переменных. В выражениях Паскаля любая ссылка на переменную интерпретируется как текущее содержимое этой переменной. В ассемблерных выражениях это справедливо только тогда, когда все выражение в целом состоит из имени переменной. Во всех остальных случаях ссылка на переменную интерпретируется как адрес переменной. Например, выражение
х+10
в Паскале означает: «к содержимому переменной X прибавить 10». В ассемблерной команде это означает: .«к адресу (смещению) переменной X прибавить 10». Однако команда
mov ах,X
означает: «поместить в регистр АХ первые два байта переменной X». Если бы нам понадобилось загрузить в АХ адрес переменной X, мы должны были бы написать
mov ax,OFFSET X  
Замечу, что попытка «перехитрить» ассемблер командами типа
mov ax,X+0 mov ax,X+1-1
и т.п. не дает желаемого результата: ассемблер просто загружает в АХ содержимое переменной X.
Как и в Паскале, ассемблерные выражения имеют тип, но в отличие от Паскаля этот тип определяет только размер объекта в памяти и не ограничивает применяемые к нему операции.
Встроенный ассемблер имеет следующие предопределенные типы:

Имена предопределенных типов можно использовать для приведения типов выражений. Например, если определены переменные
var
Flag: Boolean; 
X : Word;
то такие ассемблерные выражения недопустимы:
mov Flag,bx 
mov ah,X
Для корректного задания последней команды можно использовать следующие варианты:
mov ah,BYTE PTR X 
mov ah,Byte(X) 
mov ah,X.Byte
Во всех случаях в АН будет загружен первый (младший) байт переменной X. Встроенный ассемблер поддерживает операции, перечисленные в следующей таблице (в порядке убывания приоритета).
Операции встроенного ассемблера

Операция &
Осуществляет перекрытие идентификатора: следующий за знаком & идентификатор считается определенным в программе, даже если он совпадает с зарезервированным словом. Например:
var
Ch: Byte;
.......
mov ch,0 {Посылаем 0 в регистр СН} 
mov &Ch,0 {Посылаем 0 в переменную Ch}
Операция 0
Круглые скобки используются обычным для Паскаля образом - для изменения порядка исчисления выражения (подвыражение, ограниченное скобками, вычисляется в первую очередь). Если перед скобками стоит имя типа, все выражение приобретает указанный тип. Например:
mov ах,((1+2)*3+4)*5 {АХ = 65} 
mov bх,1+2*3+4*5 {ВХ = 27}
Операция[]
Определяет ссылку на память. Выражение внутри скобок вычисляется в первую очередь. Обычно оно связывается с регистрами BX, BP ,SI, DI и может использовать операции + и - для указания индексации. Например:
mov ah,100 mov ah,[100]
{АН = 100} {Загружаем в АН содержимое байта по адресу DS-.100}
Операция . (точка)
Селектор элемента структуры. Результат - сумма выражений до и после точки с типом второго выражения. Например: 
var
R: record
X: Word; У: Byte 
end;
.......
mov ax, R. X 
mov R.Y,al
Операции HIGH и LOW
HIGH возвращает старший, a LOW - младший байт выражения типа слова, следующего за символами операции. Выражение должно иметь абсолютное непосредственное значение. Например:
mov al,High $1000 {AL = $10}
Операция : (двоеточие)
Указывает ассемблеру, что выражение после операции должно относиться к сегменту, указанному до операции. Результат - ссылка на память со значением второго выражения. Например:
mov ax, [10] {AX = слово по адресу DS:10} 
mov ax,BS:[10] {АХ = слово по адресу BS:10}
Операция OFFSET
Возвращает смещение выражения, следующего за операцией. Результат имеет непосредственное значение. Например:
mov ах,Х {АХ = слово по адресу переменной X}
mov ax,offset X {АХ = смещение адреса X}
Операция SEG

Возвращает сегмент выражения, следующего за операцией. Результат имеет непосредственное значение.
Операция PTR
Осуществляет приведение типа. Результат - ссылка на память со значением выражения после операции и типом выражения до операции. Например:
Function Swap(X: Integer): Integer;
{Меняет местами байты в слове X}
begin 
asm
mov ax,X
mov BYTE PTR @Result,ah 
mov BYTE PTR @Result+l,al 
end; 
end;
Операции * и /
* - умножение, / - целочисленное деление. Оба выражения должны иметь непосредственные абсолютные значения, такое же значение имеет и результат операции. Например:
mov ax,2*2 {АХ = 4}
mov ах,17/3. {АХ = 5}
Операция MOD
Возвращает остаток от целочисленного деления. Оба выражения должны иметь непосредственные абсолютные значения, такое же значение имеет и результат операции. Например:
mov ах,17 mod 3 {АХ =2}
Операции SHL и SHR
Осуществляют логический сдвиг влево (SHL) или вправо (SHR) выражения, стоящего до операции, на количество разрядов, определяемое выражением после операции. Оба выражения должны иметь непосредственные абсолютные значения, такое же значение имеет и результат операции. Например:
mov ah,1 shl 7 {Ah = $80 = 128}
Бинарная операция +
Осуществляет сложение двух выражений. Выражения могут быть непосредственными значениями или ссылками на память, но только одно из них может быть перемещаемым. Если одно из выражений - ссылка на память, результат также определяет ссылку на память, а если одно из выражений - перемещаемое, результат будет перемещаемым.
Бинарная операция
Вычитание двух выражений. Первое выражение может быть любого класса, а второе должно быть абсолютным непосредственным значением. Результат относится к тому же классу, что и первое выражение.
Побитовые операции NOT, AND, OR, XOR
Имеют такой же смысл, что и одноименные операции Турбо Паскаля над целыми числами. Оба выражения должны иметь непосредственные абсолютные значения, такое же значение имеет и результат операции.
 
Директивы ассемблера
Встроенный ассемблер не поддерживает никакие директивы, обычно используемые в других ассемблерах, за исключением DB, DW, DD. Структура директив такова:
Dx <константа> [,<константа>,...,<константа>]
Здесь Dx - DB, DW или DD; <константа> - ассемблерная константа или константное выражение.
DB определяет цепочку байт, DW- слов, DD - двойных слов. Например:
db 'Турбо Паскаль',13,10
dw 0,$ FFFF, NearProc
dd 'ABCD1,999999999, FarProc
В качестве константных выражений разрешается использовать любые ассемблерные константы со значением, не выходящим из диапазона байта (DB), слова (DW) или двойного слова (DD). В любой директиве можно определять строковую константу, которая приводит к побайтовому заполнению памяти ASCII-кодами символов. Поскольку слово (двойное слово) размещается в памяти, начиная со своего младшего байта, старший (старшие) байт в директивах DW и DD при размещении строкой константы может остаться неопределенным и заполняется нулем. Например, два следующих объявления эквивалентны:
dw '5' 
dw $35 {$35 - ASCII-код символа '5'}
В директивах DW и DD разрешается также указывать имена, которые в этом случае интерпретируются как адреса соответствующих объектов, причем для DW это - ближний адрес (смещение), а для DD - дальний. Например:
dw X {Размещает смещение переменной X} 
dd Proc {Размещает FAR-адрес процедуры Рrос}
Данные, определяемые директивами Dx, всегда размещаются в текущем кодовом сегменте. Разместить таким образом данные в сегменте данных (т.е. определить константу или типизированную константу) невозможно - для этого используются стандартные средства Турбо Паскаля. Более того, директивы не могут снабжаться именами, а поэтому использовать размещаемые с их помощью данные не так-то просто. В следующем примере на экран выводится текстовое сообщение. Для этого используется функция 9 вызова ДОС, в соответствии с которой в регистрах DS:DX должен содержаться адрес текстовой строки, а сама строка должна заканчиваться символом «$»:
asm
jmp ©NextCode {Обходим фрагмент данных}
@:
db 'Текстовая строка,13,10,'$'
@NextCode:
push ds {Сохраняем DS}
push cs
pop ds {DS = CS}
mov dx,OFFSET @ {DS:DX - адрес строки}
mov ah,9 {AH - код функции вывода}
int 21h {Выводим строку}
pop ds {Восстанавливаем DS}
end;
Обратите внимание на использование регистра DS. В соответствии с требованиями функции 9, он должен содержать сегмент выводимой строки. В нашем случае строка располагается в кодовом сегменте, поэтому мы вынуждены сначала сохранить значение DS в стеке, а затем восстановить его. Если бы мы этого не сделали, по завершении ассемблерного оператора регистр DS указывал бы на сегмент кода и была бы потеряна связь программы Турбо Паскаля с глобальными переменными и константами.
 
Ассемблерные прграммы
Ассемблерные подпрограммы - это процедуры и функции, объявленные с директивой Assembler. В таких подпрограммах исполняемая часть не содержит begin... end и состоит из единственного ассемблерного оператора asm... end. Например:
Function LongMul(X,Y:Integer):LongInt; Assembler; 
asm
mov ax, X
imul Y {DX/AX содержат "длинный" результат} 
end;
При компиляции ассемблерных подпрограмм выполняется ряд оптимизаций кода, в том числе:

  • параметры-значения строкового типа, а также длиной в 1, 2 и 4 байта не копируются во временную память, т.е. внутри подпрограммы они считаются параметрами-переменными ;
  • компилятор не создает переменную @Result для результата функции, и ссылка на эту переменную в ассемблерной функции недопустима; исключением являются функции, возвращающие значения строкового типа, для них разрешается использовать ссылку на @Result;
  • генерируются следующие команды на входе в подпрограмму и на ее выходе:

push bp {Сохраняется ВР}
mov bp,sp {ВР содержит текущую границу стека}
sub sp,Locals {Резервируется часть стека для размещения локальных переменных}
.......
mov sp,bp {Восстанавливается граница стека}
pop bp {Восстанавливается ВР}
ret Params {Из стека удаляются параметры подпрограммы и осуществляется выход из нее}
Здесь Locals - общая длина в байтах всех объявленных в подпрограмме локальных переменных, a Params - длина (в байтах) всех формальных параметров. Если Locals и Params равны нулю, входной код не создается, а выходной содержит единственную инструкцию RET.
Все локальные переменные Турбо Паскаль размещает в стеке. Это относится как к обычным, так и к ассемблерным подпрограммам. Для ссылки на локальные переменные используется адресация по базе, задаваемой парой DS: ВР, поэтому при входе в процедуру всегда создается так называемый локальный стек: в регистр ВР помещается текущая граница стека, а сама эта граница смещается вверх на суммарную длину всех локальных переменных, чтобы работа со стеком внутри подпрограммы не разрушила локальные переменные. Например:
Procedure ...;
Assembler;
var
X: Word;
Y: Byte; 
asm
mov X, ax {Компилируется в mov [BP-2], ax}
mov ah,Y {Компилируется в mov ah,[BP-3]} 
end;
Ассемблерные функции должны следующим образом возвращать результат своей работы:

  •  длиной 1 байт (Byte, Char и т.п.) - в регистре AL;
  •  длиной 2 байта (Integer, Word) - в регистре АХ;
  •  длиной 4 байта (Pointer, LongInt) - в регистрах DX (старшее слово) и АХ (младшее слово);
  •  типа Real - в регистрах DX, BX, АХ (старшее слово - в DX, младшее в АХ);
  •  вещественных типов Single, Double, Extended, Comp - в регистре ST (0) сопроцессора;

 строкового типа - во временной области памяти, на которую ссылается @Result.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика