Глава 6. Более подробно о программировании на Турбо Ассемблере
-----------------------------------------------------------------
Прочитав последние две главы, вы, конечно, очень много узна-
ли о языке Ассемблера, но остается узнать еще намного больше. В
данной главе мы коснемся некоторых довольно развитых, но весьма
полезных аспектов Турбо Ассемблера и программирования на языке
Ассемблера.
Мы обсудим в частности следующие темы:
- Директивы Турбо Ассемблера EQU и =, которые позволят вам
присваивать именам значения и текстовые строки.
- Мощные строковые инструкции Турбо Ассемблера.
- Возможность ассемблирования с помощью Турбо Ассемблера
нескольких исходных файлов и последующего использования утилиты
TLINK для компоновки их в одну программу.
- Возможность Турбо Ассемблера включать отдельные файлы ис-
ходного кода в любую программу на Ассемблере.
- Исчерпывающие файлы листингов исходного кода Турбо Ассем-
блера.
Имеется возможность писать программы на Ассемблере таким об-
разом, что они будут ассемблироваться по-разному при различных
обстоятельствах. Мы рассмотрим, почему это может оказаться полез-
ным, и директивы, делающие это возможным. Наконец, мы рассмотрим
некоторые наиболее общие ошибки, которые обычно делают програм-
мисты, работающие на Ассемблере.
Вам следует обязательно просмотреть данную главу, возможно,
даже не вникая глубоко в ее содержание. Сегодня эта информация
может вам не потребоваться, но завтра, когда в ней появится необ-
ходимость, вы будете знать, где ее искать.
Использование директив присваивания
-----------------------------------------------------------------
Давайте начнем с рассмотрения директив EQU и = для присваи-
вания меток значениям и текстовым строкам. Это очень полезное
средство, позволяющее сделать программу на Ассемблере более по-
нятной и легко обслуживаемой.
Директива EQU
-----------------------------------------------------------------
Причина использования меток для имен переменных, подпрограмм
и конкретных инструкций очевидна. Ведь в противном случае мы не
могли бы ссылаться по именам на эти элементы программ. В равной
степени важна, но менее очевидна, необходимость присваивать мет-
кам значения и текстовые строки.
Присвоить метке числовое значение или текстовую строку поз-
воляет директива EQU. Ссылка на метку директивы EQU транслируется
в литеральное приравнивание. Рассмотрим следующий фрагмент прог-
раммы:
.
.
.
END_OF_DATA EQU '!' ; "конец данных"
STORAGE_BUFFER_SIZE EQU 1000 ; размер буфера
.DATA
StorageBuffer DB STORAGE_BUFFER_SIZE DUP (?)
.
.
.
.CODE
mov ax,@Data
mov ds,ax
sub di,di ; установить указатель
; буфера в значение 0
StorageLoop:
mov ah,1
int 21h ; получить следующую
; нажатую клавишу
mov [StarageBuffer+di],al ; сохранить следующую
; нажатую клавишу
cmp al,END_OF_DATA ; это клавиша "конец
; данных"?
je DataAckquired ; да, перейти к
; обработке данных
inc di ; подсчитать это
; нажатие клавиши
cmp di,STORAGE_BUFFER_SIZE ; мы переполнили
; буфер?
jb StorageLoop ; нет, получить
; следующую клавишу
; Буфер переполнен...
.
.
.
; Мы получили данные
DataAcquired:
.
.
.
Здесь директива EQU используется для определения двух ме-
ток: STORAGE_BUFFER_SIZE и END_OF_DATA. Метка END_OF_DATA прирав-
нивается к символу "!" и сравнение с ней выполняется при каждом
нажатии клавиши, чтобы определить, не встретили ли мы конец дан-
ных. Это показывает одно из существенных преимуществ использова-
ния директивы приравнивания (EQU), ведь метки значительно более
информативны, чем значения-константы. Кроме того, назначение
инструкции:
cmp al,END_OF_DATA
определенно понятней, чем назначение инструкции:
cmp al,'!'
(END_OF_DATA означает "КОНЕЦ_ДАННЫХ").
Использование метки STORAGE_BUFFER_SIZE иллюстрирует еще
один довод в пользу применения приравнивания (присваивания).
STORAGE_BUFFER_SIZE, для которого устанавливается значение 1000,
используется как для создания буфера в памяти размером в 1000
байт, так и для проверки этого буфера на переполнение. В обоих
случаях можно было бы использовать константу 1000, но это гораздо
менее информативно, чем метка STORAGE_BUFFER_SIZE ("РАЗМЕР_БУФЕРА
_В_ПАМЯТИ").
Предположим теперь, что вы ходите изменить размер буфера в
памяти. Для этого вам придется изменить операнд только в одной
директиве EQU, этим самым вы внесете изменения по всему тексту
программы. Конечно, в противном случае изменить две константы в
программе было бы нетрудно, но данная константа могла бы исполь-
зоваться в десятках или сотнях мест. А в этом случае гораздо лег-
че (и при этом меньше вероятность внесения в программу ошибки)
изменить одно приравнивание, чем десятки или сотни констант.
Операнд в директиве приравнивания метки может сам содержать
метки, присваивание для которых выполняется в других местах. Нап-
ример:
.
.
.
TABLE_OFFSET EQU 1000h
INDEX_START EQU (TABLE_OFFSET+2)
DICT_START EQU (TABLE_OFFSET+100h)
.
.
.
mov ax,WORD PTR ]bx+INDEX_START] ; получить
; первую индексную запись
.
.
.
lea si,[bx+DICT_START] ; указатель на первую
; запись словаря
.
.
.
что эквивалентно следующему:
.
.
.
mov ax,WORD PTR [bx+1000h+2]
lea si,[bx+1000h+100h]
.
.
.
Приравненные метки удобно использовать для присваивания раз-
личным прерываниям, портам и ячейкам памяти компьютера РС понят-
ных имен. Проиллюстрируем некоторые случаи такого использования
директивы EQU на следующем примере:
.
.
.
DOS_INT EQU 21h ; прерывания по вызову
; функции DOS
CGA_STATUS EQU 3dah ; порт состояния адаптера
; CGA
VSYNC_MASK EQU 00001000b ; выделить бит в состоянии
; порта CGA, указывающий,
; когда вы можете изменять
; изменять экран, не вызывая
; помех ("снежных хлопьев")
BIAS_SEGMENT EQU 40h ; сегмент BIOS, в котором
; хранятся данные
EQUIPMENT_FLAG EQU 10h ; смещение в сегменте BIOS
; переменной флага аппарат-
; ного обеспечения
.
.
.
mov ah,2
mov di,'Z'
int DOS_Int ; напечатать символ "Z"
.
.
.
; Подождать, пока можно будет обновить экран, не вызывая
; помех
mov dx,CGA_STATUS
WaitForVerticalSync:
in al,dx ; получить статус CGA
and al,VSYNC_MASK ; идет вертикальная
; синхронизация?
jz WaitForVerticalSync ; нет, завершить
; ожидание
.
.
.
mov ax,BIOS_SEGMENT
mov ds,ax ; DS указывает на сегмент
; данных BIOS
mov bx,EQUIPMENT_FLAG ; ссылка на флаг
; аппаратного обеспечения
and BYTE PTR [bx],NOT 30h
or BYTE PTR [bx],20h ; установить флаг
; аппаратного обеспечения
; так, чтобы был выбран
; цветной режим с 80
; позициями в строке
.
.
.
Приравненные метки, в которых используются другие приравнен-
ные метки, расширяют принцип использования присваиваний для об-
легчения изменения ваших программ. Например, если в предыдущем
примере вы переместите все ссылки в таблице на 10 байт ближе к
BX, вам придется изменить только присваивание для TABLE_OFFSET
на:
TABLE_OFFSET EQU (1000h - 10)
После выполнения ассемблирования INDEX_START и DICT_START
будут настроены в соответствии с TABLE_OFFSET, так как их значе-
ния основываются на TABLE_OFFSET.
Кстати, скобки в которые заключен операнд директивы EQU,
являются необязательными, но они не помешают, поскольку помогают
визуально выделить операнд.
Директиву EQU можно использовать для присваивания метке тек-
стовой строки или значения. Например, далее метка используется
для хранения выводимой на печать текстовой строки:
.
.
.
EQUATED_STRING EQU 'Пример текстовой строки'
.
.
.
TextMessage DB EQUATED_STRING
.
.
.
mov dx,OFFSET TextMessage
mov ah,9
int 21h ; напечатать TextMessage
.
.
.
Метки, приравненные к тестовым строкам, могут использоваться
в качестве операндов. Например:
.
.
.
REGISTER_BX EQU BX
.
.
.
mov ax,REGISTER_BX
.
.
.
Это ассемблируется в инструкцию:
mov ax,bx
Обычно не возникает необходимости приравнивать метку к ре-
гистру, но вы можете, например, использовать приравненные метки
или ARG для присваивания имени передаваемым в стеке параметрам и
выделяемой в стеке динамической памяти:
;
; Вызываемая из Си (модель NEAR) подпрограмма, выполняющая
; сложение трех целых параметров и возвращающая целый
; результат. Прототип функции:
;
; int AddThree(int I, int J, int K)
;
Temp EQU [bp-2]
I EQU [bp+4]
J EQU [bp+6]
K EQU [bp+8]
;
_AddThree PROC
push bp ; сохранить BP вызывающей
; программы
mov bp,sp ; ссылка на рамку стека
sub sp,2 ; выделить место для Temp
mov ax,I ; получить I
add ax,J ; вычислить I + J
mov ax,K ; получить K
mov Temp,ax ; вычислить I + J + K
mov sp,bp ; освободить выделенное
; для Temp пространство
pop bp ; восстановить значение BP
; вызывающей программы
ret
_AddThree ENDP
Вы можете применять директиву EQU для присваивания имени лю-
бой текстовой строке, которая может использоваться в качестве
операнда. В действительности вы можете использовать приравненные
метки в поле инструкции или директивы, а не только в поле операн-
да, хотя трудно представить, для чего это может понадобиться.
Чтобы операнд директивы EQU рассматривался, как текстовая
строка, а не как выражение, можно использовать угловые скобки (<
и >). Например:
TABLE_OFFSET EQU 1
INDEX_START EQU <TABLE_OFFSET+2>
Здесь метке INDEX_START присваивается текстовая строка
"TABLE_OFFSET+2", в то время как в директивах:
TABLE_OFFSET EQU 1
INDEX_START EQU TABLE_OFFSET+2
метке INDEX_START присваивается значение 3 (результат сложения 1
+ 2). В общем случае полезно всегда заключать в директиве EQU
операнды, представляющие собой текстовые строки, в угловые скоб-
ки. Этим будет обеспечено, что такие операнды случайно не будут
вычислены, как выражения.
Если в данном исходном модуле с помощью директивы EQU метка
приравнивается к значению или текстовой строке, она не может быть
переопределена в данном модуле. Например, следующий код приведет
к ошибке:
.
.
.
X EQU 1
.
.
.
X EQU 101
.
.
.
Если вам требуется переопределять в программе приравненные
метки (и для этого есть какая-то весомая причина), то вам нужно
будет использовать директиву = (мы обсудим ее позднее).
Предопределенный идентификатор $
-----------------------------------------------------------------
Вспомним, что в Турбо Ассемблере имеется несколько предопре-
деленных идентификаторов (например, @data). Еще один простой, но
удивительно полезный предопределенный идентификатор - это иденти-
фикатор $, который всегда установлен в текущее значение счетчика
адреса. Другими словами, идентификатор $ всегда равен текущему
смещению в сегменте, в котором Турбо Ассемблер в данным момент
выполняет ассемблирование. $ представляет собой постоянное значе-
ние смещения, аналогичное OFFSET MemVar. Это позволяет использо-
вать $ в выражениях или в любом месте, где допускается использо-
вание константы.
Идентификатор $ очень удобно использовать для вычисления
длины данных и кода. Предположим, например, что вы хотите прирав-
нять идентификатор STRING_LENGTH к длине строки в байтах. Без
предопределенного идентификатора $ вам придется сделать следую-
щее:
.
.
.
StringStart LABEL BYTE
db 0dh,0ah,'Текстовая строка'odh,0ah
StringEnd LABEL BYTE
STRING_LENGTH EQU (StringEnd-StringStart)
.
.
.
а с помощью идентификатора $ вы можете записать:
.
.
.
StringStart LABEL BYTE
db 0dh,0ah,'Текстовая строка'odh,0ah
STRING_LENGTH EQU ($-StringStart)
.
.
.
Длину (в словах) массива слов можно вычислить следующим об-
разом:
.
.
.
WordArray DW 90h, 25h, 0, 16h, 23h
WORD_ARRAY_LENGTH EQU (($-WordArray)/2)
.
.
.
Вы, конечно, можете сосчитать отдельные элементы вручную, но
для больших массивов и строк это довольно затруднительно.
Три другие полезные предопределенные переменные - это
??DATA, ??TIME и ??FILENAME. ??DATE содержит дату ассемблирова-
ния в виде текстовой строки в формате 01/02/88. ??TIME содержит
время ассемблирования в виде 13:45:06, а ??FILENAME - имя ассем-
блируемого файла в виде заключенной в кавычки строки из 8 симво-
лов (например, "TEST.ASM").
Директива =
-----------------------------------------------------------------
Директива = аналогична директиве EQU во всех отношениях,
кроме одного: в то время как метки, определенные с помощью дирек-
тивы EQU, переопределять не допускается (в этом случае происходит
ошибка), метку, определенную с помощью директивы =, можно свобод-
но переопределять.
Например, в следующем фрагменте директива = используется для
генерации таблицы первых 100 произведений числа 10:
.
.
.
.DATA
MultipleOf10 LABEL WORD
TEMP = 0
REPT 100
DW TEMP
TEMP = TEMP+10
ENDM
.
.
.
shl bx,1 ; BX - число, которое нужно
; умножить на 10
; сдвиг влево для умножения
; на 2 (для формирования
; таблицы слов)
mov ax,[MultipleOf10+bx] ; получить число
; * 10
.
.
.
При вычислении всех операндов директивы = должно получаться
числовое значение - в отличие от директивы EQU с помощью директи-
вы = меткам нельзя присваивать текстовые строки.
Строковые инструкции
-----------------------------------------------------------------
Теперь мы подошли к рассмотрению наиболее мощных и необычных
инструкций процессора 8086 - инструкций для работы со строками.
Строковые инструкции отличаются от прочих инструкций процессора
8086. Они могут (в одной инструкции) обращаться к памяти и увели-
чивать или уменьшать регистр-указатель. Одна строковая инструкция
может обращаться к памяти 130000 раз.
Как ясно из их названия, строковые инструкции особенно по-
лезны при работе с текстовыми строками. Их можно также использо-
вать при работе с массивами, буферами данных и любыми типами
строк байт или слов. Строковыми инструкциями следует пользоваться
там, где только это возможно, поскольку они, как правило, короче
и работают быстрее, чем эквивалентная им комбинация обычных инс-
трукций процессора 8086, таких, как MOV, INC и LOOP.
Мы рассмотрим две различные по функциональному назначению
группы строковых инструкций: строковые инструкции для перемещения
данных (LODS, STOS и MOVS) и строковые инструкции, используемые
для поиска и сравнения данных (SCAS и CMPS).
Строковые инструкции перемещения данных
-----------------------------------------------------------------
Строковые инструкции перемещения данных во многом аналогичны
инструкции MOV, но могут выполнять больше функций, чем инструкция
MOV и работают быстрее. Мы рассмотрим сначала инструкцию LODS.
Заметим, что во всех строковых инструкциях флаг указания направ-
ления задает направление, в котором изменяются регистры-указате-
ли.
Инструкция LODS
-----------------------------------------------------------------
Инструкция LODS, которая загружает байт или слово из памяти
в аккумулятор (накопитель), подразделяется на две инструкции -
LODSB и LODSW. Инструкция LODSB загружает байт, адресуемый с по-
мощью пары регистров DS:SI, в регистр AL и уменьшает или
увеличивает регистр SI (в зависимости от состояния флага направ-
ления). Если флаг направления равен 0 (установлен с помощью инст-
рукции CLD), то регистр SI увеличивается, а если флаг направления
равен 1 (установлен с помощью инструкции STD), то регистр SI
уменьшается. И это верно не только для инструкции LODSB, флаг
направления управляет направлением, в котором изменяются все ре-
гистры-указатели строковых инструкций.
Например, в следующем фрагменте программы:
.
.
.
cld
mov si,0
lodsb
.
.
.
инструкция LODSB загружает регистр AL содержимым байта со смеще-
нием 0 в сегменте данных и увеличивает значение регистра SI на 1.
Это эквивалентно выполнению следующих инструкций:
.
.
.
mov si,0
mov al,[si]
inc si
.
.
.
однако инструкция LODSB работает существенно быстрее (и занимает
на два байта меньше), чем инструкции:
mov al,[si]
inc si
Инструкция LODSW аналогична инструкции LODSB. Она сохраняет
в регистре AX слово, адресуемое парой регистров DS:SI, а значение
регистра SI уменьшается или увеличивается на 2, а не на 1. Напри-
мер, инструкции:
.
.
.
std
mov si,0
lodsw
.
.
.
загружают слово со смещением 10 в сегменте данных в регистр RU, а
затем значение SI уменьшается на 2.
Инструкция STOS
-----------------------------------------------------------------
Инструкция STOS - это дополнение инструкции LODS. Она запи-
сывает значение размером в байт или слово из аккумулятора в ячей-
ку памяти, на которую указывает пара регистров ES:DI, а затем
увеличивает или уменьшает DI. Инструкция STOSB записывает байт,
содержащийся в регистре AL, в ячейку памяти по адресу ES:DI, а
затем увеличивает или уменьшает регистр DI, в зависимости от флага
направления. Например, инструкции:
.
.
.
std
mov di,0ffffh
mov al,55h
stosb
.
.
.
записывают значение 55h в байт со смещением 0FFFFh в сегменте, на
который указывает регистр ES, а затем уменьшает DI до значения
0FFFEh.
Инструкция STOSW работает аналогично, записывая значение
размером в слово, содержащееся в регистре AX, по адресу ES:DI, а
затем увеличивает или уменьшает значение регистра DI на 2. Напри-
мер, инструкции:
.
.
.
cld
mov di,0ffeh
mov al,102h
stosw
.
.
.
записывают значение 102h размером в слово, записанное в регистре
AX, по смещению 0FFEh в сегменте, на который указывает регистр
ES, а затем значение регистра DI увеличивается до 1000h.
Инструкции LODS и STOS можно прекрасно использовать вместе
для копирования буферов. Например, следующая подпрограмма копиру-
ет завершающуюся нулевым символом строку, записанную по адресу
DS:SI, в строку по адресу ES:DI:
;
; Подпрограмма для копирования завершающейся нулем строки
; в другую строку
;
; Ввод:
; DS:SI - строка, из которой выполняется копирование
; ES:DI - строка, в которую выполняется копирование
;
; Вывод: нет
;
; Изменяемые регистры: AL, SI, DI
;
CopyString PROC
cld ; обеспечить увеличение SI и
; DI в строковых инструкциях
CopyStringLoop:
lodsb ; получить символ исходной
; строки
stosb ; записать символ в выходную
; строку
cmp al,0 ; последним символом строки
; был 0?
jnz CopyStringLoop ; нет, обработать следую-
; щий символ
ret ; да, выполнено
CopyString ENDP
Аналогично вы можете использовать инструкции LODS и STOS для
копирования блока байт, которые не завершаются нулем, используя
для этого цикл:
.
.
.
mov cx,ARRAY_LENGTH_IN_WORDS ; размер массива
mov si,OFFSET SourceArray ; исходный массив
mov ax,SEG SourceArray
mov dx,ax
mov di,OFFSET DestArray ; целевой массив
mov ax,SEG DestArray
mov es,ax
cld
CopyLoop:
lodsw
stosw
loop CopyLoop
.
.
.
Однако для перемещения байта или слова из одного места в па-
мяти в другое есть еще более лучший способ. Это инструкция MOVS.
Инструкция MOVS
-----------------------------------------------------------------
Инструкция MOVS аналогична инструкциям LODS и STOS, если их
объединить в одну инструкцию. Эта инструкция считывает байт или
слово, записанное по адресу DS:SI, а затем записывает это значе-
ние по адресу, определяемому парой регистров ES:DI. Слово или
байт не передается при этом через регистры, поэтому содержимое
регистра AX не изменяется. Инструкция MOVSB имеет минимально воз-
можную для инструкции длину. Она занимает только один байт, а ра-
ботает еще быстрее, чем комбинация инструкций LODS и STOS. С при-
менением инструкции MOVS последний пример приобретает вид:
.
.
.
mov cx,ARRAY_LENGTH_IN_WORDS
mov si,OFFSET SourceArray
mov ax,SEG SourceArray
mov ds,ax
mov di,OFFSET DestArray
mov ax,SEG DestArray
mov es,ax
cld
CopyLoop:
movsw
loop CopyLoop
.
.
.
Повторение строковой инструкции
-----------------------------------------------------------------
Хотя в последнем примере код выглядит довольно эффективным,
неплохо было бы избавиться от инструкции LOOP и перемещать весь
массив с помощью одной инструкции. Инструкции процессора 8086
предоставляют такую возможность. Это форма строковых инструкций с
префиксом REP.
Префикс повторения REP - это не инструкция, а префикс инс-
трукции. Префикс инструкции изменяет работу последующей инструк-
ции. Префикс REP делает следующее: он указывает, что последующую
инструкцию нужно повторно выполнять до тех пор, пока содержимое
регистра CX не станет равным 0. (Если регистр CX равен 0 в начале
выполнения инструкции, то инструкция выполняется 0 раз, другими
словами, никаких действий не производится.)
Используя префикс REP, можно заменить в последнем примере
инструкции:
CopyLoop:
movsw
loop CopyLoop
на инструкцию:
rep movsb
Эта инструкция будет перемещать блок из 65535 слов (0FFFFh)
из памяти, начинающейся с адреса DS:SI в память, начинающуюся с
адреса, определяемого регистрами ES:DI.
Конечно, для выполнения инструкции 65535 раз потребуется го-
раздо больше времени, чем для выполнения инструкции один раз,
ведь для обращения ко всей этой памяти требуется время. Однако
каждое повторение (с помощью префикса) строковой инструкции вы-
полняется быстрее, чем выполнение одной строковой инструкции. Это
позволяет получить очень быстрый способ чтения из памяти, записи
в память и копирования.
Префикс REP можно использовать не только с инструкцией MOVS,
но также и с инструкциями LODS и STOS (и инструкциями SCAS и CMPS
- это мы обсудим позднее). Инструкцию STOS можно с успехом повто-
рять для очистки или заполнения блоков памяти, например:
.
.
.
cld
mov ax,SEG WordArray
mov es,ax
mov di,OFFSET WordArray
sub ax,ax
mov cx,WORD_ARRAY_LENGTH
rep stosw
.
.
.
Здесь массив WordArray заполняется нулями. Для повторения
инструкции LODS соответствующее полезное приложение придумать
трудно.
Префикс REP вызывает повторение только строковой инструкции.
Инструкция типа:
rep mov ax,[bx]
не имеет смысла. В этом случае префикс REP игнорируется и выпол-
няется инструкция:
mov ax,[bx]
Выход указателя за границы строки
-----------------------------------------------------------------
Заметим, что при выполнении строковой инструкции увеличение
или уменьшение регистров SI и DI выполняется после обращения к
памяти. Это означает, что после выполнения инструкции регистры не
указывают на ту ячейку, к которой только что выполнялось обраще-
ние, они указывают на следующую ячейку, к которой нужно обратить-
ся. В действительности это очень удобно, поскольку позволяет вам
эффективно организовывать циклы, аналогичные тем, которые приве-
дены в примерах последнего раздела. Однако иногда это может при-
водить к путанице, особенно когда с помощью строковой инструкции
выполняется поиск данных.
Поиск данных с помощью строковой инструкции
-----------------------------------------------------------------
Как работают строковые инструкции перемещения данных, вы уже
видели. Теперь мы рассмотрим строковые инструкции просмотра и
сравнения - SCAS и CMPS. Эти инструкции используются для просмот-
ра и сравнения блоков памяти.
Инструкция SCAS
-----------------------------------------------------------------
Инструкция SCAS используется для просмотра памяти и поиска
совпадения или несовпадения с конкретным значением размером в
байт или слово. Как и все строковые инструкции, инструкция SCAS
имеет две формы - SCASB и SCASW.
Инструкция SCASB сравнивает содержимое регистра AL с байто-
вым значением по адресу ES:DI, устанавливая при этом флаги, отра-
жающие результат сравнения (как при выполнении инструкции CMP).
Как и при выполнении инструкции STOSB, при выполнении инструкции
SCASB увеличивается или уменьшается значение регистра DI. Напри-
мер, в следующем фрагменте программы находится первое t (строчная
буква) в строке TextString:
.
.
.
.DATA
TextString DB 'Test text',0
TEXT_STRING_LENGTH EQU ($-TextString) ; длина строки
.
.
.
.CODE
.
.
.
mov ax,@Data
mov es,ax
mov di,OFFSET TextString ; ES:DI указывает
; на начало строки
; TextString
mov al,'t' ; искомый символ
mov cx,TEXT_STRING_LENGTH ; длина просматри-
; ваемой строки
cld ; увеличивать DI
; при просмотре
Scan_For_t_Loop:
csasb ; ES:DI совпадает
; c AL?
je Found_t ; да, мы нашли "t"
loop Scan_For_t_Loop ; нет, анализировать
; следующий символ
; Символ "t" не найден
.
.
.
; Символ "t" найден
Fount_t:
dec di ; ссылка обратно на
; смещение "t"
.
.
.
Заметим, что в данном примере после того, как найден символ
t, регистр DI увеличен, что отражает выход указателя за границы
строки (как мы уже обсуждали это ранее). Когда в данной программе
успешно выполняется последняя инструкция SCASB, после сравнения
DI увеличивается, поскольку последнее действие строковой инструк-
ции состоит в увеличении указателя (указателей). В результате ре-
гистр DI указывает на байт после символа t и его нужно выровнять,
чтобы компенсировать этот выход за пределы найденного символа и
сделать так, чтобы он указывал на t.
Лучше понять действие инструкции SCAS вам поможет сравнение
последнего пример с аналогичным фрагментом программы, но без ис-
пользования строковых инструкций:
.
.
.
Scan_For_t_Loop:
cmp es:[di],al ; ES:DI совпадает с AL?
je Found_t ; да, мы нашли "t"
inc di
loop Scan_For_t_Loop ; нет, анализировать
; следующий символ
.
.
.
Последний пример не совпадает в точности с примером, в кото-
ром используется инструкция SCASB, так как SCASB увеличивает DI
сразу, а в последнем примере DI увеличивается после инструкции
JE, чтобы избежать изменения флагов, установленных инструкцией
CMP.
Это позволяет сделать важное замечание, касающееся строковых
инструкций в целом. Строковые инструкции никогда не устанавливают
флаги таким образом, чтобы они отражали изменения значений ре-
гистров SI, DI и/или CX. Инструкции STOS и MOVS вообще не изменя-
ют никаких флагов, а инструкции SCAS и CMPS изменяют флаги только
в соответствии с результатом выполняемого ими сравнения.
Определенно, было бы удобно свести в предыдущем примере весь
цикл к одной инструкции. Как вы уже возможно догадались, это поз-
воляет сделать инструкция REP. Однако, может оказаться желатель-
ным прекратить выполнение цикла в случае совпадения или несовпа-
дения. Для этого существует две формы префикса REP, которые можно
использовать с инструкцией SCAS (и с CMPS) - REPE и REPNE.
Префикс REPE (который также называется префиксом REPZ) ука-
зывает процессору 8086, что инструкцию SCAS (или CMPS) нужно пов-
торять до тех пор, пока регистр CX не станет равным нулю, или
пока не произойдет несовпадение. Префикс REPE можно рассматри-
вать, как префикс, означающий "повторять, пока равно". Аналогич-
но, префикс REPNE (REPNZ) указывает процессору 8086, что инструк-
цию SCAS (CMPS) нужно повторять, пока CX не станет равным нулю
или пока не произойдет совпадения. Префикс REPNE можно рассматри-
вать, как префикс "повторять, пока не равно".
Приведем пример фрагмента программы, в котором для поиска в
строке TextString символа t используется одна инструкция SCASB:
.
.
.
.DATA
TextString DB 'Test text',0
TEXT_STRING_LENGTH EQU ($-TextString) ; длина строки
.
.
.
.CODE
.
.
.
mov ax,@Data
mov es,ax
mov di,OFFSET TextString ; ES:DI указывает
; на начало строки
; TextString
mov al,'t' ; искомый символ
mov cx,TEXT_STRING_LENGTH ; длина просматри-
; ваемой строки
cld ; увеличивать DI
; при просмотре
repne csasb ; искать во всей
; строке символ "t"
je Found_t ; да, мы нашли "t"
loop Scan_For_t_Loop ; нет, анализировать
; следующий символ
; Символ "t" не найден
.
.
.
; Символ "t" найден
Fount_t:
dec di ; ссылка обратно на
; смещение "t"
.
.
.
Как и все строковые инструкции, инструкция SCAS увеличивает
регистр-указатель DI, если флаг направления равен 0 (очищен с по-
мощью инструкции CLD), и увеличивает DI, если флаг направления
равен 1 (установлен с помощью инструкции STD).
Инструкция SCASW - это форма инструкции SCASB для работы со
словом. Она сравнивает содержимое регистра AX с содержимым памяти
по адресу ES:DI и увеличивает или уменьшает значение регистра DI
в конце каждого выполнения на 2, а не на 1. В следующем фрагменте
программы инструкция REPE SCASW используется, чтобы найти послед-
нюю ненулевую запись в массиве целых чисел размером в слово:
.
.
.
mov ax,SEG ShortIntArray
mov es,ax
mov di,OFFSET
ShortIntArray+(ARRAY_LEN_IN_WORDS-1)*2)
; ES:DI указывает на
; конец
; массива ShortIntArray
mov cx,ARRAY_LEN_IN_WORDS ; длина массива в словах
sub ax,ax ; поиск на несовпадение
; с нулем
std ; поиск в обратном
; направлении, DI
; уменьшается
repe scasw ; выполнять поиск, пока
; мы не встретим ненуле-
; вое слово или не вый-
; дем за границы массива
jne FondNonZero
; Весь массив заполнен нулями.
.
.
.
; Мы нашли ненулевой элемент - настроить DI, чтобы он
; указывал на этот элемент.
inc di
inc di
.
.
.
Инструкция CMPS
-----------------------------------------------------------------
Инструкция CMPS позволяет выполнять сравнение двух байт или
слов. При одном выполнении инструкции CMPS сравниваются две ячей-
ки памяти, а затем увеличиваются регистры SI и DI. Инструкцию
CMPS можно рассматривать, как аналог инструкции MOVS, который
вместо копирования одной ячейки памяти в другую сравнивает две
ячейки памяти.
Инструкция CMPSB сравнивает байт по адресу DS:SI с байтом по
адресу ES:DI, устанавливая соответствующим образом флаги и увели-
чивая или уменьшая регистры SI и DI (в зависимости от флага нап-
равления). Регистр AX при этом не изменяется.
Как и все строковые инструкции, инструкция CMPS имеет две
формы (для работы с байтами и для работы со словами), может уве-
личивать или уменьшать регистры SI и DI и будет повторяться при
наличии префикса REP. Приведем пример фрагмента программы, в ко-
тором проверяется идентичность первых 50 элементов двух массивов
элементов размером в слово, и для этого используется инструкция
CMPSW:
.
.
.
mov si,OFFSET Array1
mov ax,SEG Array1
mov ds,ax
mov di,OFFSET Array2
mov ax,SEG Array2
mov es,ax
mov cx,50 ; сравнить первые 50
; элементов
cd
repe cmpsw
jne ArraysAreDifferent ; массивы различны
; Первые 50 элементов совпадают.
.
.
.
; В массивах отличаются по крайней мере два элемента.
ArraysAreDifferent:
dec si
dec si ; обеспечить, чтобы
dec di ; SI и DI указывали на
dec di ; отличающиеся
; элементы
.
.
.
Использование в строковых инструкциях операндов
-----------------------------------------------------------------
Мы только что рассмотрели явные формы (для работы с байтами
и со словами) строковых инструкций. Другими словами, мы увидели,
как работают инструкции LODSB и LODSW, но не использовали инст-
рукцию LODS. Допускается также использование таких форм строковых
инструкций, где размер операнда явно не указывается. При этом
нужно обеспечить задание операндов таким образом, чтобы Турбо Ас-
семблер знал, работает он с байтами или со словами.
Можно привести следующий пример, эквивалентный использованию
инструкции MOVSB:
.
.
.
.DATA
String1 LABEL BYTE
db 'abcdefghi'
STRING1_LENGTH EQU ($-String1)
String2 DB 50 DUP (?)
.
.
.
.CODE
mov ax,@Data
mov ds,ax
mov es,ax
mov si,OFFSET String1
mov di,OFFSET String2
mov cx,STRING1_LENGTH
cld
rep movs es:[String2],[String2]
.
.
.
После того, как вы в качестве операндов инструкции MOVS за-
дадите String1 и String2, Турбо Ассемблер использует в качестве
размера данных размер операндов (в данном случае байт).
Однако в строковых инструкциях использование операндов име-
ет особый смысл. Операнды строковых инструкций - это "не настоя-
щие" операнды в том смысле, что они встроены в инструкцию, а
строковая инструкция работает в соответствии с указателями SI
и/или DI. Операнды используются только для задания размера дан-
ных, а не для действительной загрузки указателей. Взглянем на это
так: когда вы используете инструкцию типа:
mov al,[String1]
смещение String1 "встраивается" прямо в инструкцию на машинном
языке, соответствующую инструкции MOV. Однако, когда вы использу-
ете инструкцию:
lods [String1]
инструкция на машинном языке будет занимать 1 байт и соответство-
вать инструкции LODSB: Stirng1 в инструкцию не встраивается. В
этом случае вы должны обеспечить, чтобы регистры DS:SI указывали
на начало String1.
Операнды строковых инструкций аналогичны тем операндам, ко-
торые используются в директиве ASSUME в качестве сегментов. Ди-
ректива ASSUME не устанавливает сегментный регистр, она просто
сообщает Турбо Ассемблеру, что вы установили сегментный регистр,
поэтому Турбо Ассемблер может выполнить для вас проверку на нали-
чие ошибок. Аналогично, операнды строковых инструкций не устанав-
ливают регистры, они просто указывают Турбо Ассемблеру, что вы
установили SI и/или DI, благодаря чему Турбо Ассемблер может оп-
ределить размер операнда и выполнить проверку на наличие ошибок.
Дальнейшее обсуждение операндов строковых инструкций приведено в
разделе "Операнды строковых инструкций".
В разделе "Ошибки при работе со строковыми инструкциями" мы
обсудим некоторые моменты, касающиеся использования строковых
инструкций.
Программы, состоящие из нескольких модулей
-----------------------------------------------------------------
Рано или поздно вы подойдете к тому, что хранить исходный
текст каждой программы станет затруднительно. Использование для
исходного кода программы одного файла прекрасно подходит для не-
больших программ, таких, как примеры данного руководства, но даже
программы среднего размера приходится разбивать на несколько фай-
лов или модулей, которые ассемблируются отдельно и компонуются
вместе. Основное преимущество программ, состоящих из нескольких
модулей, состоит в том, что после того, как вы отредактируете ис-
ходный код, вам потребуется переассемблировать только те модули,
которые вы изменили, не затрагивая остальных модулей программы.
К тому же гораздо проще ориентироваться среди нескольких строк,
чем в одном большом файле.
Создать программу, состоящую из нескольких модулей, очень
легко. Для обеспечения таких программ Турбо Ассемблер предусмат-
ривает три директивы: PUBLIC, EXTRN и GLOBAL. Мы рассмотрим их
поочередно, но сначала мы проанализируем пример программы, состо-
ящей из двух модулей, после чего вам будет ясен контекст, в кото-
ром мы будет обсуждать указанные выше директивы. Основная прог-
рамма MAIN.ASM имеет следующий вид:
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
String1 DB 'Добрый ',0
String2 DB 'день!',0dh,0ah,'$',0
GLOBEL FinalString:BYTE
FinalString DB 50 DUP (?)
.CODE
EXTRN ConcatenateStrings:PROC
ProgramStart:
mov ax,@Data
mov ds,ax
mov ax,OFFSET String1
mov bx,OFFSET String2
call ConcatenateStrings ; объединение двух
; строк в одну строку
mov ah,9
mov dx,OFFSET FinalString
int 21h ; печать строки-
; результата
mov ax,4ch
int 21h ; выполнить
END ProgramStart
А вот другой модуль программы, SUB1.ASM:
DOSSEG
.MODEL SMALL
.DATA
GLOBAL FinalString:BYTE
.CODE
;
; Подпрограмма копирует сначала одну строку,
; затем другую, в строку FinalString
;
; Ввод:
; DS:AX = указатель на первую копируемую строку
; DS:BX = указатель на вторую копируемую строку
;
; Вывод: отсутствует
;
; Изменяемые регистры: AL, SI, DI, ES
;
PUBLIC ConcatenateStrings
ConcatenateStrings PROC
cld ; отсчет в прямом
; направлении
mov di,SEG FinelString
mov es,di
mov di,OFFSET FinelString ; ES:DI указывает
; на целевую строку
mov si,ax ; первая строка для
; копирования в StringLoop
lodsb ; получить символ строки 1
and al,al ; это 0?
jz DoString2 ; да, со строкой 1 покончено
stosb ; сохранить символ стоки 1
jmp StringLoop
DoString2:
mov si,bx ; вторая строка для копиро-
; вания в цикле String2Loop
lodsb ; получить символ строки 2
stosb ; сохранить символ строки 2
; (включая 0, когда мы
; его встретим)
and al,al ; это 0?
jnz String2Loop ; нет, обработать следующий
; символ
ret ; выполнено
ConcatenateString ENDP
END
Эти два модуля можно ассемблировать отдельно с помощью ко-
манд:
TASM main
и
TASM sub1
а затем скомпоновать их в программу MAIN.EXE с помощью команды:
TLINK main+sib1
При запуске командой:
main
программа MAIN.EXE, как можно догадаться, выводит на экран стро-
ку:
Добрый день!
Теперь, когда вы увидели программу, состоящую из нескольких
модулей, в действии, давайте рассмотрим три директивы, обеспечи-
вающие такое программирование.
Директива PUBLIC
-----------------------------------------------------------------
Действие директивы PUBLIC достаточно просто. Она указывает
Турбо Ассемблеру, что соответствующую метку или метки нужно сде-
лать доступными для других модулей. Здесь можно использовать
практически любые метки, включая имена процедур и переменных па-
мяти, а также приравненные метки. Директива PUBLIC обеспечивает
доступность этих меток другим модулям. Например:
.
.
.
.DATA
PUBLIC MemVar, Array1, ARRAY_LENGTH
ARRAY_LENGTH EQU 100
MemVar DW 10
Array1 DB ARRAY_LENGTH DUP (?)
.
.
.
.CODE
PUBLIC NearProc, FarProc
NearProc PROC NEAR
.
.
.
NearProc ENDP ; ближняя процедура
.
.
.
FarProc LABEL PROC ; дальняя процедура
.
.
.
END
Здесь имена приравненной метки, переменной размером в слово,
массива и процедуры с ближнем типом обращения доступны другому
модулю, который будет компоноваться с данным модулем.
Однако имеется один тип меток, которые нельзя сделать обще-
доступными. Это приравненные метки, которые равны значениям-конс-
тантам с размерами, отличными от 1 или 2 байт. Например, следую-
щие метки общедоступными сделать нельзя:
LONG_VALUE EQU 1000h
TEXT_SYMBOL EQU <TextString>
При ассемблировании Турбо Ассемблер обычно игнорирует ре-
гистр буквы, поэтому все общедоступные метки преобразуются в про-
писные символы (верхний регистр). Если вы хотите, чтобы в обще-
доступных метках различались буквы верхнего и нижнего регистров,
то при ассемблировании всех модулей, содержащих ссылки на обще-
доступные метки, нужно использовать в командной строке Турбо Ас-
семблера параметр /ML или /MX.
Например, без параметра /MX или /ML в других модулях следую-
щие две метки будут эквивалентными:
PUBLIC Symbol1, SYMBOL1
При использовании для обеспечения различимости строчных и
прописных букв в общедоступных и внешних идентификаторах парамет-
ра командной строки /MX нужно внимательно указывать буквы верхне-
го или нижнего регистров в директивах PUBLIC или EXTRN. Турбо Ас-
семблер делает доступным для других модулей тот идентификатор,
который указывает в директиве PUBLIC или EXTRN, а не тот на кото-
рый делается ссылка или который переопределяется внутри модуля.
Например, директива:
PUBLIC Abc
abC Dw
приводит к тому, что общедоступным будет имя Abc, а не abC.
Для каждого идентификатора в директиве PUBLIC можно также
задать язык: C, FORTRAN, PASCAL, BASIC, PROLOG и NOLANGUAGE (нет
языка). Это приводит к тому, что к имени идентификатора до того,
как одно в объектном файле станет общедоступным, автоматически
применяются правила конкретного языка. Например, если вы описали:
PUBLIC C myprog
то идентификатор myprog в исходном файле станет общедоступным,
как _myproc, поскольку по соглашениям языка Си перед именами
идентификаторов следует символ подчеркивания. Использование иден-
тификатора языка в директиве PUBLIC временно отменяет текущее за-
дание языка (используемое по умолчанию или заданное в директиве .
MODEL). (Чтобы работало данное средство, не обязательно должна
действовать директива .MODEL.)
Директива EXTRN
-----------------------------------------------------------------
В последнем разделе, чтобы сделать метки MemVar, Array1,
ARRAY_LENGTH, NearProc и FarProc доступными для других модулей,
мы использовали директиву PUBLIC. На далее возникает вопрос, ка-
ким образом другие модули могут ссылаться на эти метки?
Для того, чтобы сделать метки из другого модуля доступными в
данном модуле, используется директива EXTRN. После того, как ди-
ректива EXTRN будет использована, чтобы сделать доступным в дан-
ном модуле метку из другого модуля, эту метку можно использовать
также, как если бы она была определена в текущем модуле. Приве-
дем пример другого модуля, в котором директива EXTERN использует-
ся для ссылок на общедоступные метки, описанные в последнем раз-
деле:
.
.
.
.DATA
EXTRN MemVar:WORD,Array1:BYTE,ARRAY_LENGTH:ABS
.
.
.
.CODE
EXTRN NearProc:NEAR,FarProc:FAR
.
.
.
mov ax,[MemVar]
mov bx,OFFSET Array1
mov cx,ARRAY_LENGTH
.
.
.
call NearProc
.
.
.
call FarProc
.
.
.
Заметим, что все пять меток используются как обычно. Единст-
венное отличие от программы на Ассемблере, состоящей из одного
модуля, является директива EXTRN.
За каждой меткой, объявленной в директиве EXTRN, следует
двоеточие и тип. Тип необходимо указывать, иначе Турбо Ассемблер
не будет знать какую именно метку вы объявляете с помощью дирек-
тивы EXTRN. За одним исключением используемые для внешних
(external) меток типы совпадают с типами, которые могут использо-
ваться в директиве LABEL. Допустимы следующие типы:
ABS - абсолютное значение;
BYTE - переменная (данные) размером в байт;
DWORD - переменная (данные) размером в двойное
слово (4 байта);
DATAPTR - указатель на данные ближнего или дальнего ти-
па, в зависимости от модели памяти;
FAR - метка кода с дальним типом обращения
(переход осуществляется загрузкой
регистров CS:IP);
FWORD - 6-байтовая переменная (данные);
NEAR - метка кода с ближним типом обращения
(при переходе загружается только
регистр IP);
PROC - метка процедуры (NEAR или FAR, в
соответствии с директивой .MODEL);
QWORD - переменная (данные) размером в
четверное слово (8 байт);
Имя структуры - имя определенного пользователем типа STRUC;
TBYTE - 10-байтовая переменная (данные);
UNKNOWN - неизвестный тип;
WORD - переменная (данные) размером в слово
(2 байта).
Единственным незнакомым типом внешних данных является тип
ABS, который используется для объявления метки, определенной в ее
исходном модуле с помощью директивы EQU или =. Другими словами,
это метка, которая просто представляет собой имя константы и не
связана с адресами кода или данных.
Очень важно, чтобы для внешних меток вы задавали корректный
тип данных, так как Турбо Ассемблер будет генерировать код на ос-
нове заданных вами типов данных, и у него нет другого способа оп-
ределить, что ваша спецификация некорректна. Например, если вы
случайно ввели:
.
.
.
.CODE
EXTRN FarProc:NEAR
.
.
.
call FarProc
.
.
.
а в другом модуле содержится:
.
.
.
PUBLIC FarProc
FARPROC PROC FAR
.
.
.
ret
FarProc ENDP
.
.
.
то Турбо Ассемблер в соответствие с типом данных, заданным вами в
директиве EXTRN, сгенерирует ближнее обращение к процедуре
FarProc. Можно с определенностью сказать, что такая программа
корректно работать не будет, поскольку FarProc на самом деле яв-
ляется процедурой с дальним типом обращения и завершается соот-
ветствующей инструкцией RET.
Как уже описывалось в последнем разделе, Турбо Ассемблер
обычно (по умолчанию) не различает верхний и нижний регистры
букв, поэтому общедоступные метки преобразуются в верхний ре-
гистр. Это означает, что в обычном состоянии внешние метки будут
интерпретироваться в верхнем регистре. Если вы ходите, чтобы во
внешних метках регистры букв различались, используйте параметры
командной строки /ML или /MX.
Для каждого идентификатора в директиве EXTRN вы можете также
задать язык: C, PASCAL, BASIC, FORTRAN, PROLOG или NOLANGUAGE
(в последнем случае язык не используется). Это приводит к тому,
что перед тем,как имя станет в объектном файле общедоступным, к
нему автоматически применяются правила указанного языка. Напри-
мер, если вы описали:
EXTRN C myprog:NEAR
то идентификатор myprog в исходном файле преобразуется во внешний
идентификатор _myprog. Использование спецификатора языка в дирек-
тиве EXTRN временно отменяет текущее задание языка (который ис-
пользуется по умолчанию или задан в директиве .MODEL). (Чтобы
работало данное средство, директива .MODEL не обязательно должна
действовать.)
Директива GLOBAL
-----------------------------------------------------------------
Прочитав последние разделы, вы можете удивиться, зачем для
выполнения одной работы (обеспечения совместного использования
меток разными модулями) нужны две директивы - PUBLIC и EXTRN? В
действительности единственная причина использования двух директив
заключается в необходимости обеспечить совместимость с более ран-
ними ассемблерами. В Турбо Ассемблере имеется директива GLOBAL,
которая делает все то, что делают директивы PUBLIC и EXTRN.
Если с помощью данной директивы вы объявите метку глобаль-
ной, а затем определите ее (с помощью директив DB, DW, PROC,
LABEL или других подобных директив), то метка станет доступной
другим модулям аналогично тому, как если бы вы вместо директивы
GLOBAL использовали директиву PUBLIC. Если же вы, с другой сторо-
ны, объявляете метку глобально, а затем используете ее без опре-
деления, то эта метка интерпретируется, как внешняя метка, анало-
гично тому, как если бы вы объявили ее с помощью директивы EXTRN.
Рассмотрим, например, следующий фрагмент:
.
.
.
.DATA
GLOBAL FinalCount:WORD,PromptString:BYTE
FinalCount DW ?
.
.
.
.CODE
GLOBAL DoReport:NEAR,TallyUp:FAR
TallyUp PROC FAR
.
.
.
call DoReport
.
.
.
Здесь метки FinalCount и TallyUp определены, поэтому они
становятся общедоступными (для других модулей) метками (public).
Метки PromptString и DoReport не определены, поэтому подразумева-
ется, что это внешние (external) метки, которые объявлены обще-
доступными в других модулях.
Директиву GLOBAL очень удобно использовать, например, во
включаемых файлах (эти файлы мы обсудим в следующем разделе).
Предположим, у вас есть множество меток, которые вы хотите сде-
лать доступными в программе (состоящей из нескольких модулей) для
других модулей. Неплохо было бы, если бы мы смогли определить все
эти метки во включаемом файле, а затем включить этот файл в каж-
дый модуль. К сожалению, с помощью директив PUBLIC и EXTRN это
невозможно, так как директива EXTRN не будет работать в том моду-
ле, в котором определена данная метка, а директива PUBLIC будет
работать только в том модуле, в котором данная метка определена.
Однако директива GLOBAL допустима во всех модулях, поэтому вы мо-
жете сформировать включаемый файл, где все нужные метки объявлены
глобальными, а затем включить данный файл во все ваши модули.
Для каждого идентификатора в директиве GLOBAL, как и для ди-
ректив PUBLIC или EXTRN, можно также задать язык: C, FORTRAN,
PASCAL, BASIC, PROLOG и NOLANGUAGE (нет языка). Это приводит к
тому, что к имени идентификатора до того, как одно в объектном
файле станет общедоступным, автоматически применяются правила
конкретного языка. Например, если вы описали:
GLOBAL C myprog
то идентификатор myprog в исходном файле станет общедоступным,
как _myproc, поскольку по соглашениям языка Си перед именами
идентификаторов следует символ подчеркивания. Использование иден-
тификатора языка в директиве PUBLIC временно отменяет текущее за-
дание языка (используемое по умолчанию или заданное в директиве
.MODEL). (Чтобы работало данное средство, не обязательно должна
действовать директива .MODEL.)
Включаемые файлы
-----------------------------------------------------------------
Часто оказывается желательным включить один и тот же блок
исходного кода Ассемблера в несколько исходных модулей. Вы можете
захотеть использовать в различных модулях одной программы ка-
кие-либо присваивания или макрокоманды, или использовать их в
разных программах. При этом пришлось бы написать длинную програм-
му, которую нежелательно разбивать на несколько компонуемых моду-
лей (например, программу, которая должна записываться в ПЗУ), но
такая программа слишком велика и ее неудобно будет хранить в од-
ном файле. В этом случае чрезвычайно удобной оказывается директи-
ва INCLUDE.
Когда Турбо Ассемблер встречает директиву INCLUDE (вклю-
чить), он помечает это место в текущем модуле Ассемблера, обраща-
ется к диску и находит указанный включаемый файл и начинает ас-
семблирование этого файла, как если бы все строки включаемого
файла были записаны прямо в исходном модуле. При достижении конца
включаемого файла Турбо Ассемблер возвращается к строке, следую-
щей в исходном модуле за директивой INCLUDE, и возобновляет ас-
семблирование с этой точки. Таким образом, там, где встречается
директива INCLUDE, текст включаемого файла включается в ассембли-
рование текущего исходного модуля Ассемблера.
Например, если файл MAINPROG.ASM содержит:
.
.
.
.CODE
mov ax,1
INCLUDE INCPROG.ASM
push ax
.
.
.
а файл INCPROG.ASM содержит:
mov bx,5
add ax,bx
то результат ассемблирования файла MAINPROG.ASM будет в точности
эквивалентен ассемблированию кода:
.
.
.
.CODE
mov ax,1
mov bx,5
add ax,bx
push ax
.
.
.
Допускается вложенность включаемых файлов на произвольную
глубину, другими словами, включаемые файлы также могут содержать
директивы INCLUDE (включать другие файлы). Включенные строки мож-
но легко обнаружить в файле листинга, так как Турбо Ассемблер
слева от каждой включенной строки помещает номер, указывающий
глубину вложенности файлов модулей (включаемые файлы могут иметь
произвольную вложенность).
Откуда Турбо Ассемблер знает, где искать включаемые файлы?
Если в операнде директивы INCLUDE, определяющим имя включаемого
файла, укажете диск или маршрут доступа к файлу, то Турбо Ассемб-
лер будет искать файл только в указанном вами месте. Если же вы
зададите имя файла без указания маршрута и диска, то Турбо Ас-
семблер сначала ищет файл в текущем каталоге. Если он не может
найти заданный файл в текущем каталоге, то поиск продолжается в
каталогах, заданных в параметре командной строки -I (если он ука-
зывается). Например, при задании команды:
TASM -ic:\include testprog
и строки:
INCLUDE MYMACROS.ASM
(в файле TESTPROG.ASM) Турбо Ассемблер будет сначала искать в те-
кущем каталоге файл MYMACROS.ASM, а не найдя его, выполнит поиск
в каталоге C:\INCLUDE. Если файл MYMACROS.ASM не будет найден и
там, то Турбо Ассемблер выведет сообщение об ошибке.
Кстати, в спецификации маршрута в директиве INCLUDE можно
указывать обратную косую черту (\). Это обеспечивает совмести-
мость с MASM.
Включаемые файлы полезно использовать для обеспечения дос-
тупности ваших библиотек и макрокоманд в самых различных модулях
на языке Ассемблера. Включаемые файлы также оказываются очень по-
лезными при совместном использовании в различных модулях програм-
мы строковых присваиваний, объявлений глобальных меток и сегмен-
тов данных. Во включаемых файлах исходный код Ассемблера исполь-
зуется редко, поскольку отдельные модули исходного кода можно
просто компоновать вместе, но во включаемых файлах вполне допус-
тимо использовать строки исходного кода (любые допустимые строки
Ассемблера).
Файлы листинга
-----------------------------------------------------------------
Обычно Турбо Ассемблер создает в результате ассемблирования
только объектный файл (файл с расширением .OBJ), имя которого
совпадает с именем исходного (.ASM) файла.
Если вы хотите, можно также указать Турбо Ассемблеру, что
нужно создать файл листинга (с расширением .LST). Для этого в ко-
мандной строке просто вводятся две дополнительные запятые (или
имени файла). Например, если команда:
TASM hello
ассемблирует файл HELLO.ASM и создает объектный файл HELLO.OBJ,
то командная строка:
TASM hello,,
генерирует файл листинга HELLO.LST. Вместо последней командной
строки можно использовать следующие эквивалентные команды:
TASM hello,hello,hello
и
TASM /L hello
Результат при этом будет тот же.
Имена объектного файла и/или файла листинга не обязательно
должны совпадать с именем исходного файла, однако довольно редко
возникает необходимость задавать для них разные имена.
Основу содержания файла листинга составляет содержание ис-
ходного файла, дополненного различной информацией о результатах
ассемблирования. Для каждой исходной строки Турбо Ассемблер
включает в листинг соответствующую инструкцию машинного кода и
смещение каждой строки машинного кода в текущем сегменте. Кроме
того, Турбо Ассемблер выводит в файле листинга таблицы, где со-
держится информация о метках и сегментах, используемых в програм-
ме, включая значение и тип каждой метки и атрибуты каждого сег-
мента.
Турбо Ассемблер может также (по запросу) генерировать для
всех меток исходного файла таблицу перекрестных ссылок, в которой
указывается, где была определена каждая метка и где на нее есть
ссылка (см. описание параметра командной строки /C в Главе 3).
Рассмотрим сначала основные элементы листинга - ассемблиро-
ванный машинный код и смещение каждой инструкции.
Пример файла листинга
-----------------------------------------------------------------
Приведем листинг примера программы HELLO.ASM.
Turbo Assembler Version 2.0 06-29-90 16:21:27 Page 1
Hello.ASM
1 DOSSEG
2 0000 .MODEL SMALL
3 0000 .STACK 100h
4 0100 .DATA
5 0000 48 65 6C 6C 6F 2C 20 + Message DB 'Hello, word',13,10,12
6 77 6F 72 6C 64 0D 0A +
7 0C
8 = 000F HELLO_MESSAGE_LENGTH EQU $-Message
9 000F .CODE
10 0000 B8 0000s mov ax,@Data
11 0003 8E D8 mov ds,ax ; установить DS в значение
12 ; сегмента данных
13 0005 B4 40 mov ah,40h ; функция DOS вывода на
14 ; устройство
15 0007 BB 0001 mov bx,1 ; стандартный указатель
16 ; вывода
17 000A B9 000F mov cx,HELLO_MESSAGE_LENGTH ; число
18 ; выводимых символов
19 000D BA 000F mov dx,OFFSET Message ; выводимая
20 ; строка
21 001D CD 21 int 21h ; вывести "Hello"
22 0012 B4 4C mov ah,4ch ; функция DOS завершения
23 ; программы
24 END
Turbo Assembler Version 2.0 06-29-90 16:21:27 Page 2
Symbol Table
Symbol Name Type Value
??DATE Text "06-29-88"
??FILENAME Text "HELLO "
??TIME Text "16:21:26"
??VERSION Number 004A
@CODE Text _TEXT
@CODESIZE Text 0
@CPU Text 0101H
@CURSEG Text _TEXT
@DATA Text DGROUP
@DATASIZE Text 0
@FILENAME Text HELLO
@WODRSIZE Text 2
MESSAGE Byte DGROUP:0000
HELLO_MESSAGE_LENGTH Number 000F
Groups & Segments Bit Size Align Combine Class
DGROUP Group
STASK 16 0100 Para Stack STASK
_DATA 16 000F Word Public DATA
_TEXT 16 0016 Word Public CODE
В верхней части каждой страницы листинга выводится заголо-
вок, состоящий из версии Турбо Ассемблера (с помощью которого вы-
полнено ассемблирование файла), даты и времени ассемблирования и
номера страницы листинга.
Листинг состоит из двух частей: расширенного исходного кода
и таблицы идентификаторов (Symbol Table). Сначала выводится ис-
ходный код Ассемблера, с заголовком и имя файла, в котором нахо-
дится исходный код. Исходный код Ассемблера сопровождается инфор-
мацией о машинном коде инструкций, из которых Турбо Ассемблер вы-
полнил трансляцию. Все ошибки и предупреждения, обнаруженные при
ассемблировании, включаются в листинг непосредственно за той
строкой, где они встретились.
Строки кода в листинге имеют следующий формат:
<глубина> <номер_строки> <смещение> <машинный_код> <исходный_код>
"Глубина" указывает уровень вложенности включаемых файлов и
макрокоманд в вашем файле листинга.
"Номер_строки" представляет собой номер строки файла листин-
га (исключая строки заголовка и титульные строки). Номера строк
особенно полезны при использовании средства Турбо Ассемблера ге-
нерации перекрестных ссылок, где в ссылках указываются номера
строк. В файле HELLO.LST директива DOSSEG содержится на строке 1
файла листинга, директива .MODEL - на строке 2 и т.д.
Учтите, что номера строки в поле "номер_строки" - это не но-
мера строк исходного модуля. Например, при расширении макрокоман-
ды или включении файла отсчет строк продолжается, хотя текущая
строка в исходном файле остается той же. Чтобы перевести номер
строки (сгенерированный, например, при создании перекрестных ссы-
лок), вы должны найти соответствующую строку в листинге, а затем
(по номеру или на глаз) найти ее в исходном файле.
"Смещение" - это смещение в текущем сегменте (от начала) ма-
шинного кода, генерируемого соответствующей строкой исходного
кода. Например, Message начинается со смещения 0 в сегменте дан-
ных.
"Машинный_код" представляет собой действительную последова-
тельность шестнадцатиричного значения байт и слов, которые ас-
семблируются из соответствующей исходной строки Ассемблера. Нап-
ример, инструкция MOV AX,@Data начинается по смещению 0 в сегмен-
те кода. Информация справа от данной инструкции - это машинный
код, в который ассемблируется инструкция, то есть инструкция MOV
AX,@Data ассемблируется в B8 0000s (в шестнадцатиричном представ-
лении). 0B8h - это инструкция на машинном языке, загружающая в
регистр AX значение-константу, а 0000s - это постоянное значение
@Data, которое загружается в AX. Вся инструкция MOV AX,@Data ас-
семблируется в три байта машинного кода.
Заметим, что в файле листинга указано, что следующая за MOV
AX,@Data инструкция (которой является инструкция MOV DS,AX) начи-
нается со смещения 3 в сегменте кода. И это имеет совершенно чет-
кий смысл, поскольку инструкция MOV AX,@Data начинается со смеще-
ния 0 и имеет длину 3 байта. Машинные код, получающийся в
результате ассемблирования инструкции (8 D8) имеет длину 2 байта,
поэтому следующая инструкция начинается со смещения 5. Взглянув
на файл листинга, мы можем убедиться, что это именно так.
Как вы можете заметить, в файле листинга за показаны только
первые 7 байт машинного кода (за которыми следует символ +), ге-
нерируемые строкой:
Message DB 'Hello, word',13,10,'$'
Поля машинного кода, которые имеют слишком большую длину,
чтобы уместиться в поле "машинный_код", обычно усекаются и завер-
шаются символом +, что говорит о том, ассемблированы дополнитель-
ные байты, но они не указаны. Если вам необходимо увидеть все
байты машинного кода, можно использовать директиву %NOTHING (о
ней мы расскажем дальше). При указании данной директивы не вмес-
тившийся код будет переноситься на следующие строки.
Наконец,поле "исходный_код" - это просто исходная строка Ас-
семблера (вместе с комментариями). Некоторые строки на Ассембле-
ре (например, строки, содержащие только комментарии) не генериру-
ют никакого машинного кода, и поля "смещение" и "машинный_код" в
таких строках отсутствуют. Тем не менее номер строки им присваи-
вается.
Значение 0000s, соответствующее @Data, это только "замести-
теть" для действительного значения инструкции:
mov ax,@Data
Это происходит потому, что значения сегментов присваиваются
компоновщиком, а не Турбо Ассемблером, поэтому Турбо Ассемблер не
может занести сюда корректное значение. Все, что может сделать
Турбо Ассемблер - это дать вам понять, что данное значение явля-
ется значением сегмента, которое будет вычислено компоновщиком.
Об этом говорит буквы s в конце генерируемого данной инструкцией
машинного кода.
Аналогично, смещение в машинном коде, полученном из инструк-
ции:
mov dx,OFFSET Message
завершается буквой r, которая указывает, что смещение может быть
перемещаемым внутри сегмента при комбинировании его компоновщиком
с другими сегментами.
Приведем полный список обозначений, используемых Турбо Ас-
семблером для указания характеристик ассемблирования (таких, как
переместимость):
-----------------------------------------------------------------
Обозначение Значение
-----------------------------------------------------------------
r Указывает тип коррекции смещения идентификаторов
в модуле.
s Указывает тип коррекции сегментов для идентифика-
торов в модуле.
sr Указывает тип коррекции смещений и сегментов для
идентификаторов в модуле.
e Показывает коррекцию смещения для внешних иденти-
фикаторов.
se Показывает коррекцию указателя для внешних иден-
тификаторов.
so Показывает коррекцию только сегмента.
+ Показывает, что объектный код усечен.
-----------------------------------------------------------------
В листинге объектного кода обозначения r, s и sr используют-
ся для обозначения типа коррекции смещения, сегмента и указателя
(сегмента и смещения) для идентификаторов модуля. Обозначение e
показывает коррекцию смещения для внешних идентификаторов, а нe
указывает коррекцию указателя внешнего идентификатора. Коррекция
сегментов для внешних идентификаторов (обозначаемая, как s) ана-
логична локальным идентификаторам. Объектный код может также со-
держать в последнем столбце символ +, указывающий, что имеется
дополнительный объектный код, который нужно вывести, но он усе-
чен.
Самое левое поле листинга представляет собой счетчик уровня.
При ассемблировании из основного файла это поле остается пустым.
При ассемблировании из включаемых файлов это поле принимает зна-
чение 1 или 2, 3 и т.д., в зависимости от уровня вложенности каж-
дого включаемого файла. Тоже самое происходит при расширении мак-
рокоманд.
Как вы можете заметить, в файле листинга некоторые записи в
машинном коде показаны, как байтовые значения (две шестнадцати-
ричные цифры), а другие - как значения, размеров в слово. В этом
есть определенная логика: когда Турбо Ассемблер транслирует ма-
шинный код, представляющий собой значение размером в слово (нап-
ример, OFFSET Message, представляющее собой 16-битовое смещение),
то это смещение показывается, как значение размером в слово. Это
бывает очень полезно, поскольку в противном случае использующийся
в процессоре 8086 для хранения слов механизм "младший байт пер-
вым" приводил бы к тому, что порядок байт в словах был бы изменен
на обратный.
Например, инструкция:
mov ax,1234h
ассемблируется в 3 байта машинного кода: 0B8h, 034Hh и 012h (а
таком порядке). Если Турбо Ассемблер выведет эту инструкцию в
виде трех байт, то она будет показана, как:
B8 34 12
При этом байты значения размером в слово будет переставлены.
Турбо Ассемблер выводит такой машинный код, как:
B8 1234
что определенно легче читается.
Когда мы рассказывали о поле "смещение", мы уже говорили о
смещении в текущем сегменте меток и строк программы. Откуда же мы
можем узнать, в каком именно сегменте находится метка? Для этого
служат таблицы листингов, о которых мы далее расскажем.
Таблицы идентификаторов листинга
-----------------------------------------------------------------
Вторая часть файла листинга начинается с заголовка "Symbol
Table" (таблица идентификаторов). Эта часть состоит из двух таб-
лиц, в одной из которых описываются используемые в исходном коде
метки, а в другой перечисляются используемые сегменты.
Кстати, если вы не хотите, чтобы в листинге генерировалась
таблица идентификаторов, вы можете указать Турбо Ассемблеру, что
нужно генерировать только расширенный листинг кода (это можно
сделать с помощью параметра командной строки /N).
Таблица меток
-----------------------------------------------------------------
В первой таблице, которую мы называем таблицей меток, приве-
ден список всех меток исходного кода (в алфавитном порядке), а
также также их типы и значения. Например, файл листинга HELLO.LST
содержит следующую запись:
MESSAGE BYTE DGROUP:0000
Здесь MESSAGE - это имя метки или идентификатор. Оно указы-
вается прописными буквами, так как если вы не указываете парамет-
ры командной строки /MX или /ML, Турбо Ассемблер преобразует все
идентификаторы в верхний регистр. BYTE указывает размер данных
для того элемента данных, на который ссылается имя Message
(байт). DGROUP:0000 - это значение метки Message, означающее, что
эта метка начинается со смещения 0 в сегменте данных DGROUP.
(Помните, однако, что ссылка на Message в последнем разделе поме-
чена символом r. Это означает, что метка может быть перемещена
компоновщиком по другому смещению, когда в программе выполняется
компоновка других сегментов DGROUP. Информация о перемещениях
сегментов содержится в файле карты памяти, создаваемом компонов-
щиком.)
Аналогично, ProgramStart показана, как метка ближнего типа
со значением _TEXT:0000. _TEXT представляет собой имя сегмента,
определенного с помощью директивы .CODE, поэтому ProgramStart
расположена по первому адресу в сегменте кода. Мы ответили на
возникавший ранее вопрос о том, как можно определить, в каком
сегменте находится каждая метка, так как это указывается значени-
ем соответствующего поля таблицы меток (поля Value).
Другие перечисленные в листинге файла HELLO.ASM метки - это
метки, предопределенные Турбо Ассемблером при использовании упро-
щенных директив определения сегментов. Все эти метки устанавлива-
ются в значения, соответствующие текстовым строкам, и содержат
такие значения, как _TEXT и DGROUP (поле Value таблицы Symbol
Table).
Метки могут иметь один из следующих типов данных (см. поле
Type):
ABS DWORD NUMBER TBYTE
ALIAS FAR QWORD TEXT
BYTE NEAR STRUCT WORD
Как мы обсуждали в начале данной главы, с помощью присваива-
ния метки можно приравнять к любому постоянному значению или тек-
стовой строке. В поле значения (Value) таблицы меток указываются
те значения меток, которые вы задали. Для меток, связанных с ад-
ресами памяти (таких, как Message), в поле значения указывается
адрес метки.
Таблица меток - это то место в листинге, где можно найти ин-
формацию о типе и значении каждой метки, использованной в исход-
ном коде.
Таблица сегментов и групп
-----------------------------------------------------------------
Другой таблицей в этой части листинга является таблица сег-
ментов и групп (Groups & Segments). Группы сегментов, такие, как
DGROUP, просто указываются здесь, как группы, поскольку группы
сегментов сами не имеют атрибутов, а состоят из одного или более
сегментов. Сегменты, образующие в данном модуле группу, указыва-
ются в таблице сегментов и групп непосредственно под именем груп-
пы, при этом два предшествующих пробела показывают их принадлеж-
ность к группе. В файле HELLO.LST сегменты STACK и _DATA являются
членами группы сегментов DGROUP.
Сегменты имеют атрибуты, и в таблице сегментов и групп для
каждого сегмента приведен список из 5 атрибутов. Если читать сле-
ва, то в таблице указываются следующие атрибуты: размер данных
(Bit), общий размер (Size), выравнивание (Align), тип комбиниро-
вания (Combine) и класс (Class). Рассмотрим каждый из них в от-
дельности.
Размер данных всегда равен 16 (за исключением сегментов, ас-
семблируемых с директивой USE32 для процессора 80386; об этом
рассказывается в Главе 9).
Размер сегмента задается в виде четырех шестнадцатиричных
цифр. Например, сегмент STACK имеет размер 0200h байт (512 в де-
сятичном виде).
Тип выравнивания описывает, на какой границе памяти может
начинаться сегмент. Имеются следующие типы выравнивания:
BYTE - сегмент может начинаться с любого адреса;
DWORD - сегмент может начинаться с любого адреса,
кратного 4;
PAGE - сегмент может начинаться с любого адреса,
кратного 256;
PARA - сегмент может начинаться с любого адреса,
кратного 16 (выравнивается на границу слова);
WORD - сегмент может начинаться с любого четного
адреса.
В файле HELLO.LST сегмент STACK начинается на границе параг-
рафа, а сегменты _DATA и _TEXT выравниваются на границу слова
(более подробная информация о выравнивании приведена в Главе 9).
Тип комбинирования определяет, как сегменты с таким же име-
нем будут комбинироваться с данным сегментом. Например, сегменты
с идентичными именами с типом комбинирования PUBLIC объединяются
(конкатенируются) в один сегмент большего размера, а если эти
сегменты будут иметь тип комбинирования COMMON, то они будут сли-
ваться в один общий сегмент (перекрытие). Более подробно о комби-
нировании типов и классов сегментов рассказывается в Главе 9.
Наконец, класс сегмента определяет общий класс, к которому
принадлежит сегмент (например, CODE, DATA или STACK). Компоновщик
использует эту информацию для упорядочивания сегментов при компо-
новки их в программу (см. Главу 9).
Таблица перекрестных ссылок
-----------------------------------------------------------------
В таблице идентификаторов файла листинга приводится масса
информации о метках, группах и сегментах, но имеется 2 вещи, ко-
торые здесь не указаны: где определены метки, группы и сегменты и
где они используются. Другими словами, в таблице идентификаторов
отсутствуют перекрестные ссылки для меток, групп и сегментов. Ин-
формация о перекрестных ссылках облегчает нахождение меток и от-
слеживание поэтапного выполнения программы при ее отладке.
Существует два способа указания Турбо Ассемблеру на необхо-
димость генерации в конце файла листинга информации о перекрест-
ных ссылках. Одним из них является параметр командной строки /C,
например:
TASM /c hello,,
При этом в файле листинга HELLO.LST будет генерироваться ин-
формация о перекрестных ссылках. Заметим, однако, что сам пара-
метр /C недостаточен для генерации информации о перекрестных
ссылках. Вы должны также указать Турбо Ассемблеру, что нужно соз-
давать файл листинга, в который эта информация будет помещена.
Запросить у Турбо Ассемблера необходимость генерации инфор-
мации а перекрестных ссылках можно также с помощью добавления в
командной строке четвертого поля, например:
TASM hello,hello,hello,hello
или
TASM hello,,,
Предположим, вы ассемблируете файл REVERSE.ASM (см. вторую
программу в Главе 5), указывая в командной строке параметр /C:
TASM /C reverse,,
Турбо Ассемблер создает следующий файл листинга (с именем
REVERSE.LST):
1 DOSSEG
2 .MODEL SMALL
3 .STACK 100h
4 .DATA
5 = 03EB MAXIMUM_STRING_LENGTH EQU 1000
6 0000 03EB*(??) StringToReverse DB MAXIMUM_STRING_LENGTH DUP
(?)
7 03E8 03E8*(??) ReverseString DB MAXIMUM_STRING_LENGTH DUP
(?)
8 .CODE
9 ProgramStart:
10 0000 B8 0000s mov ax,@Data
11 0003 8E D8 mov dx,ax ; установить регистр
DS таким образом,
чтобы он указывал
на сегмент данных
13 0005 B4 3F mov ah,3fh ; функция DOS чтения
ввода
14 0007 ЕЕ 0000 mov bx,0 ; описатель стандарт-
ного ввода
15 000A B9 03E8 mov cx,MAXIMUM_STRING_LENGTH ; считано до
16 ; максимального
числа символов
17 000D BA 0000r mov dx,OFFSET StringToReverse ; сохранить
18 ; строку
19 0010 OD 21 int 21h ; получить строку
20 0012 23 C0 and ax,ax ; были считаны
символы?
21 0014 74 1F jz Done ; нет, конец
22 0016 8B C8 mov cx,ax ; поместить длину
23 ; строки в регистр
СХ, который
можно использовать,
как счетчик
24 0018 51 push cx ; сохранить в стеке
длину строки
25 0019 BB 0000r mov bx,OFFSET StringToReverse
26 001C BE 03E8r mov si,OFFSET ReverseString
27 001F 03 F1 add si,cx
28 0021 4E dec si ; указывает на конец
29 ; буфера строки
30 ReverseLoop:
31 0022 8A 07 mov al,[bx] ; получить следу-
ющий символ
32 0024 88 04 mov [si],al ; сохранить символы
в обратном
порядке
33 0026 43 inc bx ; указатель на
следующий символ
34 0027 4E dec si ; указатель на
35 ; предыдущую ячейку
buffer
36 0028 E2 F8 loop ReverseLoop ; переместить
следующий символ,
если он имеется
37 002Д 59 pop cx ; извлечь длину
строки
38 002Е Е4 40 mov ax,40h ; функция записи
DOS
39 002D BB 0001 mov bx,1 ; описатель
стандартного
вывода
40 00030 ЕД 03З8у mov dx,OFFSET ReverseString ; напечатать
строку
41 0037 ЕЖ 21 кпх 21й ; напечатать строку
42 Done:
43 0035 B4 4C mov ah,4ch ; функция DOS
завершения
программы
44 0037 ЕЖ 21 int 21h ; завершить
программу
45 END
Symbol Table
Symbol Name Type Value Cref defined at #
@Code Text _TEXT #2 #8
@Curseg Text _TEXT #2 #3 #4 #8
DONE Near _TEXT:0035 21 #42
MAXIMUM_STRING_LENGTH Number 03E8 #5 6 7 15
PROGRAMSTART Near _TEXT:0000 #9 45
REVERSELOOP Near _Text:0022 #30 36
REVERSESTRING Byte DGROUP:03E8 #7 26 40
STRINGTOREVERSE Byte DGROUP:0000 #6 17 25
Groups & Segments Bit Size Align Combine Class Cref defined at
DGROUP Group #2 2 10
STASK 16 0200 Para Stack STASK #3
_DATA 16 07D0 Word Public DATA #2 #4
_TEXT 16 0039 Word Public CODE #2 2 #8 8
Этот файл листинга также содержит расширенный исходный код и
таблицы идентификаторов. Однако в таблице идентификаторов появи-
лось новое поле - поле перекрестных ссылок (поле с названием
"Cref defined at").
В поле перекрестных ссылок указываются для каждого идентифи-
катора (метки, группы или сегмента) номера всех тех строк в прог-
рамме, где имеется ссылка на данный идентификатор. Перед строка-
ми, на которых был определен идентификатор, указывается символ #.
Например, давайте найдем, где определяется и используется
метка MAXIMUM_STRING_LENGTH. В файле листинга указывается, что
она была определена на строке 5. Если вы посмотрите на листинг
исходного кода, то убедитесь, что это именно так. (Отметим, кста-
ти, что в таблице меток указывается, что значение
MAXIMUM_STRING_LENGTH представляет собой число 03Е8h, десятичное
значение которого 1000.)
В поле перекрестных ссылок для метки MAXIMUM_STRING_LENGTH
также указывается, что ссылки на эту метку (но не определения
метки) имеются на строках 6, 7 и 15. Если взглянуть но первую
часть листинга, то можно увидеть, что это так.
Нужно иметь в виду, что между таблицей идентификаторов с пе-
рекрестными ссылками и таблицей идентификаторов без перекрестных
ссылок существует одно отличие. Идентификатор, который был опре-
делен, но на которые нет ссылок в программе, в таблице меток с
перекрестными ссылками показан не будет, поскольку перекрестные
ссылки на него отсутствуют. Однако такой идентификатор указывает-
ся в обычной таблице идентификаторов (без перекрестных ссылок).
Разрешить генерацию перекрестных ссылок для всего файла мож-
но с помощью параметра командной строки /C. Можно с уверенностью
сказать, что вы не захотите получать листинг перекрестных ссылок
для каждого идентификатора. Для больших исходных файлов такие
листинги будут иметь огромные размеры. В Турбо Ассемблере предус-
мотрены директивы, позволяющие вам разрешать и запрещать перек-
рестные ссылки для выбранных частей исходного файла.
Директива %CREF разрешает разрешает генерацию для последую-
щих строк текста перекрестных ссылок. Директивы %NOCREF запрещает
их генерацию. Любая из этих директив отменяет действие, указанное
в командной строке с помощью параметра /C. Если перекрестные
ссылки разрешены где-либо в исходном модуле, то в таблице иденти-
фикаторов указываются строки, на которых были определены все сег-
менты, группы и метки. Однако в записи о перекрестных ссылках
приводятся только те строки, на которых имеются ссылки на соот-
ветствующие сегменты, группы и метки и для которых в исходном
файле разрешена генерация перекрестных ссылок.
Рассмотрим, например, следующий фрагмент программы:
.
.
.
#NOCREF
ProgrammStart PROC ; строка 1
.
.
.
jmp LoopTop ; строка 2
.
.
.
#CREF
LoopTop: ; строка 3
.
.
.
loop LoopTop ; строка 4
#NOCREF
mov ax,OFFSET ProgramStart ; строка 5
.
.
.
Для метки ProgramStart строка 1 будет указана, как строка
определения (с символом #), хотя она и находится в области, гене-
рация перекрестных ссылок "выключена". Это происходит потому, что
если где-либо в модуле генерация перекрестных ссылок задается, то
в перекрестных ссылках указываются все строки определения меток.
Аналогично, строка 3 будет указана, как строка определения метки
LoopTop.
Строка 4 будет указана в перекрестных ссылках для LoopTop,
так как она находится после директивы %CREF и перед директивой
%NOCREF. Однако, строка 2 в перекрестных ссылках для LoopTop ука-
зана не будет, потому что она находится в зоне запрещения генера-
ции перекрестных ссылок. По той же причине в перекрестных ссылках
для ProgramStart не будет указана строка 5.
Для совместимости с другими ассемблерами в Турбо Ассемблере
предусмотрены директивы .CREF и .XCREF, управляющие генерацией
перекрестных ссылок аналогично директивам %CREF и %NOCREF.
Управление содержимым и форматом листингов
-----------------------------------------------------------------
Турбо Ассемблер предоставляет вам широкие возможности по уп-
равлению выводом в листингах строк исходного кода и форматом лис-
тинга в целом. Директивы управления листингом можно разделить на
две категории: директивы управления содержимым листинга, с по-
мощью которых выбирается включаемая в файл листинга информация, и
директивы управления форматом листинга, определяющие формат фай-
лов листинга.
Директивы управления содержимым листинга
-----------------------------------------------------------------
Директивы управления содержимым листинга разрешают или зап-
рещают включение в файл листинга отдельных строк. В общем случае
эти директивы полезно использовать для подавления вывода в файл
листинга той информации, которая вас в данный момент не интересу-
ет, что позволяет уменьшить объем файла листинга и упростить ра-
боту с ним.
Директивы %LIST и %NOLIST
-----------------------------------------------------------------
Директивы %LIST и %NOLIST - это основные директивы, управля-
ющие выводом строк листинга. Они разрешают (директива %LIST) или
запрещают (директива %NOLIST) включение последующих строк листин-
га в файл листинга. Например, при указании директив:
.
.
.
%NOLIST
mov ax,1
%LIST
mov bx,2
%NOLIST
add ax,bx
.
.
.
в файл листинга будет включена только средняя строка mov bx,2. По
умолчанию выбирается директива %LIST.
Директивы %COND и %NOCOND
-----------------------------------------------------------------
Директивы %COND и %NOCOND позволяют вам разрешать (директива
%COND) или запрещать (директива %NOCOND) включение в листинг бло-
ков условного ассемблирования с неудовлетворенным условием. Лис-
тинг таких блоков обычно (по умолчанию) подавляется. Например,
при наличии директив:
.
.
.
%CONDS
IFE IS8086
shl ax,7
ELSE
mov cl,7
shl ax,cl
ENDIF
.
.
.
в файл листинга будут помещены оба условных блока вместе дирек-
тивами условного ассемблирования, а не только тот блок условного
ассемблирования, который во время трансляции имеет истинное зна-
чение.
Директивы %INCL и %NOINCL
-----------------------------------------------------------------
Директивы %INCL и %NOINCL позволяют вам разрешать (директива
%INCL) или запрещать (%NOINCL) вывод в листинге строк, включаемых
из других файлов по директиве INCLUDE. По умолчанию вывод в лис-
тинге включаемого текста разрешен. Например, по директивам:
.
.
.
%NOINCL
INCLUDE HEADER.ASM
%INCL
INCLUDE INIT.ASM
.
.
.
строки, включаемые из файла HEADER.ASM, не будут помещены в файл
листинга, а строки из файла INIT.ASM - будут (однако в листинге
будут указаны обе директивы INCLUDE).
Директивы %MACS и %NOMACS
-----------------------------------------------------------------
Директивы %MACS и %NOMACS позволяют вам разрешить (директива
%MACS) или запретить (директива %NOMACS) включение в листинг
текста макрорасширений. Листинг макрорасширений обычно подавляет-
ся. Например, в результате ассемблирования исходного кода:
.
.
.
MAKE_BYTE MACRO VALUE
DB VALUE
ENDM
.
.
.
%NOMACS
MAKE_BYTE 1
%MACS
MAKE_BYTE 1
.
.
.
текст, генерируемый первым макрорасширением макрокоманды
MAKE_BYTE, DB 1, в файл листинга включен не будет (однако обе
директивы MACRO будут включены в файл листинга).
Директивы %CTLS и %NOCTLS
-----------------------------------------------------------------
Директивы %CTLS и %NOCTLS позволяют вам разрешить (%CTLS)
или запретить (директива %NOCTLS) включение в листинг самих уп-
равляющих директив. По умолчанию включение в листинг управляющих
директив запрещено. Например, в результате выполнения директив:
.
.
.
%NOCTLS
%NOINCL
%CTLS
%NOMACS
.
.
.
директива управления листингом %NOINCL в листинг включена на бу-
дет, а директива %NOMACS - будет.
Директивы %UREF и %NOUREF позволяют вам разрешать или запре-
щать включение в таблицу идентификаторов листинга идентификато-
ров, на которые нет ссылок (другими словами, идентификаторов, ко-
торые определяются, но не используются). По умолчанию включение в
листинг таких идентификаторов разрешено. Чтобы эти директивы дей-
ствовали, нужно задать создание листинга перекрестных ссылок.
Директивы %SYMS и %NOSYMS позволяют вам разрешать или запре-
щать включение в файл листинга таблицы идентификаторов. По умол-
чанию включение в листинг такой таблицы (как вы уже наверное за-
метили) разрешено.
Директивы управления форматом листинга
-----------------------------------------------------------------
Директивы управления форматом листинга изменяют формат файла
листинга. Эти директивы можно использовать для генерации файла
листинга такого формата, который вас больше устраивает.
Директива %TITLE задает заголовок, выводимый в верхней части
каждой страницы расширенного листинга исходного кода. Для каждой
программы можно задать только один заголовок. Директива %SUBTTL
задает подзаголовок, который должен выводиться под заголовком на
каждой странице листинга. В программе число подзаголовком может
быть любым. Например, если в исходном модуле SPACEWAR.ASM содер-
жатся директивы:
.
.
.
%TITLE 'Программа игры З в е з д н ы е в о й н ы'
%SUBTTL 'Подпрограммы гравитационных эффектов'
.
.
.
то на каждой странице расширенного листинга исходного кода будут
содержаться следующие строки:
Turbo Assembler Version 2.0 06-29-90 16:21:27 Page 1
SPACEWAR.ASM
Программа игры З в е з д н ы е в о й н ы
Подпрограммы гравитационных эффектов
Директива %NEWPAGE указывает Турбо Ассемблеру, что в файле
листинга нужно начать новую страницу.
Директива %TRUNC указывает Турбо Ассемблеру, что нужно усе-
кать поля, превышающие максимальную длину. Директива %NOTRUNC
указывает, что такие поля нужно переносить на следующую строку.
По умолчанию эти поля усекаются.
Директива %PAGESIZE задает вертикальный (число строк) и го-
ризонтальный (число позиций) размер страниц листинга, генерируе-
мых Турбо Ассемблером. Например, директива:
%PAGESIZE 66,132
указывает Турбо Ассемблеру, что нужно создавать страницы листинга
с размером 66 строк на страницу и 132 позиции в строке. Отметим,
что директива %PAGESIZE не посылает на принтер команды задания
размера страницы, поэтому перед печатью листинга вы должны сами
установить параметры принтера, затем использовать директиву
%PAGESIZE для генерации Турбо Ассемблером страниц, размер которых
совпадает с теми размерами, которые вы установили на принтере.
Директивы задания размера полей
-----------------------------------------------------------------
Размером пяти полей расширенного листинга исходного кода уп-
равляют пять директив. Полный формат строки данной части листинга
имеет вид:
<глубина> <номер_строки> <смещение> <машинный_код> <исходный_код>
Четыре из этих полей мы описывали ранее, пятым полем явля-
ется поле "глубина", которое указывает, какова для текущей строки
глубина вложенности макрокоманд или включаемых файлов. Например,
если данная строка генерируется макрокомандой, которая сама вызы-
вается из другой макрокоманды, то поле "глубина" будет иметь зна-
чение 2.
Размер в символах поля "глубина" задает директива %DEPTH.
Директива %LINUM задает длину в символах поля "номер_строки".
Директива %PCNT определяет размер поля "смещение". Директива %BIN
определяет величину поля "машинный_код". Наконец, директива %TEXT
задает длину поля комментария.
Директивы %PUSHLCTL и %POPLCTL
-----------------------------------------------------------------
Иногда вам, возможно, потребуется изменить на время текущее
состояние управления листингом, а затем восстановить его. Возмож-
но, чтобы включить в листинг каждый байт таблицы данных, вам пот-
ребуется разрешить перенос на другую строку и изменить размеры
полей, или же вы захотите в целях отладки разрешить включение в
листинг всех типов строк. После того, как вы измените состояние
управления листингом, было бы очень неплохо иметь возможность
сразу восстановить управляющие параметры в их определенные ранее
значения, особенно, если эти значения задаются во включаемом фай-
ле или в какой-нибудь дальней части исходного модуля.
Для управления такой ситуацией в Турбо Ассемблере предусмот-
рены директивы %PUSHLCTL и %POPLCTL. Директива %PUSHLCTL заносит
текущее состояние управления листингом во внутренний стек, а ди-
ректива %POPLCTL извлекает его из стека (обе директивы имеют мак-
симум 16 уровней). Эти директивы только сохраняют и восстанавли-
вают состояние управления листингом, который может быть разрешен
или запрещен (аналогично директивам %TRUNC и %NOTRUNC), и не тре-
буют числовых аргументов. Например, в следующем исходном коде
состояние управления листингом после выполнения директивы
%POPLCTL то же, что и перед выполнением директивы %PUCHLCTL:
.
.
.
%LIST
%TRUNC
%PUSHLCTL
%NOLIST
%NOTRUNC
%NEWPAGE
.
.
.
%POPLCTL
.
.
.
Другие директивы управления листингом
-----------------------------------------------------------------
Чтобы обеспечить совместимость с другими ассемблерами, в
Турбо Ассемблере предусмотрены некоторые другие директивы управ-
ления листингами. Они включают в себя директивы TITLT, SUBTTTL,
PAGE, .LST, .XLST, .LFCOND, .SFCOND, .TFCOND, .LALL, .SALL и
.XALL. (Подробное описание данных директив приведено в Главе 2
"Справочного руководства").
Вывод сообщения во время ассемблирования
-----------------------------------------------------------------
В Турбо Ассемблере предусмотрены две директивы, позволяющие
выводить строку на экран дисплея во время ассемблирования (тран-
сляции). Эти директивы можно использовать для вывода информации о
ходе ассемблирования, чтобы вы могли определить, какая часть
программы уже оттранслирована или о том, что Ассемблер достиг оп-
ределенной части исходного кода.
Директива DISPLAY выводит заключенную в кавычки строку на
экран. Директива %OUT выводит на экран строку, не заключенную в
кавычки. Например, в результате выполнения следующих директив:
.
.
.
DISPLAY 'Выведено по директиве DISPLAY'
%OUT Выведено по директиве OUT
.
.
.
на экран выведутся сообщения:
Выведено по директиве DISPLAY
Выведено по директиве OUT
Условное ассемблирование исходного кода
-----------------------------------------------------------------
Настанет момент, когда вы поймете, что чрезвычайно удобно
иметь один исходный модуль на Ассемблере, в результате трансляции
которого получается несколько различных версий программы. Напри-
мер, для одной программы иногда желательно иметь две версии, одна
из которых использует стандартные инструкции процессора 8086, а
другая - расширенный набор инструкций процессоров 80186 (80286).
(Конечно, в этом случае можно использовать два исходных модуля
(для каждой версии свой), но вносить в них, например, изменения,
в этом случае было бы очень неудобно).
Турбо Ассемблер предоставляет вам такую возможность (и даже
более). Рассмотрим, например, следующий фрагмент программы:
.
.
.
IF IS8086
mov ax,3dah
push ax
ELSE
push 3dah
ENDIF
call GetAdapterStatus
.
.
.
Если значение метки IS8086 будет ненулевым, то значение па-
раметра 3dah заносится в стек поэтапно (за два шага), как это
требуется в процессоре 8086. Если же, однако, IS8086 имеет нуле-
вое значение, то значение параметра заносится в стек непосредст-
венно, с помощью специальной формы инструкции PUSH, которую можно
использовать при работе на процессорах 80186 и 80286 (но не в
процессоре 8086).
Турбо Ассемблер поддерживает множество директив условного
ассемблирования, а также дает вам возможность несколькими спосо-
бами генерировать ошибки ассемблирования. Давайте рассмотрим сна-
чала директивы условного ассемблирования.
Директивы условного ассемблирования
-----------------------------------------------------------------
Простейшими и самыми полезными директивами условного ассемб-
лирования являются директивы IF и ENDIF, которые используются
совместно с директивами ENDIF и (необязательно) ELSE. Часто также
используются директивы IFDEF и IFNDEF, а директивы условного ас-
семблирования IFB, IFNB, IFIDN, IFDIF, IF1 и IF2 полезны только в
отдельных случаях.
Директивы IF и IFE
-----------------------------------------------------------------
Директива условного ассемблирования IF приводит к тому, что
последующий блок исходного кода (до соответствующей директивы
ELSE или ENDIF) будет ассемблироваться только в том случае, если
значение операнда будет ненулевым. Операнд может представлять со-
бой константу или выражение, при вычислении которого получается
константа. Например, в результате выполнения директив:
.
.
.
IF REPORT_ASSEMBLY_STATUS
DISPLAY 'Ассемблирование достигло контрольной точки 1'
ENDIF
.
.
.
в том случае, если REPORT_ASSEMBLY_STATUS имеет при достижении
директивы IF ненулевое значение, на экран выводится сообщение:
Ассемблирование достигло контрольной точки 1
Условие IF (если) может завершаться директивами ENDIF (конец
блока) или ELSE (иначе). Если условие IF завершается директивой
ELSE, то следующий за ELSE исходный код ассемблируется только в
том случае, если операнд соответствующей директивы IF был нуле-
вым. Блок кода, следующего за директивой ELSE, должен завершаться
директивой ENDIF. Условия IF могут быть вложенными, например, в
программе:
.
.
.
; Проверить, нужно ли определять массивы (в противном случае
; они распределяются динамически).
IF DEFINE_ARRAY
; Убедиться, что массив не слишком длинный
IF (ARRAY_LENGTH GT MAX_ARRAY_LENGTH)
ARRAY_LENGTH = MAX_ARRAY_LENGTH
ENDIF
; Если это указано, установить массив в начальное значение
IF INITIALIZE_ARRAY
Array DB ARRAY_LENGTH DUP (?)
ENDIF
ENDIF
.
.
.
директивы IF и IF...ELSE вложены внутри другого блока IF.
Директива IFE аналогична директиве IF, но последующий код
ассемблируется в том случае, если операнд нулевой. В следующем
примере исходный код после директивы IFE ассемблируется всегда:
.
.
.
IF 0
.
.
.
ENDIF
.
.
.
Как и директива IF, директива IFE может иметь соответствую-
щую директиву ELSE.
Необходимо понимать, что директивы условного ассемблирования
работают только во время ассемблирования, а не во время выполне-
ния программы. Это не то же самое, что операторы в языке Си, вы-
полняющие различный код в зависимости от различных условий этапа
выполнения. Директивы условного ассемблирования обеспечивают
трансляцию различного кода в зависимости от условий ассемблирова-
ния.
Директивы IFDEF и IFNDEF
-----------------------------------------------------------------
Директивы условного ассемблирования IFDEF и IFNDEF - это ваш
основной инструмент для построения программ, при ассемблировании
которых получается несколько версий. Директивы IFDEF и IFNDEF в
этом случае чрезвычайно полезны.
Блок исходного кода, заключенный между директивой IFDEF и
соответствующей ей директивой ENDIF, ассемблируется только в том
случае, если метка, являющаяся операндом директивы IFDEF, сущест-
вует (другими словами, если при выполнении директивы IFDEF метка
уже определена). Например, при трансляции исходного кода:
.
.
.
DEFINED_LABEL EQU 0
.
.
.
IFDEF DEFINED_LABEL
DB 0
ENDIF
.
.
.
будет ассемблироваться директива DB. Если же вы удалите директиву
EQU, которая устанавливает значение для DEFINED_LABEL (и в пред-
положении, что эта метка нигде больше в программе не определяет-
ся), то директива DB ассемблироваться не будет. Заметим, что зна-
чение метки DEFINED_LABEL для директивы IFDEF не важно.
Действие директивы IFNDEF обратно действию директивы IFDEF.
Соответствующий код ассемблируется только в том случае, если яв-
ляющаяся операндом метка не определена.
У вас может возникнуть вопрос, для чего используются дирек-
тивы IFDEF и IFNDEF? Одним из применений является предохранение
от повторного определения метки с помощью директивы EQU в сложной
программе: если метка уже определена, то чтобы избежать ее пов-
торного определения (что вызовет ошибку), вы можете использовать
директиву IFDEF. Другое использование данной директивы - это вы-
бор версии ассемблируемой программы (аналогично тому, как это де-
лалось ранее с помощью директивы IF). Вместо того, чтобы прове-
рять, скажем, является ли массив INITIALIZE_ARRAYS нулевым или
ненулевым, вы можете просто проверить, определен ли он вообще.
Удобный способ выбора версии программы предоставляет пара-
метр командной строки Турбо Ассемблера /D. Параметр /D определяет
соответствующую локальную метку и (возможно) присваивает этой
метке значение. Поэтому вы, например, можете использовать следую-
щую команду:
TASM /dINITIALIZE_ARRAYS=1 test
При этом при ассемблировании программы TEST.ASM метка
INITIALIZE_ARRAYS будет установлена в значение 1.
Хотя это определенно полезно, здесь могут возникнуть различ-
ные проблемы. Что будет в том случае, если вы будете полагаться
на определение INITIALIZE_ARRAYS в командной строке, но забудете
указать соответствующий параметр /D? Предположим также, что вы
хотите инициализировать массивы в особом случае и не хотите в
других случаях вводить /dINITIALIZE_ARRAYS.
В этом случае вам на выручку придет директивы IFNDEF. Вы мо-
жете использовать ее для проверки того, что метка
INITIALIZE_ARRAYS уже определена (в командной строке), а затем
инициализировать ее только в том случае, если ее значение еще не
задано. Таким образом, определение в командной строке имеет преи-
мущество (старшинство), но если определение в командной строке не
задано, то для метки имеется состояние, используемое по умолча-
нию. Приведем пример программы, в которой массив
INITIALIZE_ARRAYS определяется только в том случае, если он еще
не определен:
.
.
.
IFNDEF INITIALIZE__ARRAYS
INITIALIZE__ARRAYS EQU 0 ; по умолчанию не инициализируется
ENDIF
.
.
.
Когда вы таким образом используете директиву IFNDEF для оп-
ределения идентификатора, который еще не был определен, вы полу-
чите предупреждающее сообщение, показывающее, что вы используете
конструкцию, зависящую от прохода. Если вы просто определяете
внутри условного блока IFNDEF идентификатор, то это сообщение
можно игнорировать. Данное сообщение выводится потому, что Турбо
Ассемблер не может сообщить вам, что вы собираетесь поместить в
блок директивы или инструкции. Если вы делаете в блоке что-то
еще, а не просто определяете идентификатор, то вы должны с по-
мощью параметра /m разрешить выполнение нескольких проходов. Если
вы только определяете идентификатор, то разрешение выполнения
нескольких проходов не приведет к выводу предупреждающего сообще-
ния.
Другие директивы условного ассемблирования
-----------------------------------------------------------------
Для проверки параметров, передаваемых в макрокоманды, ис-
пользуются директивы IFB, IFNB, IFIDN и IFDIF. (О макрокомандах
рассказывается в Главе 9 "Развитое программирование на Турбо Ас-
семблере"). Директива IFB приводит к тому, что соответствующий
исходный код будет ассемблирован в том случае, если параметр, яв-
ляющийся операндом директивы, пустой (пробел). По директиве IFNB
исходный код будет ассемблироваться, если параметр не пуст (не
является пробелом). Директивы IFNB и IFB - это своего рода экви-
валент директив IFNDEF и IFDEF для параметров макрокоманд.
Рассмотрим в качестве примера следующую макрокоманду TEST,
которая определена следующим образом:
;
; Макрокоманда для определения байта или слова
;
; Ввод:
; VALUE = значение байта или слова
; DEFINE_WIRD = 1 для определения слова и 0 для определения
; байта
;
; Примечание: Если параметр PARM2 не задан, то определяется байт.
;
TEST MACRO VALUE, DEFINE_WORD
IFB <DEFINE_WORD>
DB VALUE ; определить байт, если PARM2 - пробел, иначе
IF DEFINE_WORD
DW VALUE ; определить слова, если PARM2 не = 0
ELSE
DB VALUE ; определить байт, если PARM2 = 0
ENDIF
ENDIF
ENDM
Если макрокоманда TEST вызывается оператором:
TEST 19
то определяется байт со значением 19, а если макрокоманда вызыва-
ется с помощью оператора:
TEST 19,1
то определяется слово со значением 19.
По директиве IFIDN соответствующий исходный код будет ассем-
блироваться в том случае, если два ее параметра совпадают, а по
директиве IFDIF - если параметры различны. Например, следующая
макрокоманда, преобразующая байт со знаком в слово со знаком в
регистре AX, не копирует исходный операнд, если он находится в
регистре AL:
;
; Макрокоманда для преобразования байта со знаком в 8-битовом
; регистре или ячейке памяти в слово со знаком в регистре AX.
;
; Ввод:
; SIGNED_BYTE - имя регистра или ячейки памяти,
; в которой содержится байт со знаком,
; преобразуемый в слово со знаком.
;
MAKE_SIGNED_WORD MACRO SIGNED_BYTE
IFDIFI <AL>,<SIGNED_BYTE> ; убедиться, что операндом
; не является регистр AL
mov al,SIGNED_BYTE
ENDIF
cwb
ENDM
В аргументах директив IFDIF и IFIDN строчные и прописные
буквы различаются. Чтобы интерпретировать эти буквы, как совпада-
ющие, имеются две другие эквивалентные директивы - IFINDI и
IFDIFI.
Заметим, что все операнды директив IFB, IFNB, IFIDN и IFDIF
требуется заключать в угловые скобки.
Если вы не указываете для разрешения выполнения нескольких
прохoдов параметр командной строки /m, то условие IF1 всегда при-
нимает истинное значение, а IF2 - ложное (так как второй проход
не выполняется). Если Турбо Ассемблер встречает директиву IF1 или
IF2, то выводится предупреждающее сообщение:
"Pass dependent construction encountered"
(обнаружена конструкция, зависящая от прохода)
Eсли вы используете параметр /m, то если в модуле содержатся
директивы IF1 или IF2, автоматически выполняется два прохода. В
этом случае директива IF1 принимает истинное значение на первом
проходе, а IF2 - на втором. При этом также выводится предупрежда-
ющее сообщение:
"Module is pass dependent - compatibility pass was done"
(модуль зависит от прохода - выполнен проход для совмести-
мости)
Семейство директив ELSEIF
-----------------------------------------------------------------
Каждая из директив IF (IF, IFB, IFIDN и т.д.) имеет соот-
ветствующую директиву семейства ELSEIF (например, ELSEIF, ELSE-
IFB, ELSEIFIDN). Они работают, как сочетание директивы ELSE с од-
ной из директив IF. Вы можете их использовать, чтобы обеспечить
лучшую читаемость исходного кода, когда требуется проверять мно-
жество условий или значений и ассемблировать только отдельный
блок кода. Рассмотрим следующий фрагмент программы:
IF BUFLENGHT GT 1000
CALL DOBIGBUF ; большой буфер
ELSE
IF BUFLENGTH GT 100 ; средний буфер
CALL MEDIUMBUF
ELSE
IF BUFLENGTH GT 10 ; небольшой буфер
CALL SMALLBUF
ELSE
CALL TINYBUFP ; маленький буфер
ENDIF
ENDIF
ENDIF
Чтобы улучшить читаемость кода, вы можете использовать ди-
рективу ELSEIF:
IF BUFLENGHT GT 1000
CALL DOBIGBUF ; большой буфер
ELSE
ELSEIF BUFLENGTH GT 100 ; средний буфер
CALL MEDIUMBUF
ELSEIF BUFLENGTH GT 10 ; небольшой буфер
CALL SMALLBUF
ELSE
CALL TINYBUFP ; маленький буфер
ENDIF
Это приблизительно соответствует операторам case или switch
в Паскале и Си. Однако, такая конструкция является гораздо более
общей, поскольку во всем блоке условного ассемблирования вам не
требуется использовать один и тот же вид проверок ELSEIF. Допус-
тимо, например, следующее:
PUSHREG MACRO ARG
IFIDN <ARG>,<INDEX>
PUSH SI
PUSH DI
ELSEIFB <ARG>
PUSH AX
ENDIF
ENDM
Условные директивы вывода сообщений об ошибках
-----------------------------------------------------------------
Турбо Ассемблер позволяет вам выполнять условную (по выпол-
нению или невыполнению определенного условия) генерацию ошибок
ассемблирования. Для этого используются условные директивы вывода
сообщений об ошибках .ERR, .ERR1, ERR2, .ERRDEF, .ERRNDEF, .ERRB,
.ERRNB, .ERRIDN, .ERRIDNI, .ERRDIFI, .ERRE, .ERRNZ и .ERRDIF. Для
чего нужно преднамеренно генерировать сообщение об ошибке ассемб-
лирования? Условные директивы вывода сообщений об ошибках позво-
ляют вам перехватывать в программах множество ошибок, например,
присваивание меткам слишком больших или малых значений, использо-
вание неопределенных меток и пропуск аргументов макрокоманд.
Если вглянуть на перечень условных директив вывода сообщений
об ошибках, то можно заметить, что эти директивы очень похожи на
директивы условного ассемблирования. И это не случайное совпаде-
ние, поскольку большинство условных директив вывода сообщений об
ошибках проверяют те же условия. Например, директива .ERRNDEF ге-
нерирует ошибку в том случае, если метка, являющаяся ее операн-
дом, не определена, так же как директива IFNDEF ассемблирует со-
ответствующий код в том случае, если метка не определена.
Директивы .ERR, .ERR1 и .ERR2
-----------------------------------------------------------------
Когда Турбо Ассемблер обнаруживает директиву .ERR, то гене-
рируется ошибка. Само по себе это не является полезной функцией,
однако директиву .ERR полезно использовать в сочетании с директи-
вой условного ассемблирования.
Например, предположим, вы хотите хотите сгенерировать ошибку
в том случае, если в присваивании длины для данного массива уста-
навливается слишком большое значение. Это можно сделать следующим
образом:
IF (ARRAY_LENGTH GT MAX_ARRAY_LENGTH)
.ERR
ENDIF
Если массив не является достаточно длинным (длина массива
ARRAY_LENGTH не превосходит максимального значения длины массива
MAX_ARRAY_LENGTH), то Турбо Ассемблер не будет генерировать код
внутри блока IF и ошибка генерироваться не будет.
Директивы .ERR1 и .ERR2 работают точно также, как директива
.ERR, но только, соответственно, на первом и втором проходах.
Если для разрешения выполнения нескольких проходов вы не исполь-
зуете параметр командной строки /m, то по директиве .ERR1 всегда
будет выводиться ошибка, а по директиве .ERR2 - не будет (так как
второй проход не выполняется). В том случае, если Турбо Ассемблер
обнаруживает в модуле директивы .ERR1 или .ERR2, он выводит сооб-
щение:
"Pass dependent construction encountered"
(обнаружена конструкция, зависимая от прохода)
Если вы используете параметр командной строки /m, то когда
ваш модуль содержит директиву .ERR1 или .ERR2, автоматически вы-
полняется два прохода. В этом случае директива .ERR1 будет выво-
дить сообщение об ошибке на первом проходе, а директиве .ERR2 -
на втором проходе. Кроме того, выводится предупреждающее сообще-
ние:
"Module is pass dependent - compatibility pass was done"
(модуль зависит от прохода - выполнен проход для совмести-
мости)
Директивы .ERRE и .ERRNZ
-----------------------------------------------------------------
Директива .ERRE генерирует ошибку в том случае, если ее опе-
ранд, при вычислении которого должна получаться константа, равен
нулю. Директива .ERRE эквивалента выполнению директивы .IFE в со-
четании с директивой .ERR. Например:
.ERRE TEST_LABEL-1
эквивалентно:
IFE TEST_LEBEL-1
.ERRE
ENDIF
Директиву .ERRE можно использовать для вывода ошибки в том
случае, когда в выражении отношения генерируется ложное значение
(так как ложное выражение равно 0).
Аналогично, директива .ERRNZ генерирует ошибку в том случае,
если ее операнд не равен нулю. Это эквивалентно директиве IF, за
которой следует директива .ERR. Директиву .ERRNZ можно использо-
вать для вывода ошибки в том случае, когда в выражении отношения
генерируется истинное значение (так как истинное выражение не
равно 0). Например:
.ERRNZ ARRAY_LENGTH GT MAX_ARRAY_LENGTH
выполняет то же действие, что и директивы IF и .ERR в примере
последнего раздела.
Директивы .ERRDEF и .ERRNDEF
-----------------------------------------------------------------
Директива .ERRDEF генерирует ошибку в том случае, если мет-
ка, являющаяся ее операндом, определена, а директива .ERRNDEF ге-
нерирует ошибку в том случае, если метка-операнд является неопре-
деленной. Эти директивы позволяют в одной строке реализовать эк-
вивалент сочетания директив IFDEF или IFNDEF и директивы .ERR.
Например:
.ERRNDEF MAX_PATH_LENGTH
эквивалентно:
IFNDEF MAX_PATH_LENGTH
.ERR
ENDIF
Другие условные директивы генерации сообщения об ошибке
-----------------------------------------------------------------
Четыре оставшиеся условные директивы предназначены только
для использования в макрокомандах и являются непосредственным
аналогом четырех директив условного ассемблирования, использую-
щихся в макрокомандах и обсуждавшихся в предыдущем разделе
"Другие директивы условного ассемблирования".
Директива .ERRB генерирует ошибку в том случае, если являю-
щийся ее операндом параметр макрокоманды пуст, а директива .ERRNB
- в том случае, если этот параметр не пустой. Директива .ERRIDN
генерирует ошибку, если два параметра макрокоманды, которые явля-
ются ее операндами, совпадают, а директива .ERRDIF - в том слу-
чае, если они различны.
Например, в следующей макрокоманде ошибка генерируется в том
случае, если она вызывается с любым числом параметров, отличным
от двух. Это реализовано с помощью директив .ERRB и .ERRNB (они
позволяют проверить, что PARM2 не пуст, а PARM3 - пустой пара-
метр). Чтобы убедиться, что в качестве PARM2 не используется ре-
гистр DX, в макрокоманде используется также директива .ERRIND.
Макрокоманды выглядит следующим образом:
;
; Макрокоманда для сложения двух констант, регистров или
; именованных ячеек памяти и сохранения результата в DX.
;
; Ввод:
; PARM1 - один операнд-слагаемое
; PARM2 - другой операнд-слагаемое
;
ADD_TWO_OPERANDS MACRO PARM1,PARM2,PARM3
.ERRB <PARM2> ; должно быть два параметра,
.ERRNB <PARM3> ; но не три
.ERRIDN <PARM2>,<DX> ; второй параметр не может
; быть регистром DX
mov dx,PARM1
add dx,PARM2
ENDM
Обратите внимание на использование в макрокоманде директивы
.ERRIDN, чтобы обеспечить, что PARM2 отличен от DX (в этом случае
при загрузке PARM1 он будет отброшен).
Типичные ошибки при программировании на Ассемблере
-----------------------------------------------------------------
В каждом языке имеется свое множество ошибок, которые обычно
очень легко сделать, но не всегда просто обнаружить. Не является
исключением и язык Ассемблера. Мы рассмотрим некоторые типичные
ошибки, которые допускаются при программировании на Ассемблере, и
дадим рекомендации, как можно их избежать.
Программист забывает о возврате в DOS
-----------------------------------------------------------------
В Паскале, Си и других языках программа завершается и возв-
ращается в операционную систему DOS автоматически, когда нет
больше выполняемого кода, даже если в программе отсутствует явная
команда ее завершения. В языке Ассемблера это не так. Ассемблер
выполняет только те действия, которые вы явно указываете. Когда
вы запускаете программу, в которой отсутствует команда возврата в
DOS, она просто продолжает работать до конца выполняемого кода
программы и переходит в код, который находится в примыкающей па-
мяти.
Рассмотрим, например, следующую программу:
DOSSEG
.MODEL SMALL
.CODE
DoNothing PROC NEAR
nop
DoNothing ENDP
END DoNothing
Имеющийся опыт может подсказывать вам, что директивы ENDP
или END должным образом завершат программу, аналогично } и end.
do в Паскале и Си, но это не так. Выполняемый код, сгенерирован-
ный при ассемблировании и компоновке данной программы, состоит
только из отдельной инструкции NOP. В Ассемблере директива ENDP
(как и все другие директивы) не генерирует кода, она просто уве-
домляет Ассемблер, что код для процедуры DoNothing закончился.
Аналогично, директива END DoNothing просто сообщает Ассемблеру,
что код данного модуля закончился, и программа должна начать вы-
полнение с метки DoNothing. Нигде в выполняемом коде не содержит-
ся инструкции для передачи управления обратно в операционную сис-
тему DOS, когда программа закончится. В результате, когда прог-
рамма будет запущена, то после инструкции NOP будут выполняться
инструкции, которые случайно окажутся в памяти непосредственно за
NOP. В этой точке управление будет потеряно и для возврата в опе-
рационную систему DOS может потребоваться программная или аппа-
ратная перезагрузка.
Хотя имеется несколько способов, с помощью которых программа
на Ассемблере может вернуться в DOS, рекомендуемым способом возв-
рата в DOS является функция 4Ch. Правильно завершать работу будет
следующая версия предыдущей программы:
DOSSEG
.MODEL SMALL
.CODE
DoNothing PROC NEAR
nop
mov ah,4Ch ; функция DOS завершения процесса
int 21h ; вызвать DOS для завершения программы
DoNothing ENDP
END DoNothing
Всегда нужно помнить о том, что директивы не генерируют ко-
да, и что Турбо Ассемблер генерирует программы, которые делают
только то, что им указывает исходный код, не больше и не меньше.
Программист забывает об инструкции RET
-----------------------------------------------------------------
Заметим, что правильный вызов подпрограммы состоит из вызова
подпрограммы из другой части кода, выполнения подпрограммы и
возврата из подпрограммы в вызывающую программу. Не забудьте
включать в каждую подпрограмму инструкцию RET, по которой управ-
ление будет передаваться в вызывающий код. При наборе программы
эту директиву легко пропустить и закончить код следующим образом:
;
; Подпрограмма для умножения значения на 80
; Ввод: AX - значение, которое нужно умножить на 80
; Вывод: DX:AX - произведение
;
MultiplyBy80 PROC NEAR
mov dx,80
mul dx
MultiplyBy80 ENDP
; Подпрограмма для получения следующе нажатой клавиши
; Вывод: AL - следующая нажатая клавиша
; Содержимое регистра AH теряется
;
GetKey PROC NEAR
mov ah,1
int 21h
ret
GetKey PROC NEAR
Директива MultipleBy80 ENDP может ввести вас в заблуждение,
и вы подумаете, что подпрограмма MultipleBy80 уже завершена кор-
ректно, тогда как при вызове это подпрограммы не только будет
содержимое AX умножаться на 80, но и продолжиться выполнение под-
программы GetKey, и в регистре AL будет возвращаться код следую-
щей нажатой клавиши. Корректной эта подпрограмма будет в следую-
щем виде:
;
; Подпрограмма для умножения значения на 80
; Ввод: AX - значение, которое нужно умножить на 80
; Вывод: DX:AX - произведение
;
MultiplyBy80 PROC NEAR
mov dx,80
mul dx
ret
MultiplyBy80 ENDP
; Подпрограмма для получения следующей нажатой клавиши
; Вывод: AL - следующая нажатая клавиша
; Содержимое регистра AH теряется
;
GetKey PROC NEAR
mov ah,1
int 21h
ret
GetKey PROC NEAR
Генерация неверного типа возврата
-----------------------------------------------------------------
Директива PROC действует двояко. Во-первых, она определяет
имя, по которому будет вызываться процедура. Во-вторых, она уп-
равляет типом (ближним или дальним) процедуры.
Тип процедуры используется Турбо Ассемблером для определения
того, какой тип вызовов нужно генерировать при вызове процедуры
из того же исходного файла. Тип процедуры также используется для
определения типа инструкции RET, которая выполняется, когда про-
цедура возвращает управление в вызывающий код. Рассмотрим следую-
щий пример:
; Подпрограмма ближнего типа для сдвига DX:AX вправо на 2 байта
;
LongShiftRight2 PROC NEAR
shr dx,1
rcr ax,1 ; сдвиг DX:AX вправо на 1 бит
shr dx,1
rcr ax,1 ; сдвиг DX:AX вправо еще на 1 бит
ret
LongShiftRight2 ENDP
Турбо Ассемблер обеспечивает, что инструкция RET будет ближ-
него типа, так как LongShiftRight2 - это процедура ближнего типа
(NEAR). Однако, если директиву PROC изменить следующим образом:
LongShiftRight2 PROC FAR
то будет генерироваться инструкция RET дальнего типа (FAR).
Таким образом, идея здесь очевидна. Инструкции RET в проце-
дуре должны соответствовать ее типу, не правда ли?
Эти и так и не так. Проблема состоит в том, что можно (и
часто желательно) группировать в одной и той же процедуре нес-
колько процедур. Поскольку в этих процедурах отсутствует соот-
ветствующая директива PROC, их инструкции RET будут иметь тип той
процедуры, в которую они заключены, а для конкретных подпрограмм
этот тип не всегда может оказаться корректным. Например, програм-
ма:
; Подпрограмма дальнего типа для сдвига DX:AX на 2 бита.
;
LongShiftRight2 PROC FAR
call LongShiftRight ; сдвиг DX:AX вправо на 1 бит
call LongShiftRight ; сдвиг DX:AX вправо еще на 1 бит
ret
LongShiftRight:
shr dx,1
rcr ax,1 ; сдвиг DX:AX вправо на 1 бит
ret
LongShiftRight2 ENDP
работает неправильно. Процедура LongShiftRight2 обращается с вы-
зовом ближнего типа к LongShiftRight (так как они находятся в од-
ном сегменте кода). Однако, так как LongShiftRight встроена в
процедуру LongShiftRight2, то возврат в конце подпрограммы
LongShiftRight становится возвратом дальнего типа, а когда вызову
ближнего типа соответствует возврат дальнего типа, это с большой
вероятностью может привести к сбою (аварийному завершению прог-
раммы).
Хорошим решением здесь будет наличие в каждой подпрограмме
директивы PROC. Вложенные директивы PROC прекрасно работают:
; Подпрограмма дальнего типа для сдвига DX:AX на 2 бита.
;
LongShiftRight2 PROC FAR
call LongShiftRight ; сдвиг DX:AX вправо на 1 бит
call LongShiftRight ; сдвиг DX:AX вправо еще на 1 бит
ret
LongShiftRight PROC NEAR
shr dx,1
rcr ax,1 ; сдвиг DX:AX вправо на 1 бит
ret
LongShiftRight2 ENDP
LongShiftRight ENDP
также как и последовательные процедуры:
; Подпрограмма дальнего типа для сдвига DX:AX на 2 бита.
;
LongShiftRight2 PROC FAR
call LongShiftRight ; сдвиг DX:AX вправо на 1 бит
call LongShiftRight ; сдвиг DX:AX вправо еще на 1 бит
ret
LongShiftRight2 ENDP
LongShiftRight PROC NEAR
shr dx,1
rcr ax,1 ; сдвиг DX:AX вправо на 1 бит
ret
LongShiftRight ENDP
Для явной генерации ближнего или дальнего возврата можно ис-
пользовать, соответственно, инструкции RETN и RETF. Вы можете
обеспечить с их помощью корректную генерацию инструкций возврата.
Неправильный порядок операндов
-----------------------------------------------------------------
Многие программисты ошибаются и изменяют порядок операндов в
инструкциях процессора 8086 на обратный. Это, вероятно, связано с
тем, что строка:
mov ax,bx
которая означает "поместить AX в BX", читается слева направо, и
многие создатели микропроцессоров строят соответствующим образом
свои ассемблеры. Однако в языке Ассемблера процессора 8086 фирма
Intel использовала другой подход, поэтому для нас эта строка оз-
начает "поместить BX в AX", что иногда приводит к путанице.
Порядок операндов, принятый фирмой Intel, основан на порядке
операндов, принятой в Паскале и Си, где целевой операнд (прием-
ник) находится слева. Таким образом, чтобы не перепутать порядок
операндов в языке Ассемблера процессора 8086, нужно на место за-
пятой, разделяющей операнды, поместить знак равенства, придав
строке форму присваивания. Например, строку:
mov ax,bx
можно рассматривать, как
ax = bx
Операнды-константы, такие, как:
add bx,(OFFSET BaseTble * 4) + 2
можно представить в виде:
bx += (OFFSET BaseTable * 4) + 2
Программист забывает о стеке или резервирует маленький стек
-----------------------------------------------------------------
В большинстве случаев не выделять явно пространство для сте-
ка, это все равно, что ходить по тонкому льду. Иногда программы,
в которых не выделяется пространство для стека, будут работать,
поскольку может оказаться так, что назначенный по умолчанию стек
попадет в неиспользуемую область памяти. Но нет никакой гарантии,
что такие программы будет работать при любых обстоятельствах,
поскольку нет гарантии, что для стека будет доступен по крайней
мере один байт. В большинстве программ для резервирования прост-
ранства для стека должна присутствовать директива .STACK, и для
любой программы эта директива должна резервировать достаточное
пространство, чтобы его хватило для максимальных потребностей в
программе.
Почему это пространство должно быть более чем достаточным, а
не просто достаточным? Трудно иметь уверенность в том, какой объ-
ем стека может оказаться в программе необходимым. Ошибки, которые
возникают, когда увеличивающийся стек переходит в другие части
программы и портит мат информацию, обычно бывает трудно воспроиз-
вести и отслеживать. Кроме того, многие отладчики для возврата
управления из программы используют небольшое дополнительное
пространство в стеке. Поэтому не следует скупиться при выделении
пространства для стека. Это избавит вас от многих возможных неп-
риятностей. Хорошим правилом является выделение стека, минималь-
ный размер которого составляет 512 байт.
Единственным видом программ на Ассемблере, где не следует
выделять стек, являются программы, которые предполагается преоб-
разовать в файлы типа .COM или .BIN. Файлы .BIN содержат код, ко-
торый жестко привязан к отдельным адресам, и, поскольку файлы
.BIN используются обычно, как интерпретированные подпрограммы
Бейсика, они используют стек Бейсика. Файлы .COM выполняются со
стеком, расположенным в самой вершине программного сегмента (ко-
торый имеет размер 64К или меньше, если доступно меньше 64К), по-
этому максимальный размер стека в этом случае просто равен объему
памяти, оставшейся в программном сегменте. При написании программ
в формате .COM следует иметь в виду этот размер в 64К, так как
при увеличении программы соответственно уменьшается стек. Нужно
также учитывать, что при работе больших программ в формате .COM,
выполняющиеся на компьютерах с небольшой доступной памятью, или
запущенных из операционной среды DOS наряду с другими программа-
ми, могут возникнуть проблемы со стеком. Простейший способ избе-
жать этих потенциальных проблем состоит в написании программ в
формате .EXE, а не в формате .COM, и резервировании стека большо-
го объема.
Вызов подпрограммы, которая портит содержимое нужных регистров
-----------------------------------------------------------------
При разработке программы на Ассемблере регистры удобно расс-
матривать, как локальные переменные, выделенные для использования
в процедуре, с которой вы в данный момент работаете. В частности,
нередко подразумевают, что при обращении к другим процедурам ре-
гистры остаются неизмененными. На самом деле это не так. Регистры
- это глобальные переменные, и каждая процедура может сохранить
или уничтожить содержимое любого из регистров.
Рассмотрим следующий пример:
.
.
.
mov bx,[TableBase] ; BX указывает на начало таблицы
mov ax,[Element] ; получить элемент
call DivideBy10 ; разделить элемент на 10
add bx,ax ; ссылка на соответствующую запись
.
.
.
; Подпрограмма для деления значения на 10.
;
; Ввод: AX - значение, которое требуется разделить на 10
; Вывод: AX - значение, разделенное на 10
; DX - остаток значения, деленного на 10
DivideBy10 PROC NEAR
mov dx,0 ; подготовить DX:AX, как
; 32-битовое делимое
mov bx,10 ; BX - 16-битовый делитель
div dx
ret
DivideBy10 ENDP
В вызывающей программе подразумевается, что BX в процедуре
DivideBy10 сохраняется, хотя фактически от устанавливается проце-
дурой DivideBy10 в значение 10. В этом конкретном случае сущест-
вует несколько возможных решений. Например, в начале процедуры
DivideBy10 BX можно заносить в стек, а при выходе из процедуры -
извлекать из стека:
.
.
.
mov bx,[TableBase] ; BX указывает на начало
; таблицы
mov ax,[Element] ; получить элемент
call DivideBy10 ; разделить элемент на 10
add bx,ax ; ссылка на соответствующую
; запись
.
.
.
; Подпрограмма для деления значения на 10.
;
; Ввод: AX - значение, которое требуется разделить на 10
; Вывод: AX - значение, разделенное на 10
; DX - остаток значения, деленного на 10
DivideBy10 PROC NEAR
push bx ; сохранить BX
mov dx,0 ; подготовить DX:AX, как
; 32-битовое делимое
mov bx,10 ; BX - 16-битовый делитель
div dx
pop bx ; восстановить BX
ret
DivideBy10 ENDP
или сделать это в вызывающей программе до (сохранение) и после
(восстановление) вызова процедуры DivideBy10:
.
.
.
mov bx,[TableBase] ; BX указывает на начало
; таблицы
mov ax,[Element] ; получить элемент
push bx ; сохранить BX
call DivideBy10 ; разделить элемент на 10
pop bx ; восстановить BX
add bx,ax ; ссылка на соответствующую
; запись
.
.
.
; Подпрограмма для деления значения на 10.
;
; Ввод: AX - значение, которое требуется разделить на 10
; Вывод: AX - значение, разделенное на 10
; DX - остаток значения, деленного на 10
DivideBy10 PROC NEAR
mov dx,0 ; подготовить DX:AX, как
; 32-битовое делимое
mov bx,10 ; BX - 16-битовый делитель
div dx
ret
DivideBy10 ENDP
либо регистр BX можно загрузить после вызова процедуры, а не пе-
ред ним:
.
.
.
mov ax,[Element] ; получить элемент
call DivideBy10 ; разделить элемент на 10
mov bx,[TableBase] ; BX указывает на начало
; таблицы
add bx,ax ; ссылка на соответствующую
; запись
.
.
.
; Подпрограмма для деления значения на 10.
;
; Ввод: AX - значение, которое требуется разделить на 10
; Вывод: AX - значение, разделенное на 10
; DX - остаток значения, деленного на 10
DivideBy10 PROC NEAR
mov dx,0 ; подготовить DX:AX, как
; 32-битовое делимое
mov bx,10 ; BX - 16-битовый делитель
div dx
ret
DivideBy10 ENDP
Общее решение этой проблемы состоит в том, чтобы все под-
программы, которые могут изменять содержимое регистров, сохраняли
их, а затем восстанавливали. К сожалению для этого требуется до-
полнительное время и такие операции увеличивают объем программы,
что приводит к некоторым потерям в преимуществах программирования
на Ассемблере. Другой подход состоит в том, чтобы каждая подпрог-
рамма сопровождалась комментарием, в котором указывается, какие
регистры сохраняются, а какие разрушаются, и аккуратно проверять,
когда вы подразумеваете, что в подпрограмме регистр не изменяет-
ся. Еще один подход заключается в явном сохранении нужных регист-
ров при вызове подпрограмм.
Ошибки при использовании условных переходов
-----------------------------------------------------------------
Использование в языке Ассемблера инструкций условных перехо-
дов (JE, JNE, JC, JNC, JA, JB, JG и т.д) обеспечивает большую
гибкость в программировании, но при этом также очень просто оши-
биться, выбрав неверный переход. Кроме того, поскольку в языке
Ассемблера анализ условия и переход требуют по крайней меру двух
строк исходного кода (а сложных условных переходов нескольких
строк), условные переходы в языке Ассемблера менее очевидны и
больше способствуют ошибкам, чем соответствующие операторы Паска-
ля и Си.
1. Одной из общих ошибок является использование инструкций
JA, JB, JAE или JBE для сравнения значений со знаком
или, соответственно, инструкций JG, JL, JGE или JLE для
сравнения беззнаковых значений.
2. Еще одна общая ошибка заключается в использовании, ска-
жем, инструкции JA там, где нужно использовать JAE. Нуж-
но помнить о том, что без буквы E в конце инструкции в
сравнении не учитывается случай, когда два операнда рав-
ны.
3. Еще одной общей ошибкой является использование инверти-
рованной логики, например, применение инструкции JS там,
где нужно использовать JNS.
Один из подходов, позволяющий минимизировать ошибки при ис-
пользовании условных переходов состоит в комментировании перехо-
дов в соответствии с обозначениями, аналогичными языку Си. Напри-
мер:
.
.
.
;
; if ( Length > MaxLength ) (
;
mov ax,[Length]
cmp ax,[MaxLength]
jng LengthIsLessThanMax
.
.
.
jng EndMaxLengthTest
;
; ) else (
;
LengthIsLessThanMax:
.
.
.
;
; )
;
EndMaxLengthTest:
.
.
.
Ошибки в строковых инструкциях
-----------------------------------------------------------------
Строковые инструкции - это самые мощные и уникальные инст-
рукции среди набора инструкций процессора 8086. Эти особеннос-
ти порождают несколько описываемых далее проблем.
Неверное понимание работы префикса REP
-----------------------------------------------------------------
Строковые инструкции обладают любопытной особенностью: после
их выполнения используемые ими указатели ссылаются на адрес, пре-
вышающие на 1 байт (или на два байта в случае инструкции для ра-
боты со словами) последний обработанный адрес. Например, после
выполнения следующего кода:
.
.
.
cld ; отсчет в строковой инструкции
; в прямом направлении
mov si,0 ; ссылка на смещение 0
lodsb ; считать байт по смещению 0
.
.
.
регистр SI будет содержать не 0, а 1. Это имеет смысл, поскольку
в следующей инструкции LODSB вы, вероятно, захотите обратиться к
адресу 1, а в еще одной - к адресу 2. Но при повторении строковых
инструкций это может вызвать некоторую путаницу, особенно при
использовании REP SCAS и REP CMPS. Рассмотрим следующий фрагмент
программы:
.
.
.
cld ; отсчет в строковой
; инструкции в прямом
; направлении
les di,[bp+ScanString] ; ES:DI указывают на
; просматриваемую строку
mov cx,MAX_STRING_LEN ; проверить до самой
; длинной строки
mov al,0 ; поиск завершающего нуля
repne scasb ; выполнить поиск
.
.
.
Предположим, значение регистра ES равно 2000h, DI = 0, а па-
мять, начинающаяся по адресу 2000:0000 содержит значения:
41h 61h 72h 64h 00h
После выполнения этого кода регистр DI будет содержать зна-
чение 5 - смещение байта после того байта, в котором найдено зна-
чение 0. Чтобы возвратить указатель на последний символ строки,
предыдущий фрагмент программы должен иметь следующий вид:
.
.
.
cld ; отсчет в строковой
; инструкции в прямом
; направлении
les di,[bp+ScanString] ; ES:DI указывают на
; просматриваемую строку
mov cx,MAX_STRING_LEN ; проверить до самой
; длинной строки
mov al,0 ; поиск завершающего нуля
repne scasb ; выполнить поиск
jne NoMatch ; ошибка: завершающий 0
; не найден
dec di ; ссылка обратно на 0
dec di ; ccылка обратно на
; последний символ
ret
NoMatch:
mov di,0 ; возвратить нулевой
; указатель
mov es,di
ret
.
.
.
Нужно помнить о том, что когда флаг направления установлен
таким образом, что в строковой инструкции будет выполняться об-
ратный отсчет, регистр DI будет указывать на предыдущий байт, а
не на последующий (после последнего найденного символа).
Аналогичная путаница может произойти, когда при использова-
нии инструкций REP SCAS и REP CMPS регистр CX уменьшается на еди-
ницу больше, чем этого можно ожидать. Значение регистра CX умень-
шается не только для каждого байта, удовлетворяющего условию
"повторять, пока равно (или не равно)", но и еще на 1 для того
байта, для которого условие не выполнено (что приводит к прекра-
щению выполнения инструкции).
Например, если в последнем примере байт 2000:0000 содержит
0, то после выполнения инструкции регистр CX содержал бы значение
MAX_STRING_LEN-1, даже если ни один ненулевой символ не был
найден. С учетом всего сказанного подпрограмма для подсчета числа
символов в строке должна иметь следующий вид:
; Возвращает длину в байтах строки, завершающейся нулем.
; Ввод: ES:DI - начало строки
; Вывод: AX - длина строки, исключая завершающий 0
; ES:DI - указывают на последний байт строки или
; содержат 0000:0000, если завершающий 0 не был
; найден
;
StringLength PROC NEAR
cld ; отсчет в прямом направлении
push cx ; сохранить значение CX
mov cx,0FFFFh ; максимальная длина поиска
mov al,0 ; завершающий байт, до которого
; нужно выполнять поиск
repne scasb ; поиск завершающего нуля
jne StringLengthError ; ошибка, если конец строки
; не найден
mov ax,0FFFFh ; максимальная длина
; просматриваемой строки
sub ax,cx ; посмотреть, сколько байт
; было подсчитано
dec ax ; не считать завершающий 0
dec di ; переместить указатель обратно
; на завершающий 0
dec di ; переместить указатель на
; последний символ
jmp short StringLengthEnd
StringLenghtError:
mov di,0 ; возвратить нулевой указатель
mov es,di
StringLengthEnd:
pop cx ; восстановить исходное значение CX
ret
StringLength ENDP
Другая потенциальная проблема, возникающая из-за того, что
регистр CX указывает со смещением на один байт после выполнения
инструкций REP SCAS или REP CMPS, состоит в том, значение CX в
конце сравнения может быть нулевым, даже если условие завершения
не обнаружено. Следующий код не будет корректно определять, сов-
падают ли два массива, так как регистр CX примет значение 0 при
сравнении двух несовпадающих массивов, которые отличаются только
последним байтом:
.
.
.
repz cmpsb
jcxz ArraysAreTheSame
.
.
.
Корректными инструкциями, проверяющими равенство массивов,
будут следующие:
.
.
.
repz cmpsb
jz ArraysAreTheSame
.
.
.
Короче говоря, регистр CX следует использовать только как
счетчик байт, просматриваемых в инструкциях REP SCAS и REP CMPS,
а не как указатель того, что просматриваемые или сравниваемые
данные оказались равны или не равны.
Если при работе в ваших программах повторяемых строковых
инструкций вы встретите затруднения, то лучше всего будет с по-
мощью карандаша и бумаги или отладчика отследить по шагам, что
делает ваша повторяющаяся строковая инструкция.
Нулевое содержимое регистра CX и работа с целым сегментом
-----------------------------------------------------------------
При выполнении любой строковой инструкции с содержимым ре-
гистра CX, равным 0, не будет выполняться никаких функций. Это
может оказаться удобным, так как перед выполнением строковой инс-
трукции не нужно делать проверку на 0. С другой стороны, нет спо-
соба получить доступ к каждому байту сегмента с помощью байтовой
строковой инструкции. Например, в следующем фрагменте кода прос-
матривается сегмент, заданный регистром ES, и ищется первое вхож-
дение буквы A:
.
.
.
cld ; поиск в прямом направлении
sub di,di ; начать по смещению 0
mov al,'A' ; до обнаружения буквы 'A'
mov cx,0FFFFh ; сначала проверить первые 64К
repne SCASb ; просмотреть первые 64К-1 байт
je AFound ; найти A
scasb ; еще не найдена: просмотреть
; последний байт
je AFound ; найти ее в последнем байте
. ; в данном сегменте нет буквы 'A'
.
.
AFound: ; DI - 1 указывает на букву 'A'
.
.
.
В использовании при отсчете нулевых значений регистра CX в
наборе инструкций процессора 8086 имеется "несимметрия". В то
время, как повторяющаяся строковая инструкция при нулевом значе-
нии CX вообще не выполняет никаких операций, инструкция LOOP при
значении CX, равным 0, выполняется, уменьшая CX до значения
0FFFFh и осуществляя переход на адрес цикла. Это означает, что в
одном цикле можно обработать все 64К. Предыдущий пример, где со-
держимое сегмента, заданного регистром CX, просматривается на
предмет наличия буквы A, можно реализовать с помощью инструкции
LOOP следующим образом:
.
.
.
cld ; просмотр в прямом направлении
sub di,di ; начать со смещения 0
mov al,'A' ;
sub cx,cx ; поиск в 64К
ASearchLoop:
scasb ; проверить следующий байт
je AFound ; это буква 'A'
loop ASearchLoop ; в этом сегменте нет буквы 'A'
.
.
.
AFound: ; на букву 'A' указывает DI - 1
.
.
.
С другой стороны, случай, когда CX = 0, требует специальной
проверки при использовании инструкции LOOP (в противном случае
будут обработаны 64К кода с возможно катастрофическими для прог-
раммы последствиями). В таких случаях полезно использовать инст-
рукцию JCXZ:
; Подпрограмма для заполнения 64К - 1 байт заданным значением.
; Ввод: AL - заданное значение-заполнитель
; CX - количество заполняемых байт
; DS:BX - начальный адрес заполнения
; Регистры BX и CX изменяются.
;
FillBytes PROC NEAR
jcxz FillBytesEnd ; если число заполняемых байт
; равно 0, выполнить
FillBytesLoop:
mov [bx],al ; заполнить байт
inc bx ; ссылка на следующий байт
loop FillBytesLoop ; выполнить для заданного
; числа байт
FillBytesEnd:
ret
FillBytes ENDP
Без инструкции JCXZ, когда значение CX равно 0, процедура
FillBytes заполнила бы весь сегмент, на который указывает регистр
ES, значением в регистре AL, вместо того, чтобы оставить память
без изменений.
Использование некорректно заданного флага направления
-----------------------------------------------------------------
При выполнении строковой инструкции в зависимости от состоя-
ния флага направления соответствующие регистры-указатели (или ре-
гистр) SI, DI или оба регистра увеличиваются или уменьшаются.
Флаг направления можно очистить с помощью инструкции CLD,
при этом указатели будут увеличиваться (отсчет в прямом направле-
нии), или установить с помощью инструкции STD, при этом указатели
будут уменьшаться (отсчет в обратном направлении). Будучи очищен-
ным или установленным, флаг направления остается в таком состоя-
нии до выполнения следующей инструкции CLD или STD или до извле-
чения флагов из стека с помощью инструкции POPF или IRET. Хотя
очень удобно бывает установить один раз в программе флаг направ-
ления, а затем выполнять серию строковых инструкций, работающих в
одном направлении, из-за него могут возникать также трудно обна-
руживаемые ошибки, что приводит к неожиданному поведению строко-
вых инструкций, в зависимости от кода, который выполнялся намного
раньше.
Почему это происходит? В большинстве программ флаг направ-
ления почти всегда сбрасывается, поскольку отсчет в прямом нап-
равлении интуитивно проще, чем отсчет в обратном направлении, и
работает он во многих случаях прекрасно. Однако, имеются отдель-
ные ситуации, когда можно использовать только отсчет в обратном
направлении. Если вы по привычке будете подразумевать, что флаг
направления всегда очищен, но забудете очистить флаг после одной
(или нескольких) из процедур, где флаг направления устанавливает-
ся, то в результате та часть вашей программы, где отсчет ведется
в прямом направлении, будет работать прекрасно, до того момента,
пока не выполнится процедура, устанавливающая флаг направления.
Выход очевиден. Перед использованием строковых инструкций,
если есть даже небольшая вероятность того, что флаг направления
не установлен должным образом, его всегда нужно устанавливать в
нужное состояние. В общем случае хорошим стилем является установ-
ка флага в соответствующее значение в начале любой процедуры, где
используются строковые инструкции.
Неправильное использование повторяемого сравнения строк
-----------------------------------------------------------------
Инструкция CMPS сравнивает две области памяти, в то время,
как инструкция SCAS сравнивает аккумулятор области памяти. Если
перед инструкцией следует префикс REPE, то каждая из этих инст-
рукций может выполнять сравнение, пока не станет равным содержи-
мое регистра CX или не обнаружится несовпадение. К сожалению,
легко можно спутать, какой из префиксов повторения REP что дела-
ет.
Хороший способ запомнить функцию данного префикса REP состо-
ит в мысленном включении после REP (повторить) слова "пока". При
этом, например, REPE принимает вид "повторять, пока E" (то есть
"повторять, пока равно"), REPNE - "повторять, пока не равно" и
т.д.
Программист забывает об использовании сегментов по умолчанию
-----------------------------------------------------------------
В каждой строковой инструкции используется (если он имеется)
исходный сегмент (источник), заданный регистром DS, и целевой
сегмент (приемник), заданный регистром ES. Об этом легко забыть и
попытаться применить, скажем, инструкцию STOSB к сегменту данных,
поскольку именно там обычно находятся все данные, которые вы об-
рабатываете с помощью нестроковых инструкций. Можно легко напи-
сать следующее:
.
.
.
cld ; отсчет при поиске в прямом направлении
mov al,0
mov cx,80 ; длина буфера
repe scasb ; найти первый ненулевой символ, если он
; имеется
jz AllZero ; нет ненулевого символа
dec di ; ссылка обратно на первый ненулевой
; символ
mov al,[dl] ; получить первый ненулевой символ
.
.
.
AllZero:
.
.
.
Проблема здесь состоит в том, что если DS и ES не совпадают,
последняя инструкция MOV не будет загружать в AL корректное зна-
чение, так как инструкция STOSB работает относительно регистра
ES, а MOV - относительно регистра DS. В правильном коде в инст-
рукции MOV следовало бы использовать префикс переопределения сег-
мента (пояснение этого приводится в Главе 9).
.
.
.
cld ; отсчет при поиске в прямом направлении
mov al,0
mov cx,80 ; длина буфера
repe scasb ; найти первый ненулевой символ, если он
; имеется
jz AllZero ; нет ненулевого символа
dec di ; ссылка обратно на первый ненулевой
; символ
mov al,es:[dl] ; получить первый ненулевой символ
; (из ES!)
.
.
.
AllZero:
.
.
.
Нужно также помнить о том, что хотя можно переопределить
сегмент DS, используемый в качестве сегмента-источника, например:
.
.
.
lods es:[SourceArray]
.
.
.
сегмент-приемник ES переопределить нельзя. Поэтому следующий ва-
риант работать не будет:
.
.
.
stos DS:[DestArray]
.
.
.
Такие ошибки Турбо Ассемблер распознает на этапе ассемблиро-
вания.
Ошибки при использовании байтовых операций и операций со словами
-----------------------------------------------------------------
В общем случае в строковых инструкциях желательно использо-
вать максимально возможный размер данных (обычно это слово, или
двойное слово при работе с процессором 80386), поскольку такие
строковые инструкции обычно выполняются быстрее. Например:
.
.
.
mov cx,200 ; число перемещаемых байт
.
.
.
shr cx,1 ; преобразовать из байт в слова
rep movsw ; переместить блок размером в слово
.
.
.
На процессоре 8088 это работает почти на 50% быстрее, чем:
.
.
.
mov cx,200 ; число перемещаемых байт
.
.
.
rep movsw ; переместить блок размером в байт
.
.
.
Однако здесь имеется пара потенциальных ошибок. Во-первых,
при преобразовании счетчика байт в счетчик слов просто с помощью
операции:
shr cx,1
теряется байт, если регистр CX нечетный, поскольку при сдвиге на-
именее значащий бит будет потерян. Случаи, когда значение CX мо-
жет оказаться нечетным, можно обрабатывать следующим образом:
.
.
.
shr cx,1 ; преобразовать в счетчик слов
jnc MoveWord ; счетчик байт нечетный?
movsb ; да, он нечетный - переместить
; нечетный байт
MoveWord:
rep movsw ; перемещение четного числа байт
; пословно
.
.
.
Во-вторых, нужно помнить о том, что инструкция SHR делит
счетчик байт на 2. Использование, скажем, инструкции STOSW со
счетчиком байт, а не со счетчиком слов, может отбросить другие
данные и привести к различным проблемам. Например, при использо-
вании инструкций:
.
.
.
mov cx,200 ; число перемещаемых данных
.
.
.
rep movsv ; перемещать по блоку размером
; в слово
.
.
.
будет отброшено 200 байт (100 слов), которые следуют непосредс-
твенно за целевым блоком.
Использование нескольких префиксов
-----------------------------------------------------------------
Строковые инструкции с несколькими префиксами надежно рабо-
тать не будут. В общем случае их следует избегать. В качестве
примера можно привести инструкцию:
.
.
.
rep movs es:[DestArray],ss:[SourceArray]
.
.
.
где присутствует как префикс REP, так и префикс переопределения
сегмента SS. Множественные префиксы могут привести к ошибкам, так
как по аппаратному прерыванию строковая инструкция может прекра-
тить работу в процессе цикла повторения. В некоторых процессорах
фирмы Intel, включая процессоры 8086 и 8088, после того, как
строковая инструкция возобновляет работу послу обслуживания пре-
рывания, все префиксы, кроме последнего, игнорируются. В резуль-
тате инструкция может не отработать заданное число раз, или прои-
зойдет обращение к неверному сегменту.
Если вам абсолютно необходимо использовать строковые инст-
рукции с несколькими префиксами, то на время выполнения инструк-
ции нужно запретить прерывания. Например:
.
.
.
cli
rep mov es:[DestArray],ss:[SourceArray]
sti
.
.
.
Ошибки при использовании операндов строковых инструкций
-----------------------------------------------------------------
Необязательный операнд или операнды строковых инструкций ис-
пользуются только для задания размера данных и переопределения
сегмента и не обеспечивают того, что соответствующие ячейки памя-
ти действительно будут доступны. Например, в программе:
.
.
.
DestArray DW 256 dup (?)
.
.
.
cld ; отсчет в прямом направлении
; при заполнении
mov al,'*' ; байт для заполнения
mov cx,256 ; число заполняемых слов
mov di,0 ; адрес начала заполнения
rep stos es:[DestArray] ; выполнить заполнение
.
.
.
256 байт, начиная со смещения 0 в сегменте ES, заполняются симво-
лом '*', независимо от того, где находится массив DestArray.
ES:[DestArray] просто указывает Ассемблеру, что нужно использо-
вать инструкцию STOSW, так как DestArray - это массив слов. Имен-
но содержимое SI и/или DI, а не операнды, определяет, по какому
смещению будет осуществляться доступ в строковых инструкциях. Тем
не менее, использование необязательных операндов (операнда) в
строковых инструкциях обеспечивает, что вы, например, случайно не
будете случайно осуществлять пословный доступ к байтовому масси-
ву.
Аналогично, необязательный операнд в инструкции XLAT исполь-
зуется только для проверки типа и переопределения сегмента. Прог-
рамма:
.
.
.
LookUpTable LABEL BYTE
.
.
.
ASCIITable LABEL BYTE
.
.
.
mov bx,OFFSET ASCIITabel ; ссылка на таблицу
; просмотра
mov al,[CharacterToTranslate] ; получить байт
xlat [LookUpTable] ; отобразить его
.
.
.
отображает байт, задаваемый регистром AL, в таблице ASCIITable,
а не в LookUpTable, но Ассемблер здесь будет работать правильно,
поскольку все, что делает операнд в инструкции XLAT - это обеспе-
чение байтового размера и переопределение сегмента. Инструкция
XLAT всегда отображает (транслирует) содержимое по смещению BX +
AL, независимо от используемого операнда.
Программист забывает о необычных побочных эффектах
-----------------------------------------------------------------
Поскольку программы Ассемблера записаны на "родном" языке
процессора 8086, любые изменения в состоянии регистров или флагов
процессора 8086 должны представлять для работающего на Ассемблере
программиста особый интерес. Большинство способов, с помощью ко-
торых программа на Ассемблере может изменить состояние процессо-
ра, достаточно непосредственны и очевидны. Например, инструкция:
add bx,[Grade]
прибавляет 16-битовое значение по адресу Grade к BX и, чтобы от-
разить результат сложения, изменяет флаги переполнения, знака,
нуля, дополнительного переноса, четности и переноса. Однако неко-
торые инструкции изменяют состояние процессора менее очевидным
образом. Рассмотрим кратко некоторые из таких инструкций.
Потеря содержимого регистра при умножении
-----------------------------------------------------------------
При умножении (8-разрядного значения на 8-разрядное,
16-разрядного значения на 16-разрядное или 32-разрядного значения
на 32-разрядное) всегда теряется содержимое по крайней мере одно-
го регистра, отличного от той части аккумулятора, которая исполь-
зуется в качестве операнда-источника. Это неизбежно приводит к
тому, что результат перемножения двух 8-разрядных значений будет
занимать 16 бит, результат перемножения 16-разрядных значений -
32 бита, а 32-разрядных значений - 64 бита. Перемножение операнда
-источника и операнда-приемника показано в Таблице 6.1.
Таблица 6.1
Источник и приемник в операциях MUL и IMUL
-----------------------------------------------------------------
Источник Источник Приемник
Размер операнда Явный Неявный
в байтах операнд операнд Старший Младший Пример
-----------------------------------------------------------------
8х8 reg8 (*) AL AH AL mul dl
16х16 reg16 (**) AX DX AX imul bx
32х32 (***) reg32 (****) EAX EDX EAX mul esi
-----------------------------------------------------------------
* reg8 может представлять собой любой из следующих регистров:
AH, AL, BH, BL, CH, CL, DH или DL.
** reg16 может быть любым из следующих регистров: AX, BX, CX,
DX, SI, DI, BP или SP.
*** Операция умножения 32х32 процессорами 8086, 8088, 80186,
80188 и 80286 не поддерживается.0
**** reg32 может быть любым из следующих регистров: EAX, EBX,
ECX, EDX, ESI, EDI, EBP или ESP.
Хотя все это выглядит достаточно простым, в синтаксисе инс-
трукций MUL и IMUL скрыто много деталей, так как явно указывается
только один из операндов и размер, а регистры используемые в ка-
честве операнда-приемника, просто подразумеваются. Эти скрытые
детали приводят к тому, что легко можно упустить из виду исполь-
зование какого-либо неявного регистра. Есть много случаев, в ко-
торых, скажем, программист знает, что результат перемножения
16-разрядного значения на 16-разрядную величину, поместится в ре-
гистр AX. При этом часто забывают, что теряется содержимое ре-
гистра CX. Поэтому всегда нужно помнить о том, что при использо-
вании инструкций MUL и IMUL уничтожается содержимое не только
регистров AL, AX, или EAX, но также и AH, DX или EDX.
В строковых инструкциях изменяется несколько регистров
-----------------------------------------------------------------
При выполнении только одной из строковых инструкций (MOVS,
STOS, LODS, CMPS или SCAS) может изменяться содержимое нескольких
флагов и до трех регистров. Как и в инструкции MUL, в строковых
инструкциях многие эффекты не выражаются явно в операндах этих
инструкций. При использовании данных инструкций нужно помнить о
том, что SI или DI (или оба регистра) увеличиваются или уменьша-
ются (в зависимости от состояния флага направления) при каждом
выполнении строковой инструкции. Регистр CX также уменьшается по
крайней мере один раз, а при использовании префикса REP - возмож-
но до тех пор, пока его содержимое не станет равным нулю.
Изменение отдельными инструкциями флага переноса
-----------------------------------------------------------------
В то время как некоторые инструкции "непредвиденным" образом
изменяют содержимое регистров или флагов, другие инструкции не
влияют на все те флаги, изменения которых вы ожидаете. Например,
инструкция:
inc ah
выглядит логически эквивалентной инструкции:
add ah,1
и это действительно так, но с одним исключением. В то время как
инструкция ADD в случае слишком большого для операнда-приемника
результата устанавливает флаг переноса, инструкция INC никоим об-
разом не него не влияет. В результате инструкции:
.
.
.
add ax,1
adc dx,0
.
.
.
можно использовать для увеличения 32-битового значения, храняще-
гося в регистрах DX:AX, а инструкции:
.
.
.
inc ax
adc dx,0
.
.
.
нельзя. Тоже самое имеет место для инструкции DEC, в то время как
инструкции LOOP, LOOPZ и LOOPNZ не влияют на состояние флагов. На
практике это иногда можно выгодно использовать, так как в отдель-
ных случаях может оказаться удобным выполнить одну из этих инст-
рукций без нарушения установки флага переноса. Всегда важно точно
знать, что делает каждая используемая вами инструкция. Если у вас
есть сомнения относительно того, как влияет конкретная инструкция
на содержимое флагов, лучше обратитесь к справочнику.
Программист долго не использует состояние флагов
-----------------------------------------------------------------
Состояние флагов сохраняется до тех пор, пока следующая
инструкция их не изменит. Обычно это не слишком долгий интервал
времени. Хорошей практикой программирования является возможно
скорейшее использование флагов их установки, что позволяет избе-
жать многих потенциальных ошибок. Например, часто хочется прове-
рить условие, задать содержимое одного-двух регистров, и только
после этого в соответствии с результатом проверки выполнить пере-
ход. Инструкции:
.
.
.
cmp ax,1
mov ax,0
jg HandlePositive
.
.
.
представляют собой вполне допустимый способ проверки состояния
регистра AX, затем установки его в значение 0 и обработки резуль-
тата. С другой стороны, инструкции:
.
.
.
cmp ax,1
sub ax,ax
jg HandlePositive
.
.
.
которые выглядят более привлекательно, поскольку такой код короче
и выполняются быстрее, правильно работать не будут, так как при
вычитании теряется содержимое всех флагов, установленных при
сравнении. Это типичная проблема, возникающая, когда вы не торо-
питесь использовать состояние флагов.
Не путайте операнды в памяти и непосредственные операнды
-----------------------------------------------------------------
Программа на Ассемблере может ссылаться либо на переменную
памяти по смещению, либо на значение, хранящееся в этой перемен-
ной. К сожалению, в языке Ассемблера отсутствует строгость или
очевидность в отношении того, как можно выполнять эти два вида
ссылок. В результате ссылки на переменную в памяти по смещению и
по значению часто путают.
На Рис. 6.1 показано различие между смещением и значением
переменной в памяти. Переменная в памяти размером в слово MemLoc
имеет смещение 5002h, а значение ее равно 1234h.
|
---------------
3000 4FFE | 0001 |
|-------------| Значение MemLoc
3000 5000 | 205F | |
Смещение |-------------| |
MemLoc 3000 5002 | 1234 <---|----------
| ---- |-------------|
| ^ | |
| | | |
------------------- | |
3000 5004 | 9145 |
|-------------|
3000 5006 | 0000 |
---------------
Рис. 6.1 Переменные в памяти: значение и смещение.
На Рис. 6.1 смещение переменной в памяти размером в слово
MemLoc представляет собой константу 5002, которую можно получить
с помощью оператора OFFSET. Например, инструкция:
mov bx,OFFSET MemLoc
загружает значение 5002h в регистр BX. Значение 5002h представля-
ет собой непосредственный операнд. Другими словами, оно встроено
непосредственно в инструкцию и не изменяется.
Значением MemLoc является 1234h. Оно считывается из памяти
со смещением 5002h в сегменте данных. Один из способов считывания
данного значения состоит в загрузке в регистр BX, SI, DI или BP
смещения MemLoc и использования данного регистра для адресации к
памяти. Инструкции:
mov bx,OFFSET MemLoc
mov ax,[bx]
загружают значение MemLoc в регистр AX. Значение MemLoc можно
также загрузить непосредственно в AX с помощью инструкции:
mov ax,MemLoc
или
mov ax,[MemLoc]
Здесь значение 1234h получается, как прямой, а не как непос-
редственный операнд: инструкция MOV использует встроенное в нее
смещение 5002h и загружает в AX значение по смещению 5002h, кото-
рое в данном случае равно 1234h.
В итоге значение 1234h не связывается постоянно с переменной
MemLoc. Например, инструкции:
mov [MemLoc],5555h
mov ax,[MemLoc]
загружают в регистр AX значение 5555h, а не 1234h.
Основная идея заключается в том, что если смещение перемен-
ной MemLoc представляет собой значение-константу, которая описы-
вается фиксированным адресом в сегменте данных, то значение
MemLoc - это изменяемое число, хранящееся по данному адресу. Пос-
ле выполнения инструкций:
mov [MemLoc],1
add [MemLoc],3
переменная MemLoc получает значение 3, но инструкция:
add OFFSET MemLoc,2
эквивалентна инструкции:
add 5002h,2
которая не имеет смысла, так как невозможно выполнить операцию
ADD, прибавив одну константу к другой.
Удивительно часто встречающейся ошибкой является то, что ув-
лекшись написанием программы часто забывают использовать операцию
OFFSET, например:
mov si,MemLoc
где нужно использовать смещение MemLoc. на первый взгляд данная
строка не выглядит неправильной, и так как MemLoc - это перемен-
ная размером в слово, то эта строка не приведет к ошибке ассем-
блирования. Однако при выполнении в SI будут загружены содержащи-
еся в переменной MemLoc данные (1234h), а не ее смещение (5002h),
и результаты будут непредсказуемы.
Надежного способа избежать этой проблемы нет, но можно при-
нять за правило заключать все ссылки на память в квадратные скоб-
ки. Когда перед ссылками на адресные константы будет указываться
префикс OFFSET, а ссылки на память - заключаться в квадратные
скобки, это устранит двусмысленность и неопределенность при ис-
пользовании имен переменных памяти. При таком соглашении работа
инструкций:
mov si,OFFSET MemLoc
и
mov si,[MemLoc]
становится совершенно понятной, в то время как инструкция:
mov si,MemLoc
будет настораживать.
Границы сегментов
-----------------------------------------------------------------
Один из наиболее трудных моментов при программировании для
процессоров 8086 заключается в том, что к памяти нельзя обращать-
ся, как к одному большому массиву байт. Она доступна только по
частям, каждая из которых равна 64К и связана с сегментным ре-
гистром. Использование сегментов может приводить к трудноуловимым
ошибкам, так как если программа пытается обратиться к адресу за
концом сегмента, она в действительности вернется назад и будет
обращаться к началу сегмента.
В качестве примера предположим, что память, начинающаяся с
адреса 10000h, содержит данные, показанные на Рис. 6.2. Когда ре-
гистр DS устанавливается в значение 1000h, программа, которая об-
ращается к строке "Testing" по адресу 1000:FFF9, после символа g
по адресу 1000:FFFF, возвращается назад к байту по адресу
1000:0000, так как смещение не может превышать 0FFFFh (максималь-
ное 16-битовое значение).
Первый байт, адресуемый относительно
DS, равен 1000h (адрес 1000 0000)
| |
--------------- |
10000 | 21 |<----------------
|-------------|
10001 | 90 |
|-------------|
10002 | 29 |
|-------------|
10003 | 52 |
|-------------|
10004 | 7F |
---------------
---------------
1FFF9 | 54 ('T') |
|-------------|
1FFFA | 65 ('e') |
|-------------|
1FFFB | 73 ('s') |
|-------------|
1FFFC | 74 ('t') |
|-------------|
1FFFD | 69 ('i') | Последний байт, адресуемый
|-------------| относительно DS = 1000h
1FFFE | 6E ('n') | (адрес 1000 FFFF)
|-------------| |
1FFFF | 67 ('g') |<---------------
|-------------|
20000 | 00 (NULL) |
---------------
Рис. 6.2 Пример достижения границы сегмента.
Предположим теперь, что при DS:SI, равном 1000:FFF9, вызыва-
ется подпрограмма для преобразования строки "Testing" в верхний
регистр:
; Подпрограмма для преобразования завершающейся нулевым
; символом строки в верхний регистр.
;
; Ввод: DS:DI - указатель на строку.
;
ToUpper PROC NEAR
mov ax,[si] ; получить следующий символ
cmp al,0 ; если 0...
jz ToUpperDone ; ...преобразовать строку
cmp al,'a' ; это строчная буква?
jb ToUpperCase ; не строчная буква
cmp al,'z'
ja ToUpperNext ; не строчная буква
and al,NOT 20h ; строчная буква, преобразовать
; ее в верхний регистр
mov [si],al ; сохранить прописную букву
ToUpperNext:
inc si ; ссылка на следующий символ
jmp ToUpper
ToUpperDone:
ret
ToUpper ENDP
После того, как процедура ToUpper обработает первые семь
символов строки, SI изменит значение с 0FFFFh на 0. (SI - это
16-разрядный регистр, поэтому отсчет с превышением значения
0FFFFh выполнить нельзя.) Завершающий строку нулевой байт, запи-
санный по адресу 20000h, достигнут не будет. Вместо этого проце-
дура начнет преобразовывать не относящиеся к делу байты по адресу
10000h и не остановится, пока не встретит нулевой байт. На более
позднем этапе эти измененные байты могут вызвать некорректную ра-
боту программы. Часто такие ошибки, вызванные случайным изменени-
ем байт при достижении программы конца сегмента, бывает очень
трудно отследить, поскольку эти ошибки могут проявляться совсем в
другом месте программы и в другое время.
Простую рекомендацию здесь дать трудно. Нужно просто обеспе-
чивать, чтобы ваша программа непреднамеренно не выходила за конец
сегмента. Кроме того не следует обращаться к слову с адресом
0FFFFh. Машина может "зависнуть".
Неполное сохранение состояния в обработчике прерываний
-----------------------------------------------------------------
Обработчик прерываний - это программа, на которую осуще-
ствляется переход при аппаратном прерывании, например, прерывании
от клавиатуры. Обработчик прерываний выполняет множество функций,
таких, как буферизация клавиш или изменение системного таймера.
Прерывание может произойти в любое время в процессе работы любой
программы, поэтому обработчик прерываний после завершения работы
(выход из обработчика) должен вернуть регистры и флаги процессора
в точности в тоже самое состояние, которое было при входе в обра-
ботчик прерываний. Если это не делается, то программа, при выпол-
нении которой произошло прерывание, может обнаружить, что состоя-
ние процессора непредвиденным образом изменилось.
Например, при выполнении программы:
.
.
.
mov ax,[ReturnValue]
ret
.
.
.
прерывание может произойти между этими двумя инструкциями. Если
обработчик прерываний не сохранит содержимое AX, то возвращаемое
в вызывающую программу значение будет основываться на том, что
делал обработчик прерываний, а не на значении переменной
ReturnValue.
Поэтому каждый обработчик прерываний должен явным образом
сохранять содержимое всех регистров. Хотя допускается сохранять
только те регистры, которые изменяет обработчик прерываний, для
большей гарантии лучше при входе в обработчик занести все регист-
ры в стек, а при выходе - восстановить их из стека. Ведь в один
прекрасный день вы можете вернуться к коду обработчика прерыва-
ний, модифицируете его и используете дополнительные регистры, за-
быв при этом данные регистры сохранить.
Сохранение флагов в обработчике прерываний необходимым не
является. Когда происходит прерывание, все флаги автоматически
заносятся в стек, а когда обработчик прерываний выполняет инст-
рукцию IRET, чтобы вернуться в прерванную программу, флаги авто-
матически восстанавливаются из стека. Итогом абсолютной необходи-
мости сохранения в обработчике прерываний всех регистров является
следующее правило: нельзя делать предположения о состоянии ре-
гистров или флагов при входе в обработчик прерываний. Классичес-
ким примером этого является обработчик прерываний, который вы-
полняет строковые инструкции, не установив предварительно явным
образом флаг направления. Нужно помнить о том, что когда происхо-
дит прерывание, может выполняться любая программа, поэтому после
того, как вы сохраните регистры прерванной программы, вы должны
установить регистры (включая сегментные регистры) и флаги таким
образом, как это необходимо. Только после этого вы можете выпол-
нять другие функции.
Не забывайте об определении группы в операндах и таблицах данных
-----------------------------------------------------------------
Концепция группы сегментов проста и полезна. Вы можете опре-
делить, что несколько сегментов принадлежат к одной и той же
группе, а компоновщик комбинирует эти сегменты в один сегмент.
При этом все данные в сгруппированных сегментах адресуются отно-
сительно одного и того же сегментного регистра. На Рис. 6.3 пока-
зано три сегмента: Seg1, Seg2 и Seg3, сгруппированные в GroupSeg.
Адресация ко всем трем сегментам осуществляется одновременно от-
носительно одного сегментного регистра, загруженного базовым ад-
ресом GroupSeg.
Смещение 0 в GroupSeg
= смещению 0 в Seg1
|
/ ---------------------- <-------
| | |
| | |
| | Seg1 | Смещение 2000h в
| | (размер 8K) | GroupSeg = смещению 0
| | | в Seg2
| | | |
| |--------------------| <-------
| | |
| | |
| | Seg2 |
| | (размер 12K) |
GroupSeg | | | Смещение 5000h в
| | | GroupSeg = смещению 0
| | | в Seg3
| | | |
| |--------------------| <-------
| | |
| | |
| | Seg3 |
| | (размер 64K) |
| | |
| | |
| | |
| | |
\ ----------------------
Рис. 6.3 Три сегмента, объединенные в одну группу сегментов.
Группы сегментов позволяют вам логически разделить данные на
ряд областей без необходимости загружать сегментный регистр каж-
дый раз, когда вы хотите перейти от одной логической области дан-
ных к другой.
К сожалению, в обработке групп сегментов в Макроассемблере
фирмы Microsoft MASM имеется ряд проблем, поэтому, пока не поя-
вился Турбо Ассемблер, группы сегментов могли привести к неприят-
ностям. Группы сегментов использовались для компоновки кода Ас-
семблера с языками высокого уровня (например, Си).
Улучшенный режим Турбо Ассемблера (Ideal mode) избавит вас
от проблем, связанных с определением групп сегментов. Это еще
один довод в пользу перехода от программирования в стиле MASM к
улучшенному режиму.
Проблема, которую порождает MASM при работе с группами сег-
ментов заключается в том, что MASM интерпретирует смещения, полу-
ченные с помощью операции OFFSET в данном сегменте группы, как
смещение в этом сегменте, а не как смещение в группе сегментов.
Например, если мы имеет группу сегментов, показанную на Рис. 6.3,
то Ассемблер транслировал бы инструкцию:
mov ax,OFFSET Var1
в
mov ax,0
так как Var1 - это смещение 0 в Seg2, хотя Var1 представляет со-
бой смещение 2000h в GroupSeg. Поскольку предполагается, что дан-
ные в группе сегментов адресуются относительно группы сегментов,
а не относительно отдельного сегмента, это порождает ряд проблем.
Решением здесь является использование префикса определения
группы. Строка:
mov ax,OFFSET GroupSeg:Var1
позволяет выполнить корректное ассемблирование Var1, вычисляя его
относительно группы сегментов GruopSeg.
В MASM есть другие аналогичные проблемы, касающиеся исполь-
зуемых в группах сегментов таблиц данных. Как и операция OFFSET,
смещения, ассемблируемые в таблицы данных, генерируются относи-
тельно сегментов, а не относительно групп сегментов. Суть этой
проблемы показана в следующем примере:
Stack SEGMENT WORD STASK 'STACK'
DB 512 DUP (?) ; зарезервировать простран-
; ство для стека размеров
; 1/2 К
Stack ENDS
;
; Определить группу сегмента данных DGROUP, состоящую
; из Data1 и Data2
;
DGROUP GROUP Data1, Data2
;
; Первый сегмент в DGROUP.
;
Data1 SEGMENT WORD PUBLIC 'DATA'
Scratch DB 100h DUP (0) ; буфер размером 256K
Data1 ENDS
;
; Второй сегмент в DGROUP.
;
Data2 SEGMENT WORD PUBLIC 'DATA'
Buffer DB 100h DUP ('@') ; буфер размером 256К,
; заполненный знаками @
BufferPtr DW Buffer ; указатель на буфер
Data2 ENDS
Code SEGMENT PARA PUBLIC 'CODE'
ASSUME CS:Code, DS:DGROUP
;
Start PROC NEAR
mov ax,DGROUP
mov ds,ax ; DS указывает на DGROUP
mov bx,OFFSET GROUP:BufferPtr ; ссылка на указатель
; буфера
; (для получения корректного
; смещения используется
; определение группы)
mov bx,[bx] ; ccылка на сам буфер
;
; (Здесь должен следовать код для обработки буфера)
;
mov ah,4Ch ; функция DOS завершения
; программы
int 21h ; завершить программу и
; выйти в DOS
Start ENDP
Code ENDS
END Start
В данной программе смещение BufferPtr в инструкции:
mov bx,OFFSET DGROUP:BufferPtr
ассемблируется корректно, так как используется префикс определе-
ния DGROUP:группа. Однако другая ссылка на смещение:
BufferPtr DW Buffer
которая должна приводить к инициализации значения BufferPtr сме-
щением Buffer, не будет корректно ассемблироваться, так как сме-
щение Buffer берется относительно сегмента Data2, а не относи-
тельно группы сегментов DGROUP. Решением здесь опять является ис-
пользование префикса определения DGROUP, для чего нужно изменить
BufferPtr DW Buffer
на
BufferPtr DW DGROUP:Buffer ; указатель на Buffer
; (для получения
; корректного указателя
; используется
; определение группы)
Пропуск префикса определения группы при использовании групп
сегментов в режиме MASM приводит к некоторым неприятным ошибкам,
так как ваша программа может при этом выполнять чтение, переход
или модификацию неверных областей памяти. В качестве общего пра-
вила можно порекомендовать не использовать группы при ассемблиро-
вании в режиме MASM, если без этого можно обойтись. Когда вам
приходится использовать группы сегментов (например, при организа-
ции интерфейса с языком высокого уровня) не забывайте при задании
смещений для всех данных группы использовать префикс определения
группы. Этим префиксом довольно легко пользоваться, нужно только
не забывать об этом.
Полезным методом при работе с группой сегментов в режиме
MASM является использование вместо MOV OFFSET инструкции LEA.
Например, инструкция:
lea ax,Var1
выполняет то же действие, что и инструкция:
mov ax,OFFSET GroupSeg:Var1
не требуя использования префикса определения группы. Однако инст-
рукция LEA на байт длиннее и выполняется несколько дольше, чем
инструкция MOV OFFSET.
Кстати, при использовании групп сегментов проблемы могут
возникать только со смещениями, а не с доступом к памяти. В таких
строках, как:
mov ax,[Var1]
не требуется использовать префикс определения группы.
Глава 7. Интерфейс Турбо Ассемблера и Турбо Си
-----------------------------------------------------------------
Хотя некоторые программисты могут разрабатывать программы
целиком на языке Ассемблера (и делают это), другие предпочитают
писать основную часть программы на языке высокого уровня, обраща-
ясь к языку Ассемблера только для осуществления управления нижне-
го уровня или когда требуется высокая производительность. Некото-
рые предпочитают писать преимущественно на Ассемблере, только
иногда используя конструкции и библиотечные средства языков высо-
кого уровня.
Для смешанного программирования на языке высокого уровня и
Ассемблере прекрасно подходит Турбо Си. Для объединения кода Ас-
семблера и Си в нем предусмотрен не один, а целых два механизма.
Средство встроенного Ассемблера в Турбо Си обеспечивает быстрый и
удобный способ для включения кода Ассемблера непосредственно в
функцию Си. Для тех, кто предпочитает при программировании на Ас-
семблере использовать отдельные модули, целиком написанные на
этом языке, такие модули можно ассемблировать отдельно, а затем
компоновать с программами Турбо Си.
Сначала мы рассмотрим использование в Турбо Си встроенного
Ассемблера. Затем мы подробно обсудим компоновку отдельно ассем-
блированых модулей Турбо Ассемблера с модулями Турбо Си и иссле-
дуем процесс вызова функций Турбо Ассемблера и кода Турбо Си. На-
конец, мы рассмотрим вызов функций Турбо Си из Турбо Ассемблера.
Примечание: Когда мы говорим о Турбо Си, речь идет о
Турбо Си версии 1.5 и выше.
Использование в Турбо Си встроенного Ассемблера
-----------------------------------------------------------------
Если вам приходилось искать идеальный способ использования
Ассемблера для тонкой доводки программы на языке Си, у вас, воз-
можно, возникало желание иметь возможность включать инструкции
Ассемблера в те критические места программы на языке Си, где ско-
рость Ассемблера и его возможности управления нижнего уровня при-
вели бы к существенному улучшению производительности. Если это
так, вам возможно не хотелось также связываться с обычными слож-
ностями организации интерфейса Турбо Ассемблера и Си. Более того,
вы, вероятно, хотели бы все это сделать, не меняя ни одного бита
в остальной части программы на языке Си, то есть чтобы уже отла-
женную часть программы на Си не пришлось бы изменять.
TASM2 #2-5/Док = 141 =
В Турбо Си все это можно осуществить с помощью встроенного
Ассемблера. Встроенный Ассемблер - это не что иное, как возмож-
ность помещать практически любой код Ассемблера в программы на
языке Си, при сохранении полного доступа к константам, переменным
и даже функциям Си. На самом деле встроенный Ассемблер прекрасно
подходит не только для тонкой доводки программы, поскольку по
своим возможностям он приближается к программированию на чистом
Ассемблере. Например, высокопроизводительный код в библиотеках
Турбо Си написан с использованием встроенного Ассемблера. Встро-
енный Ассемблер позволяет вам делать в ваших программах точно то,
что вы захотите. При этом вам не придется беспокоиться о деталях
смешения языков.
Рассмотрим следующий код на Си, который служит примером ис-
пользование встроенного Ассемблера:
.
.
.
i = 0; /* установить i в значение 0 (на Си) */
asm dec WORD PTR i; /* уменьшить значение i (на
Ассемблере */
i++; /* увеличить i (на Си) */
.
.
.
Первая и последняя строки выглядят достаточно обычно, а что
представляет собой средняя строка? Как вы уже наверное догада-
лись, строка, начинающаяся с asm, - это встроенный код на Ассемб-
лере. Если вам приходилось использовать отладчик и просматривать
выполняемый код на Си, полученный при компиляции исходного кода,
то применив его, вы можете обнаружить, что между скомпилированным
кодом операторов:
i = 0;
и
i++;
будут включены инструкции:
.
.
.
mov WORD PTR [bp-02],0000
TASM2 #2-5/Док = 142 =
dec WORD PTR [bp-02]
inc WORD PTR [bp-02]
.
.
.
где можно видеть встроенную инструкцию Ассемблера DEC.
Каждый раз, когда Турбо Си обнаруживает ключевое слово asm,
указывающее, что это строка Ассемблера, он помещает данную строку
Ассемблера непосредственно в скомпилированный код с одним измене-
нием: ссылки на переменные Си преобразуются в соответствующий эк-
вивалент на Ассемблере (ссылка на i в предыдущем примере была за-
менена WORD PTR [BP-2]). Короче говоря, ключевое слово asm позво-
ляет вам включать в программу на Си практически любой код на Ас-
семблере (однако здесь есть некоторые ограничения, о которых мы
расскажем далее в разделе "Ограничения при использовании встроен-
ного Ассемблера").
Возможность включать код Ассемблера непосредственно в код
Турбо Си выглядит на первый взгляд несколько рискованной. В самом
деле, определенный риск при использовании встроенного Ассемблера
есть. Но Турбо Си компилирует свой код таким образом, чтобы избе-
жать многих потенциально опасных взаимодействий со встроенным Ас-
семблером. Тем не менее, неправильно функционирующий встроенный
код Ассемблера определенно может привести к серьезным ошибкам.
С другой стороны, любой неправильно написанный код Ассембле-
ра (встроенный или в отдельном модуле) потенциально может повести
себя неправильно: это та цена, которую в Ассемблере приходится
платить за скорость и возможность управления на нижнем уровне.
Однако ошибки во встроенном коде Ассемблера гораздо менее вероят-
ны, чем в программе, целиком написанной на Ассемблере, поскольку
Турбо Си берет на себя множество "мелочей", таких, как вход в
функции и выход из них, передачу параметров и выделение памяти
для переменных. Подводя итоги, можно сказать, что преимущества
использование в Си встроенного кода Ассемблера перекрывают те
неприятности, с которыми можно столкнуться из-за случайной ошибки
в коде Ассемблера.
О программировании с использованием встроенного Ассемблера
можно сделать несколько важных замечаний:
1. Чтобы использовать встроенный Ассемблер, вы должны вы-
звать TCC.EXE, командную версию Турбо Си. TC.EXE (диало-
говая версия Турбо Си) не поддерживает встроенный Ассем-
TASM2 #2-5/Док = 143 =
блер.
2. Весьма возможно, что версия утилиты TLINK, которая пос-
тавляется с вашей копией Турбо Ассемблера, не совпадает
в версией этой утилиты, которая поставляется в комплекте
Турбо Си. Для обеспечения поддержки Турбо Ассемблера в
утилиту TLINK были внесены существенные изменения и,
поскольку эти изменения без сомнения будут продолжаться
в дальнейшем, важно при компоновке модулей Турбо Си, со-
держащих встроенные инструкции Ассемблера, использовать
самую последнюю версию TLINK, которая у вас имеется. Са-
мым надежным способом гарантировать это является хране-
ние на диске, используемом для запуска компоновщика
TLINK, только одного файла TLINK.EXE. Номер версии этого
файла должен быть самым последним среди всех подобных
файлов, которые вы получали от фирмы Borland.
TASM2 #2-5/Док = 144 =
Как работает встроенный Ассемблер
-----------------------------------------------------------------
Обычно Турбо Си компилирует каждый файл исходного кода на
языке Си в объектный файл, а затем вызывает утилиту TLINK для
компоновки объектных файлов в выполняемую программу. Такой цикл
компиляции и компоновки показан на Рис. 7.1 Чтобы начать данный
цикл, нужно ввести команду:
tcc имя_файла
которая указывает Турбо Си, что нужно сначала компилировать файл
имя_файла.С в файл имя_файла.OBJ, а затем вызвать TLINK для ком-
поновки файла имя_файла.OBJ в файл имя_файла.EXE.
----------------------------------
| Исходный файл на языке Си |
| имя_файла.С |
----------------------------------
|
V
------------
( Турбо Си ) Компиляция
------------
|
V
----------------------------------
| Объектный файл языка Си |
| имя_файла.OBJ |
----------------------------------
|
V
------------
( TLINK ) Компоновка
------------
|
V
----------------------------------
| Выполняемый файл |
| имя_файла.EXE |
----------------------------------
Рис. 7.1 Цикл компиляции и компоновки Турбо Си.
Однако при использовании встроенного Ассемблера Турбо Си до-
бавляет в цикл компиляции и компоновки дополнительный шаг.
TASM2 #2-5/Док = 145 =
При обработке компилятором Турбо Си каждого модуля, где со-
держится встроенный код Ассемблера, сначала весь модуль компили-
руется в исходный файл на языке Ассемблера, а затем для трансля-
ции полученного кода Ассемблера а объектный код вызывается Турбо
Ассемблер. После этого для компоновки объектных файлов вызывается
утилита TLINK. Этот процесс показан на Рис. 7.2. Запустить этот
цикл можно с помощью командной строки:
tcc имя_файла
которая указывает Турбо Си, что сначала нужно компилировать файл
имя_файла.С в файл имя_файла.ASM, потом вызвать Турбо Ассемблер
для ассемблирования файла имя_файла.ASM в файл имя_файла.OBJ, а
затем вызвать утилиту TLINK для компоновки файла имя_файла.OBJ в
файл имя_файла.EXE.
----------------------------------
| Исходный файл на языке Си |
| имя_файла.С |
----------------------------------
|
V
------------
( Турбо Си ) Компиляция
------------
|
V
----------------------------------
| Исходный файл на Ассемблере |
| имя_файла.ASM |
----------------------------------
|
V
------------------
( Турбо Ассемблер ) Ассемблирование
------------------
|
V
----------------------------------
| Объектный файл языка Си |
| имя_файла.OBJ |
----------------------------------
|
V
------------
TASM2 #2-5/Док = 146 =
( TLINK ) Компоновка
------------
|
V
----------------------------------
| Выполняемый файл |
| имя_файла.EXE |
----------------------------------
Рис. 7.2 Цикл компиляции, ассемблирования и компоновки Турбо
Си.
Встроенный код Ассемблера просто передается компилятором
Турбо Си в файл на языке Ассемблера. Прелесть это схемы заключа-
ется в том, что Турбо Си не нужно ничего знать об ассемблировании
встроенного кода, вместо этого Турбо Си компилирует исходный код
языка Си на тот же уровень, что и встроенный код (уровень Ассемб-
лера), а затем позволяет Турбо Ассемблеру выполнить трансляцию.
Чтобы увидеть, как Турбо Си работает со встроенным Ассембле-
ром, введем под именем PLUSONE.C следующую программу:
#include <stdio.h>
int main(void)
{
int TestValue;
scanf('%d, &testValue); /* получить значение
для увеличения */
asm inc WORD PTR TestValue; /* увеличить его
(на Ассемблере) */
printf("%d",TestValue); /* напечатать увеличенное
значение */
}
и скомпилируем ее с помощью командной строки:
tcc -s plusone
Параметр -s указывает Турбо Си, что нужно скомпилировать
программу в код на языке Ассемблера и остановиться. После этого
на вашем диске окажется файл PLUSONE.ASM:
ifndef ??version
?debug macro
TASM2 #2-5/Док = 147 =
endm
endif
name Plusone
_TEXT SEGMENT BYTE PUBLIC 'CODE'
DGROUP GROUP _DATA, _BSS
ASSUME cs:_TEXT,ds:DGROUP,ss:DGROUP
_TEXT ENDS
_DATA SEGMENT WORD PUBLIC 'DATA'
_d@ label BYTE
_d@w label WORD
_DATA ENDS
_BSS SEGMENT WORD PUBLIC 'BSS'
_b@ label BYTE
_b@w label WORD
?debug C E90156E11009706C75736F6E652E6
?debug C E90009B9100F696E66C7564655C737464696F2E68
?debug C E90009B9101010696E636C754655C7564655C7374646172672E68
_BSS ENDS
_TEXT SEGMENT BYTE PUBLIC 'CODE'
; ?debug L 3
_main PROC NEAR
push bp
mov bp,sp
dec sp
dec sp
?debug L 8
lea ax,WORD PTR [bp-2]
push ax
mov ax,OFFSET DGROUP:_s@
push ax
call NEAR PTR _scanf
pop cx
pop cx
; debug L 9
inc WORD PTR [bp-2]
; ?debug L 10
push WORD PTR [bp-2]
mov ax,OFFSET GDROUP:_s@+3
push ax
call NEAR PTR _printf
pop cx
pop cx
@1:
; debug L 12
mov sp,bp
pop bp
TASM2 #2-5/Док = 148 =
ret
_main ENDP
_TEXT ENDS
_DATA SEGMENT WORD PUBLIC 'DATA'
-s@ label BYTE
db 37
db 100
db 0
db 37
db 100
db 0
_DATA ENDS
_TEXT SEGMENT BYTE PUBLIC CODE
EXTRN _printf:NEAR
EXTRN _scanf:NEAR
_TEXT ENDS
PUBLIC _main
END
Взглянув на данный код, вы можете в полной мере оценить,
сколько усилий помогает вам сэкономить Турбо Си, обеспечивая под-
держку встроенного Ассемблера.
В комментарии:
; ?debug L 8
вы можете видеть код Ассемблера для вызова scanf. Далее следует:
; ?debug L 9
inc WORD PTR [bp-2]
что представляет собой встроенную инструкцию Ассемблера для уве-
личения значения переменной TestValue. (Заметим, что Турбо Си ав-
томатически выполняет преобразование переменной Си TestValue в
соответствующую адресацию этой переменной на Ассемблере [BP-2].)
За строкой встроенной инструкции Ассемблера следует код Ассембле-
ра для вызова функции printf.
Обратите внимание, что Турбо Си компилирует функцию scanf в
язык Ассемблера, помещая встроенную инструкцию Ассемблера непос-
редственно в выходной файл на Ассемблере, а затем транслирует на
язык Ассемблера функцию printf. Полученный в результате файл
представляет собой файл на языке Ассемблера, готовый к обработке
Турбо Ассемблером.
TASM2 #2-5/Док = 149 =
Если бы вы не использовали параметр -s, Турбо Си продолжил
бы работу, вызвав для ассемблирования файла PLUSONE.ASM Турбо Ас-
семблер, а затем для компоновки полученного в результате объек-
тного файла - утилиту TLINK (будет получен выполняемый файл
PLUSONE.EXE). Это обычный режим работы Турбо Си со встроенным Ас-
семблером. Параметр-s мы использовали только для демонстрационных
целей, чтобы вы могли ознакомиться с промежуточным этапом, кото-
рый выполняет Турбо Ассемблер. Когда компилируемый код должен
компоноваться с другими программами, параметр -s практически бес-
полезен, но его можно использовать в том случае, когда вы хотите
ознакомиться с кодом, окружающим ваш встроенный код на Ассембле-
ре, и с кодом, генерируемым Турбо Си в целом. Если вы не уверены
в результатах, генерируемых при использовании встроенного кода
Ассемблера, проверьте полученный с помощью параметра -s файл
с расширением .ASM.
TASM2 #2-5/Док = 150 =
Откуда Турбо Си знает об использовании режима Ассемблера?
-----------------------------------------------------------------
Обычно Турбо Си компилирует исходный код непосредственно в
объектный код. Существует несколько способов, с помощью которых
можно сообщить Турбо Си, что нужно поддерживать встроенный Ассем-
блер путем компиляции на язык Ассемблера и последующего вызова
утилиты TLINK.
Параметр командной строки -s указывает Турбо Си, что нужно
транслировать исходный код в код Ассемблера, после чего прекра-
тить работу. Файл с расширением .ASM, сгенерированный Турбо Си
при использовании параметра -s, можно отдельно ассемблировать и
скомпоновать с другими модулями Си и Ассемблера. Вызвать Турбо
Ассемблер автоматически позволяет параметр -b. Если вам это не
требуется для отладки или просто в целях ознакомления с кодом Ас-
семблера, параметр -s перед параметром -b указывать не нужно.
Директива #pragma вида:
#pragma inline
действуют аналогично параметру -b, указывая компилятору Турбо Си,
что нужно выполнить трансляцию в Ассемблер, а затем для трансля-
ции полученного результата вызвать Турбо Ассемблер. Когда Турбо
Си встречает указание (директиву) #pragma inline, компиляция про-
должается в режиме вывода Ассемблера. Лучше помещать указание
#pragma inline возможно ближе к началу исходного кода языка Си,
так как любой исходный код языка Си, после которым следует следу-
ет данная директива, будет компилироваться дважды: один раз в
обычном режиме (Си -> объектный файл) а другой раз в режиме Си ->
Ассемблер. Хотя это и не повредит, не стоит попусту тратить вре-
мя.
Наконец, если Турбо Си обнаруживает встроенный код Ассембле-
ра при отсутствии параметра -b или -s и указания #pragma inline,
то выдается предупреждающее сообщение:
Warning test.c 6: Restarting compile using assembly in func-
tion main
(компиляция начинается повторно с использованием ассемблиро-
вания в основной функции)
после чего компиляция возобновляется в режиме вывода Ассемблера,
как если бы в данной точке было обнаружено указание #pragma. Из-
бежать данного сообщения можно, если вы будете использовать пара-
TASM2 #2-5/Док = 151 =
метр -b или указание #pragma inline. В противном случае компиля-
ция будет выполняться медленнее.
Вызов для ассемблирования встроенного кода Турбо Ассемблера
-----------------------------------------------------------------
Для того, чтобы Турбо Си смог вызвать Турбо Ассемблер, нужно
сначала, чтобы он смог найти Турбо Ассемблер. В различных версиях
Турбо Си это происходит по-разному.
В версии старше 1.5 предполагается, что Турбо Ассемблер на-
ходится в файле с именем TASM.EXE, который записан в текущем ка-
талоге или в одном из каталогов, указанный с помощью переменной
операционной среды DOS PATH. В общем случае Турбо Си сможет вы-
звать Турбо Ассемблер в том случае, если можно выполнить команду
(введенную в ответ на подсказку DOS) TASM. Поэтому, если Турбо
Ассемблер находится в текущем каталоге или в одном из каталогов,
определенных с помощью PATH, Турбо Си автоматически найдет и за-
пустит его для выполнения встроенного ассемблирования.
Версии 1.0 и 1.5 Турбо Си ведут себя несколько по-другому.
Поскольку данные версии Турбо Си были созданы до того, как был
разработан Турбо Ассемблер, для выполнения ассемблирования встро-
енного кода они вызывают макроассемблер фирмы Microsoft MASM. По-
этому данные версии будут искать в текущем каталоге и в каталоге,
заданном переменной PATH, файл с именем MASM.EXE, а не TASM.EXE.
Примечание: О том, как скорректировать данные версии
компилятора TCC, чтобы можно было использовать TASM, рас-
сказывается в файле README на дистрибутивном диске Турбо
Ассемблера.
TASM2 #2-5/Док = 152 =
Когда Турбо Си транслирует встроенный код Ассемблера
-----------------------------------------------------------------
Встроенный код Ассемблера может заканчиваться в сегменте
кода или сегменте данных Турбо Си. Встроенный код Ассемблера мо-
жет находиться в функции и ассемблироваться в сегмент кода Турбо
Си, а встроенный код Ассемблера, размещенный вне функции, может
ассемблироваться в сегмент данных Турбо Си.
Например, программа:
/* Таблица квадратов значений */
asm SquareLookUpTable label word;
asm dw 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100;
/* Функция для поиска квадрата значения между 0 и 10 */
int LookUpSquare(int Value)
{
asm mov bx,Value; /* получить значение для возведения
в квадрат */
asm shl bx,1; /* умножить на 2 для поиска в
таблице элементов размером в
слово */
asm mov ax,[SquareLookUpTable+bx]; /* поиск в таблице */
return(_AX);
}
помещает данные для таблицы SquareLookUpTable в сегмент данных
Турбо Си, а встроенный код Ассемблера в LookUpTable в сегмент
кода Турбо Си. С равным успехом данные можно было бы поместить
данные в сегмент кода. Рассмотрим, например, следующую версию
программы LookUpSquare, где SquareLookUpTable находится в сегмен-
те кода Турбо Си:
/* Функция для поиска квадрата значения между 0 и 10 */
int LookUpSquare(int Value)
{
asm jmp SkipAroundData /* пропустить таблицу данных */
/* Таблица квадратов значений */
asm SquareLookUpTable label word;
asm dw 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100;
TASM2 #2-5/Док = 153 =
a
SkipAroundData:
asm mov bx,Value; /* получить значение для возведения
в квадрат */
asm shl bx,1; /* умножить на 2 для поиска в
таблице элементов размером в
слово */
asm mov ax,[SquareLookUpTable+bx]; /* поиск в таблице */
return(_AX);
}
Так как SquareLookUpTable находится в таблице кода Турбо Си,
то чтобы из нее можно было считывать, казалось бы требуется ис-
пользовать префикс переопределения сегмента CS:. Фактически для
доступа к SquareLookUpTable данный код автоматически ассемблиру-
ется с префиксом CS:. Турбо Си генерирует корректный код Ассем-
блера, чтобы Турбо Ассемблер знал, к каком сегменте находится
SquareLookUpTable, а Турбо Ассемблер затем генерирует необходимые
префиксы переопределения сегментов.
TASM2 #2-5/Док = 154 =
Параметр -1 для генерации инструкций процессоров 80186/80286
-----------------------------------------------------------------
Если вы хотите использовать уникальные инструкции процессора
80186, такие как:
shr ax,3
и
push 1
то проще всего использовать в командной строке Турбо Си параметр
-1, например:
tcc -1 -b heapmgr
где HEAPMGR.C - это программа, которая содержит встроенные инст-
рукции Ассемблера, уникальные для процессора 80186.
Основное назначение параметра -1 состоит в том, чтобы ука-
зать Турбо Си, что при компиляции можно использовать полный набор
инструкций процессора 80186. Параметр -1 приводит также к тому,
что начало выходного файла на языке Ассемблера будет включена ди-
ректива .186. Это укажет Турбо Ассемблеру, что ассемблирование
нужно выполнять с использованием полного набора инструкций. Без
данной директивы Турбо Ассемблер пометит все встроенные инструк-
ции, уникальные для процессора 80186, как ошибочные. Если вы хо-
тите ассемблировать инструкции процессора 80186, не принуждая
Турбо Си использовать полный набор инструкций процессора 80186,
включите в начало каждого модуля Турбо Си, содержащего встроенные
инструкции процессора 80186, строку:
asm .186
Данная строка будет передана в файл Ассемблера, где она ука-
жет Турбо Ассемблеру, что нужно использовать инструкции процессо-
ра 80186.
В компиляторе Турбо Си не предусмотрена встроенная поддержка
процессоров 80286, 80386, 80287 и 80387. Во встроенном коде Ас-
семблера, где используются инструкции этих процессоров, разрешить
их использование можно аналогичным способом, с помощью ключевого
слова asm и директив Турбо Ассемблера .286, .286С, .286Р, .386,
.386С, .386Р, .287 и .387.
Строка:
TASM2 #2-5/Док = 155 =
asm .186
показывает важный момент во встроенном коде Ассемблера: с помощью
префикса asm в файл Ассемблера можно передать любую допустимую
директиву Ассемблера, включая директивы определения сегментов,
приравнивания, макрокоманды и т.д.
TASM2 #2-5/Док = 156 =
Формат встроенных операторов на языке Ассемблера
-----------------------------------------------------------------
Встроенные операторы на языке Ассемблера во многом похожи на
обычные строки Ассемблера, но имеется несколько отличий. Встроен-
ный оператор на языке Ассемблера имеет следующий формат:
asm [<метка>] <инструкция/директива> <операнд><; или нов. строка>
где:
- ключевое слово asm должно начинать каждый встроенный опе-
ратор на Ассемблере;
- [<метка>] является допустимой меткой Ассемблера. Квадрат-
ные скобки показывают, что метка является необязательной, так же,
как в Ассемблере. (См. раздел "Память и ограничения адресации
операнда", где приводится информация о метках Ассемблера и Си.);
- <инструкция/директива> представляет собой любую допустимую
директиву Ассемблера;
- <операнды> содержит операнды, воспринимаемые в инструкции
или директиве. Здесь может также присутствовать ссылка на конс-
танты, переменные и метки Си (при соблюдении ограничений, описан-
ных в разделе "Ограничения встроенного Ассемблера");
- <; или нов. строка> это точка с запятой или новая строка
(и то и другое говорит о завершении оператора asm.
Примечание: Важная информация относительно меток со-
держится в разделе "Память и ограничения при адресации к
операнду".
TASM2 #2-5/Док = 157 =
Использование во встроенном Ассемблере точки с запятой
-----------------------------------------------------------------
В использовании встроенного Ассемблера есть один аспект, ко-
торый может упустить из вида программист, работающий на языке Си:
точка с запятой, в отличие от других операторов языка Си, в опе-
раторах встроенного Ассемблера не является обязательной, хотя она
может использоваться для их завершения. При ее отсутствии концом
оператора считается начало новой строки. Поэтому, если вы не раз-
мещаете на одной строке несколько операторов встроенного Ассемб-
лера (чего делать не стоит, так как текст будет менее понятным),
точка с запятой необязательна. Хотя может показаться, что это не
соответствует принципам Си, но здесь соблюдаются соглашения, при-
нятые в некоторых компиляторах, работающих в среде операционной
системы UNIX.
Комментарии во встроенном Ассемблере
-----------------------------------------------------------------
В предыдущем описании формата оператора встроенного Ассем-
блера отсутствует один ключевой элемент - поле комментария. Хотя
комментарии можно помещать в конце операторов встроенного Ассем-
блера, они только отмечают конец встроенного оператора, как и в
других операторах языка Си, ведь во встроенном коде Ассемблера
комментарии не начинаются с точки с запятой.
Как же тогда комментировать код встроенного Ассемблера? Как
это ни странно, делать это можно с помощью комментариев Си. На
самом деле это естественно, поскольку препроцессор языка Си обра-
батывает встроенный код Ассемблера так же, как и остальную часть
кода на Си. Благодаря этому во всей программе на Си, где содер-
жится код Ассемблера, можно использовать один и тот же тип ком-
ментирования. И в коде на языке Си, и в коде Ассемблера в этом
случае можно также использовать определенные в Си символические
имена. Например, в программе:
.
.
.
#define CONSTANT 51
int i;
.
.
.
i = CONSTANT; /* присвоить i
TASM2 #2-5/Док = 158 =
постоянное значение */
asm sub WORD PTR i,CONSTANT; /* вычесть
из i постоянное
значение */
.
.
.
и в коде Си, и в коде Ассемблера используется определенный в язы-
ке Си идентификатор CONSTANT, и при вычислении i получается зна-
чение 0.
Последний пример показывает одно замечательное свойство
встроенного Ассемблера, которое состоит в том, что в поле операн-
да могут содержаться не только ссылки на имена идентификаторов,
определенные в Си, но и на переменные Си. Как вы увидите далее в
данной главе, обычно доступ к переменным Си в Ассемблере проблемы
не составляет, поэтому удобство ссылок на такие переменные - это
основная причина, почему в большинстве прикладных задач стоит ис-
пользовать встроенный Ассемблер, а не просто объединять Ассемблер
и Си.
Обращение к элементам структуры/объединения
-----------------------------------------------------------------
Встроенный код Ассемблера может ссылаться непосредственно на
элементы структуры. Например, в следующем фрагменте программы:
.
.
.
struct Student {
char Teacher[30];
int Grade;
} JohnQPublic;
.
.
.
asm mov ax,JohnQPublic.Grade;
.
.
.
в регистр AX загружается содержимое элемента Grade структуры
JohnQPublic типа Student.
TASM2 #2-5/Док = 159 =
Встроенный код Ассемблера может также обращаться к элементам
структуры, адресуясь к ним относительно базового или индексного
регистра. Например:
.
.
.
asm mov bx,OFFSET JohnQPublic;
asm mov ax,[bx].Grade;
.
.
.
Здесь в регистр AX также загружается элемент Grade структуры
JohnQPublic. Поскольку Grade находится в структуре Student по
смещению 30, последний пример на самом деле принимает вид:
.
.
.
asm mov bx,OFFSET JohnQPublic;
asm mov ax,[bx]+30
.
.
.
Возможность обращаться к элементам структуры относительно
регистра-указателя является достаточно мощным средством, позволя-
ющим во встроенном коде Ассемблера работать с массивами и струк-
турами и использовать указатели на структуры.
Если же, однако, две или более структуры, к которым вы обра-
щаетесь во встроенном коде Ассемблера, содержат элемент с одним и
тем же именем, вы должны включить следующее:
asm mov bx,[di].(struct tm) tm_hour > alt
Например:
.
.
.
struct Student {
char Teacher[30];
int Grade;
TASM2 #2-5/Док = 160 =
} JohnQPublic;
.
.
.
struct Teacher {
int Grade;
long Income;
};
.
.
.
asm mov ax,JohnQPublic.(struct Student) Grade
.
.
.
TASM2 #2-5/Док = 161 =
Пример использования встроенного Ассемблера
-----------------------------------------------------------------
Теперь, когда вы увидели несколько фрагментов программ, в
которых используется встроенный Ассемблер, можно познакомиться с
реальной работающей программой со встроенным кодом Ассемблера.
Это мы и сделаем в данном разделе. В представленной здесь прог-
рамме встроенный Ассемблер используется для увеличения скорости
работы (преобразования текста в верхний регистр). Эта программа
может служить и примером того, что можно делать с помощью встро-
енного Ассемблера, и той схемой, на основе которой вы можете раз-
рабатывать свои собственные программы со встроенным кодом Ассем-
блера.
Давайте посмотрим сначала, какая задача решается с помощью
данной программы-примера. Нам хотелось бы написать функцию с име-
нем StringToUpper, которая копирует одну строку в другую, преоб-
разуя в процессе работы все символы нижнего регистра (строчные
буквы) в символы верхнего регистра (прописные буквы). Хотелось бы
также, чтобы эта функция работала одинаково хорошо для всех строк
и всех моделей памяти. Один из хороших способов обеспечить это
заключается в передаче в функцию указателей дальнего типа, так
как указатели ближнего типа (NEAR) всегда могут быть приведены к
указателям дальнего типа (FAR), а обратное верно не всегда.
К сожалению, здесь возникает проблема производительности. В
то время, как Турбо Ассемблер работает с дальними указателями
достаточно хорошо, обработка указателей дальнего типа в Турбо Си
выполняется существенно медленнее, чем обработка указателей ближ-
него типа. Это не является недостатком Турбо Си, скорее это неиз-
бежное следствие использования для программирования процессора
8086 языка высокого уровня.
С другой стороны, обработка строк и дальних указателей - это
та область, в которой себя превосходно показывает Ассемблер. Та-
ким образом, логическое решение состоит в том, чтобы использовать
для работы с дальними указателями и копирования строки встроенный
Ассемблер, а остальную часть писать на языке Си. Это и реализова-
но в следующей программе, которая называется STRINGUP.C:
/* Программа для демонстрации использования StringToUpper().
Для преобразования строки TestString в верхний регистр
вызывается функция StringToUpper, после чего печатается
полученная в результате строка UpperCaseString и ее длина. */
#pragma inline
TASM2 #2-5/Док = 162 =
#include <stdio.h>
/* Прототип функции для StringToUpper() */
extern unsigned int StringToUpper(
unsigned char far * DestFarString,
unsigned char far * SourceFarString);
#define MAX_STRING_LENGTH 100
char *TestString = "This started Out At Lowercase!";
/* строка, преобразуемая в верхний регистр */
char UpperCaseString[MAX_STRING_LENGTH];
main()
{
unsigned int StringLength;
/* Скопировать строку TestString в верхнем регистре в
UpperCaseString */
StringLength = StringToUpper(UpperCaseString, TestString);
/* Вывести результаты преобразования */
printf("Исходная строка:\n%s\n\n", TestString);
printf("Строка в верхнем регистре:\n%s\n\n", UpperCaseString);
printf("Число символов: %d\n\n", StringLength);
}
/* Функция для выполнения быстрого преобразования в верхний
регистр одной строки дальнего типа в другую
:
DestFarString - массив для хранения преобразованной
в верхний регистр строки (будет
завершаться нулем)
SourceFarString - строка, содержащая символы, которые
нужно преобразовать в верхний регистр
(должна завершаться нулевым символом)
Возвращаемые результаты:
Длина исходной строки в символах, без учета
завершающего нулевого символа. */
unsigned int StringToUpper(unsigned char far * DestFarSring,
unsigned char far * SourceFarString)
{
unsigned int CharacterCount;
TASM2 #2-5/Док = 163 =
#define LOWER_CASE_A 'a'
#define LOWER_CASE_Z 'z'
asm ADJUST_VALUE EQU 20h; /* число, которое нужно
вычесть из значений
букв в нижнем регист-
ре, чтобы преобра-
зовать */
asm cld;
asm push ds; /* сохранить сегмент данных Си */
asm lds si,SourceFarString; /* загрузить дальний
указатель на исходную строку */
asm les di,DestFarString; /* загрузить указатель
дальнего типа на целевую
строку */
CharacterCount = 0; /* число символов */
StringToUpperLoop:
asm lodsb; /* получить следующий символ */
asm cmp al,LOWER_CASE_A; /* если < а, то это не
строчная буква (нижний
регистр) */
asm jb SaveCharacter;
asm cmp al,LOWER_CASE_Z; /* если > z, то это не
строчная буква */
asm ja SaveCharacter;
asm sub al,ADJUST_VALUE; /* это нижний регистр,
преобразовать в верхний
регистр */
SaveCharacter:
asm stosb; /* сохранить символ */
CharacterCount++; /* подсчитать этот символ */
asm and al,al; /* это завершающий символ?
(ноль) */
asm jnz StringToUpperLoop; /* нет, обработать
следующий символ, если он
имеется */
CharacterCount--; /* не учитывать завершающий ноль */
asm pop ds; /* восстановить сегмент данных
Си */
return(CharacterCount);
}
Тогда при запуске STRINGUP.C на экран выводится:
Исходная строка:
String to convert to uppercase
TASM2 #2-5/Док = 164 =
Строка в верхнем регистре:
STRING TO CONVERT TO UPPERCASE
Число символов: 30
Это показывает, что действительно все строчные буквы (нижний
регистр) преобразуются в прописные (верхний регистр).
Основу программы STRINGUP.C составляет функция
StringToUpper, которая выполняет весь процесс копирования строки
и преобразования ее в верхний регистр. Эта функция написана на Си
и встроенном Ассемблере и воспринимает в качестве параметров два
указателя дальнего типа. Один из указателей дальнего типа ссыла-
ется на строку, содержащую текст. Другой указывает на еще одну
строку, в которую будет скопирован весь текст из первой строки,
строчные символы которого будут преобразованы в верхний регистр.
Описание функции и определение параметров обрабатываются на Си:
действительно, прототип функции StringToUpper указывается в нача-
ле программы. Основная программа вызывает функцию StringToUpper
также, как если бы она была написана целиком на языке Си. Таким
образом, можно использовать все преимущества программирования на
Турбо Си, хотя функция StringToUpper содержит встроенный код Ас-
семблера.
Тело функции StringToUpper содержит смесь кода Си и Ассем-
блера. Ассемблер используется для считывания каждого символа из
исходной строки, проверки его, и, если это необходимо, преобразо-
вания символа в верхний регистр, после чего символ записывается в
целевую строку (приемник). Встроенный Ассемблер позволяет исполь-
зовать в функции StringToUpper такие мощные строковые инструкции,
как LODSB и STOSB, которые считывают и записывают символы.
При разработке функции StringToUpper мы знали, что нам не
потребуется обращаться к сегменту данных Турбо Си, поэтому мы
просто занесли в начале функции регистр DS в стек и использовали
его для ссылки на исходную строку (источник), не изменяя содержи-
мое DS в остальной части функции. Одно из больших преимуществ ис-
пользования встроенного Ассемблера по сравнению с чистым програм-
мированием на языке Си состоит в возможности загрузки дальних
указателей в начале функции без последующей их перезагрузки в ос-
тальной части функции. В отличие от этого Турбо Си и другие языки
высокого уровня перезагружают дальние указатели всякий раз, когда
они используются. Возможность загружать дальний указатель один
раз означает, что функция StringToUpper обрабатывает строки даль-
него типа так же быстро, как и строки ближнего типа.
TASM2 #2-5/Док = 165 =
Другое интересное замечание по функции StringToUpper касает-
ся того, каким образом чередуются операторы Ассемблера и языка
Си. Для того, чтобы установить значение LOWER_CASE_A и LOWER_CASE
_Z используется директива #define, а для задания значения ADJUST_
VALUE - директива Ассемблера EQU. Однако в коде встроенного Ас-
семблера все три идентификатора используются одинаково. Подста-
новки идентификаторов, определяемых в языке Си, выполняются преп-
роцессором Турбо Си, а подстановка для ADJUST_VALUE - Турбо Ас-
семблером. При этом оба идентификатора можно использовать во
встроенном Ассемблере.
В теле функции StringToUpper для работы со счетчиком
CharacterCount используются операторы языка Си. Это сделано толь-
ко для того, чтобы показать, что код языка Си и встроенный Ассем-
блер могут чередоваться. Значение переменной CharacterCount можно
было бы изменять и в коде встроенного Ассемблера, используя для
этого свободный регистр (например, регистр CX или DX). Функция
StringToUpper работала бы в этом случае даже быстрее.
Свободное чередование кода языка Си и встроенного Ассемблера
довольно рискованно, если вы не понимаете четко, какой код гене-
рирует Турбо Си между операторами встроенного Ассемблера. Исполь-
зование параметра компилятора Турбо Си -s представляет собой наи-
лучший способ исследовать, что происходит, когда вы чередуете
встроенный Ассемблер и код языка Си. Например, вы можете исследо-
вать, насколько соответствуют друг другу код Си и встроенного Ас-
семблера, если скомпилируете программу STRINGUP.C с параметром -s
и просмотрите полученный результате файл STRINGUP.ASM.
Программа STRINGUP.C ясно демонстрирует превосходные качест-
ва встроенного Ассемблера. Включение в функцию StringToUp около
15 строк на языке Ассемблера почти удваивает скорость обработки
строки по сравнению с эквивалентным кодом на языке Си.
TASM2 #2-5/Док = 166 =
Ограничения встроенного Ассемблера
-----------------------------------------------------------------
При использовании встроенного Ассемблера имеется несколько
ограничений. Как мы уже знаем, операторы встроенного Ассемблера
просто передаются без изменений Турбо Ассемблеру. Однако сущест-
вуют отдельные ограничения, касающиеся определенных операндов
в памяти и адресации, и некоторые ограничения, относящиеся к ис-
пользованию регистров и отсутствия во встроенном Ассемблере наз-
начения по умолчанию размера для динамических локальных перемен-
ных Си.
Ограничения адресации к операндам в памяти
-----------------------------------------------------------------
Единственное изменение, которое Турбо Си вносит в операторы
встроенного Ассемблера, состоит в преобразовании ссылок на память
и адреса памяти (например, имен переменных и адресов перехода) из
их представления в Си в соответствующий эквивалент на Ассемблере.
Такие изменения налагают два ограничения: в инструкциях перехода
встроенного Ассемблера можно ссылаться только на метки Си, а в
прочих инструкциях можно ссылаться на что угодно, кроме меток
языка Си. Например, программа:
.
.
.
asm jz NoDec;
asm dec cx;
NoDec:
.
.
.
вполне корректна, а программа:
.
.
.
asm jz NoDec;
asm dec cx;
asm NoDec:
.
.
.
TASM2 #2-5/Док = 167 =
не будет правильно компилироваться. Аналогично, в инструкциях пе-
рехода встроенного Ассемблера нельзя использовать в качестве опе-
рандов имена функций.
Встроенные инструкции Ассемблера, отличные от переходов, мо-
гут содержать любые операнды, кроме меток Си. Например, програм-
ма:
.
.
.
asm BaseValue db '0';
.
.
.
asm mov al,BYTE PTR BaseValue;
.
.
.
компилируется, а программа:
.
.
.
BaseValue:
asm db '0';
.
.
.
asm mov al,BYTE PTR BaseValue;
.
.
.
компилироваться не будет. Заметим, что вызов подпрограммы не счи-
тается переходом, поэтому в качестве операндов при вызове функции
Си в инструкции встроенного Ассемблера можно указывать имена
функций Си и метки Ассемблера (но не метки Си). Если в коде
встроенного Ассемблера имеется ссылка на имя функции Си, то перед
именем функции должен указываться символ подчеркивания (более
подробно об этом рассказывается в разделе "Подчеркивания").
TASM2 #2-5/Док = 168 =
Встроенный Ассемблер и размер динамических локальных переменных
-----------------------------------------------------------------
Когда Турбо Си заменяет в операторе встроенного Ассемблера
ссылку на динамическую локальную переменную операндом вида
[BP-02], он не помещает в измененный оператор операцию назначения
размера (типа WORD PTR или BYTE PTR). Это означает, что:
.
.
.
int i;
.
.
.
asm mov ax,i;
.
.
.
выводится в файл Ассемблера а виде:
mov ax,[bp-02]
Проблем в данном случае не возникает, так как использование
регистра AX сообщает Турбо Ассемблеру, что это 16-битовая ссылка
на память. Более того, отсутствие операции задания размера дает
вам большую гибкость в управлении размером операнда при использо-
вании встроенного Ассемблера. Рассмотрим следующий пример:
.
.
.
int i;
.
.
.
asm mov i,0;
asm inc i;
.
.
.
который принимает вид:
mov [bp-02],0
TASM2 #2-5/Док = 169 =
inc [bp-02]
Ни одна из этих инструкций здесь не содержит предполагаемого
размера, поэтому Турбо Ассемблер не может их ассемблировать. В
итоге, когда вы в Турбо Ассемблере обращаетесь к динамической ло-
кальной переменной, не используя в качестве источника или прием-
ника регистр, то нужно указывать размер операнда. С учетом ска-
занного последний пример должен выглядеть следующим образом:
.
.
.
int i;
.
.
.
asm mov WORD PTR i,0;
asm inc BYTE PTR i;
.
.
.
TASM2 #2-5/Док = 170 =
Необходимость сохранения регистров
-----------------------------------------------------------------
В конце каждого используемого вами кода встроенного Ассем-
блера регистры BP, CS, DS и SS должны содержать те же значения,
которые они имели перед началом выполнения кода встроенного Ас-
семблера. Несоблюдение этого правила часто будет приводить к ава-
рийному завершению программы (crash) и перезагрузкам системы. Ре-
гистры AX, BX, CX, DX, SI, DI, ES и флаги в коде встроенного Ас-
семблера можно свободно изменять.
Сохранение при вызовах функций регистровых переменных
-----------------------------------------------------------------
В Турбо Си требуется, чтобы регистры DI и SI, которые ис-
пользуются в виде регистровых переменных, не нарушались при вызо-
вах функций. Однако вам не нужно беспокоиться о явном сохранении
регистров DI и SI при использовании их в коде встроенного Ассемб-
лера, поскольку он сохраняет их в начале функций и восстанавлива-
ет в конце (это еще одно из удобств использования встроенного Ас-
семблера).
TASM2 #2-5/Док = 171 =
Подавление внутренних регистровых переменных
-----------------------------------------------------------------
Поскольку регистровые переменные хранятся в регистрах SI и
DI, это, казалось бы, может приводить к возможному конфликту меж-
ду регистровыми переменными в данном модуле и встроенном коде Ас-
семблера, в котором DI и SI используются в том же модуле. Турбо
Си предвидит такую проблему: любое использование регистра DI или
SI во встроенном коде приведет к запрету использования данного
регистра для хранения регистровых переменных.
В Турбо Си версии 1.0 устранение конфликта между регистровой
переменной и встроенным кодом Ассемблера не обеспечивается. Если
вы используете версию 1.0, то нужно либо явным образом сохранять
регистры DI и SI перед их использованием во встроенном коде Ас-
семблера, либо перейти к более поздней версии компилятора.
Недостатки использования встроенного Ассемблера
-----------------------------------------------------------------
Мы посвятили уже довольно много времени исследованию того,
как работает встроенный Ассемблер, и изучению потенциальных преи-
муществ его использования. Хотя для многих прикладных задач
встроенный Ассемблер представляет собой прекрасное средство с ши-
роким спектром возможностей, он имеет также некоторые недостатки.
Давайте рассмотрим эти недостатки, после чего вы сможете сделать
вывод, когда в ваших программах следует использовать встроенный
Ассемблер.
TASM2 #2-5/Док = 172 =
Уменьшения возможностей переносимости и обслуживаемости
-----------------------------------------------------------------
Возможность непосредственного программирования процессора
8086 - то самое качество, которое делает встроенный Ассемблер та-
ким эффективным - уменьшает одновременно возможности основного
преимущества языка Си, его переносимости. Если вы используете
в программе на языке Си встроенный Ассемблер, то весьма возможно,
что вы не сможете без изменений перенести вашу программу на дру-
гой процессор или использовать другой компилятор языка Си.
Кроме того, Ассемблер не является структурированным языком и
не дает той ясности и понятности исходного кода, которую может
обеспечить хорошо форматированная программа на языке Си. В ре-
зультате встроенный код Ассемблера в общем случае гораздо труднее
читать и обслуживать (анализировать и модифицировать программу),
чем исходный код на языке Си.
Когда вы используете встроенный код Ассемблера, то хорошо
выделять его в отдельные модули и аккуратно и подробно комменти-
ровать. При этом программу становится проще обслуживать. Кроме
того, гораздо легче будет найти встроенный код Ассемблера и пере-
писать его на языке Си, если вы захотите перенести программу в
другую среду.
Более медленная компиляция
-----------------------------------------------------------------
Компиляция модулей Си, содержащих встроенный код Ассемблера,
выполняется существенно медленнее, чем компиляция одного исходно-
го кода языка Си. Это в основном связано с тем, что встроенный
код Ассемблера должен компилироваться дважды - сначала компилято-
ром Турбо Си, а затем Турбо Ассемблером. Если Турбо Си вынужден
повторно начинать компиляцию, поскольку не использовались ни па-
раметры -b или -s, ни указание (директива) #pragma inline, то
время компиляции встроенного кода Ассемблера еще более увеличит-
ся. К счастью, медленная компиляция модулей, содержащих встроен-
ный код Ассемблера, теперь представляет собой гораздо меньшую
проблему, чем это было раньше, так как Турбо Ассемблер работает
гораздо быстрее, чем более ранние версии ассемблеров.
Возможность использования только компилятора ТСС
-----------------------------------------------------------------
Как мы уже упоминали ранее, возможность использования встро-
TASM2 #2-5/Док = 173 =
енного Ассемблера - это уникальное средство для компилятора
TCC.EXE (версии компилятора Турбо Си TC.EXE, работающей с коман-
дной строкой). Компилятор Турбо Си с интерактивной средой прог-
раммирования TC.EXE встроенный Ассемблер не поддерживает.
Потери в оптимизации
-----------------------------------------------------------------
При использовании встроенного Ассемблера происходит некото-
рая потеря управления Турбо Си кодом программы, поскольку непос-
редственно в исходный код программы на языке Си вы включаете код
Ассемблера. В определенной степени вы, как программист, работа-
ющий на Ассемблере, можете это компенсировать, если будете избе-
гать определенных нежелательных действий (например, несохранения
регистра DS, или записи в неверную область памяти).
С другой стороны, Турбо Си не требует, чтобы при использова-
нии встроенного Ассемблера вы соблюдали все его внутренние прави-
ла (если бы это было так, то лучше было бы не пользоваться
встроенным Ассемблером, а позволить сгенерировать код Ассемблера
компилятору Турбо Си). В функциях, содержащих встроенные операто-
ры Ассемблера, Турбо Си "выключает" некоторые виды оптимизации,
позволяя вам тем самым относительно свободно работать со встроен-
ным кодом Ассемблера. Например, при использовании встроенного Ас-
семблера "выключаются" некоторые виды оптимизации встроенных пе-
реходов, а если во встроенном коде используются регистры DI и SI,
то запрещаются регистровые переменные. Однако такая частичная по-
теря оптимизации в значительной степени компенсируется тем, что
части программы, реализованные на Ассемблере, работают с макси-
мально возможной скоростью.
Если при использовании встроенного Ассемблера для вас имеет
большое значение получение наиболее быстрого и компактного кода,
то для вас может оказаться желательным писать функции, содержащие
встроенный код Ассемблера, целиком на языке Ассемблера (то есть
не смешивая в одной функции встроенный Ассемблер с языком Си).
Таким образом, вы получите полный контроль над кодом функции, на-
писанной целиком на встроенном Ассемблере, А Турбо Си будет уп-
равлять кодом функций, реализованных на языке Си. При этом и вы,
и Турбо Си получаете полную свободу и возможность генерации на-
илучшего кода без ограничений.
TASM2 #2-5/Док = 174 =
Ограничения при обнаружении ошибок
-----------------------------------------------------------------
Поскольку Турбо Си выполняет в операторах встроенного Ассем-
блера слабую проверку на ошибки, ошибки во встроенном коде Ассем-
блера часто распознаются Турбо Ассемблером, а не Турбо Си. С со-
жалению, иногда оказывается довольно трудно установить связь меж-
ду выдаваемым Турбо Ассемблером сообщением об ошибке и исходным
кодом на языке Си, поскольку эти сообщения об ошибках и выводимые
номера строк основываются на выводимом Турбо Си файле с расшире-
нием .ASM, а не на самом исходном коде языка Си.
Например, в ходе компиляции программы TEST.C (программы на
языке Си, содержащей встроенный код Ассемблера) Турбо Ассемблер
может вывести сообщение о некорректном размере операнда на строке
23. К сожалению, номер 23 относится к номеру ошибочной строки в
файле TEST.ASM (промежуточном файле, который компилятор Турбо Си
генерирует для обработки его Турбо Ассемблером). Вам самим при-
дется выяснить, какая именно строка в программе TEST.C вызвала
ошибку.
Для этого лучше всего сначала найти ошибочную строку в про-
межуточном файле TEST.ASM, который сохраняется на диске компилят-
ором Турбо Си в том случае, если Турбо Ассемблер выдает сообщения
об ошибках. Файл .ASM содержит специальные комментарии, идентифи-
цирующие строку в файле Си, из которой генерируется данный блок
операторов Ассемблера. Например, строки на Ассемблере, за которы-
ми следует комментарий:
; Line 15
(строка 15), генерируются из строки 15 исходного файла Си. После
того, как вы найдете в файле .ASM строку, которая вызвала ошибку,
для определения соответствующей строки в исходном файле Си можно
использовать указанный в ней (в комментарии) номер строки.
Ограничения при отладке
-----------------------------------------------------------------
Версии компилятора Турбо Си, номер которых не превышает 1.5,
не могут генерировать информацию для отладки на уровне исходных
кодов (эта информация необходима для того, чтобы при отладке вы
могли просматривать исходный код на языке Си) для тех модулей,
которые содержат встроенный код Ассемблера. При использовании
встроенного Ассемблера компиляторы Турбо Си версии 1.5 и более
TASM2 #2-5/Док = 175 =
ранние генерируют просто код Ассемблера без включения информации
для отладки. Возможность отладки на уровне исходного кода оказы-
вается утраченной, и возможна только отладка (модулей Си, содер-
жащих встроенный код Ассемблера) на уровне Ассемблера.
Более поздние версии Турбо Си используют преимущества специ-
альных средств Турбо Ассемблера, обеспечивающих при использовании
Турбо отладчика отладку на уровне исходного кода как для модулей,
содержащих встроенных код Ассемблера, так и для модулей, написан-
ных целиком на языке Си.
TASM2 #2-5/Док = 176 =
Разработка на Си и последующее использование Ассемблера
-----------------------------------------------------------------
В свете описанных нами недостатков и ограничений использова-
ния встроенного Ассемблера может показаться, что встроенный Ас-
семблер лучше использовать только в случае крайней необходимости.
Это не так. При разработке программы встроенный Ассемблер до са-
мого последнего этапа может оказать существенную помощь.
Большая часть недостатков встроенного Ассемблера сводится к
одной проблеме: использование встроенного Ассемблера может су-
щественно замедлить цикл редактирования-компиляции-отладки. Более
медленная компиляция, невозможность использования встроенной ин-
терактивной среды и трудности в нахождении ошибок компиляции оз-
начают, что разработка программы, содержащей встроенные операторы
Ассемблера, потребует, вероятно, больше времени, чем разработка
программы, написанной целиком на языке Си. Однако правильное ис-
пользование встроенного Ассемблера может значительно улучшить ка-
чество программы. Что же делать?
Ответ прост. На первом этапе нужно писать программу целиком
на языке Си, полностью используя преимущества интерактивной прог-
раммной среды компилятора TC.EXE. Когда программа будет полностью
готова, отлажена и будет выдавать правильные результаты, перейди-
те к использованию командного компилятора TCC.EXE и начните пре-
образовывать критические куски программы в код встроенного Ассем-
блера. Такой подход позволит вам эффективно разработать и отла-
дить программу, а затем выделить и улучшить отдельные части кода
и перейти к тонкой ее доводке.
TASM2 #2-5/Док = 177 =
Вызов функций Турбо Ассемблера из Турбо Си
-----------------------------------------------------------------
Ассемблер и Си традиционно используются совместно: отдельные
модули пишутся целиком на языке Си или Ассемблере, выполняется
компиляция модулей Си и ассемблирование модулей Ассемблера, а за-
тем компоновка этих отдельно скомпилированных модулей в один вы-
полняемый файл. Именно так можно компоновать модули Турбо Ассемб-
лера и Турбо Си (этот процесс показан на Рис. 7.3).
Выполняемый файл получается путем "смешивания" исходных фай-
лов Си и Ассемблера. Этот процесс можно начать командой:
tcc имя_файла1 имя_файла.asm
которая указывает Турбо Си, что нужно сначала компилировать файл
имя_файла_1.C для получения объектного файла имя_файла_1.OBJ, за-
тем вызвать Турбо Ассемблер для трансляции исходного файла
имя_файла_2.ASM в файл имя_файла_2.OBJ, и, наконец, вызвать ути-
литу TLINK для компоновки файлов имя_файла_1.OBJ и имя_фай-
ла_2.OBJ и получения выполняемого файла имя_файла_1.XE.
-------------------- --------------------
| Исходный файл на | | Исходный файл на |
| языке Си FILE1.C | | языке Ассемблера |
| | | FILE2.ASM |
-------------------- --------------------
| |
v v
---------- -----------------
( Турбо Си ) ( Турбо Ассемблер )
---------- -----------------
| Компиляция | Ассемблирование
v v
-------------------- -------------------
| Объектный файл | | Объектный файл |
| FILE1.OBJ | | FILE2.OBJ |
-------------------- -------------------
| |
----------------- -------------------
| |
v v
----------
( TLINK )
----------
|
TASM2 #2-5/Док = 178 =
v
------------------------------
| Выполняемый файл FILE1.EXE |
------------------------------
Рис. 7.3 Компиляция, ассемблирование и компоновка с помощью
Турбо Си, Турбо Ассемблера и утилиты TLINK.
Раздельная компиляция оказывается очень полезной для прог-
рамм, содержащих значительной объем кода Ассемблера, поскольку
она позволяет полностью использовать возможности Турбо Ассемблера
и программировать на языке Ассемблера в чисто ассемблерном окру-
жении, без ключевого слова asm, дополнительного времени на компи-
ляцию и относящихся к Си дополнительных издержек при использова-
нии встроенного Ассемблера.
Однако за раздельную компиляцию приходится платить следующую
цену: программист, работающий на Ассемблере, должен вникать во
все детали организации интерфейса языка Си и кода Ассемблера.В то
время как Турбо Си обрабатывает спецификацию сегментов, передачу
параметров, ссылки на переменные Си, сохранение регистровых пере-
менных и т.д., раздельно компилируемые функции, написанные на Ас-
семблере, должны все это (и даже более) делать явным образом.
Во взаимодействии Турбо Си и Ассемблера имеется два основных
аспекта. Во-первых, различные части кода Си и Ассемблера должны
правильно компоноваться, а функции и переменные в каждой части
кода должны быть доступны (если это необходимо) в остальной части
кода. Во-вторых, код Ассемблера должен правильно работать с вызо-
вами функций, соответствующих соглашениям языка Си, что включает
в себя доступ к передаваемым параметрам, возврат значений и соб-
людение правил сохранения регистров, которых требуется придержи-
ваться в функциях Си.
Давайте теперь приступим к изучению правил компоновки прог-
рамм Турбо Ассемблера и Турбо Си.
TASM2 #2-5/Док = 179 =
Основные моменты в интерфейсе Турбо Ассемблера и Турбо Си
-----------------------------------------------------------------
Чтобы скомпоновать вместе модули Турбо Си и Турбо Ассембле-
ра, должны быть соблюдены следующие три пункта:
1. В модулях Турбо Ассемблера должна использоваться схема
наименования сегментов, совместимая с Турбо Си.
2. Турбо Си и Турбо Ассемблер должны совместно использовать
соответствующие функции и имена переменных в форме, при-
емлемой для Турбо Си.
3. Для комбинирования модулей в выполняемую программу нужно
использовать утилиту-компоновщик TLINK.
Здесь ничего не говориться о том, что в действительности де-
лают модули Турбо Ассемблера. Пока мы коснемся только основных
моментов, обеспечивающих разработку функций Турбо Ассемблера,
совместимых с Си.
TASM2 #2-5/Док = 180 =
Модели памяти и сегменты
-----------------------------------------------------------------
Чтобы данная функция Ассемблера могла могла вызываться из
Си, она должна использовать ту же модель памяти, что и программа
на языке Си, а также совместимый с Си сегмент кода. Аналогично,
чтобы данные, определенные в модуле Ассемблера, были доступны в
программе на языке Си (или данные Си были доступны в программе
Ассемблера), в программе на Ассемблере должны соблюдаться согла-
шения языка Си по наименованию сегмента данных.
Модели памяти и обработку сегментов на Ассемблере может ока-
заться реализовать довольно сложно. К счастью, Турбо Ассемблер
сам выполняет почти всю работу по реализации моделей памяти и
сегментов, совместимых с Турбо Си, при использовании упрощенных
директив определения сегментов (см. раздел "Стандартные директивы
определения сегментов" в Главе 5, где дается введение в упрощен-
ные директивы определения сегментов).
Упрощенные директивы определения сегментов и Турбо Си
-----------------------------------------------------------------
Директива DOSSEG указывает Турбо Ассемблеру, что нужно упо-
рядочивать сегменты в соответствии с соглашениями по упорядочива-
нию сегментов фирмы Intel. Те же соглашения соблюдаются в Турбо
Си (и во многих других известных продуктах, включая языки фирмы
Microsoft).
Директива .MODEL указывает Турбо Ассемблеру, что сегменты,
создаваемые с помощью упрощенных директив определения сегментов,
должны быть совместимы с выбранной моделью памяти (сверхмалой,
малой, компактной, средней, большой или сверхбольшой) и управляет
назначаемым по умолчанию типом (FAR или NEAR) процедур, создавае-
мых по директиве PROC. Модели памяти, определенные с помощью ди-
рективы .MODEL, совместимы с моделями Турбо Си с соответствующими
именами.
Наконец, упрощенные директивы определения сегментов .DATA,
.CODE, .DATA?, .FARDATA, .FARDATA? и .CONST генерируют сегменты,
совместимые с Турбо Си.
Например, рассмотрим следующий модуль Турбо Ассемблера с
именем DOTOTAL.ASM:
DOSSEG ; выбрать упорядочивание сегментов,
TASM2 #2-5/Док = 181 =
; принятое фирмой Intel
.MODEL SMALL ; выбрать малую модель памяти
; (ближний код и данные)
.DATA ; инициализация сегмента данных,
; совместимого с Турбо Си
EXTRN _Repetitions:WORD ; внешний идентификатор
PUBLIC _StartingValue ; доступен для других модулей
_StartValue DW 0
.DATA? ; инициализированный сегмент
; данных, совместимый с Турбо Си
RunningTotal DW ?
.CODE ; сегмент кода, совместимый с
; Турбо Си
PUBLIC _DoTotal
_DoTotal PROC ; функция (в малой модели памяти
; вызывается с помощью вызова
; ближнего типа)
mov cx,[_Repetitions] ; счетчик выполнения
mov ax,[_StartValue]
mov [RunningTotal],ax ; задать начальное
; значение
TotalLoop:
inc [RunningTotal] ; RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ; возвратить конечное
; значение (результат)
ret
_DoTotal ENDP
END
(Перед многими метками в процедуре DoTotal указывается сим-
вол подчеркивания (_), так как это обычно требуется в Турбо Си.
Более подробно это описывается далее в разделе "Подчеркивания".)
Написанная на Ассемблере процедура _DoTotal при использова-
нии малой модели памяти может вызываться из Турбо Си с помощью
оператора:
DoTotal();
Заметим, что в процедуре DoTotal предполагается, что где-то
в другой части программы определена внешняя переменная
Repetitions. Аналогично, переменная StartingValue объявлена, как
общедоступная, поэтому она доступна в других частях программы.
Следующий модуль Турбо Си (который называется SHOWTOT.C) обраща-
ется к данным в DOTOTAL.ASM и обеспечивает для модуля DOTOTAL.ASM
TASM2 #2-5/Док = 182 =
внешние данные:
extern int StartingValue;
extern int DoTotal(word);
int Repetitions;
main()
{
int i;
Repetitions = 10;
StartingValue = 2;
print("%d\n", DoTotal());
}
Чтобы создать из модулей DOTOTAL.ASM и SHOWTOT.C выполняемую
программу SHOWTOT.EXE, введите команду:
tcc showtot dototal.asm
Если бы вы захотели скомпоновать процедуру _DoTotal с прог-
раммой на языке Си, использующей компактную модель памяти, то
пришлось бы просто заменить директиву .MODEL на .MODEL COMPACT, а
если бы вам потребовалось использовать в DOTATOL.ASM сегмент
дальнего типа, то можно было бы использовать директиву .FARDATA.
Короче говоря, при использовании упрощенных директив опреде-
ления сегментов генерация корректного упорядочивания сегментов,
моделей памяти и имен сегментов труда не составляет.
TASM2 #2-5/Док = 183 =
Старые директивы определения сегментов и Турбо Си
-----------------------------------------------------------------
Коснемся теперь проблемы организации интерфейса Турбо Ассем-
блера с кодом языка Си, где используются директивы определения
сегментов старого типа (стандартные директивы определения сегмен-
тов). Например, если вы замените в модуле DOTOTAL.ASM упрощенные
директивы определения сегментов директивами старого типа, то по-
лучите:
DGROUP group _DATA,_BSS
_DATA segment word public 'DATA'
EXTRN _Repetitions:WORD ; внешний идентификатор
PUBLIC _StartingValue ; доступен для других модулей
_StartValue DW 0
_DATA ends
_BSS segment word public 'BSS'
RunningTotal DW ?
_BSS ends
_TEXT segment byte public 'CODE'
assume cs:_TEXT.ds:DGROUP,ss:DGROUP
PUBLIC _DoTotal
_DoTotal PROC ; функция (в малой модели памяти
; вызывается с помощью вызова
; ближнего типа)
mov cx,[_Repetitions] ; счетчик выполнения
mov ax,[_StartValue]
mov [RunningTotal],ax ; задать начальное
; значение
TotalLoop:
inc [RunningTotal] ; RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ; возвратить конечное
; значение (результат)
ret
_DoTotal ENDP
_TEXT ENDS
END
Данная версия директив определения сегментов старого типа не
только длиннее, то также и хуже читается. К тому же при использо-
вании в программе на языке Си различных моделей памяти ее труднее
изменять. При организации интерфейса с Турбо Си в общем случае
в использовании старых директив определения сегментов нет никаких
преимуществ. Если же вы тем не менее захотите использовать при
организации интерфейса с Турбо Си старые директивы определения
TASM2 #2-5/Док = 184 =
сегментов, вам придется идентифицировать корректные сегменты, со-
ответствующие используемым в коде на языке Си моделям памяти. Об-
зор использования сегментов в Турбо Си содержится в "Руководстве
пользователя по Турбо Си".
Простейший способ определения необходимых для компоновки с
данной программой на Турбо Си директив старого типа состоит в
том, чтобы скомпилировать основной модуль программы на Турбо Си с
нужной моделью памяти и параметром -S. При этом Турбо Си сгенери-
рует версию исходного кода Си на языке Ассемблера. В этом коде вы
найдете старые директивы, используемые Турбо Си. Остается просто
скопировать их в ваш код на Ассемблере. Например, если вы введете
команду:
tcc -s showtot.c
то генерируется файл SHOWTOT.ASM, содержащий:
ifndef ??version
?debug macro
endm
endif
name showtot
_TEXT segment byte public 'CODE'
DGROUP group _DATA,_BSS
assume cs:_TEXT.ds:DGROUP,ss:DGROUP
_TEXT ends
_DATA segment word public 'DATA'
_d@ label byte
_d@w label word
_DATA ends
_BSS segment word public 'BSS'
_b@ label byte
_b@w label word
?debug C E91481D5100973688F77746F742E63
_BSS ends
_TEXT segment byte public 'CODE'
; ?debug L 3
_main proc near
; ?debug L 6
mov word ptr DGROUP:_Repetitions,10
; ?debug L 7
mov word ptr DGROUP:_StartingValue,2
; ?debug L 8
call near ptr _DoTotal
push ax
TASM2 #2-5/Док = 185 =
mov ax,offset DGROUP:_s@
push ax
call near ptr _printf
pop cx
pop cx
@1:
; debug L 9
ret
_main endp
_TEXT ends
_BSS segment word public 'BSS'
_Repetitions label word
db 2 dup (?)
?debug C E9
_BSS ends
_DATA segment word public 'DATA'
_s@ label byte
db 37
db 100
db 10
db 0
_DATA ends
extrn _StartingValue:word
_TEXT segment byte public 'CODE'
extrn _DoTotal:near
extrn _printf:near
_TEXT ends
public _Repetitions
public _main
end
Директивы определения сегментов _DATA (инициализированный
сегмент данных), _TEXT (сегмент кода) и _BSS (неинициализирован-
ный сегмент данных), а также директивы GROUP и ASSUME имеют гото-
вый для ассемблирования вид, поэтому вы можете их использовать
(так, как они указываются).
TASM2 #2-5/Док = 186 =
Значения по умолчанию: когда необходимо загружать сегменты?
-----------------------------------------------------------------
В некоторых случаях вызываемые из языка Си функции Ассембле-
ра могут использовать (загружать) для обращения к данным регистры
DS и/или ES. Полезно знать соотношение между значениями сегмен-
тных регистров при вызове из Турбо Си, так как иногда Ассемблер
использует преимущества эквивалентности двух сегментных регист-
ров. Давайте рассмотрим значения сегментных регистров в тот мо-
мент, когда функция Ассемблера вызывается из Турбо Си, а также
соотношения между сегментными регистрами, и случаи, когда в функ-
ции Ассемблера требуется загружать один или более сегментных ре-
гистров.
При входе в функцию Ассемблера из Турбо Си регистры CS и DS
имеют следующие значения, которые зависят от используемой модели
памяти (регистр SS всегда используется для сегмента стека, а ES
всегда используется, как начальный сегментный регистр):
Значения регистров при входе в Ассемблер из Турбо Си
Таблица 7.1
-----------------------------------------------------------
Модель CS DS
-----------------------------------------------------------
Сверхмалая _TEXT DGROUP
Малая _TEXT DGROUP
Компактная _TEXT DGROUP
Средняя имя_файла_TEXT DGROUP
Большая имя_файла_TEXT DGROUP
Сверхбольшая имя_файла_TEXT имя_вызывающего_файла_DATA
-----------------------------------------------------------
Здесь "имя_файла" - это имя модуля на Ассемблере, а
"имя_вызывающего_файла" - это имя модуля Турбо Си, вызывающего
модуль на Ассемблере.
В компактной модели памяти _TEXT и DGROUP совпадают, поэтому
при входе в функцию содержимое регистра CS равно содержимому DS.
При использовании сверхмалой, малой и компактной модели памяти
при входе в функцию содержимое SS равно содержимому регистра DS.
Когда же в функции на Ассемблере, вызываемой из программы на
языке Си, необходимо загружать сегментный регистр? Отметим для
начала, что вам никогда не придется (более того, этого не следует
делать) загружать регистры SS или CS: при дальних вызовах, пере-
TASM2 #2-5/Док = 187 =
ходах или возвратах регистр CS автоматически устанавливается в
нужное значение, а регистр SS всегда указывает на сегмент стека и
в ходе выполнения программы изменять его не следует (если только
вы не пишете программу, которая "переключает" стеки; в этом слу-
чае вам нужно четко понимать, что вы делаете).
Регистр ES вы можете всегда использовать так, как это требу-
ется. Вы можете установить его таким образом, чтобы он указывал
на данные с дальним типом обращения, или загрузить в ES сег-
мент-приемник для строковой функции.
С регистром DS дело обстоит иначе. Во всех моделях памяти
Турбо Си, кроме сверхбольшой, регистр DS при входе в функцию ука-
зывает на статический сегмент данных (DGROUP), и изменять его не
следует. Для доступа к данным с дальним типом обращения всегда
можно использовать регистр ES, хотя вы можете посчитать, что для
этого временно нужно использовать регистр DS (если вы собираетесь
осуществлять интенсивный доступ к данным), что исключит необходи-
мость использования в вашей программе множества инструкций с пре-
фиксом переопределения сегмента. Например, вы можете обратиться к
дальнему сегменту одним из следующих способов:
.
.
.
.FARDATA
Counter DW 0
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
.
.
.
mov ax,@FarData
mov es,ax ; ES указывает на
; сегмент данных с
; дальним типом
; обращения
inc es:[Counter] ; увеличить значение
; счетчика
.
.
.
TASM2 #2-5/Док = 188 =
_AsmFunction ENDP
.
.
.
или
.
.
.
.FARDATA
Counter DW 0
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
.
.
.
assume ds:@FarData
mov ax,@FarDAta
mov ds,ax ; DS указывает на
; сегмент данных с
; дальним типом
; обращения
inc [Counter] ; увеличить значение
; счетчика
assume ds:@Data
mov ax,@Data
mov dx,ax ; DS снова указывает
; на DGROUP
.
.
.
_AsmFunction ENDP
.
.
.
Второй вариант имеет то преимущество, что при каждом обраще-
нии к дальнему сегменту данных в нем не требуется переопределение
ES:. Если для обращения к дальнему сегменту вы загружаете регистр
DS, убедитесь в том, что перед обращением к другим переменным
DGROUP вы его восстанавливаете (как это делается в приведенном
TASM2 #2-5/Док = 189 =
примере). Даже если в данной функции на Ассемблере вы не обращае-
тесь к DGROUP, перед выходом из нее все равно обязательно нужно
восстановить содержимое DS, так как в Турбо Си подразумевается,
что регистр DS не изменялся.
При использовании в функциях, вызываемых из Си, сверхбольшой
модели памяти работать с регистром DS нужно несколько по-другому.
В сверхбольшой модели памяти Турбо Си совсем не использует
DGROUP. Вместо этого каждый модуль имеет свой собственный сег-
мент данных, который является дальним сегментом относительно всех
других модулей в программе (нет совместно используемого ближнего
сегмента данных). При использовании сверхбольшой модели памяти на
входе в функцию регистр DS должен быть установлен таким образом,
чтобы он указывал на этот дальний сегмент данных модуля и не из-
менялся до конца функции, например:
.
.
.
.FARDATA
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
push ds
mov ax,@FarData
mov ds,ax
.
.
.
pop ds
ret
_AsmFunction ENDP
.
.
.
Заметим, что исходное состояние регистра DS сохраняется при
входе в функцию _AsmFunction с помощью инструкции PUSH и перед
выходом восстанавливается с помощью инструкции POP. Даже в сверх-
большой модели памяти Турбо Си требует, чтобы все функции сохра-
няли регистр DS.
TASM2 #2-5/Док = 190 =
Общедоступные и внешние идентификаторы
-----------------------------------------------------------------
Программы Турбо Ассемблера могут вызывать функции Си и ссы-
латься на внешние переменные Си. Программы Турбо Си аналогичным
образом могут вызывать общедоступные (PUBLIC) функции Турбо Ас-
семблера и обращаться к переменным Турбо Ассемблера. После того,
как в Турбо Ассемблере устанавливаются совместимые с Турбо Си
сегменты (как описано в предыдущих разделах), чтобы совместно ис-
пользовать функции и переменные Турбо Си и Турбо Ассемблера, нуж-
но соблюдать несколько простых правил.
Подчеркивания
-----------------------------------------------------------------
Обычно в Турбо Си предполагается, что все внешние метки на-
чинаются с символа подчеркивания (_). Турбо Си автоматически
включает символы подчеркивания во все имена функций и имена внеш-
них переменных, когда они используются в коде языка Си, поэтому в
коде Ассемблера вы должны просто добавить подчеркивания. Нужно
убедиться, что все ссылки в Ассемблере на функции и переменные
Турбо Си начинаются с подчеркивания. Все функции и переменные Ас-
семблера, которые являются общедоступными и к которым обращается
Турбо Си, должны также начинаться с символа подчеркивания.
Например, следующая программа на языке Си:
extrn int ToggleFlag();
int Flag;
main()
{
ToggleFlag();
}
правильно компонуется со следующей программой на Ассемблере:
DOSSEG
.MODEL SMALL
.DATA
EXTRN _Flag:word
.CODE
PUBLIC _ToggleFlag
_ToggleFlag PROC
cmp [_Flag],0 ; флаг сброшен?
jz SetFlag ; да, установить его
TASM2 #2-5/Док = 191 =
mov [_Flag],0 ; нет, сбросить его
jmp short EndToggleFlag ; выполнено
SetFlag:
mov [_Flag],1 ; установить флаг
EndToggleFlag:
ret
_ToggleFlag ENDP
END
Заметим, что в метках, на которые нет ссылок в программе на
языке Си (например, SetFlag), не требуется указывать символ под-
черкивания.
Кстати, с помощью параметра командной строки -u можно ука-
зать Турбо Си, чтобы он не использовал символы подчеркивания.
Хотя это может показаться достаточно привлекательным способом, но
все библиотеки исполняющей системы Турбо Си скомпилированы с раз-
решением символа подчеркивания. Таким образом, вам потребуется
получить у фирмы Borland исходный код библиотечных модулей и пе-
рекомпилировать их с параметром -u (см. далее "Соглашения по вы-
зовам Паскаля", где рассказывается о параметре -p, который запре-
щает использование символа подчеркивания и различимость строчных
и прописных символов).
TASM2 #2-5/Док = 192 =
Строчные и прописные символы
-----------------------------------------------------------------
В именах идентификаторов Турбо Ассемблер обычно не различает
строчные и прописные буквы (верхний и нижний регистр). Поскольку
в Си они различаются, желательно задать такое различие и в Турбо
Ассемблере (по крайней мере для тех идентификаторов, которые сов-
местно используются Ассемблером и Си). Это можно сделать с по-
мощью параметров /ML и /MX.
Переключатель (параметр) командной строки /ML приводит к
тому, что в Турбо Ассемблере во всех идентификаторах строчные и
прописные символы будут различаться (считаться различными). Пара-
метр командной строки /MX указывает Турбо Ассемблеру, что строч-
ные и прописные символы нужно различать в общедоступных (PUBLIC)
идентификаторах, внешних (EXTRN) идентификаторах глобальных
(GLOBAL) идентификаторах и общих (COMM) идентификаторах.
Типы меток
-----------------------------------------------------------------
Хотя в программах Турбо Ассемблера можно свободно обращаться
к любой переменной или данным любого размера (8, 16, 32 бита и т.
д.), в общем случае хорошо обращаться к переменным в соответствии
с их размером. Например, если вы записываете слово в байтовую пе-
ременную, то обычно это приводит к проблемам:
.
.
.
SmallCount DB 0
.
.
.
mov WORD PTR [SmallCount],0ffffh
.
.
.
Поэтому важно, чтобы в операторе Ассемблера EXTRN, в котором
описываются переменные Си, задавался правильный размер этих пере-
менных, так как при генерации размера доступа к переменной Си
Турбо Ассемблер основывается именно на этих описаниях.
Если в программе на языке Си содержится оператор:
TASM2 #2-5/Док = 193 =
char c
то код Ассемблера:
.
.
.
EXTRN c:WORD
.
.
inc [c]
.
.
.
может привести к весьма неприятным ошибкам, поскольку после того,
как в коде на языке Си переменная c увеличится очередные 256 раз,
ее значение будет сброшено, а так как она описана, как переменная
размером в слово, то байт по адресу OFFSET c + 1 будет увеличи-
ваться некорректно, что приведет к непредсказуемым результатам.
Между типами данных Си а Ассемблера существует следующее со-
отношение:
--------------------------------------------------------------
Тип данных Си Тип данных Ассемблера
--------------------------------------------------------------
unsigned char byte
char byte
enum word
unsigned short word
short word
unsigned int word
int word
unsigned long dword
long dword
float dword
double qword
long double tbyte
near* word
far* dword
---------------------------------------------------------------
Внешние дальние идентификаторы должны лежать вне любого сегмента
TASM2 #2-5/Док = 194 =
-----------------------------------------------------------------
Если вы используете упрощенные директивы определения сегмен-
тов, то описания идентификаторов EXTRN в сегментах дальнего типа
не должны размещаться ни в каком сегменте, так как Турбо Ассем-
блер рассматривает идентификаторы, описанные в данном сегменте,
как связанные с данным сегментом. Это имеет свои недостатки: Тур-
бо Ассемблер не может проверить возможность адресации к идентифи-
катору, описанному, как внешний (EXTRN), вне любого сегмента и
поэтому не может в случае необходимости сгенерировать определе-
ние сегмента или сообщить вам, что была попытка обратиться к дан-
ной переменной, когда сегмент не был загружен корректным значени-
ем. Тем не менее Турбо Ассемблер генерирует для ссылок на такие
внешние идентификаторы правильный код, но не может обеспечить
обычную степень проверки возможности адресации к сегменту.
Если вы все-таки захотите, то можно использовать для явного
описания каждого внешнего идентификатора сегмента старые директи-
вы определения сегментов, а затем поместить директиву EXTRN для
этого идентификатора внутрь описания сегмента. Это довольно уто-
мительно, поэтому если вы не хотите обеспечивать загрузку коррек-
тного значения сегмента при обращении к данным, то проще всего
просто разместить описания EXTRN для идентификаторов дальнего
типа вне всех сегментов. Предположим, например, что файл
FILE1.ASM содержит следующее:
.
.
.
.FARDATA
FileVariable DB 0
.
.
.
и он компонуется с файлом FILE2.ASM, который содержит:
.
.
.
.DATA
EXTRN FileVariable:BYTE
.CODE
Start PROC
mov ax,SEG FileVariable
mov ds,ax
TASM2 #2-5/Док = 195 =
.
.
.
SEG File1Variable не будет возвращать корректного значения
сегмента. Директива EXTRN размещена в области действия директивы
файла FILE2.ASM DATA, поэтому Турбо Ассемблер считает, что пере-
менная File1Variable должна находиться в ближнем сегменте DATA
файла FILE2.ASM, а не в дальнем сегмента DATA.
В следующем коде FILE2.ASM SEG File1Variable будет возвра-
щать корректное значение сегмента:
.
.
.
.DATA
@CurSeg ENDS
EXTRN File1Variable:BYTE
.CODE
Start PROC
mov ax,SEG File1Variable
mov ds,ax
.
.
.
"Фокус" здесь состоит в том, что директива @CurSeg ENDS за-
вершает сегмент .DATA, поэтому, когда переменная FileVariable
описывается, как внешняя, никакая сегментная директива не дей-
ствует.
TASM2 #2-5/Док = 196 =
Командная строка компоновщика
-----------------------------------------------------------------
Простейший способ скомпоновать модули Турбо Си с модулями
Турбо Ассемблера состоит в том, чтобы ввести одну командную стро-
ку Турбо Си, после чего он выполнит всю остальную работу. При за-
дании нужной командной строки Турбо Си выполнит компиляцию исход-
ного кода Си, вызовет Турбо Ассемблер для ассемблирования, а за-
тем вызовет утилиту TLINK для компоновки объектных файлов в вы-
полняемый файл. Предположим, например, что у вас есть программа,
состоящая из файлов на языке Си MAIN.C и STAT.C и файлов ассемб-
лера SUMM.ASM и DISPLAY.ASM. Командная строка:
tcc main stat summ.asm display.asm
выполняет компиляцию файлов MAIN.C и STAT.C, ассемблирование фай-
лов SUMM.ASM и DISPLAY.ASM и компоновку всех четырех объектных
файлов, а также кода инициализации Си и необходимых библиотечных
функций в выполняемый файл MAIN.EXE. При вводе имен файлов Ассем-
блера нужно только помнить о расширениях .ASM.
Если вы используете утилиту TLINK в автономном режиме, то
генерируемые Турбо Ассемблером объектные файлы представляют собой
стандартные объектные модули и обрабатываются также, как объек-
тные модули Си.
Взаимодействие между Турбо Ассемблером и Турбо Си
-----------------------------------------------------------------
Теперь, когда вы понимаете, как нужно строить и компоновать
совместимые с Си модули Ассемблера, нужно знать, какой код можно
помещать в функции Ассемблера, вызываемые из Си. Здесь нужно про-
анализировать три момента: получение передаваемых параметров, ис-
пользование регистров и возврат значений в вызывающую программу.
Передача параметров
-----------------------------------------------------------------
Турбо Си передает функциям параметры через стек. Перед вызо-
вом функции Турбо Си сначала заносит передаваемые этой функции
параметры, начиная с самого правого параметра и кончая левым, в
стек. В Си вызов функции:
.
TASM2 #2-5/Док = 197 =
.
.
Test(i, j, 1);
.
.
.
компилируется в инструкции:
mov ax,1
push ax
push word ptr DGROUP:_j
push word ptr DGROUP:_i
call near ptr _Test
add sp,6
где видно, что правый параметр (значение 1), заносится в стек
первым, затем туда заносится параметр j и, наконец, i.
При возврате из функции занесенные в стек параметры все еще
находятся там, но они больше не используются. Поэтому непосредст-
венно после каждого вызова функции Турбо Си настраивает указатель
стека обратно в соответствии со значением, которое он имел перед
занесением в стек параметров (параметры, таким образом, отбрасы-
ваются). В предыдущем примере три параметра (по два байта каждый)
занимают в стеке вместе 6 байт, поэтому Турбо Си добавляет значе-
ние 6 к указателю стека, чтобы отбросить параметры после обраще-
ния к функции Test. Важный момент здесь заключается в том, что по
соглашениям Турбо Си за удаление параметров из стека отвечает вы-
зывающая программа (см. далее раздел "Соглашения по вызовам в
Паскале").
Функции Ассемблера могут обращаться к параметрам, передавае-
мым в стеке, относительно регистра BP. Например, предположим, что
функция Test в предыдущем примере представляет собой следующую
функцию на Ассемблере:
DOSSEG
.MODEL SMALL
.CODE
PUBLIC _Test
_Test PROC
push bp
mov bp,sp
mov ax,[bp+4] ; получить параметр 1
add ax,[bp+6] ; прибавить параметр 2
TASM2 #2-5/Док = 198 =
; к параметру 1
sub ax,[bp+8] ; вычесть из суммы 3
pop bp
ret
_Test ENDP
Как можно видеть, функция Test получает передаваемые из
программы на языке Си параметры через стек, относительно BP.
(Если вы помните, BP адресуется к сегменту стека.) Но откуда она
знает, где найти параметры относительно BP?
На Рис. 7.4 показано, как выглядит стек перед выполнением
первой инструкции в функции Test:
i = 25;
j = 4;
Test(1, j, 1);
. .
. .
| |
|-----------------------|
| |
|-----------------------|
SP --> | Адрес возврата |
|-----------------------|
SP + 2 | 25 (i) |
|-----------------------|
SP + 4 | 4 (j) |
|-----------------------|
SP + 6 | 1 |
|-----------------------|
| |
|-----------------------|
| |
. .
Рис. 7.4 Состояние стека перед выполнением первой инструкции
функции Test.
Параметры функции Test представляют собой фиксированные ад-
реса относительно SP, начиная с ячейки, на два байта старше адре-
са, по которому хранится адрес возврата, занесенный туда при вы-
зове. После загрузки регистра BP значением SP вы можете обращать-
ся к параметрам относительно BP. Однако, вы должны сначала сохра-
нить BP, так как в вызывающей программе предполагается, что при
TASM2 #2-5/Док = 199 =
возврате BP изменен не будет. Занесение в стек BP изменяет все
смещения в стеке. На Рис. 7.5 показано состояние стека после вы-
полнения следующих строк кода:
.
.
.
push bp
mov bp,sp
.
.
.
. .
. .
| |
|-----------------------|
SP --> | BP вызывающей прогр. | <-- BP
|-----------------------|
SP + 2 | Адрес возврата | BP + 2
|-----------------------|
SP + 4 | 25 (i) | BP + 4
|-----------------------|
SP + 6 | 4 (j) | BP + 6
|-----------------------|
SP + 8 | 1 | BP + 8
|-----------------------|
| |
|-----------------------|
| |
. .
Рис. 7.5 Состояние стека после инструкций PUSH и MOVE.
Организация передачи параметров функции через стек и исполь-
зование его для динамических локальных переменных - это стандар-
тный прием в языке Си. Как можно заметить, неважно, сколько пара-
метров имеет программа на языке Си: самый левый параметр всегда
хранится в стеке по адресу, непосредственно следующим за сохра-
ненным в стеке адресом возврата, следующий возвращаемый параметр
хранится непосредственно после самого левого параметра и т.д.
Поскольку порядок и тип передаваемых параметров известны, их
всегда можно найти в стеке.
Пространство для динамических локальных переменных можно за-
резервировать, вычитая из SP требуемое число байт. Например,
TASM2 #2-5/Док = 200 =
пространство для динамического локального массива размером в 100
байт можно зарезервировать, если начать функцию Test с инструк-
ций:
.
.
.
push bp
mov bp,sp
sub sp,100
.
.
.
как показано на Рис. 7.6.
. .
. .
| |
|-----------------------|
SP --> | | BP - 100
|-----------------------|
| |
|-----------------------|
. .
. .
. .
. .
| |
|-----------------------|
SP + 100 | BP вызывающей прогр. | <-- BP
|-----------------------|
SP + 102 | Адрес возврата | BP + 2
|-----------------------|
SP + 104 | 25 (i) | BP + 4
|-----------------------|
SP + 106 | 4 (j) | BP + 6
|-----------------------|
SP + 108 | 1 | BP + 8
|-----------------------|
| |
|-----------------------|
| |
. .
TASM2 #2-5/Док = 201 =
Рис. 7.6 Состояние стека после инструкций PUSH, MOVE и SUB.
Поскольку та часть стека, где хранятся динамические локаль-
ные переменные, представляет собой более младшие адреса, чем BP,
для обращения к динамическим локальным переменным используется
отрицательное смещение. Например, инструкция:
mov byte ptr [bp-100]
даст значение первого байта ранее зарезервированного
100-байтового массива. При передаче параметров всегда использует-
ся положительная адресация относительно регистра BP.
Хотя можно выделять пространство для динамических локальных
переменных описанным выше способом, в Турбо Ассемблере предусмот-
рена специальная версия директивы LOCAL, которая существенно уп-
рощает выделение памяти и присваивание имен для динамических ло-
кальных переменных. Когда в процедуре встречается директива
LOCAL, то подразумевается, что она определяет для данной процеду-
ры динамические локальные переменные. Например, директива:
LOCAL LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE
определяет динамические переменные LocalArray и LocalCount.
LocalArray на самом деле представляет собой метку, приравненную к
[BP-100], а LocalCount - это метка, приравненная к [BP-102]. Од-
нако вы можете использовать их, как имена переменных. При этом
вам даже не нужно будет знать их значения. AUTO_SIZE - это общее
число байт (объем памяти), необходимых для хранения динамических
локальных переменных. Чтобы выделить пространство для динамичес-
ких локальных переменных, это значение нужно вычесть из SP.
Приведем пример того, как нужно использовать директиву
LOCAL:
.
.
.
_TestSub PROC
LOCAL
LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE
push bp ; сохранить указатель стека
; вызывающей программы
mov bp,sp ; установить собственный
; указатель стека
sub sp,AUTO_SIZE ; выделить пространство для
TASM2 #2-5/Док = 202 =
; динамических локальных
; переменных
mov [LocalCount],10 ; установить переменную
; LocalCount в значение 10
; (LocalCount это [BP-102])
.
.
.
mov cx,[LocalCount] ; получить значение
; (счетчик) из локальной
; переменной
mov al,'A' ; заполним символом 'A'
lea bx,[LocalArray] ; ссылка на локальный
; массив LocalArray
; (LocalArray это [BP-100])
FillLoop:
mov [bx],al ; заполнить следующий байт
inc bx ; ссылка на следующий байт
loop FillLoop ; обработать следующий байт,
; если он имеется
mov sp,bp ; освободить память,
; выделенную для динамичес-
; ких локальных переменных
; (можно также использовать
; add sp,AUTO_SIZE)
pop bp ; восстановить указатель
; стека вызывающей программы
ret
_TestSub ENDP
.
.
.
В данном примере следует обратить внимание не то, что первое
поле после определения данной динамической локальной переменной
представляет собой тип данных для этой переменной: BYTE, WORD,
DWORD, NEAR и т.д. Второе поле после определения данной динами-
ческой локальной переменной - это число элементов указанного ти-
па, резервируемых для данной переменной. Это поле является необя-
зательным и определяет используемый динамический локальный массив
(если он используется). Если данное поле пропущено, то резервиру-
ется один элемент указанного типа. В итоге LocalArray состоит из
100 элементов размером в 1 байт, а LocalCount - из одного элемен-
та размером в слово (см. пример).
Отметим также, что строка с директивой LOCAL в данном приме-
TASM2 #2-5/Док = 203 =
ре завершается полем =AUTO_SIZE. Это поле, начинающееся со знака
равенства, необязательно. Если оно присутствует, то метка, следу-
ющая за знаком равенства, устанавливается в значение числа байт
требуемой динамической локальной памяти. Вы должны затем исполь-
зовать данную метку для выделения и освобождения памяти для дина-
мических локальных переменных, так как директива LABEL только ге-
нерирует метки и не генерирует никакого кода или памяти для
данных. Иначе говоря, директива LOCAL не выделяет память для ди-
намических локальных переменных, а просто генерирует метки, кото-
рые вы можете использовать как для выделения памяти, так и для
доступа к динамическим локальным переменным.
Очень удобное свойство директивы LOCAL заключается в том,
что область действия меток динамических локальных переменных и
общего размера динамических локальных переменных ограничена той
процедурой, в которой они используются, поэтому вы можете свобод-
но использовать имя динамической локальной переменной в другой
процедуре.
Как можно заметить, с помощью директивы LOCAL определять и
использовать автоматические переменные намного легче. Отметим,
что при использовании в макрокомандах директива LOCAL имеет со-
вершенно другое значение (см. Главу 9). (Вы можете обратиться
также к Главе 3 "Справочного руководства", где приведена дополни-
тельная информация о видах директивы LOCAL.)
Кстати, Турбо Си работает с границами стека так же, как мы
здесь описали. Вы можете скомпилировать несколько модулей Турбо
Си с параметром -s и посмотреть, какой код Ассемблера генерирует
Турбо Си и как там создаются и используются границы стека.
Все это прекрасно, но здесь есть некоторые трудности.
Во-первых, такой способ доступа к параметрам, при котором исполь-
зуется постоянное смещение относительно BP достаточно неприятен:
при этом не только легко ошибиться, но если вы добавите другой
параметр, все другие смещения указателя стека в функции должны
измениться. Предположим, например, что функция Test воспринимает
три параметра:
Test(Flag, i, j, 1);
Тогда i находится по смещению 6, а не по смещению 4, j - по
смещению 8, а не 6 и т.д. Для смещений параметров можно использо-
вать директиву EQU:
.
TASM2 #2-5/Док = 204 =
.
.
Flag EQU 4
AddParm1 EQU 6
AddParm2 EQU 8
SubParm1 EQU 10
mov ax[bp+AddParm1]
add ax,[bp+AddParm1]
sub ax,[bp+SubParm1]
.
.
.
но вычислять смещения и работать с ними довольно сложно. Однако
здесь могут возникнуть и более серьезные проблемы: в моделях па-
мяти с дальним кодом размер занесенного в стек адреса возврата
увеличивается на два байта, как и размеры передаваемых указателей
на код и данные в моделях памяти с дальним кодом и дальними дан-
ными, соответственно. Разработка функции, которая с равным успе-
хом будет ассемблироваться и правильно работать с указателем сте-
ка при использовании любой модели памяти было бы весьма непростой
задачей.
Однако опасения излишни. В Турбо Ассемблере предусмотрена
директива ARG, с помощью которой можно легко выполнять передачу
параметров в программах на Ассемблере.
Директива ARG автоматически генерирует правильные смещения в
стеке для заданных вами переменных. Например:
arg FillArray:WORD,Count:WORD,FillValue:BYTE
Здесь задается три параметра: FillArray, параметр размером в
слово, Count, также параметр размером в слово и FillValue - пара-
метр размером в байт. Директива ARG устанавливает метку
FillArray в значение [BP+4] (подразумевается, что код находится
в процедуре ближнего типа), метку Count - в значение [BP+6], а
метку FillValue - в значение [BP+8]. Однако особенно ценна дирек-
тива ARG тем, что вы можете использовать определенные с ее по-
мощью метки не заботясь о тех значениях, в которые они установле-
ны.
Например, предположим, что у вас есть функция FillSub кото-
рая вызывается из Си следующим образом:
TASM2 #2-5/Док = 205 =
main()
{
#define ARRAY_LENGTH 100
char TestArray[ARRAY_LENGTH];
FillSub(TestArray,ARRAY_LENGTH,'*');
}
В FillSub директиву ARG для работы с параметрами можно ис-
пользовать следующим образом:
_FillSub PROC NEAR
ARG FillArray:WORD,Count:WORD,FillValue:BYTE
push bp ; сохранить указатель стека
; вызывающей программы
mov bp,sp ; установить свой собственный
; указатель стека
mov bx,[FillArray] ; получить указатель на
; заполняемый массив
mov cx,[Count] ; получить заполняемую длину
mov al,[FillValue] ; получить значение-заполнитель
FillLoop:
mov [bx],al ; заполнить символ
inc bx ; ссылка на следующий символ
loop FillLoop ; обработать следующий символ
pop bp ; восстановить указатель стека
; вызывающей программы
ret
_FillSub ENDP
Не правда ли, удобно работать с параметрами с помощью дирек-
тивы ARG? Кроме того, директива ARG автоматически учитывает раз-
личные размеры возвратов ближнего и дальнего типа. Другое удобст-
во состоит в том, что метки, определенные с помощью директивы
ARG, ограничены по области действия той процедурой, где они ис-
пользуются, и вам не приходится беспокоиться о возможном конфлик-
те между именами параметров в различных процедурах.
Дополнительная информация о директиве ARG содержится в Главе
3 "Справочного руководства".
TASM2 #2-5/Док = 206 =
Сохранение регистров
-----------------------------------------------------------------
При взаимодействии Турбо Ассемблера и Турбо Си вызываемые из
программы на языке Си функции Ассемблера могут делать все что
угодно, но при этом они должны сохранять регистры BP, SP, CS, DS
и SS. Хотя при выполнении функции Ассемблера эти регистры можно
изменять, при возврате из вызываемой подпрограммы они должны
иметь в точности такие значения, какие они имели при ее вызове.
Регистры AX, BX, CX, DX и ES, а также флаги могут произвольно из-
меняться.
Регистры DI и SI представляют собой особый случай, так как в
Турбо Си они используются для регистровых переменных. Если в мо-
дуле Си, из которого вызывается ваша функция на Ассемблере, ис-
пользование регистровых переменных разрешено, то вы должны сохра-
нить регистры SI и DI, если же нет, то сохранять их не нужно. Од-
нако неплохо всегда сохранять эти регистры, независимо от того,
разрешено или запрещено использование регистровых переменных.
Трудно заранее гарантировать, что вам не придется компоновать
данный модуль Ассемблера с другим модулем на языке Си, или пере-
компилировать модуль Си с разрешением использования регистровых
переменных. При этом вы можете забыть, что изменения нужно также
внести и в код Ассемблера.
Возврат значений
-----------------------------------------------------------------
Вызываемые из программы на языке Си функции на Ассемблере,
так же как и функции Си, могут возвращать значения. Значения
функций возвращаются следующим образом:
-----------------------------------------------------------------
Тип возвращаемого значения Где находится возвращаемое значение
-----------------------------------------------------------------
unsigned char AX
char AX
enum AX
unsigned short AX
short AX
unsigned int AX
int AX
unsigned long DX:AX
long DX:AX
float регистр вершины стека сопроцессора
8087 (ST(0))
TASM2 #2-5/Док = 207 =
double регистр вершины стека сопроцессора
8087 (ST(0))
long double регистр вершины стека сопроцессора
8087 (ST(0))
near* AX
far* DX:AX
-----------------------------------------------------------------
В общем случае 8- и 16-битовые значения возвращаются в ре-
гистре AX, а 32-битовые значения - в AX:DX (при этом старшие 16
бит значения находятся в регистре DX). Значения с плавающей точ-
кой возвращаются в регистре ST(0), который представляет собой ре-
гистр вершины стека сопроцессора 8087 или эмулятора сопроцессора
8087, если используется эмулятор операций с плавающей точкой.
Со структурами дело обстоит несколько сложнее. Структуры,
имеющие длину 1 или 2 байта, возвращаются в регистре AX, а стук-
туры длиной 4 байта - в регистрах AX:DX. Трехбайтовые структуры и
структуры, превышающие 4 байта должны храниться в области стати-
ческих данных, при этом должен возвращаться указатель на эти ста-
тические данные. Как и все указатели, указатели на структуры, ко-
торые имеют ближний тип (NEAR), возвращаются в регистре AX, а
указатели дальнего типа - в паре регистров AX:DX.
Давайте рассмотрим вызываемую из программы на языке Си функ-
цию на Ассемблере с малой моделью памяти FindLastChar, которая
возвращает указатель на последний символ передаваемой строки. На
языке Си прототип этой функции выглядел бы следующим образом:
extern char * FindLastChar(char * StringToScan);
где StringToScan - это непустая строка, для которой должен возв-
ращаться указатель на последний символ.
Функция FindLastChar имеет следующий вид:
DOSSEG
.MODEL SMALL
.CODE
PUBLIC _FindLastChar
_FindLastChar PROC
push bp
mov bp,sp
cld ; в строковой инструкции нужно
; выполнять отсчет в прямом
; направлении
TASM2 #2-5/Док = 208 =
mov ax,ds
mov es,ax ; теперь ES указывает на
; ближний сегмент данных
mov di, ; теперь ES:DI указывает на
; начало передаваемой строки
mov al,0 ; найти нулевой символ,
; завершающий строку
mov cx,0ffffh ; работать в пределах
; 64К-1 байт
repne scasb ; найти нулевой символ
dec di ; установить указатель
; обратно на 0
dec di ; ссылка обратно на
; последний символ
mov ax,dx ; возвратить в AX указатель
; ближнего типа
pop bp
ret
_FindLastChar ENDP
END
Конечный результат, указатель на передаваемую строку, возв-
ращается в регистре AX.
TASM2 #2-5/Док = 209 =
Вызов функций Турбо Ассемблера из Турбо Си
-----------------------------------------------------------------
Теперь мы рассмотрим пример программы на Турбо Си, вызываю-
щей функцию Турбо Ассемблера. Модуль Турбо Ассемблера COUNT.ASM
содержит функцию LineCount, которая возвращает значение счетчика
числа строк и символов в передаваемой строке:
; Вызываемая из Си функция на Ассемблере с малой моделью памяти
; для подсчета числа строк и символов в завершающейся нулем
; "строке".
;
; Прототип функции:
; extern unsigned int LineCount(char * near StringToCount,
; unsigned int near * CharacterCountPtr);
;
; Ввод:
; char near * StringToCount: указатель на "строку", в
; которой нужно выполнить подсчет строк.
;
; unsigned int near * CharacterCountPtr: указатель на
; целую переменную, в которую нужно записать значение
; счетчика
NEWLINE EQU 0ah ; символ перевода строки в Си
DOSSEG
.MODEL SMALL
.CODE
PUBLIC _LinaCount
_LineCount PROC
push bp
mov bp,sp
push si ; сохранить регистровую
; переменную вызывающей
; программы
mov si,[bp+4] ; SI указывает на строку
sub cx,cx ; установить значение
; счетчика символов в 0
mov dx,cx ; установить в 0 счетчик
; строк
LineCountLoop:
lodsb ; получить следующий символ
and al,al ; это 0? конец строки?
jz EndLineCount ; да, выполнено
inc cx ; нет, подсчитать следующий
; символ
cmp al,NEWLINE ; это новая строка?
TASM2 #2-5/Док = 210 =
jnz LineCountLoop ; нет, проверить
; следующий символ
inc dx ; да, подсчитать еще одну
; строку
jmp LineCountLoop
EndLineCount:
inc dx ; подсчитать строку, которая
; завершается нулевым символом
mov [bx],cx ; задать значение переменной-
; счетчика
mov ax,dx ; возвратить счетчик строк в
; качестве значения счетчика
pop si ; восстановить регистровую
; переменную вызывающей
; программы
pop bp
ret
_LineCount ENDP
END
Следующий модуль на языке Си с именем CALLC.C представляет
собой пример вызова функции LineCount:
char * TestString="Line 1\nline 2\n Line3";
extern unsigned int LineCount(char * StringToCount,
unsigned int * CharacterCountPtr);
main()
{
unsigned int LCount;
unsigned int CCount;
LCount = LineCount(TestString, &CCount);
printf("Строк: %d\nCимволов: %d\n", LCount, CCount);
}
Два модуля компилируются и компонуются вместе с помощью ко-
мандной строки:
tcc -ms callc count.asm
Как здесь показано, функция LineCount будет работать только
при компоновке с программами на языке Си, в которых используется
малая модель памяти, так как в других моделях размеры указателей
и адресов в стеке изменятся. Приведем пример версии функции
LineCount (COUNTLG.ASM), которая будет работать с программами на
Си, использующим большую модель памяти (но не малую модель: пос-
TASM2 #2-5/Док = 211 =
кольку передаются дальние указатель, функция LineCount также опи-
сана, как функция дальнего типа):
; Вызываемая из Си функция на Ассемблере для подсчета числа
; строк и символов в завершающейся нулем "строке".
;
; Прототип функции:
; extern unsigned int LineCount(char * far StringToCount,
; unsigned int far * CharacterCountPtr);
;
; Ввод:
; char far * StringToCount: указатель на "строку", в
; которой нужно выполнить подсчет строк.
;
; unsigned int far * CharacterCountPtr: указатель на
; целочисленную переменную, в которую нужно записать
; значение счетчика
NEWLINE EQU 0ah ; символ перевода строки в Си
DOSSEG
.MODEL LARGE
.CODE
PUBLIC _LinaCount
_LineCount PROC
push bp
mov bp,sp
push si ; сохранить регистровую
; переменную вызывающей
; программы
push ds ; сохранить стандартный
; сегмент данных
lds si,[bp+6] ; DS:SI указывает на строку
sub cx,cx ; установить значение
; счетчика символов в 0
mov dx,cx ; установить в 0 счетчик
; строк
LineCountLoop:
lodsb ; получить следующий символ
and al,al ; это 0? конец строки?
jz EndLineCount ; да, выполнено
inc cx ; нет, подсчитать следующий
; символ
cmp al,NEWLINE ; это новая строка?
jnz LineCountLoop ; нет, проверить
; следующий символ
inc dx ; да, подсчитать еще одну
; строку
TASM2 #2-5/Док = 212 =
jmp LineCountLoop
EndLineCount:
inc dx ; подсчитать строку, которая
; завершается нулевым символом
les bx,[bp+10] ; ES:BX указывает на ячейку,
; в которой возвращается
; значение счетчика
mov es:[bx],cx ; задать значение переменной-
; счетчика
mov ax,dx ; возвратить счетчик строк в
; качестве значения счетчика
pop ds ; восстановить стандартный
; сегмент данных Си
pop si ; восстановить регистровую
; переменную вызывающей
; программы
pop bp
ret
_LineCount ENDP
END
Программу COUNTLG.ASM можно скомпоновать с CALLC.C с помощью
следующей командной строки:
tcc -ml callc countlg.asm
TASM2 #2-5/Док = 213 =
Соглашения по вызовам, использующиеся в Паскале
-----------------------------------------------------------------
Итак, теперь вы уже знаете, как обычно в Си передаются пара-
метры функциям: вызывающая программа заносит параметры (справа
налево) в стек, вызывает функцию, и извлекает параметры из стека
(отбрасывает их) после вызова. Турбо Си может также работать по
соглашениям, принятым в Паскале. Согласно этим соглашениям пара-
метры передаются слева направо, а отбрасывает параметры (из сте-
ка) вызываемая программа. Разрешить использование соглашений Пас-
каля в Турбо Си можно с помощью параметра командной строки -p или
ключевого слова pascal.
Приведем пример функции на Ассемблере, в которой используют-
ся соглашения Паскаля:
;
; Вызывается, как: TEST(i, j ,k)
;
i equ 8 ; левый параметр
j equ 6
k equ 4 ; правый параметр
;
DOSSEG
.MODEL SMALL
.CODE
PUBLIC TEST
TEST PROC
push bp
mov bp,sp
mov ax,[bp+i] ; получить i
add ax,[bp+j] ; прибавить к i j
sub ax,[bp+k] ; вычесть из суммы k
pop bp
ret 6 ; возврат, отбросить
; 6 байт параметров
; (очистка стека)
TEST ENDP
END
На Рис. 7.7 показано состояние стека после выполнения инст-
рукции MOV BP,SP:
. .
. .
| |
|-----------------------|
TASM2 #2-5/Док = 214 =
SP --> | BP вызывающей прогр. | <-- BP
|-----------------------|
SP + 2 | Адрес возврата | BP + 2
|-----------------------|
SP + 4 | k | BP + 4
|-----------------------|
SP + 6 | j | BP + 6
|-----------------------|
SP + 8 | i | BP + 8
|-----------------------|
| |
|-----------------------|
| |
. .
Рис. 7.7 Состояние стека после инструкции MOV BP,SP.
Заметим, что для очистки стека от передаваемых параметров
используется инструкция RET 6.
Соглашения по вызовам Паскаля требуют также, чтобы все внеш-
ние и общедоступные идентификаторы указывались в верхнем регистре
и без предшествующих подчеркиваний. Зачем может потребоваться ис-
пользовать в программе на Си соглашения по вызовам Паскаля? Прог-
рамма, использующая соглашения Паскаля, занимает обычно несколько
меньше места в памяти и работает быстрее, чем обычная программа
на языке Си, так как для очистки стека от параметров не требуется
выполнять n инструкций ADD SP. Более подробно о соглашениях по
вызовам, принятым в Паскале, рассказывается в Главе 8.
TASM2 #2-5/Док = 215 =
Вызов Турбо Си из Турбо Ассемблера
-----------------------------------------------------------------
Хотя больше принято для выполнения специальных задач вызы-
вать из Си функции, написанные на Ассемблере, иногда вам может
потребоваться вызывать из Ассемблера функции, написанные на языке
Си. Оказывается, на самом деле легче вызвать функцию Турбо Си из
функции Турбо Ассемблера, чем наоборот, поскольку со стороны Ас-
семблера не требуется отслеживать границы стека. Давайте рассмот-
рим кратко требования для вызова функций Турбо Си из Турбо Ассем-
блера.
Компоновка с кодом инициализации Си
-----------------------------------------------------------------
Хорошим правилом является вызов библиотечных функций Турбо
Си только из Ассемблера в программах, которые компонуются с моду-
лем инициализации Си (используя его в качестве первого компонуе-
мого модуля). Этот "надежный" класс включает в себя все програм-
мы, которые компонуются с помощью командной строки TC.EXE или
TCC.EXE, и программы, в качестве первого компонуемого файла кото-
рых используется файл C0T, C0S, C0C, C0M, C0L или C0H.
В общем случае вам не следует вызывать библиотечные функции
Турбо Си из программ, которые не компонуются с модулем инициали-
зации Турбо Си, так как некоторые библиотечные функции Турбо Си
не будут правильно работать, если не выполнялась компоновка с ко-
дом инициализации. Если вы действительно хотите вызывать библио-
течные функции Турбо Си из таких программ, мы предлагаем вам
взглянуть на код инициализации(файл C0.ASM на дистрибутивных дис-
ках Турбо Си) и приобрести у фирмы Borland исходный код библиоте-
ки языка Си, после чего вы сможете обеспечить правильную инициа-
лизацию для нужных библиотечных функций. Другой возможный подход
состоит просто в том, чтобы скомпоновать нужную библиотечную
функцию с программой на Ассемблере, которая называется, например,
X.ASM, и которая просто вызывает каждую функцию. Компоновку можно
выполнить с помощью командной строки типа:
tlink x,x,,cm.lib
где m - это первая буква желаемой модели памяти (t - сверхмалая,
s - малая, c - компактная и т.д.). Если TLINK выдаст сообщения о
неопределенных идентификаторах, то данную библиотечную функцию
без компоновки с кодом инициализации Си вызывать нельзя.
TASM2 #2-5/Док = 216 =
Примечание: Вызов определяемых пользователем функций
Си, которые в свою очередь вызывают библиотечные функции
языка Си, попадают в ту же категорию, что и непосредствен-
ный вызов библиотечных функций Си. Отсутствие кода инициа-
лизации Си может вызывать ошибки в любой программе Ассемб-
лера, которая прямо или косвенно обращается к библиотечным
функциям Си.
TASM2 #2-5/Док = 217 =
Убедитесь в том, что вы правильно задали сегменты
-----------------------------------------------------------------
Как мы уже говорили ранее, необходимо обеспечивать, чтобы
Турбо Си и Турбо Ассемблер использовали одну и ту же модель памя-
ти, и чтобы сегменты, которые вы используете в Турбо Ассемблере,
совпадали с теми сегментами, которые использует Турбо Си. Нужно
не забывать также помещать директиву EXTRN для внешних идентифи-
каторов вне всех сегментов или внутри правильного сегмента.
Выполнение вызова
-----------------------------------------------------------------
В разделе "Вызов функций Турбо Ассемблера из Турбо Си" мы
уже узнали о том, как Турбо Си выполняет подготовку к вызову и
вызов функции. Мы кратко рассмотрели механизм вызов функций Си,
на этот раз с точки зрения вызова функций Турбо Си из Турбо Ас-
семблера.
Все, что требуется от вас для передачи параметров в функцию
Турбо Си, это занесение в стек самого правого параметра первым,
затем следующего по порядку параметра и так далее, пока в стеке
не окажется самый левый параметр. После этого нужно просто вы-
звать функцию. Например, при программировании на Турбо Си для вы-
зова библиотечной функции Турбо Си strcpy для копирования строки
SourceString в строку DestString можно ввести:
strcpy(DestString, SourceString);
Для выполнения того же вызова на Ассемблере нужно использо-
вать инструкции:
lea ax,SourceString ; правый параметр
push ax
lea ax,DestString ; левый параметр
push ax
call _strcpy ; скопировать строку
add sp,4 ; отбросить параметры
При настройке SP после вызова не забывайте очищать стек от
параметров.
Если вы вызываете функцию Си, которая использует соглашения
Паскаля, заносите в стек параметры слева направо. После вызова
настраивать указатель стека SP не требуется.
TASM2 #2-5/Док = 218 =
lea ax,DestString ; левый параметр
push ax
lea ax,SourceString ; правый параметр
push ax
call CTRCPY ; скопировать строку
В последнем случае конечно подразумевается, что вы переком-
пилировали функцию strcpy с параметром -p, так как в стандартной
библиотечной версии данной функции используются соглашения по вы-
зову, принятые в Си, а не в Паскале. Функции Си возвращают значе-
ния, как описано в разделе "Возврат значений": 8- и 16-битовые
значения возвращаются в регистре AX, а 32-битовые значения - в
AX:DX (при этом старшие 16 бит значения находятся в регистре DX).
Значения с плавающей точкой возвращаются в ST(0) (регистр вершины
стека сопроцессора 8087 или эмулятора сопроцессора 8087, если ис-
пользуется эмулятор операций с плавающей точкой). Структуры возв-
ращаются различным образом, в соответствии с их размером.
Функции Си сохраняют следующие регистры (и только их): SI,
DI, BP, DS, SS, SP и CS. Регистры AX, BX, CX, DX, ES и флаги мо-
гут произвольно изменяться.
TASM2 #2-5/Док = 219 =
Вызов из Турбо Ассемблера функции Турбо Си
-----------------------------------------------------------------
Одним из случаев, когда вам может потребоваться вызвать из
Турбо Ассемблера функцию Турбо Си, является необходимость выпол-
нения сложных вычислений, поскольку вычисления гораздо проще вы-
полнять на Си, чем на Ассемблера. Особенно это относится к случаю
смешанных вычислений, где используются и значения с плавающей
точкой и целые числа. Лучше возложить функции по выполнению пре-
образования типов и реализации арифметики с плавающей точкой на
Си.
Давайте рассмотрим пример программы на Ассемблере, которая
вызывает функцию Турбо Си, чтобы выполнить вычисления с плавающей
точкой. Фактически в данном примере функция Турбо Си передает
последовательность целых чисел другой функции Турбо Ассемблера,
которая суммирует числа и в свою очередь вызывает другую функцию
Турбо Си для выполнения вычислений с плавающей точкой (вычисление
среднего значения).
Часть программы CALCAVG.C, реализованная на Си, выглядит
следующим образом:
extern float Average(int far * ValuePtr, int NumberOfValues);
#define NUMBER_OF_TEST_VALUES 10
int TestValues(NUMBER_OF_TEST_VALUES) = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
main()
{
printf("Среднее арифметическое равно: %f\n",
Average(TestValues, NUMBER_OF_TEST_VALUES));
}
float IntDivide(int Divedent, int Divisor)
}
return( (float) Divident / (float) Divisor );
}
а часть программы на Ассемблере (AVERAGE.ASM) имеет вид:
;
; Вызываемая из Си функция с малой моделью памяти,
; которая возвращает среднее арифметическое последова-
; тельности целых чисел. Для выполнения завершающего
; деления вызывает функцию Си IntDivide().
TASM2 #2-5/Док = 220 =
;
; Прототип функции:
; extern float Average(int far * ValuePtr,
; int NumberOfValues);
;
; Ввод:
; int far * ValuePtr: ; массив значений для
; ; вычисления среднего
; int NumberOfValues: ; число значений для
; ; вычисления среднего
DOSSEG
.MODEL SMALL
EXTRN _IntDivide:PROC
.CODE
PUBLIC _Average
_Average PROC
push bp
mov bp,sp
les bx,[bp+4] ; ES:BX указывает на
; массив значений
mov cx,[bp+8] ; число значений, для
; которых нужно
; вычислить среднее
mov ax,0
AverageLoop:
add ax,es:[bx] ; прибавить текущее
; значение
add ax,2 ; ссылка на следующее
; значение
loop AverageLoop
push WORD PTR [bp+8] ; получить снова число
; значений, переданных
; в функцию IntDivide
; в правом параметре
push ax ; передать сумму в
; левом параметре
call _IntDivide ; вычислить среднее
; значение с плавающей
; точкой
add sp,4 ; отбросить параметры
pop bp
ret ; среднее значение в
; регистре вершины
; стека сопроцессора
; 8087
_Average ENDP
TASM2 #2-5/Док = 221 =
END
Основная функция на языке Си передает указатель на массив
целых чисел TestValues и длину массива в функцию на Ассемблере
Average. Эта функция вычисляет сумму целых чисел, а затем переда-
ет эту сумму и число значений в функцию Си IntDivide. Функция
IntDivide приводит сумму и число значений к типу с плавающей точ-
кой и вычисляет среднее значение (делая это с помощью одной стро-
ки на Си, в то время как на Ассемблере для этого потребовалось бы
несколько строк). Функция IntDivide возвращает среднее значение
(Average) в регистре вершины стека сопроцессора 8087 и передает
управление обратно основной функции.
Программы CALCAVG.C и AVERAGE.ASM можно скомпилировать и
скомпоновать в выполняемую программу CALCAVG.EXE с помощью коман-
ды:
tcc calcavg average.asm
Отметим, что функция Average будет работать как с малой, так
и с большой моделью данных без необходимости изменения ее исход-
ного кода, так как во всех моделях передается указатель дальнего
типа. Для поддержки больших моделей кода (сверхбольшой, большой и
средней) пришлось бы только изменить соответствующую директиву
.MODEL.
TASM2 #2-5/Док = 222 =
Глава 8. Интерфейс Турбо Ассемблера с Турбо Паскалем
-----------------------------------------------------------------
В Турбо Ассемблере предусмотрены расширенные и мощные
средства, позволяющие вам добавлять код Ассемблера к программам
Турбо Паскаля. В данной главе мы подробно расскажем вам о том,
что нужно знать, чтобы полностью использовать данные средства,
приведем множество примеров и дадим некоторую более глубокую ин-
формацию.
Для чего нужно использовать Турбо Ассемблер с Турбо
Паскалем? Большинство программ, которые вы захотите написать,
можно реализовать целиком на Турбо Паскале. В отличие от боль-
шинства других компиляторов Паскаля, Турбо Паскаль позволяет вам
с помощью массивов Port[], Mem[], MemW[] и MemL[] непосредственно
обращаться ко всем ресурсам машины, а с помощью процедур Intr() и
MsDos() вы можете обращаться к базовой системе ввода-вывода
(BIOS) и операционной системе DOS.
Для чего же тогда может потребоваться использовать совместно
с Турбо Паскалем Ассемблер? Для этого существуют две вероятные
причины: выполнение некоторого небольшого числа операций, которые
непосредственно в Турбо Паскале недоступны, и использование преи-
муществ высокой скорости работы, которые дает Ассемблер. (Сам
Турбо Паскаль работает достаточно быстро, потому что он написан
на языке Ассемблера.) В данной главе мы покажем вам, как можно
использовать в Турбо Паскале преимущества Ассемблера.
Примечание: Если номер версии специально не оговарива-
ется, то везде далее речь идет о Турбо Паскале версии 4.0 и
старше.
TASM2 #2-5/Док = 223 =
Схема памяти Турбо Паскаля
-----------------------------------------------------------------
Перед тем, как вы начнете писать код на языке Ассемблера для
работы с Турбо Паскалем, важно понять, как компилятор располагает
информацию в памяти. Модель памяти Турбо Паскаля объединяет неко-
торые стороны средней и большой модели памяти, которые описыва-
лись в Главе 5. Здесь имеется один глобальные сегмент данных, ко-
торый позволяет организовать доступ к глобальным переменным и ти-
пизованным константам через регистр DS. Однако каждый модуль име-
ет свой сегмент кода, и динамически распределяемая область памяти
может увеличиваться до размера всей доступной памяти.
Схема памяти Турбо Паскаля показана на Рис. 8.1.
Младшие адреса памяти
----------------------------------------------------
| Префикс программного сегмента |
| (256 байт) |
|--------------------------------------------------|
| |
| Главный сегмент кода программы | Максималь-
| | ный размер
| | сегмента
| | кода - 64К
|--------------------------------------------------|
| Сегмент кода последнего модуля |
|--------------------------------------------------|
| |
. .
. .
. .
| |
|--------------------------------------------------|
| Сегмент кода первого модуля |
|--------------------------------------------------|
| |
| Сегмент кода библиотеки исполняющей системы |
| |
|--------------------------------------------------|<-- DS
| Типизованные константы |
|- - - - - - - - - - - - - - - - - - - - - - - - - |<-- Конец
| Глобальные константы | файла .EXE
|--------------------------------------------------|<-- SS
| ^ | Размер стека
| | | Минимум: 1К
TASM2 #2-5/Док = 224 =
| Стек (увеличивается в сторону младших адресов) | По умолча-
| | нию: 16K
| | Максимум:
| | 64К
|--------------------------------------------------|
| Динамически распределяемая область памяти | Не ограни-
| (увеличивается в сторону старших адресов) | чена по
| | | размеру
| v |
|--------------------------------------------------|<-- HeapPtr
| ^ | Максимальный
| | | размер спис-
| Список свободных областей динамически | ка свободных
| распределяемой памяти (увеличивается в сторону | областей
| младших адресов) |
----------------------------------------------------
Старшие адреса памяти
Рис. 8.1 Схема памяти программы Турбо Паскаля версии 5.0.
TASM2 #2-5/Док = 225 =
Префикс программного сегмента
-----------------------------------------------------------------
Префикс программного сегмента (PSP) представляет собой об-
ласть памяти размером в 256 байт, создаваемую операционной систе-
мой MS-DOS при загрузке программы. Кроме всего прочего она содер-
жит информацию о параметрах командной строки, используемой для
вызова программы, объеме доступной памяти и операционной среде
DOS (списке используемых DOS строковых переменных).
В версии 3.0 Турбо Паскаля адрес сегмента для PSP был тот
же, что и у всего остального кода. Теперь это не так. В Турбо
Паскале версии 4.0 и старше основная программа, используемые ей
модули и библиотека исполняющей системы занимают различные сег-
менты. Турбо Паскаль, таким образом, хранит адрес сегмента PSP в
предопределенной глобальной переменной с именем PrefixSeg, благо-
даря чему вы можете получить доступ к информации в PSP.
Сегменты кода
-----------------------------------------------------------------
Каждая программа Турбо Паскаля содержит по крайней мере два
сегмента кода: в одном из них содержится код основной программы,
а в другом - библиотека исполняющей системы (run-time library).
Кроме того, подпрограммы каждого модуля находятся в отдельном
сегменте кода. Так как каждый сегмент кода может иметь размер до
64К, ваша программа может занимать такой объем, какой для нее
требуется (если, конечно, такой объем памяти на вашем компьютере
доступен). Программисты, которые ранее использовали оверлеи, мо-
гут теперь для более быстрого выполнения генерировать программы,
превышающие 64К, и для более быстрого выполнения хранить весь код
в памяти (в версиях Турбо Паскаля 5.0 и 5.5 вновь введена возмож-
ность использования оверлеев, что связано с нехваткой памяти,
возникающей при разработке больших программ). С точки зрения Тур-
бо Ассемблера сегмент кода, с которым компонуется модуль на языке
Ассемблера, называется CODE или CSEG.
TASM2 #2-5/Док = 226 =
Сегмент глобальных данных
-----------------------------------------------------------------
Сегмент глобальных данных Турбо Паскаля следует за сегментом
кода библиотеки исполняющей системы. Он содержит до 64К инициали-
зированных и неинициализированных данных - типизованных констант
и глобальных переменных. Как и в Турбо Паскале 3.0, типизованные
константы на самом деле вовсе не являются константами, а предс-
тавляют собой переменные, имеющие предопределенное значение при
загрузке программы. Но в отличие от Турбо Паскаля 3.0, Турбо Пас-
каль версии 4.0 не помещает типизованные константы в сегмент ко-
да. Вместо этого он размещает их в сегменте глобальных данных,
где к ним обращаться можно даже быстрее, чем это мог делать Турбо
Паскаль 3.0. Сегмент глобальных данным называется DATA или DSEG
(по этим именам к нему можно обращаться из Турбо Ассемблера).
Стек
-----------------------------------------------------------------
В Турбо Паскале версии 4.0 и старше сегмент глобальных дан-
ных находится над стеком. Заметим, что такое расположение отлича-
ется от принятого в Турбо Паскале 3.0. Стек и динамически распре-
деляемая область памяти не растут навстречу друг другу. Для стека
выделяется фиксированный объем памяти. По умолчанию это 16К, что
вполне достаточно для большинства программ. Однако вы можете за-
дать минимальный размер стека 1К (для коротких программ) или мак-
симальный 64К (для программ, интенсивно использующих рекурсию).
Размер стека и динамически распределяемой области памяти можно
выбрать с помощью директивы компилятора $M.
В большинстве программ для процессоров 80х86 указатель стека
начинается с вершины стека и изменяет значение в сторону младших
адресов. При вызове процедуры или функции Турбо Паскаль обычно
выполняет проверку, чтобы убедиться, что стек не исчерпан. Эту
проверку можно "выключить" с помощью директивы компилятора {$S-}.
TASM2 #2-5/Док = 227 =
Динамически распределяемая область памяти
-----------------------------------------------------------------
В старших адресах памяти Турбо Паскаля находится динамически
распределяемая область памяти (heap). По умолчанию динамически
распределяемая область занимает всю память, не использованную для
сегментов кода, данных и стека. Однако для ограничения размера
динамически распределяемой области памяти можно использовать ди-
рективу $M (ее можно также использовать для предотвращения выпол-
нения программы, если не доступен минимальный объем динамически
распределяемой области памяти).
Память в динамически распределяемой области выделяется ди-
намически при обращении к процедурам New() и GetMem(), начи-
ная с нижней ее границы ("дно"). Когда используются процедуры
Dispose и FreeMem, Турбо Паскаль версии 4.0 и выше отслеживает
свободные области, образующиеся в динамически распределяемой об-
ласти памяти, с помощью специальной структуры данных, которая на-
зывается списком свободных областей (free list). Список свободных
областей, размер которого не может превышать 64К, увеличивается в
сторону младших адресов, начиная с "вершины" динамически распре-
деляемой области памяти.
Использование регистров в Турбо Паскале
-----------------------------------------------------------------
Турбо Паскаль налагает на использование регистров минималь-
ные ограничения. При вызове процедуры или функции должны сохра-
няться (и восстанавливаться) значения только трех регистров: ре-
гистра сегмента стека (SS), регистра сегмента данных (DS) и ука-
зателя базы (BP). Регистр DS указывает на глобальный сегмент дан-
ных (с именем DATA), а SS - на сегмент стека. Регистр BP исполь-
зуется в каждой процедуре и функции для ссылки на запись актива-
ции (activation record), которая представляет собой пространство
в стеке, используемое для параметров, локальных переменных и вре-
менной рабочей памяти. Все подпрограммы перед выходом должны вы-
равнивать указатель стека (SP), то есть очищать его от парамет-
ров.
Ближний или дальний?
-----------------------------------------------------------------
Поскольку программа Турбо Паскаля содержит несколько сегмен-
тов кода, для обращения к процедурам и функциям она использует
TASM2 #2-5/Док = 228 =
"смесь" вызовов ближнего (NEAR) и дальнего (FAR) типов. В чем
разница? Ближний вызов может использоваться только для обращения
к подпрограмме, которая находится в том же сегменте, что и сег-
мент, откуда делается вызов. С помощью же дальнего вызова можно
обращаться к подпрограмме, которая находится в любом месте памя-
ти. Однако это не проходит даром: дальние вызовы занимают больше
места и выполняются медленнее, чем ближние.
Каждая подпрограмма программы Турбо Паскаля должна быть на-
писана (разработана вами или использована компилятором) таким об-
разом, чтобы она вызывалась только одним из этих двух способов.
Какой из них следует выбрать? Подпрограммы, описанные в интерфей-
сной части модуля, всегда должны иметь дальний тип, так как они
могут вызываться из других модулей. Однако подпрограммы, описан-
ные в основной программе или объявленные только в разделе реали-
зации модуля имеют обычно ближний тип. (Любой подпрограмме можно
принудительно назначить дальний тип с помощью директивы компиля-
тора {$F+}.)
При разработке программ на Ассемблере, взаимодействующих с
Турбо Паскалем, вы должны убедиться, что ваша программа имеет
правильный тип вызова. Турбо Паcкаль не сообщит об ошибке, если
вы на языке Ассемблера объявите процедуру (PROC), как ближнюю, а
соответствующее описание внешней процедуры расположено таким об-
разом, что она должна иметь дальний тип.
TASM2 #2-5/Док = 229 =
Совместное использование данных c Турбо Паскалем //
Директива компилятора $L и внешние подпрограммы
-----------------------------------------------------------------
Два ключевых момента при использовании Турбо Ассемблера с
Турбо Паскалем - это директива компилятора (Турбо Паскаля) {$L} и
описание внешней (external) подпрограммы. Директива {$L
MYFILE.OBJ} приводит к тому, что Турбо Паскаль будет искать файл
объектный MYFILE.OBJ (файл в стандартном пригодном для компоновки
формате MS-DOS) и компоновать его с вашей программой Турбо Паска-
ля. Если у файла в директиве {$L} расширение не указывается, то
подразумевается расширение .OBJ.
Каждая процедура или функция Турбо Ассемблера, которую вы
хотите сделать доступной в программе Турбо Паскаля, должна объяв-
ляться, как идентификатор PUBLIC, и ей должно соответствовать в
программе описание external (внешняя). Синтаксис описания внешней
процедуры или функции в Турбо Паскале аналогичен опережающему
(forward) описанию:
procedure AsmProc(a : integer; b : real); external;
function AsmFunc(c : word; d : byte); external;
Эти описания должны соответствовать следующим описаниям в
программе Турбо Ассемблера:
CODE SEGMENT BYTE PUBLIC
AsmProc PROC NEAR
PUBLIC AsmProc
.
.
.
AsmProc ENDP
AsmFunc PROC FAR
PUBLIC Bar
.
.
.
AsmFunc ENDP
CODE ENDS
Описание внешней процедуры Турбо Паскаля должно находиться
на самом внешнем уровне программы или модуля, то есть оно не
должно быть вложенным по отношению к другому описанию процедуры
или функции. Попытка описать процедуру или функцию на любом дру-
TASM2 #2-5/Док = 230 =
гом уровне приведет к ошибке этапа компиляции.
Турбо Паскаль не делает проверку, чтобы убедиться, что все
процедуры, описанные с атрибутами NEAR или FAR, соответствуют
ближним или дальним подпрограммам в программе Турбо Паскаля. Фак-
тически, он даже не проверяет, являются ли метки AsmProc и
AsmFunc именами процедур. Поэтому вы должны обеспечить, чтобы
описания в Ассемблере и Паскале были правильными.
TASM2 #2-5/Док = 231 =
Директива PUBLIC
-----------------------------------------------------------------
В Турбо Паскале доступны только те метки Ассемблера, которые
объявлены в модуле на языке Ассемблера, как общедоступные
(PUBLIC). Метки представляют собой единственные объекты, которые
могут передаваться из языка Ассемблера в Турбо Паскаль. Более
того, каждой общедоступной метке должно соответствовать описание
процедуры или функции в программе Турбо Паскаля, иначе компилятор
выдаст сообщение об ошибке. Причем не требуется, чтобы общедос-
тупная метка была частью описания PROC. Что касается Турбо Паска-
ля, то для него описания:
AsmLabel PROC FAR
PUBLIC Bar
и
AsmLabel:
PUBLIC Bar
эквивалентны.
TASM2 #2-5/Док = 232 =
Директива EXTRN
-----------------------------------------------------------------
Модуль Турбо Ассемблера может обращаться к любой процедуре,
функции, переменной или типизованной константе Турбо Паскаля, ко-
торая описывается на самом внешнем уровне программы или модуля, с
которым она компонуется. (Заметим, что это включает в себя пере-
менные, описанные после директивы компилятора {$L} и внешние опи-
сания, связанные с данным модулем.) Метки и обычные константы
Турбо Паскаля языку Ассемблера недоступны.
Предположим, в вашем программе Турбо Паскаля описываются
следующие глобальные переменные:
var
a : byte;
b : word;
c : shortint;
d : integer;
e : real;
f : single;
g : double;
h : extended;
i : comp;
j : pointer;
В программе на языке Ассемблера вы можете получить доступ ко
всем этим переменным с помощью описаний EXTRN:
EXTRN A : BYTE ; 1 байт
EXTRN B : WORD ; 2 байта
EXTRN C : BYTE ; в Ассемблере значения со знаком и
; без знака интерпретируются одинаково
EXTRN D : WORD ; то же самое
EXTRN E : FWORD ; 6-байтовое действительное значение
; (обрабатывается программно)
EXTRN F : DWORD ; 4-байтовое значение с плавающей
; точкой в формате IEEE
EXTRN G : QWORD ; 8-байтовое значение с плавающей
; точкой (двойной точности) в
; формате IEEE
EXTRN H : TBYTE ; 10-байтовое значение с плавающей
; точкой во временном формате
EXTRN I : QWORD ; 8-байтовое целое со знаком в
; формате IEEE (сопроцессор 8087)
EXTRN J : DWORD ; указатель Турбо Паскаля
TASM2 #2-5/Док = 233 =
Аналогичным образом можно получить доступ к процедурам и
функциям Турбо Паскаля, включая библиотечные. Предположим, у вас
имеется модуль Турбо Паскаля, который выглядит следующим образом:
unit Sample;
{ Пример модуля, в котором определяется нескольку процедур
Паскаля, вызываемых из процедуры на языке Ассемблера }
interface
procedure TestSample;
procedure PublicProc; { для обращения извне должна
быть дальнего типа }
inplementation
var
A : word;
procedure AsmProc; external;
{$L ASMPROC.OBJ}
procedure PublicProc;
begin { PublicProc }
Writeln('В PublicProc');
end { PublicProc }
procedure NearProc; { должна быть ближнего типа }
begin { NearProc }
Writeln('B NearProc');
end; { NearProc }
{$F+}
procedure FarProc { должна иметь дальний тип согласно
директиве компилятора }
begin { FarProc }
Writeln('B FarProc');
end { FarProc }
{$F-}
procedure TestSample;
begin { TestSample }
Writeln('B TestSample');
A := 10;
TASM2 #2-5/Док = 234 =
Writeln('Значение A перед ASMPROC = ',A);
AsmProc;
Writeln('Значение A после ASMPROC = ',A);
end { TestSample };
end.
Процедура AsmProc вызывает процедуры PublicProc, NearProc
или FarProc, используя директиву EXTRN следующим образом:
DATA SEGMENT WORD PUBLIC
ASSUME DS:DATA
EXTRN A:WORD ; переменная из модуля
DATA ENDS
CODE SEGMENT BYTE PUBLIC
ASSUME CS:CODE
EXTRN PublicProc : FAR ; дальняя процедура
; (экспортируется модулем)
EXTRN NearProc : NEAR ; ближняя процедура
; (локальная для модуля)
EXTRN FarProc : FAR ; дальняя процедура
; (локальна, но задана,
; как дальняя)
AsmProc PROC NEAR
PUBLIC AsmProc
CALL FAR PTR PublicProc
CALL NearProc
CALL FAR PTR FarProc
mov cx,ds:A ; взять переменную из
; модуля
sub cx,2 ; изменить ее
mov ds:A,cx ; записать ее обратно
RET
AsmProc ENDP
CODE ENDS
END
Основная программа, которая проверяет эту программу на Ас-
семблере и модуль Паскаля, выглядит следующим образом:
program TSample;
uses Sample;
begin
TestSample;
end.
TASM2 #2-5/Док = 235 =
Чтобы сформировать пример программы с помощью компилятора,
работающего в режиме командной строки, и Ассемблера, используйте
следующие команды (или командный файл):
TASM ASMPROC
TPC /B SAMPLE
TSAMPLE
Так как внешняя подпрограмма должна объявляться в программе
Турбо Паскаля на самом внешнем уровне процедур, вы не можете для
доступа к объектам, являющимся локальными по отношению к процеду-
рам или функциям использовать описания EXTRN. Однако, ваша прог-
рамма на Турбо Ассемблере при вызове из программы Турбо Паскаля
может получить эти объекты, как значения параметров-переменных.
TASM2 #2-5/Док = 236 =
Ограничения при использовании объектов типа EXTRN
-----------------------------------------------------------------
Синтаксис составного идентификатора Турбо Паскаля, при кото-
ром для доступа к объекту в заданном модуле используется имя мо-
дуля и точка, несовместим с синтаксическими правилами Турбо Ас-
семблера и будет, таким образом, отвергнут. Описание:
EXTRN SYSTEM.Assing : FAR
приведет к тому, что Турбо Ассемблер выдаст сообщение об ошибке.
Имеется также два других ограничения на использование в Тур-
бо Паскале объектов EXTRN. Первое из них состоит в том, что в
ссылках на процедуру или функцию не могут выполняться арифмети-
ческие операции с адресами. Таким образом, если вы объявите:
EXTRN PublicProc : FAR
то не сможете записать оператор вида:
call PublicProc + 42
Второе ограничение относится к тому, что компоновщик Турбо
Паскаля не будет распознавать операции, которые разделяют слова
на байты, поэтому вы не можете применять такие операции к объек-
там EXTRN. Например, если вы объявите:
EXTRN i : WORD
то не сможете использовать в модуле Турбо Ассемблера выражения
LOW i или HIGH i.
Использование корректировок сегментов
-----------------------------------------------------------------
Турбо Паскаль генерирует файлы .EXE, которые могут загру-
жаться в память компьютера РС по любому доступному адресу. Пос-
кольку в программе заранее неизвестно, куда будет загружен данный
сегмент программы, компоновщик указывает загрузчику DOS.EXE, что
нужно при загрузке скорректировать в программе все ссылки на сег-
менты. После выполнения этих корректировок все ссылки на сегменты
(такие, как CODE или DATA) будут содержать корректные значения.
Ваша программа на Турбо Ассемблере может использовать это
TASM2 #2-5/Док = 237 =
средство для получения адресов объектов во время выполнения.
Предположим, например, что в вашей программе требуется изменить
значение регистра DS, но вы не хотите сохранять в цикле исходное
содержимое стека или перемещать эти значения во временную об-
ласть. Вместо этого вы можете использовать операцию Турбо Ассем-
блера SEG:
.
.
.
mov ax,SEG DATA ; получить действительный
; адрес глобального значения
; DS Турбо Паскаля
mov ds,ax ; поместить его в DS для
; использования Турбо
; Паскалем
.
.
.
Когда ваша программа будет загружаться, DOS поместит коррек-
тное значение SEG DATA прямо в поле промежуточного операнда инст-
рукции MOV. Это наиболее быстрый путь перезагрузки сегментного
регистра.
Данный метод нужно также использовать, чтобы программы обс-
луживания прерываний сохраняли информацию в глобальном сегменте
данных Турбо Паскаля. Регистр DS не обязательно во время прерыва-
ния содержит значение DS Турбо Паскаля, но для получения доступа
к переменным и типизованным константам Турбо Паскаля можно ис-
пользовать указанную выше последовательность.
Устранение неиспользуемого кода
-----------------------------------------------------------------
В Турбо Паскале имеются средства, обеспечивающие устранение
неиспользуемого кода. Это означает, что в полученный в результате
файл .EXE не будет включаться код процедур и функций, который ни-
когда не выполняется. Но поскольку нет полной информации о содер-
жимом модулей Турбо Ассемблера, Турбо Паскаль может выполнять для
них только ограниченную оптимизацию.
Турбо Паскаль будет устранять код модуля .OBJ в том и только
в том случае, если к любой доступной процедуре или функции этого
модуля нет обращения. Если же на какую либо процедуру или функцию
TASM2 #2-5/Док = 238 =
имеется ссылка, то весь этот модуль используется.
Чтобы добиться большей эффективности использования средства
Турбо Паскаля по устранению неиспользуемого кода, неплохо было бы
разбить программу на Ассемблере на небольшие модули, которые со-
держали бы только несколько процедур или функций. Это позволило
бы Турбо Паскалю, если он может это сделать, уменьшить объем ва-
шей конечной программы.
TASM2 #2-5/Док = 239 =
Соглашения Турбо Паскаля по передаче параметров
-----------------------------------------------------------------
Турбо Паскаль использует для передачи параметров стек цен-
трального процессора (или, в случае передачи значений параметров
с одинарной, двойной, расширенной точностью или сложного типа,
стек арифметического сопроцессора). Параметры всегда вычисляются
и заносятся в стек в том порядке, в котором они указываются в
описании подпрограммы, слева направо. В данном разделе мы пояс-
ним, как эти параметры представляются.
Параметры-значения
-----------------------------------------------------------------
Параметр-значение - это параметр, значение которого не может
изменяться подпрограммой, в которую он передается. В отличие от
многих компиляторов, Турбо Паскаль не выполняет слепого копирова-
ния в стек каждого параметра-значения: как мы далее поясним, ис-
пользуемый метод зависит от типа.
Скалярные типы
-----------------------------------------------------------------
Параметры-значения всех скалярных типов (boolean, char,
shortint, byte, integer, word, longint, отрезки типов и перечис-
лимые типы) передаются как значения через стек процессора. Если
размер объекта составляет 1 байт, он заносится в стек, как полное
16-битовое слово, однако более значащий (старший) байт слова не
содержит полезной информации. (Нельзя рассчитывать на то, что
значение этого байта равно 0, как в версии 3.0 Турбо Паскаля.)
Если размер объекта равен двум байтам, то он просто заносится в
стек "как есть". Если объект имеет размер 4 байта (длинное це-
лое), он заносится в стек, как два 16-битовых слова. В соответст-
вии со стандартом процессоров серии 8088 наиболее значащее (стар-
шее) слово заносится в стек первым и занимает в стеке старшие ад-
реса.
Заметим, что сложный тип (comp), в отличие от целого типа,
не считается скалярным типом (с точки зрения передачи парамет-
ров). Таким образом, в Турбо Паскале версии 4.0 параметры-значе-
ния этого типа передаются в стеке процессора 8087, а не в стеке
центрального процессора. В Турбо Паскале версии 5.0 значения типа
comp передаются в стеке центрального процессора.
TASM2 #2-5/Док = 240 =
Вещественные значения
-----------------------------------------------------------------
Параметры-значения вещественного типа (real) передаются, как
6 байт в стеке (в Турбо Паскале это тип представляет собой 6-бай-
товый программно-эмулируемый тип с плавающей точкой). Это единс-
твенный тип, превышающий 4 байта, который может передаваться че-
рез стек.
TASM2 #2-5/Док = 241 =
Типы сопроцессора 8087
-----------------------------------------------------------------
В Турбо Паскале версии 4.0 параметры-значения типов сопро-
цессора 8087 (с одиночной, двойной, расширенной точностью или
сложный тип) передаются через стек сопроцессора, а не через стек
центрального процессора. Так как стек сопроцессора 8087 имеет
глубину только 6 уровней, подпрограммы Турбо Паскаля не могут пе-
редавать более 6 параметров с типами сопроцессора 8087. Перед
возвратом из подпрограммы она должна извлечь из стека арифмети-
ческого сопроцессора все параметры такого типа.
Турбо Паскаль 5.0 использует при передаче параметров (значе-
ний процессора 8087) те же соглашения, что и Турбо Си: они пере-
даются в стеке центрального процессора наряду с другими парамет-
рами. Это могут быть параметры с одинарной, двойной, расширенной
точностью или сложного типа (сomp).
Указатели
-----------------------------------------------------------------
Значения параметров для всех типов указателей заносятся не-
посредственно в стек, как указатели дальнего типа: сначала слово,
содержащее сегмент, затем другое слово, содержащее смещение. Сег-
мент занимает старший адрес, в соответствии с соглашениями фирмы
Intel. Для извлечения параметра-указателя в программе Турбо Ас-
семблера можно использовать инструкции LDS или LES.
Строки
-----------------------------------------------------------------
Строковые параметры, независимо от размера, обычно никогда
не заносятся в стек. Вместо этого Турбо Паскаль заносит в стек
указатель (дальнего типа) на строку. Вызываемая подпрограмма не
должна изменять строку, на которую ссылается указатель. Если это
необходимо, подпрограмма может создать и работать с копией стро-
ки.
Единственное исключение из этого правила - это случай, когда
подпрограмма в перекрываемом (оверлейном) модуле A передает как
параметр-значение строковую константу подпрограмме в перекрывае-
мом модуле B. В этом контексте перекрываемый модуль означает лю-
бой модуль, скомпилированный с директивой {$O+} (допускаются ове-
рлеи). В этом случае перед тем, как будет сделан вызов и адрес
TASM2 #2-5/Док = 242 =
стека будет передан программе в модуле B, в стеке для строковой
константы резервируется временная память. Более подробная инфор-
мация содержится в Главе 6 ("Оверлеи") и в "Руководстве пользова-
теля по Турбо Паскалю" версии 5.0 или 5.5.
Записи и массивы
-----------------------------------------------------------------
Записи и массивы, занимающие ровно 1, 2 или 4 байта, дубли-
руются непосредственно в стек и передаются, как параметры-значе-
ния. Если массив или запись имеет какой-либо другой размер (вклю-
чая 3 байта), то в стек заносится указатель на этот массив или
запись. В этом случае, если подпрограмма модифицирует такую
структуру, то она должна создать ее локальную копию.
Множества
-----------------------------------------------------------------
Множества, как и строки, обычно никогда не заносятся непос-
редственно в стек. Вместо этого в стек заносится указатель на
множество. Первый бит младшего байта множества всегда соответс-
твует элементу базового типа (или порождающего типа) с порядковым
значением 0.
Единственное исключение из этого правила - это случай, когда
подпрограмма в перекрываемом (оверлейном) модуле A передает как
параметр-значение константу-множество подпрограмме в оверлейном
модуле B. В этом контексте перекрываемый модуль означает любой
модуль, скомпилированный с директивой {$O+}(допускаются оверлеи).
В этом случае перед тем, как будет сделан вызов и адрес стека бу-
дет передан программе в модуле B, в стеке для множества-константы
резервируется временная память. Более подробная информация со-
держится в Главе 6 ("Оверлеи") и в "Руководстве пользователя по
Турбо Паскалю 5.0".
Параметры-переменные
-----------------------------------------------------------------
Все параметры-переменные (var) передаются точно также: как
указатель дальнего типа на их действительные адреса в памяти.
Обеспечение стека
TASM2 #2-5/Док = 243 =
-----------------------------------------------------------------
Турбо Паскаль ожидает, что перед возвратом управления из
подпрограммы все параметры в стеке центрального процессора будут
удалены.
Есть два способа настройки стека. Вы можете использовать ин-
струкцию RET N (где N - это число байт передаваемых, то есть за-
несенных в стек, параметров), либо сохранить адрес возврата в ре-
гистрах (или в памяти) и извлечь параметры из стека поочередно.
Такую технику извлечения полезно использовать для оптимизации по
скорости при работе с процессором 8086 или 8088 (самые "медлен-
ные" процессоры серии), когда на адресацию типа "база плюс смеще-
ние" затрачивается минимум 8 циклов за обращение. Это позволяет
также сэкономить место, так как инструкция POP занимает только
один байт.
Примечание: Если вы используете директивы .MODEL, PROC
и ARG, то Ассемблер автоматически добавляет во все инструк-
ции RET число байт извлекаемых параметров.
TASM2 #2-5/Док = 244 =
Доступ к параметрам
-----------------------------------------------------------------
Когда получает управление ваша подпрограмма на Турбо Ассемб-
лере, вершина стека будет содержать адрес возврата (два или четы-
ре слова, в зависимости от того, является ли подпрограмма ближней
или дальней), а далее будут находится передаваемые параметры.
(Примечание: При вычислении адресов параметров нужно принимать во
внимание регистры, такие как BP, содержимое которых также может
быть занесено в стек.)
Существует три основных метода доступа к параметрам, переда-
ваемых Турбо Паскалем вашей подпрограмме на Турбо Ассемблере. Вы
можете:
- использовать для адресации к стеку регистр BP;
- для получения параметров использовать другой базовый или
индексный регистр;
- извлечь из стека адрес возврата, а затем параметры.
Первый и второй методы более сложны, и мы расскажем о них в
следующих двух разделах. Третий метод предусматривает извлечение
из стека и сохранение адреса возврата, а затем извлечения пара-
метров и записи их в регистры. Лучше всего этот метод работает,
когда ваша подпрограмма не требует пространства для локальных пе-
ременных.
TASM2 #2-5/Док = 245 =
Использование для адресации к стеку регистра BP
-----------------------------------------------------------------
Первый и наиболее часто используемый метод доступа к пара-
метрам, передаваемым из Турбо Паскаля в Турбо Ассемблер, заключа-
ется в том, чтобы использовать для адресации к стеку регистр BP.
Например:
CODE SEGMENT
ASSUME CS:CODE
MyProc PROC FAR ; procedure MyProc(i,j : integer);
PUBLIC MyProc
j EQU WORD PTR [bp+6] ; j находится над сохраненным BP
; и адресом возврата
i EQU WORD PTR [bp+8] ; i располагается над j
push bp ; нужно сохранить BP вызывающей
; программы
mov bp,sp ; BP теперь указывает на вершину
; стека
mov ax,i ; адресуемся к i через BP
.
.
.
При вычислении смешений в стеке параметров, к которым мы об-
ращаемся таким образом, нужно помнить, что 2 байта используются
для сохраненного регистра BP.
Обратите внимание на использование в данном примере прирав-
ниваний. Они позволяют сделать программу более понятной. У них
есть только один недостаток: поскольку для выполнения такого рода
приравниваний можно использовать только директиву EQU (а не =), в
данной исходном файле Турбо Ассемблера вы не сможете переопреде-
лить идентификаторы i и j. Один из способов обойти это заключает-
ся в том, чтобы использовать более описательные имена параметров,
чтобы они не повторялись, либо можно ассемблировать каждую под-
программу Ассемблера отдельно.
TASM2 #2-5/Док = 246 =
Директива ARG
-----------------------------------------------------------------
Хотя можно обращаться к параметрам через регистр BP, Турбо
Ассемблер предусматривает альтернативу вычислению смещений в сте-
ке и выполнению текстовых присваиваний. Это директива ARG. При
использовании ее в процедуре директива ARG автоматически опреде-
ляет смещения параметров относительно регистра BP. Она вычисляет
также размер блока параметров и использует его в инструкции RET.
Поскольку идентификаторы, создаваемые по директиве ARG, определе-
ны только в соответствующей процедуре, в каждой процедуре или
функции вам не требуется использовать уникальные имена парамет-
ров.
Покажем, как будет выглядеть пример предыдущего раздела,
если переписать его, используя директиву ARG:
CODE SEGMENT
ASSUME CS:CODE
MyProc PROC FAR ; procedure MyProc(i,j : integer);
; external;
PUBLIC MyProc
ARG j : WORD, i : WORD = RetBytes
push bp ; нужно сохранить BP вызывающей
; программы
mov bp,sp ; BP теперь указывает на вершину
; стека
mov ax,i ; адресуемся к i через BP
.
.
.
Директива ARG Турбо Ассемблера создает локальные идентифика-
торы для параметров i и j. На время выполнения процедуры строка:
ARG j : WORD, i : WORD = RetBytes
автоматически приравнивает идентификатор i к [WORD PTR BP+6],
идентификатор j к [WORD PTR BP+8], а идентификатор RetBytes - к
числу 4 (размеру в байтах блока параметров). В значениях учитыва-
ется и занесенное в стек значение BP, и размер адреса возврата:
если бы процедура MyProc имела ближний тип, то i было бы прирав-
нено к значению [BP+4], j - к [BP+6], а RetBytes также было бы
равно 4 (в любом случае процедура MyProc может завершить выполне-
ние с помощью инструкции RET RetBytes).
TASM2 #2-5/Док = 247 =
При использовании директивы ARG нужно помнить, что параметры
должны перечисляться в обратном порядке. Последний параметр про-
цедуры или функции Турбо Паскаля нужно размещать в директиве ARG
первым и наоборот.
Относительно использования директивы ARG с Турбо Паскалем
можно сделать еще одно замечание. В отличие от других языков,
Турбо Паскаль всегда заносит в стек параметр-значение размером в
байт, как 16-битовое слово. При этом сообщить Турбо Ассемблеру о
дополнительном байте должны вы. Предположим, например, что вы на-
писали функцию, описание которой в Паскале выглядит следующим об-
разом:
function MyProc(i, j : char) : string; external;
Директива ARG для этой функции должна была бы выглядеть так:
ARG j:BYTE: 2, i:BYTE: 2 = RetBytes RETURN result:DWORD
Здесь : 2 после каждого аргумента необходимо указывать для
того, чтобы сообщить Ассемблеру, что каждый идентификатор зано-
сится в стек, как массив из 2 байт (где, в данном случае, младший
байт каждой пары содержит полезную информацию).
В функции, возвращающей строковое значение (как данная функ-
ция), параметр RETURNS в директиве ARG позволяет вам определить
переменную, приравненную к тому месту в стеке, которое указывает
на временный результат функции. Переменная в RETURNS на размер (в
байтах) блока параметров. См. Главу 3 "Справочного руководства",
где о директиве ARG рассказывается более подробно.
TASM2 #2-5/Док = 248 =
Турбо Паскаль и директива .MODEL
-----------------------------------------------------------------
Директива .MODEL с параметром TPASCAL задает упрощенную сег-
ментацию, модель памяти и языковую поддержку. Ранее мы уже виде-
ли, что нужно сделать в программах Ассемблера, чтобы можно было
использовать процедуры и функции Паскаля. Преобразуем пример, ис-
пользуя в нем директивы .MODEL и PROC:
.MODEL TPASCAL
.CODE
MyProc PROC FAR i:BYTE,j:BYTE result:DWORD
PUBLIC MyProc
mov ax,i
.
.
.
ret
Заметим, что теперь не нужно задавать параметры в обратном
порядке. Не требуется также масса других операторов. Использова-
ние в директиве .MODEL ключевого слова TPASCAL задает использова-
ние соглашений Паскаля, определяет имена сегментов, выполняет
инструкции PUSH BP и MOV BP,SP и задает также возврат с помощью
инструкций POP BP и RET N (где N - число байт параметров).
Использование другого базового или индексного регистра
-----------------------------------------------------------------
Второй способ доступа к параметрам состоит в использовании
для получения этих параметров другого базового или индексного ре-
гистра (BX, SI или DI). Нужно однако помнить, что по умолчанию
сегментным регистром для них является регистр DS, а не SS. Поэ-
тому для их использования вам придется применять префикс переоп-
ределения сегмента.
Приведем пример использования для получения параметров ре-
гистра BX:
CODE SEGMENT
ASSUME CS:CODE
MyProc PROC FAR ; procedure MyProc(i,j : integer);
PUBLIC MyProc
j EQU WORD PTR SS:[BX+4] ; j находится над сохраненным
; BP и адресом возврата
TASM2 #2-5/Док = 249 =
i EQU WORD PTR SS:[bp+8] ; i располагается над j
mov bx,sp ; BX теперь указывает на вершину
; стека
mov ax,i ; адресуемся к i через BX
.
.
.
В тех программах, где нет большого числа ссылок на парамет-
ры, такой метод позволяет сэкономить время и место. Почему? Пото-
му, что в отличие от BP, регистр BX не требуется восстанавливать
в конце программы.
Результаты функции в Турбо Паскале
-----------------------------------------------------------------
В зависимости от типа результата функции Турбо Паскаля возв-
ращают свои результаты различными способами.
Результаты функции скалярного типа
Результаты функции скалярных типов возвращаются в регистрах
центрального процессора (ЦП). Байтовые значения возвращаются в
регистре AL, значения размером в 2 байта - в регистре AX,
4-байтовые значения - в паре регистров DX:AX (старшее слово нахо-
дится в регистре DX).
Результаты функции вещественного типа
Результаты используемого в Турбо Паскале 6-байтового прог-
раммно эмулируемого вещественного типа возвращаются в трех ре-
гистрах ЦП. Наиболее значащее (старшее) слово возвращается в DX,
среднее - в BX, а наименее значащее - в AX.
Результаты функции типов сопроцессора 8087
Результаты типов, использующихся сопроцессором 8087, возвра-
щаются в регистре вершины стека ST(0) (или просто ST).
Результаты функции строкового типа
Результаты строкового типа возвращаются во временной рабочей
области, выделяемой Турбо Паскалем перед вызовом. Указатель даль-
него типа на эту область заносится в стек перед занесением перво-
го параметра. Заметим, что этот указатель не является частью
TASM2 #2-5/Док = 250 =
списка параметров.
Примечание: Не удаляйте из стека полученный в резуль-
тате указатель, так как Турбо Паскаль ожидает, что после
вызова он будет доступен.
Результаты функции типа указатель
Результаты указатель возвращаются в паре регистров DX:AX
(сегмент:смещение).
Выделение пространства для локальных данных
-----------------------------------------------------------------
Ваши программы, написанные на Турбо Ассемблере, могут выде-
лять пространство для своих собственных переменных, как постоян-
ных (статических), то есть сохраняющихся в промежутке между вызо-
вами, так и для временных (которые после вызова будут потеряны).
Оба этих случая обсуждаются в следующих разделах.
Выделение общедоступной статической памяти
-----------------------------------------------------------------
Турбо Паскаль позволяет в программах Турбо Ассемблера резер-
вировать пространство для статических переменных в сегментах гло-
бальных данных (DATA или DSEG). Чтобы выделить это пространство,
можно просто использовать такие директивы, как DB, DW и т.д. Нап-
ример:
DATA SEGMENT PUBLIc
MyInt DW ? ; зарезервировать слово
MyByte DB ? ; зарезервировать байт
.
.
.
DATA ENDS
Переменных, выделяемых Турбо Ассемблером в сегменте глобаль-
ных данных, касаются два важных ограничения. Во-первых, эти пере-
менными являются "местными", они недоступны программе Турбо Пас-
каля (хотя вы можете передавать указатели на них). Во-вторых, они
не могут быть предварительно инициализированы, как типизованные
константы. Оператор:
TASM2 #2-5/Док = 251 =
MyInt DW 42 ; это не инициализирует
; MyInt значением 42
не вызовет ошибки при компоновке модуля с программой Турбо Паска-
ля, однако MyInt при выполнении программы не будет иметь значение
42.
Эти ограничения можно обойти, описав переменные или типизо-
ванные константы Турбо Паскаля с помощью директивы EXTRN, что
сделает их доступными Турбо Ассемблеру.
TASM2 #2-5/Док = 252 =
Выделение временной памяти
-----------------------------------------------------------------
В ваших программах на Турбо Паскале можно выделять также
временную память (локальные переменные) в стеке на время выполне-
ния каждого вызова. Перед возвратом управления эта память должна
быть освобождена, а значение регистра BP восстановлено. В следую-
щем примере процедура MyProc резервирует пространство для двух
целых переменных a и b:
CODE SEGMENT
ASSUME CS:CODE ; procedure MyProc(i : integer);
MyProc PROC FAR
PUBLIC MyProc
LOCAL a : WORD, b : WORD = LocalSpace ; a в [bp-2]
; b - в [bp-4]
i equ word ptr [bp+6] ; параметр i находится над
; сохраненным BP и адресом
; возврата
push bp ; нужно сохранить BP вызывающей
; программы
mov bp,sp ; теперь BP указывает на
; вершину стека
sub sp,LocalSpace ; зарезервировать пространст-
; во для двух слов
mov ax,42 ; загрузить в AX начальное
; значение A
mov a,ax ; и в A
xor ax,ax ; очистить регистр AX
mov b,ax ; инициализировать B нулем
mov b,ax ; выполнить нужные действия
.
.
.
mov sp,bp ; восстановить исходное
; значение SP
mov bp ; восстановить исходное
; значение регистра BP
ret 2
MyProc ENDP
CODE ENDS
END
Заметим, что директива Турбо Ассемблера LOCAL используется
для создания идентификаторов и выделения пространства для локаль-
ных переменных. Оператор:
TASM2 #2-5/Док = 253 =
LOCAL a : WORD, b : WORD = LocalSpace
на время выполнения процедуры присваивает идентификатору a значе-
ние [BP-2], идентификатору b - значение [BP-4], а идентификатору
LocalSpace - число 4 (размер области локальных переменных). Пос-
кольку нет соответствующего оператора для создания идентификато-
ров, ссылающихся на параметры, вы должны использовать присваива-
ние i значения [BP+6].
Более разумный способ инициализации локальных переменных
заключается в том, чтобы вместо уменьшения SP занести в стек их
значения. Таким образом, вы должны заменить SUB SP,LocalSpace
инструкциями:
mov ax,42 ; получить начальное значение
; для a
push ax ; занести его в a
xor ax,ax ; обнулить AX
push ax ; и занести 0 в b
Если вы используете этот способ, нужно внимательно отслежи-
вать стек! Не следует ссылаться на идентификаторы a и b перед
тем, как они занесены в стек.
Другой вид оптимизации предусматривает использование инст-
рукции PUSH CONST для инициализации локальных переменных (ее мож-
но использовать при наличии процессором 80186, 80286 и 80386),
или сохранение BP в регистре вместо занесения его в стек (если
есть неиспользованные регистры).
Примеры подпрограмм на Ассемблере для Турбо Паскаля
-----------------------------------------------------------------
В данном разделе вы дадим некоторые примеры подпрограмм на
языке Ассемблера, которые вы можете вызывать из программ Турбо
Паскаля.
Подпрограмма шестнадцатиричного преобразования общего назначения
-----------------------------------------------------------------
Содержащиеся в параметре num байты преобразуются в строку
шестнадцатиричных цифр длины (byteCount * 2). Поскольку каждый
байт порождает два символа, максимальное значение byteCount равно
TASM2 #2-5/Док = 254 =
127 (не проверяется). Для преобразования каждой группы (по 4 би-
та) в шестнадцатиричную цифру мы для скорости используем последо-
вательность add-daa-adc-daa.
Процедура HexStr написана так, что вызываться она должна с
помощью вызова дальнего типа. Это означает, что ее следует описы-
вать в интерфейсной части модуля Турбо Паскаля или с помощью ди-
рективы компилятора {$F+}.
CODE SEGMENT
ASSUME cs:CODE,ds:NOTHING
; Параметры (+2 с учетом push bp)
byteCount equ byte ptr ss:[bp+6]
num equ dword ptr ss:[bp+8]
; Адресация к результату функции (+2 с учетом push bp)
resultPtr equ dword ptr ss:[bp+12]
HexStr PROC FAR
PUBLIC HexStr
push bp
mov bp,sp ; получить указатель
; стека
les di,resultPtr ; получить адрес
; результата функции
mov dx,ds ; сохранить DS Турбо
; Паскаля в DX
lds si,sum ; получить адрес числа
mov al,byteCount ; сколько байт?
xor ah,ah ; слово
mov cx,ax ; отслеживать число
; байт в CX
add si,ax ; начать со старшего
; байта числа
dec si
shl ax,1 ; сколько цифр?
; (2/байт)
cld ; сохранить число цифр
; (работать в прямом
; направлении)
stosb ; в приемнике - байт
; длины строки
TASM2 #2-5/Док = 255 =
NextLoop:
std ; сканировать число от
; старшего байта к
; младшему
lodsb ; получить следующий
; байт
mov ah,al ; сохранить его
shr al,1 ; выделить старшую
; группу бит
shr al,1
shr al,1
shr al,1
add al,90h ; специальная после-
; довательность шестнадца-
; тиричного преобразования
daa ; использование инструкций
; ADD и DAA
adc al,40h
daa ; группа преобразована
; в код ASCII
cld ; сохраним ASCII и следуем
; далее
stosb
mov al,ah ; повторить преобразование
; для младшей группы
and al,0Fh
add al,90h
daa
adc al,40h
daa
stosb
loop HexLoop ; продолжать, пока не
; будет выполнено
mov ds,dx
pop bp
ret 6 ; параметры занимают
; 6 байт
HexStr ENDP
CODE ENDS
END
Пример программы на Паскале, где используется функция
HexStr, имеет следующий вид:
Program HexTest;
var
TASM2 #2-5/Док = 256 =
num : word;
{$F+}
function HexStr (var num; byteCount : byte) : string; external;
{$L HEXSTR.OBJ}
{$F-}
begin
num := word;
Writeln('Преобразованная строка имеет шестнадцатиричное
представление: ', HexStr(num,Sizeof(num)),'*');
end.
Для построения и запуска примеров программы на Паскале и
программы Ассемблера используйте следующие команды командного
файла:
TASM HEXSTR
TPC HEXTEST
HEXTEST
Если вы используете директиву .MODEL, то программу HexStr
можно записать следующим образом:
.MODEL TPASCAL
.CODE
HexStr PROC FAR num:DWORD,byteCount:BYTE RETURNS resultPtr:DWORD
PUBLIC HexStr
les di,resultPtr ; получить адрес
; результата функции
mov dx,ds ; сохранить DS Турбо
; Паскаля в DX
lds si,sum ; получить адрес числа
mov al,byteCount ; сколько байт?
xor ah,ah ; слово
mov cx,ax ; отслеживать число
; байт в CX
add si,ax ; начать со старшего
; байта числа
dec si
shl ax,1 ; сколько цифр?
; (2/байт)
cld ; сохранить число цифр
; (работать в прямом
; направлении)
TASM2 #2-5/Док = 257 =
stosb ; в приемнике - байт
; длины строки
NextLoop:
std ; сканировать число от
; старшего байта к
; младшему
lodsb ; получить следующий
; байт
mov ah,al ; сохранить его
shr al,1 ; выделить старшую
; группу бит
shr al,1
shr al,1
shr al,1
add al,90h ; специальная после-
; довательность шестнадца-
; тиричного преобразования
daa ; использование инструкций
; ADD и DAA
adc al,40h
daa ; группа преобразована
; в код ASCII
cld ; сохраним ASCII и следуем
; далее
stosb
mov al,ah ; повторить преобразование
; для младшей группы
and al,0Fh
add al,90h
daa
adc al,40h
daa
stosb
loop HexLoop ; продолжать, пока не
; будет выполнено
mov ds,dx ; восстановить DS
; Турбо Паскаля
ret
HexStr ENDP
CODE ENDS
END
При этом вы можете использовать ту же программу на Паскале и
просто ассемблировать альтернативный вариант HexStr и перекомпи-
лировать программу с помощью того же командного файла.
TASM2 #2-5/Док = 258 =
Пример обмена содержимого двух переменных
-----------------------------------------------------------------
С помощью данной процедуры вы можете выполнить обмен содер-
жимого двух переменных размера count. Если count имеет значение
0, то то процессор попытается перекопировать 64К.
CODE SEGMENT
ASSUME cs:CODE,ds:NOTHING
; Параметры (заметим, что из-за push bp смещение
; увеличивается на 2)
var1 equ DWORD PTR ss:[bp+12]
var2 equ DWORD PTR ss:[bp+8]
count equ WORD PTR ss:[bp+6]
Exchange PROC FAR
PUBLIC Exchange
cld ; обмен в прямом направлении
mov dx,ds ; сохранить регистр DS
push bp
mov bp,sp ; получить базу стека
lds si,var1 ; получить первый адрес
les di,var2 ; получить второй адрес
mov cx,count ; получить число перемещаемых
; байт
shr cx,1 ; получить счетчик слов
; (младший бит -> перенос)
jnc ExchangeWord ; если не нечетный байт,
; войти в цикл
mov al,es:[di] ; считать нечетный байт
; из var2
movsb ; переместить байт из var1
; в var2
mov [si-1],al ; записать var2 в var1
jz Finis ; выполнено, если нужно
; выполнить обмен только
; одного байта
ExchangeWords:
mov bx,-2 ; BX - это удобное место
; для хранения -2
ExchangeLoop:
mov ax,es:[di] ; считать слово из var2
movsw ; переместить из var1
; в var2
TASM2 #2-5/Док = 259 =
mov [bx][si,ax ; записать слово var2 в
; var1
loop ExchangeLoop ; повторить count/2 раз
Finis:
mov ds,dx ; получить обратно DS
; Турбо Паскаля
pop bp
ret 10
Exchange ENDP
CODE ENDS
END
Программа Турбо Паскаля, которая использует функцию
Exchange, имеет вид:
program TextExchange;
type
EmployeeRecord = record
Name : string[30];
Address : string[30];
City : string[15];
State : string[2];
Zip : string[10];
end;
var
OldEmployee, NewEmployee : EmployeeRecord;
{$F+}
procedure Exchange(var var1,var2; count : word); external;
{$L XCHANGE.OBJ}
{$F-}
begin
with OldEmployee do
begin
Name := 'John Smith';
Address := ' 123 F Street';
City := 'Scotts Valley';
State := 'CA';
Zip := ' 90000-0000';
end;
with NewEmployee do
begin
Name := 'Mary Jones';
TASM2 #2-5/Док = 260 =
Address := ' 9471 41st Avenue';
City := 'New York';
State := 'NY';
Zip := ' 10000-1111';
end;
Writeln('Before: ',OldEmployee.Name,' ',NewEmployee.Name);
Exchange(OldEmployee,NewEmployee,sizeof(OldEmployee));
Writeln('After: ',OldEmployeeName,' ',NewEmployee.Name);
Exchange(OldEmployee,NewEmployee,sizeof(OldEmployee));
Writeln('After: ',OldEmployeeName,' ',NewEmployee.Name);
end.
Чтобы сформировать и запустить данные программы на Паскале и
Ассемблере, используйте следующие команды командного файла:
TASM XCHANGE
TPC XCHANGE
XCHANGE
Если использовать директиву .MODEL, то программа Exchange на
Ассемблере будет выглядеть следующим образом:
.MODEL TPASCAL
.CODE
Exchange PROC FAR var1:DWORD,var2:DWORD,count:WORD
PUBLIC Exchange
cld ; обмен в прямом направлении
mov dx,ds ; сохранить DS
push bp
mov bp,sp ; получить базу стека
lds si,var1 ; получить первый адрес
les di,var2 ; получить второй адрес
mov cx,count ; получить число перемещаемых
; байт
shr cx,1 ; получить счетчик слов
; (младший бит -> перенос)
jnc ExchangeWord ; если не нечетный байт,
; войти в цикл
mov al,es:[di] ; считать нечетный байт
; из var2
movsb ; переместить байт из var1
; в var2
mov [si-1],al ; записать var2 в var1
jz Finis ; выполнено, если нужно
; выполнить обмен только
; одного байта
TASM2 #2-5/Док = 261 =
ExchangeWords:
mov bx,-2 ; BX - это удобное место
; для хранения -2
ExchangeLoop:
mov ax,es:[di] ; считать слово из var2
movsw ; переместить из var1
; в var2
mov [bx][si,ax ; записать слово var2 в
; var1
loop ExchangeLoop ; повторить count/2 раз
Finis:
mov ds,dx ; получить обратно DS
; Турбо Паскаля
ret
Exchage ENDP
CODE ENDS
END
Вы можете использовать ту же программу на Паскале и просто
ассемблировать альтернативный вариант процедуры Exchаnge и пере-
компилировать программу с помощью того же командного файла.
TASM2 #2-5/Док = 262 =
Пример анализа операционной среды DOS
-----------------------------------------------------------------
С помощью функции EnvString вы сможете просмотреть операци-
онную среду DOS и найти строку вида "s=НЕЧТО" и возвратить НЕЧТО,
если это найдено.
DATA SEGMENT PUBLIC
EXTRN prefixSeg : Word ; дает адрес PSP
DATA ENDS
SEGMENT PUBLIC
ASSUME cs:CODE,ds:DATA
EnvString PROC FAR
PUBLIC EnvString
push bp
cld ; работать в прямом
; направлении
mov es,[prefixSeg] ; посмотреть PSP
mov es,es:[2Ch] ; ES:DI указывают на
; операционную среду,
xor di,di ; которая выровнена на
; границу параграфа
mov bp,sp ; найти строку параметров,
lds si,ss:[bp+6] ; которая следует за
; адресом возврата
ASSUME ds:NOTHING
lodsb ; посмотреть длину
or al,al ; она равна 0?
jz RetNul ; да, возврат
mov ah,al ; в противном случае
; сохранить ее в AH
mov dx,si ; DS:SI содержат указатель
; на первый параметр
; char
xor al,al ; сделать его равным 0
Compare:
mov ch,al ; мы хотим, чтобы для
; следующего отсчета ch=0
mov si,dx ; возвратить указатель на
; просмотренную строку
mov cl,ah ; получить длину
mov si,dx ; возвратить указатель на
; строку
repe cmpsb ; сравнить байты
jne Skip ; если сравнение неудач-
TASM2 #2-5/Док = 263 =
; ное попробовать следу-
; ющую строку
cmp byte ptr es:[di],'=' ; сравнение
; завершилось успешно
; следующий символ '='?
jne NoEqual ; если нет, все еще нет
; совпадения
Found:
mov ax,es ; DI:SI будет указывать
; на найденную нами строку
mov ds,ax
mov si,di
inc si ; "пройти" символ '='
les bx,ss:[bp+10] ; получить адрес
; результата
; функции
mov di,bx ; занести его в ES:DI
inc di ; байт длины
mov cl,255 ; задать максимальную
; длину
CopyLoop:
lodsb ; получить байт
or al,al ; проверить на 0
jz Done ; если 0, выполнено
stosb ; занести его в результат
loop CopyLoop ; переместить до 255
; байт
Done: not cl ; при сохранении мы
; уменьшали от CL до 255
mov es:[bx],cl ; сохранить длину
mov ax,SEG DATE
mov ds,ax ; восстановить DS
ASSUME ds:DATA
pop bp
ret 4
ASSUME ds:NOTHING
Skip:
dec di ; проверить на 0
NoEqual:
mov cx,7FFFh ; длинный поиск, если
; нужно
sub cx,di ; операционная среда
; никогда не превышает
; 32К
jbe RetNul ; если конец, выйти
repne scasb ; посмотреть следующий
TASM2 #2-5/Док = 264 =
; 0
jcxz RetNul ; выйти, если не найден
cmp byte ptr es:[di],al ; второй 0 в строке?
jne Compare ; если нет, попытаться
; снова
RetNul:
les di,ss:[bp+10] ; получить адрес
; результата
stosb ; сохранить там 0
mov ax,SEG DATA
mov ds,ax ; восстановить DS
ASSUME ds:DATA
pop bp
ret 4
EnvString ENDP
CODE ENDS
END
Программа на Паскале, которая использует функцию EnvString,
выглядит следующим образом:
program EnvTest;
{ программа ищет строки операционной среды }
var
EnvVariable : string;
EnvValue : string;
{$F+}
function EnvString(s:string) : string; external;
{$L ENVSTRING.OBJ}
{$F-}
begin
EnvVariable := 'PROMPT';
EnvValue := EnvString(EnvVariable);
if EnvValue = '' then EnvValue := '*** не найдена ***';
Writeln('Переменная операционной среды: ',
EnvVariable,' Значение: ',EnvValue);
end.
Чтобы сформировать и запустить данные программы на Паскале и
Ассемблере, используйте следующие команды командного файла:
TASM ENVSTR
TPC ENVTEST
TASM2 #2-5/Док = 265 =
ENVTEST
Если использовать директиву .MODEL, то функцию EnvString на
Ассемблере будет выглядеть следующим образом:
.MODEL TPASCAL
.DATA
EXTRN prefixSeg : Word ; дает адрес PSP
.CODE
EnvString PROC FAR EnvVar:DWORD RETURNS EnvVal:DWORD
PUBLIC EnvString
push bp
cld ; работать в прямом
; направлении
mov es,[prefixSeg] ; посмотреть PSP
mov es,es:[2Ch] ; ES:DI указывают на
; операционную среду,
xor di,di ; которая выровнена на
; границу параграфа
mov bp,sp ; найти строку параметров,
lds si,ss:[bp+6] ; которая следует за
; адресом возврата
ASSUME ds:NOTHING
lodsb ; посмотреть длину
or al,al ; она равна 0?
jz RetNul ; да, возврат
mov ah,al ; в противном случае
; сохранить ее в AH
mov dx,si ; DS:SI содержат указатель
; на первый параметр
; char
xor al,al ; сделать его равным 0
Compare:
mov ch,al ; мы хотим, чтобы для
; следующего отсчета ch=0
mov si,dx ; возвратить указатель на
; просмотренную строку
mov cl,ah ; получить длину
mov si,dx ; возвратить указатель на
; строку
repe cmpsb ; сравнить байты
jne Skip ; если сравнение неудач-
; ное, попробовать следу-
; ющую строку
cmp byte ptr es:[di],'=' ; сравнение
; завершилось успешно
TASM2 #2-5/Док = 266 =
; следующий символ '='?
jne NoEqual ; если нет, все еще нет
; совпадения
Found:
mov ax,es ; DI:SI будет указывать
; на найденную нами строку
mov ds,ax
mov si,di
inc si ; "пройти" символ '='
les bx,ss:[bp+10] ; получить адрес
; результата функции
mov di,bx ; занести его в ES:DI
inc di ; байт длины
mov cl,255 ; задать максимальную
; длину
CopyLoop:
lodsb ; получить байт
or al,al ; проверить на 0
jz Done ; если 0, выполнено
stosb ; занести его в результат
loop CopyLoop ; переместить до 255
; байт
Done: not cl ; при сохранении мы
; уменьшали от CL до 255
mov es:[bx],cl ; сохранить длину
mov ax,SEG DATE
mov ds,ax ; восстановить DS
ASSUME ds:DATA
pop bp
ret 4
ASSUME ds:NOTHING
Skip:
dec di ; проверять на 0
NoEqual:
mov cx,7FFFh ; длинный поиск, если
; нужно
sub cx,di ; операционная среда
; никогда не превышает
; 32К
jbe RetNul ; если конец, выйти
repne scasb ; посмотреть следующий
; 0
jcxz RetNul ; выйти, если не найден
cmp byte ptr es:[di],al ; второй 0 в строке?
jne Compare ; если нет, попытаться
; снова
TASM2 #2-5/Док = 267 =
RetNul:
les di,ss:[bp+10] ; получить адрес
; результата
stosb ; сохранить там 0
mov ax,SEG DATA
mov ds,ax ; восстановить DS
ASSUME ds:DATA
ret 4
EnvString ENDP
CODE ENDS
END
Вы можете использовать ту же программу на Паскале и просто
ассемблировать альтернативный вариант функции EnvString и пере-
компилировать программу с помощью того же командного файла.
http://antibotan.com/ - Всеукраїнський студентський арх?в