Глава 12. ИСПОЛЬЗОВАНИЕ ЯЗЫКА АССЕМБЛЕРА В ПРОГРАММАХ
НА TURBO PASCAL 7.0
Данный раздел не является справочным по языку ассемблера и предполагает знание читателем
основ этого языка и устройство процессора 80X86.
Turbo Pascal позволяет писать отдельные части программы (подпрограммы или части подпрограмм)
на языке ассемблера. Здесь возможны четыре варианта:
- Во-первых, можно написать подпрограмму на языке ассемблера, скомпилировать ее отдельно
компилятором TASM (Turbo Assembler) с получением объектного файла, а затем скомпоновать его
с основной программой, написанной на Turbo Pascal, используя при этом директиву компилятора
{$L <имя файла>}, где <имя файла> - имя файла с подпрограммой на ассемблере, и директиву
external.
- Во-вторых, используя встроенный ассемблер пакета Turbo Pascal, отдельные части текста
программы можно написать непосредственно на языке ассемблера, заключив их в операторные
скобки asm...end.
- В-третьих, ту или иную подпрограмму (процедуру или функцию) можно полностью, за
исключением заголовка, написать на языке ассемблера, используя при этом директиву assembler.
В этом случае также используется встроенный ассемблер.
- Наконец, в-четвертых, можно небольшую подпрограмму написать непосредственно в кодах
процессора, используя оператор или директиву inline.
При написании отдельных частей программы на языке ассемблера следует иметь в виду, что
необходимо сохранить содержимое регистров ВР, SP, SS и DS. Если их необходимо изменить, то
исходные значения следует запомнить, а затем восстановить. Остальные регистры можно
безболезненно изменять.
Основным вопросом стыковки программы с подпрограммой, написанной на ассемблере, является
передача параметров в подпрограмму и обратно. Именно этому вопросу и будет здесь уделено
основное внимание.
Ниже будут рассмотрены особенности использования этих вариантов. В качестве примера их
использования будут приведены различные варианты подпрограммы-функции, определяющей
максимальный элемент из массива целых чисел.
12.1. Использование компилятора TASM
Как правило, этот вариант применяется, когда та или иная программа имеет большой размер и ее
целесообразно и написать, и скомпилировать отдельно, используя компилятор TASM [5]. В этом
случае можно использовать все возможности языка и компилятора TASM.
Пример. Программа, использующая подпрограмму-функцию, определяющую максимальный
элемент из массива целых чисел и написанную наязыке ассемблера.
Основная программа, использующая подпрограмму, написанную на языке ассемблера, содержит
инициализированный массив, в котором будет определяться максимальное число, а сама программа
выводит на экран значение максимального числа из этого массива:
program EXAMPLE20;
const
N = 7; {Размер массива}
Massiv: array[1..N] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
{$L SUBR} {Подключение файла SUBR.OBJ}
function Max(var Mas; N: Integer): Integer; external;
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv, N)); ReadLn
end.
Используя стандартную модель памяти, подпрограмму, определяющую максимальное число из
массива, можно написать следующим образом:
CODE SEGMENT BYTE PUBLIC
ASSUME CS:CODE
PUBLIC Max ;внешний идентификатор
AdrMas EQU DWORD PTR[BP+6] ;адрес первого параметра
N EQU WORD PTR[BP+4] ;второй параметр
Max PROC NEAR
PUSH BP ;сохранение регистра ВР
MOV BP,SP ;указатель стека
LDS SI,AdrMas ;адрес массива
XOR AX,AX ;0 - в регистр АХ
MOV BX,8001h ;минимальное целое число
MOV CX,N ;число элементов массива
CMP СХ,АХ ;cравнение с 0
JLE М3 ;0 или отрицательное число
M1: LODSW ;загрузка элемента массива
СМР АХ,ВХ ;сравнение с текущим максимумом
JLE M2 ;не больше
MOV BX,AX ;новое максимальное число
М2: LOOP Ml ;цикл
М3: MOV АХ,ВХ ;результат функции
POP BP ;восстановление регистра
ВР
RET 6 ;возврат из подпрограммы
Max ENDP
CODE ENDS
END
По приведенной подпрограмме следует сделать следующие замечания.
Первые две команды - сохранение регистра ВР и загрузка в него указателя стека - являются
типичными командами, с помощью которых можно установить доступ к передаваемым параметрам
через регистр ВР.
Параметры передаются в подпрограмму следующим образом. Параметры-значения размером в один
байт передаются одним 16-разрядным словом, причем информативным является младший байт,
параметры-значения в 2 байта передаются одним 16-разрядным словом, в 4 байта - двумя
16-разрядными словами, параметры-значения типа Real передаются тремя 16-разрядными словами,
все остальные параметры-значения (в том числе и 3-байтовые) передаются своими полными
адресами. Из этого правила есть некоторые исключения: параметры-переменные и
параметры-константы всегда передаются своими полными адресами.
Т. к. в подпрограмме первый параметр является параметром-переменной, то он передается своим
адресом, с помощью которого в дальнейшем и извлекаются элементы массива. Второй параметр
подпрограммы - параметр-значение, и он передается своим значением. Первый параметр
находится по адресу ВР+6, а второй - ВР+4. Указанные смещения определяются наличием в стеке
адреса возврата (при ближней адресации - 2 байта), размещенным в стеке значением регистра
ВР (2 байта) и для первого параметра - размером второго параметра (2 байта).
Если подпрограмма является подпрограммой-функцией, то возвращаемый параметр передается
различным образом в зависимости от своего размера. Параметр размером в байт передается в
регистре AL, параметр размером в 2 байта - в регистре АХ, параметр размером в 4 байта - в
регистрах DX (старшая часть или адрес сегмента) и АХ (младшая часть или смещение), параметры
размером в б байтов (типа Real) - в регистрах DX (старшая часть), ВХ (средняя часть) и АХ
(младшая часть). Параметры других вещественных типов передаются в нулевом элементе стека
сопроцессора ST(0). Если функция возвращает значение типа string, то при обращении к функции
резервируется память для размещения возвращаемой строки, а адрес этой области размещается в
стеке выше всех передаваемых параметров.
В рассматриваемом примере возвращаемый параметр - типа Integer, и он возвращается в регистре
АХ. При возвращении из подпрограммы в команде RET записав аргумент 6 для удаления из стека
передаваемых параметров, которые в данном примере имеют именно этот размер.
Turbo Assembler предполагает и другое оформление подпрограмм, используемых затем в
программах, написанных на языке Паскаль. Для этого используется специальная модель памяти
Large (большая), задаваемая в виде:
.MODEL Large,PASCAL.
Она позволяет несколько упростить оформление входа в подпрограмму и выхода из нее.
Подпрограмма дополняется необходимыми командами на этапе компиляции.
Пример. Вариант предыдущей подпрограммы, использующий специальную модель памяти.
.MODEL Large,PASCAL ;специальная модель памяти
.CODE
PUBLIC Max ;внешний идентификатор
Max PROC NEAR Mas: DWORD, N: WORD ;передаваемые параметры
LDS SI,Mas ;адрес массива
XOR AX,AX ;0 - в регистр АХ
MOV BX,8001h ;минимальное целое число
MOV CX,N ;число элементов массива
CMP CX,AX ;сравнение с 0
JLE @@3 ;0 или отрицательное число
@@1: LODSW ;загрузка элемента массива
CMP AX,BX ;сравнение с текущим максимумом
JLE @@2 ;не больше
MOV BX,AX ;новое максимальное число
@@2: LOOP @@1 ;цикл
@@3: MOV AX,BX ;результат функции
RET ;возврат из подпрограммы
Max ENDP
END
В этом примере не сохраняется и не восстанавливается регистр ВР - эти операции добавляются
к программе на этапе компиляции. Не указывается также и размер передаваемых параметров - они
при выходе из подпрограммы удаляются автоматически. В строке, где начинается описание
подпрограммы (начинается с имени подпрограммы - Мах), необходимо перечислить все
передаваемые параметры в том же порядке, как они заданы в заголовке, написанном на языке
Паскаль с указанием их размеров (о размерах передаваемых параметров см. выше).
Здесь показана также возможность использования в подпрограммах локальных меток, начинающихся
символами @@.
В подпрограмме, написанной на языке ассемблера, можно использовать подпрограммы, написанные
на языке Паскаль. Несколько модифицированная подпрограмма определения максимального элемента
массива, которая в случае недопустимого числа элементов массива (0 или отрицательное число)
вызывает подпрограмму, написанную на языке Паскаль для выдачи сообщения, приведена в
следующем примере.
Пример. Модифицированный вариант программы, в котором подпрограмма, написанная на языке
ассемблера, в случае недопустимого числа элементов массива (равно 0 или отрицательное)
вызывает подпрограмму, написанную на языке Паскаль, выводящую соответствующее сообщение.
Основная программа, содержащая подпрограмму на языке Паскаль, будет иметь следующий вид:
program EXAMPLE21;
const
N = 7; {Размер массива}
Massiv: array[1..n] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
{$L SUBR} {Подключение файла SUBR.OBJ}
function Max(var Mas; N: Integer): Integer; external;
procedure ErrorReport(N: Integer);
begin
WriteLn;
WriteLn(' Недопустимое число элементов: ', N);
ReadLn
end;
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv, N));
ReadLn
end.
Подпрограмма, написанная на языке ассемблера, будет в этом случае иметь
следующий вид:
.MODEL Large,PASCAL ; специальная модель памяти
.CODE
EXTRN ErrorReport: NEAR ;внешняя подпрограмма
PUBLIC Max ;внешний идентификатор
Max PROC NEAR Mas: DWORD, N: WORD
;передаваемые параметры
LDS SI,Mas ;адрес массива
XOR AX,AX ;0 - в регистр АХ
MOV BX,8001h ;минимальное целое число
MOV CX,N ;число элементов массива
CMP CX,AX ;сравнение с 0
JG @@1 ;допустимое число
PUSH BX ;сохранение регистра ВХ
PUSH CX ;передаваемый параметр
CALL ErrorReport ;обращение к подпрограмме
POP BX ;восстановление регистра ВХ
JMP @@3 ;на завершение
@@1: LODSW ;загрузка элемента массива
CMP AX,BX ;сравнение с текущим
;максимумом
JLE @@2 ;не больше
MOV BX,AX ;новое максимальное число
@@2: LOOP @@1 ;цикл
@@3: MOV AX,BX ;результат функции
RET ;возврат из подпрограммы
Max ENDP
END
Перед обращением к подпрограмме, написанной на языке Паскаль, в стек в соответствующем
порядке следует поместить передаваемые параметры. В данном случае такой параметр один -
число элементов массива.
Т. к. подпрограмма, написанная на языке Паскаль, не гарантирует сохранение регистров АХ, ВХ,
СХ и DX, то в случае необходимости сохранения их значений следует перед обращением к
подпрограмме, написанной на языке Паскаль, сохранить в стеке значения соответствующих
регистров, а после возвращения из подпрограммы - восстановить их. В данном примере
сохраняется содержимое регистра ВХ, в котором записано минимальное целое число.
При написании программ, содержащих отдельные части, написанные на языках ассемблера и
Паскаль, следует обращать внимание на способ адресации (дальний - far или ближний - near).
Здесь существует следующее правило: если подпрограмма объявляется в интерфейсной части
какого-либо модуля, то она должна иметь дальнюю адресацию, в других случаях (подпрограмма
объявляется в файле, содержащем основную программу, или в исполнительной части модуля)
следует использовать ближнюю адресацию.
И еще одно замечание: внешнюю подпрограмму нельзя объявлять внутри другой подпрограммы.
12.2. Использование встроенного ассемблера
Начиная с версии 6.0 Turbo Pascal содержит встроенный ассемблер, позволяющий писать
отдельные части программ на языке ассемблера. Встроенный ассемблер обладает многими
возможностями языка Turbo assembler, но приспособлен к использованию в программах,
написанных на языке Паскаль (позволяет использовать идентификаторы программы, написанные на
языке Паскаль, комментарии, имеющие такой же вид, как в языке Паскаль, позволяет
воспользоваться встроенным отладчиком для пошагового выполнения программы, контроля
содержимого регистров и параметров программы и т. д.).
Так же как и Turbo assembler, встроенный ассемблер предполагает использование ряда
предопределенных стандартных идентификаторов, имеющих специальное назначение. Если в
программе будет введен идентификатор с таким же именем, но имеющий другое назначение, в
частях программы, написанных на встроенном ассемблере, будет отдано предпочтение
стандартному назначению этого идентификатора. Перечень стандартных идентификаторов
встроенного ассемблера приведен в приложении В.
Наряду с возможностью использования идентификаторов языка Паскаль встроенный ассемблер
использует три дополнительных идентификатора:
@Code - текущий кодовый сегмент (используется только с оператором SEG);
@Data - текущий сегмент данных (используется только с оператором SEG);
@Result - результат, полученный функцией (можно использовать только внутри функции).
При использовании встроенного ассемблера нельзя использовать:
- стандартные процедуры и функции;
- специальные массивы Mem, MemW, MemL, Port и PortW (см. п. 13);
- константы типа string, вещественных типов и типа-множества;
- процедуры и функции, объявленные с директивой inline;
- метки, объявленные не в данном блоке.
Часть программы, написанная на языке ассемблера, помещается в операторные скобки asm...end.
В следующем примере приведена программа, выполняющая те же функции, что и предыдущие
программы, но использующая встроенный ассемблер.
Пример. Программа, использующая подпрограмму-функцию, определяющую максимальный элемент из
массива целых чисел и написанную на языке ассемблера, но использующую встроенный ассемблер.
program EXAMPLE22;
const
N = 7; {Размер массива}
Massiv: array [1..n] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
function Max(var Mas; N: Integer): Integer;
begin
asm
LDS SI,Mas {адрес массива}
XOR AX,AX {0 - в регистр АХ}
MOV BX,8001h {минимальное целое число}
MOV CX,N {число элементов массива}
CMP CX,AX {сравнение с 0}
JLE @@3 {0 или отрицательное число}
@@1: LODSW {загрузка элемента массива}
CMP AX,BX {сравнение с текущим максимумом}
JLE @@2 {не больше}
MOV BX,AX {новое максимальное число}
@@2: LOOP @@1 {цикл}
@@3: MOV @Result,BX {результат функции}
end
end;
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv,N));
ReadLn
end.
Следует заметить, что при использовании встроенного ассемблера комментарии пишутся таким же
образом, как и в языке Паскаль (в фигурных скобках). Назначение же точки с запятой несколько
другое - этим знаком отделяются друг от друга команды, написанные на одной строке (такая
возможность при использовании встроенного ассемблера существует),
например:
LDS SI,Mas; XOR AX,AX; MOV BX,8001h
Как видно из примера, можно использовать идентификаторы, объявленные на языке Паскаль
(параметры Mas, N). Нет необходимости сохранять и восстанавливать регистр ВР, удалять из
стека передаваемые параметры, включая и локальные, и т. д.
12.3. Использование директивы ASSEMBLER
Если ту или иную подпрограмму нужно полностью написать на языке ассемблера, используя
встроенный ассемблер, можно вместо операторных скобок asm...end использовать директиву
assembler, которая имеет ряд особенностей.
- Во-первых, все передаваемые параметры, размером отличные от 1, 2 или 4 байт, передаются
всегда своим адресом без создания копии в стеке.
- Во-вторых, нельзя использовать для передачи результата функции переменную @Result.
Результат передается точно так же, как и при использовании TASM (см. п. 12.1).
Исключение составляет результат типа string. В этом случае в переменной @Result находится
адрес строки, в которую следует поместить полученную информацию.
- В-третьих, для процедур и функций, не имеющих формальных и локальных параметров, вообще
не выделяется область стека.
- В-четвертых, так же как и в предыдущем случае, автоматически оформляется начало и конец
подпрограммы, связанные с сохранением регистра ВР и освобождением стека от передаваемых
параметров.
Ниже приведен пример использования такой директивы.
Пример. Программа, использующая подпрограмму-функцию, определяющую максимальный элемент из
массива целых чисел и написанную на языке ассемблера, но использующую встроенный ассемблер
и директиву assembler.
program EXAMPLE23;
const
N = 7; {Размер массива}
Massiv: array [1..n] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
function Max(var Mas; N: Integer): Integer; assembler;
asm
LDS SI,Mas {адрес массива}
XOR AX,AX {0 - в регистр АХ}
MOV BX,8001h {минимальное целое число}
MOV CX,N {число элементов массива}
CMP CX,AX {сравнение с 0}
JLE @@3 {0 или отрицательное число}
@@1: LODSW {загрузка элемента массива}
CMP AX,BX {сравнение с текущим максимумом}
JLE @@2 {не больше}
MOV BX,AX {новое максимальное число}
@@2: LOOP @@1 {цикл}
@@3: MOV AX,BX {результат функции}
end;
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv,N));
ReadLn
end.
12.4. Использование оператора или директивы INLINE
Для коротких подпрограмм (до десятка операторов) можно использовать непосредственное задание
всей подпрограммы или ее части в кодах процессора, используя оператор или директиву inline.
Отличие оператора от директивы заключается в том, что оператор может быть использован в
подпрограмме совместно с другими операторами, написанными на языках Паскаль или ассемблера,
а директива предполагает, что подпрограмма целиком состоит лишь из нее одной.
Само написание оператора или использование директивы inline практически ничем не отличается
друг от друга. Каждый из них начинается с зарезервированного слова inline, за которым в
круглых скобках через прямой слеш (/) записываются байты или слова кодов команд. При этом
каждый элемент будет либо байтом, либо словом в зависимости от фактического значения (байт,
если значение в пределах 0..255, слово - в остальных случаях). Этот стандартный размер можно
изменить, используя указатель размера < или >. Если используется указатель размера <,
информация размещается в одном байте, причем, если ее размер больше одного байта,
используется только младший байт. Указатель размера > всегда размещает информацию в одном
16-разрядном слове (см. пример ниже). В качестве элементов оператора или директивы inline
можно использовать идентификаторы языка Паскаль (в директиве нельзя только использовать
идентификаторы передаваемых в подпрограмму параметров).
Пример. Программа, использующая подпрограмму-функцию, определяющую максимальный элемент из
массива целых чисел, написанную на языке ассемблера и использующую оператор inline.
program EXAMPLE24;
const
N = 7; {Размер массива}
Massiv: array [1..n] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
function Max(var Mas; N: Integer): Integer;
begin
inline(
$C5/$76/6/ {LDS SI,Mas 5}
$B8/>$0/ {MOV AX,0}
$BB/$8001/ {MOV BX,8001h}
$8B/$4E/4/ {MOV CX,N}
$3B/$C8/ {CMP CX.AX}
$7E/$09/ {JLE @@3}
$AD/ {@@1: LODSW}
$3B/$C3/ {CMP AX.BX}
$7E/$02/ {JLE 002}
$8B/$D8/ {MOV BX,AX}
$Е2/$F7); {@@2: LOOP@@1}
asm
MOV AX,BX {@@3: MOV AX.BX}
end
end;
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv, N));
ReadLn
end. {работает неверно, где-то опечатка}
Несколько иначе используется директива inline. При использовании этой директивы подпрограмма
представляет собой макроопределение ассемблера, и при обращении к ней ее коды помещаются в
место обращения. В этом случае не формируются команды обращения к подпрограмме и выхода из
нее. Если подпрограмме передаются параметры, они, как обычно, передаются через стек, из
которого их можно взять. Такой вариант используется для очень коротких подпрограмм и дает
наиболее эффективный результат. В качестве примера рассмотрим все ту же задачу поиска в
массиве максимального числа, хотя используемая здесь подпрограмма и несколько великовата для
данного варианта.
Пример. Программа, использующая подпрограмму-функцию, определяющую максимальный элемент из
массива целых чисел, написанную на языке ассемблера и использующую директиву inline.
program EXAMPLE25;
const
N = 7; {Размер массива}
Massiv: array[1..n] of Integer =
(1, 2, 3, 2, 17, 7, 2); {Исходный массив}
function Max(var Mas; N: Integer): Integer;
inline(
$59/ {POP CX число элементов}
$5E/ {POP SI смещение массива}
$1F/ {POP DS адрес сегмента}
$33/$C0/ {XOR AX,AX}
$BB/$8001/ {MOV BX,8001h}
$3B/$C8/ {CMP CX,AX}
$7E/$09/ {JLE @@3}
$AD/ {@@1: LODSW}
$3B/$C3/ {CMP AX,BX}
$7E/$02/ {JLE @@2}
$8B/$D8/ {MOV BX,AX}
$E2/$F7/ {@@2: LOOP @@l}
$8B/$C3);
begin
WriteLn('Максимальное число массива равно: ',
Max(Massiv, N));
ReadLn
end.