Главная arrow Язык программирования Pascal arrow Турбо Паскаль Начальный курс В.В.Фаронов arrow 12.2. Специифика встроенного ассемблера Турбо Паскаль В.В. Фаронов

12.2. Специифика встроенного ассемблера Турбо Паскаль В.В. Фаронов

Приведенное выше общее описание архитектуры МП 8086/8088 является базовым для любого ассемблера, в том числе и для встроенного ассемблера Турбо Паскаля. Однако ассемблеры содержат массу дополнительных возможностей, облегчающих разработку готовых к работе программ. Эти возможности отражаются в директивах и макрокомандах ассемблера. Встроенный ассемблер не предназначен для написания законченных программ, поэтому в нем отсутствуют макрокоманды и директивы. Главной особенностью встроенного ассемблера является практически полное отсутствие в нем средств описания переменных и данных, т.к. эти объекты описываются средствами Турбо Паскаля.
12.2.1. Оператор 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).
12.2.2. Синтаксис ассемблерных команд
Здесь и далее ассемблерными командами называются команды на языке встроенного ассемблера, вставляемые в тело ассемблерного оператора 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.
Как и в Паскале, ассемблерные выражения имеют тип, но в отличие от Паскаля этот тип определяет только размер объекта в памяти и не ограничивает применяемые к нему операции.
Встроенный ассемблер имеет следующие предопределенные типы:

Тип

Длина в памяти

BYTE

1

WORD

2

DWORD

4

QWORD

8

TBYTE

10

NEAR

-

FAR

-

Имена предопределенных типов можно использовать для приведения типов выражений. Например, если определены переменные
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. Встроенный ассемблер поддерживает операции, перечисленные в следующей таблице (в порядке убывания приоритета).
Операции встроенного ассемблера

Операция

Комментарий

&

Перекрытие идентификатора

0

Подвыражение

[]

Ссылка на память

. (точка)

Селектор структуры

HIGH LOW

Доступ к байту в слове

+ -

Унарные операции задания знака

 

Перекрытие сегмента

OFFSET SEG TYPE PTR *

 

/ MOD SHL SHR

 

+ -

Бинарные операции

NOT AND OR XOR

Операции над битами

Операция &
Осуществляет перекрытие идентификатора: следующий за знаком & идентификатор считается определенным в программе, даже если он совпадает с зарезервированным словом. Например:
var
Ch: Byte;
.......
mov ch,0 {Посылаем 0 в регистр СН}
mov &Ch,0 {Посылаем 0 в переменную Ch}
Операция ()
Круглые скобки используются обычным для Паскаля образом - для изменения порядка исчисления выражения (подвыражение, ограниченное скобками, вычисляется в первую очередь). Если перед скобками стоит имя типа, все выражение приобретает указанный тип. Например:
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
Имеют такой же смысл, что и одноименные операции Турбо Паскаля над целыми числами. Оба выражения должны иметь непосредственные абсолютные значения, такое же значение имеет и результат операции.
12.2.3. Директивы ассемблера
Встроенный ассемблер не поддерживает никакие директивы, обычно используемые в других ассемблерах, за исключением 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 указывал бы на сегмент кода и была бы потеряна связь программы Турбо Паскаля с глобальными переменными и константами.
12.2.4. Ассемблерные подпрограммы
Ассемблерные подпрограммы - это процедуры и функции, объявленные с директивой 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.