Глава 6. Дополнительные сведения о программировании на Turbo Assembler.


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

В этой главе будут пройдены следующие вопросы:

— Директивы Turbo Assembler EQU и =, позволяющие присваивать
значениям и текстовым строкам имена.

— Мощные команды обработки строк Turbo Assembler.

— Средство Turbo Assembler, позволяющее раздельно ассемблиро-
вать несколько исходных файлов и затем при помощи TLINK ком-
поновать их в единую программу.

— Средство Turbo Assembler, позволяющее включать отдельные
файлы с исходным текстом программы в любую другую ассемблер-
ную программу.

— Усложненные файлы листинга исходных текстов Turbo Assembler.

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

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

Использование подстановок равенства
——————————————————————

Глава начнется с описания директив 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 [StorageBuffer+di],al
;запись следующей нажатой клавиши
cmp al,END_OF_DATA ;нажата ли клавиша конца данных?
je DataAcquired ;да, переход к обработке данных
inc di ;счетчик нажатых клавиш
cmp di,STORAGE_BUFFER_SIZE
;буфер переполнен?
jb StorageLoop ;нет, переход к следующей клавише
;буфер переполнен…
.
.
.
;данные приняты
DataAcquired:
.
.
.

В приведенном примере EQU использовалась для определения двух
меток: STORAGE_BUFFER_SIZE и END_OF_DATA. Метке END_OF_ DATA
присвоен символ «!», и происходит сравнение ее с каждой нажимаемой
клавишей для того, чтобы определить конец данных. Большое
преимущество использования приравнивания иллюстрируется следующим
примером: метки гораздо более информативны, нежели константы.
Назначение команды

cmp al,END_OF_DATA

гораздо яснее, чем при записи

cmp al,’!’

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]
.
.
.

Приравненные метки удобны для того, чтобы преобразовать
огромное множество кодов прерываний, портов и ячеек памяти PC в
легко читаемые имена. Ниже приводятся некоторые примеры
использования EQU в таких целях:

.
.
.
DOS_INT EQU 21h ;прерывание для выполнения
;функции DOS
CGA_STATUS EQU 3dah ;порт состояния CGA
VSYNC_MASC EQU 00001000b ;изолирует бит порта состояния
;CGA, сообщающий, можете ли вы
;обновить экран без «снега»
BIOS_SEGMENT EQU 40h ;сегмент хранения данных BIOS
EQUIPMENT_FLAG EQU 10h ;смещение в сегменте BIOS
;переменной флага оборудования
.
.
.
mov ah,2
mov dl,’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 EQU (1000h-10)

и реассемблировать программу, после чего INDEX_START и DICT_
START будут выравнены одинаково с TABLE_OFFSET, поскольку их
значения базируются на TABLE_OFFSET.

Между прочим, круглые скобки вокруг операнда директивы EQU не
требуются, но и не помешают, поскольку визуально отделяют операнд.

EQU может устанавливать метку равной не только числу, но и
текстовой строке символов. Например, ниже приравненная метка будет
хранить печатаемую текстовую строку:

.
.
.
EQUATED_STRING EQU ‘В директиве 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 Temp,ax ;записать I+J
mov ax,K ;прием K
add ax,Temp ;вычислить I+J+K
mov sp,bp ;перераспределить область для Temp
pop bp ;восстановить BP вызывающей программы
ret
_AddThree ENDP

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

Для того, чтобы EQU рассматривала операнд не как выражение, а
как текстовую строку, можно использовать угловые скобки (< и >).
Например,

TABLE_OFFSET EQU 1
INDEX_START EQU

назначит на метку 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
.
.
.

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

Предопределенное символическое имя $
————————————

Вспомните, что Turbo Assembler имеет несколько
предопределенных символических имен, например @data. Другой
простое, но удивительно полезное предопределенное символическое
имя это $, в котором всегда находится текущее значение счетчика
памяти; другими словами, $ всегда эквивалентно текущему смещению в
сегменте, ассемблируемом на этот момент Turbo Assembler. $
представляет значение смещения типа константа, как и OFFSET
переменная_памяти.

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

Особенно имя $ удобно использовать для вычисления длины
данных и кода. Например, вы хотите приравнять символическое имя
STRING_LENGTH к длине в байтах строки символов. Без $ это делается
так:

.
.
.
StringStart LABEL BYTE
db 0dh,0ah,’Hello, world’,0dh,0ah
StringEnd LABEL BYTE
STRING_LENGTH EQU (StringEnd-StringStart)
.
.
.

а в случае $ достаточно написать:

.
.
.
StringStart LABEL BYTE
db 0dh,0ah,’Hello, world’,0dh,0ah
STRING_LENGTH EQU ($-StringStart)
.
.
.

А вот так вычисляется длина в словах массива слов:

.
.
.
WordArray DW 90h, 25h, 0, 16h, 23h
WORD_ARRAY_LENGTH EQU (($-WordArray)/2)
.
.
.

Конечно, несколько элементов можно подсчитать и вручную, но
чем длиннее массивы и строки, тем это утомительнее.

Существует еще несколько полезных предопределенных
переменных: ??date, ??time и ??filename. ??date содержит дату
ассемблирования в виде взятой в кавычки строки, имеющей форму:
01/02/87. ??time содержит время ассемблирования в виде 13:45:06, а
??filename содержит имя ассемблируемого файла в виде взятой в
кавычки текстовой строки длиной 8 символов, например «TEST.ASM».

Директива = —————————————————

Директива = подобна EQU, за исключением одного. Если метки,
определенные с помощью EQU, переопределены быть не могут (в этом
случае выдается сообщение об ошибке), то метки, определенные
директивой =, свободно можно переопределить. Это очень полезно для
работы с метками, которые требуется изменять прямо по ходу дела
или повторно используются в пределах одного и того же исходного
модуля.

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

.
.
.
.DATA
MultiplesOf10 LABEL WORD
TEMP = 0
REPT 100
DW TEMP
TEMP = TEMP+10
ENDM
.
.
.
shl bx,1 ;BX равен # умноженному на 10.
;Сдвиг влево умножает на 2
;для просмотра в таблице
;размером в слово
mov ax,[MultiplesOf10+bx] ;прием числа * 10
.
.
.

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

Строковые команды
——————————————————————

Теперь мы подошли к наиболее необычным и мощным командам
8086, строковым командам. Строковые команды отличны от прочих
команд 8086 в том, что для них в одной и той же команды происходит
как доступ к памяти, так и инкремент или декремент регистра
указателя. Одна строковая команда может обратиться к памяти до
130,000 раз!

Из их названия понятно, что строковые команды применяются для
манипулирования текстовыми строками. Строковые команды равно
приспособлены для работы с массивами, буферами данных и любого
рода строками байтов или слов. Следует всегда, когда это возможно,
стараться использовать строковые команды, поскольку как правило,
они всегда короче и быстрее, чем эквивалентные комбинации обычных
команд 8086 типа MOV, INC и LOOP.

Мы станем рассматривать строковые команды по двум
функциональным группам: строковые команды, используемые для
пересылки данных (LODS, STOS и MOVS) и строковые команды,
используемые для сканирования и сравнения данных (SCAS и CMPS).

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

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

LODS
—-

Команда LODS, при помощи которой из памяти в сумматор
загружается байт или слово, имеет две модификации, LODSB и LODSW.
LODSB загружает в AL байт, адресуемый как DS:SI и либо
инкрементирует, либо декрементирует SI, в зависимости от состояния
флага направления. Если флаг направления равен 0 (устанавливается
при помощи CLD), то SI инкрементируется, а если равен 1
(устанавливается STD), то SI декрементируется. Такой способ
приращений указателя справедлив не только для LODSB; флаг
направления управляет направлением изменения указателей регистров
для всех строковых команд.

Например, в следующем фрагменте LODSB

.
.
.
cld
mov si,0
lodsb
.
.
.

загружает в AL содержимое байта со смещением 0 в сегменте
данных и инкрементирует SI на 1. Это эквивалентно фрагменту:

.
.
.
mov si,0
mov al,[si]
inc si
.
.
.

Однако

lodsb

сравнительно быстрее (и на 2 байта короче), чем

mov al,[si]
inc si

Команда LODSW аналогична LODSB, за исключением того, что
слово, адресуемое DS:SI, загружается в AX, а SI инкрементируется
или декрементируется не на 1, а на 2. Например,

.
.
.
std
mov si,10
lodsw
.
.
.

загрузит в AX слово из сегмента данных со смещением 10 и
затем декрементирует SI на 2, сделав его равным 8.

STOS
—-

Команда STOS служит дополнением к команде LODS и
предназначена для записи байта или слова из сумматора в ячейку
памяти, на которую указывает ES:DI, после чего она инкрементирует
или инкрементирует DI. STOSB записывает байт из AL в ячейку памяти
ES:DS, а затем инкрементирует или декрементирует DI, в зависимости
от флага направления. Например,

.
.
.
std
mov di,0ffffh
mov al,55h
stosw
.
.
.

запишет значение 55h в байт со смещением 0FFFFh в сегменте,
на который указывает ES, а затем декрементирует DI до значения
0FFFEh.

STOSW работает аналогичным образом, записывая слово из AX по
адресу ES:DI, а затем инкрементируя или декрементируя его на 2.
Например,

.
.
.
cld
mov di,0ffeh
mov ax,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 ;был ли символ нулем, заканчивающим
;строку?
jnz CopyStringLoop
;нет, переход к следующему символу
ret ;да, работа окончена
CopyString ENDP

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

.
.
.
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:
lodsw
stosw
loop CopyLoop
.
.
.

Однако существует и лучший способ пересылки байта или слова
из одного адреса памяти в другой; для этого служит команда MOVS.

MOVS
—-

Команда MOVS работает как совмещенные команды LODS и STOS.
MOVS считывает байт или слово, хранимое в DS:SI, и записывает это
значение по адресу ES:DI. При этом байт или слово вообще не
пересылается через регистр, поэтому AX не модифицируется. MOVSB
имеет малую длину, всего 1 байт, и выполняется даже быстрее,
нежели комбинация команд 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 не станет равным нулю.
(Если к моменту начала повторяющейся команды CX будет равен нулю ,
то она выполнится ноль раз — другими словами, она не сделает
ничего).

При помощи REP вы можете заменить

CopyLoop:
movsw
loop CopyLoop

в последнем примере на

rep movsw

Одна эта команда перешлет блок в 65,535 слов (0FFFFh) из
области памяти с начальным адресом в DS:SI в память с начальным
адресом в ES:DI.

Разумеется, повторение 65,535 раз одной и той же команды
занимает больше времени по сравнению с временем выполнения одной
команды; каждое обращение к памяти требует времени. Однако каждое
повторение такой повторяющейся строковой команды выполняется все
же быстрее, чем отдельная строковая команда, делая тем самым
данный метод для чтения, записи или копирования памяти
исключительно эффективным.

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 al,[bx]

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

mov al,[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:
scasb ;проверяет соответствие со-
;держимого памяти ES:DI со-
;держимому AL
je Fount_t ;да, символ ‘t’ найден
loop Scan_For_t_loop ;нет, сканирование следу-
;ющего символа
;’t’ не найден
.
.
.
;’t’ найден
Found_t:
dec di ;указатель передвигается
;назад, к смещению ‘t’
.
.
.

Отметим, что в данном примере после нахождения ‘t’ DI
декрементируется, чтобы компенсировать описанное выше нарушение
границ данных. Когда данная команда выполнит последнее, успешное
сканирование SCASB, регистр DI после сравнения будет
инкрементирован, поскольку последнее действие строковой команды
заключается в инкрементировании или декрементировании
соответствующего указателя (указателей). В результате DI указывает
на байт, расположенный после найденного символа ‘t’, и для
компенсации выхода за границы данного символа и возврата указателя
на ‘t’ следует вернуть указатель на один байт назад.

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

.
.
.
Scan_For_t_loop:
cmp es:[di],al ;проверка соответствие содержимого
;в ES:DS и AL
je Found_t ;да, ‘t’ найден
inc di
loop Scan_For_t_Loop ;нет, переход к сканированию
;следующего символа
.
.
.

Последний пример не повторяет в точности действия
предыдущего, поскольку команда SCASB сразу же сама инкрементирует
DI, а в последнем примере инкрементирование выполняется после
команды JE, чтобы избежать воздействия на состояние флагов,
устанавливаемых командой CMP.

Все это приводит к важному выводу относительно строковых
команд в целом. Строковые команды никогда не устанавливают флаги
так, чтобы те отражали изменения регистров SI, DI и/или CX. LODS,
STOS и MOS не меняют состояний каких-либо флагов, а команды SCAS и
CMPS изменяют состояния флагов только в соответствии с
результатами выполненного этими командами сравнения.

Также было бы удобно иметь возможность заменить цикл в
последнем примере одной командой, и как вы, наверное,
догадываетесь, префикс REP позволяет вам сдельть это. Однако вам,
возможно, понадобится выйти из цикла либо по условию нахождения
цели поиска, либо при ее отсутствии. В данном случае префикс REP
для использования с командой SCAS (а также CMPS) имеет две формы —
REPE и REPNE.

REPE (также может называться REPZ) говорит 8086 о том, что
необходимо повторять команду SCAS (или CMPS) либо пока CS не
станет равным нулю, либо пока не будет обнаружено условие
несоответствия заданной цели и элемента сканируемых данных.
Префикс REPE можно описать как «повторять, пока равно». Точно так
же, REPNE (может называться REPNZ) говорит 8086, что требуется
повторять команду SCAS (или CMPS) либо пока CS не станет равным
нулю, либо пока не будет обнаружено условие соответствия заданной
цели и элемента сканируемых данных. Префикс REPE можно описать как
«повторять, пока не равно».

Ниже приводится фрагмент, в котором используется одна
повторяющаяся команда SCASB, сканирующая TestString в поисках
символа t:

.
.
.
mov ax,@Data
mov es,ax
mov di,OFFSET TextString ;ES:DI указывает на начало
;TextString
mov al,’t’ ;искомый символ
mov cx,TEXT_STRING_LENGTH ;длина сканируемой строки
cld ;сканирование с инкремен-
;тированием DI
repne scasb ;сканирование всей строки
;в поисках хотя бы одного
;символа ‘t’
je Found_t ;да, ‘t’ найден
;’t’ не найден

;’t’ найден
Found_t:
dec di ;возврат указателя назад,
;к смещению ‘t’
.
.
.

Как и все строковые команды, SCAS инкрементирует свой регистр
указатель, DI, если флаг направления равен 0 (этот флаг очищается
командой CLD), и декрементирует DI, если флаг направления равен 1
(устанавливается STD).

SCASW это форма команды SCASB, работающая с размером в слово,
которая сравнивает AX с ES:DI и инкрементирует или декрементирует
DI в конце каждого выполнения не на единицу, а на два. Следующий
фрагмент при помощи 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 FoundNonZero
;Весь массив заполнен нулями.
.
.
.
;Найден ненулевой элемент — 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 элементов в двух массивах размером слово
при помощи REP 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
cld
repe cmpsw
jne ArraysAreDifferent
;Первые 50 элементов идентичны.
.
.
.
;Как минимум один элемент двух массивов различается.
ArraysAreDifferent:
dec si
dec si ;возврат в обоих массивах на элемент,
dec di ;которым они различаются
dec di
.
.
.

Использование операндов строковых команд ———————-

До сих пор мы рассматривали строковые команды с явным
объявлением размера данных — байт или слово; другими словами, мы
рассматривали LODSB и LODSW, а не LODS. Однако если вы зададите
операнды таким образом, что Turbo Assembler будет точно знать,
имеется ли в виду работа с байтами или со словами, то допускается
использование неявной формы этой команды.

Например, следующая форма является допустимой и эквивалентна
комадне 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],[String1]
.
.
.

Поскольку в качестве операндов команды MOVS вы задали String1
и String2, Turbo Assembler принимает размер данных, с которыми
будет работать эта команда, равным размеру данных указанных
операндов, в данном случае байт.

Однако с использованием операндов строковых команд связан
некоторый аспект. Операнды строковых команд фактически операндами
не являются, в том смысле, что они встроены в команду; строковая
команда просто использует текущее к моменту ее выполнения
содержимое SI и/или DI. Операнды служат только для установки
размера данных, а не для фактической загрузки указателей. Можно
смотреть на это так: при использовании команды вида

mov al,[String1]

смещение String1 встраивается прямо в соответствующую MOV
команду машинного языка. Однако при использовании

lods [String]

ассемблированная команда машинного языка для LODSB
представляет собой всего один байт; String1 в команду не
встраивается. В этом случае вы сами отвечаете за то, чтобы DS:SI
указывал на начало String1.

Операнды строковых команд в некотором смысле аналогичны
использованию директивы ASSUME для сегментов. ASSUME фактически не
выполняет установку сегментного регистра; эта директива сообщает
Turbo Assembler, как вы установили сегментный регистр, что просто
позволит Turbo Assembler контролировать возможные ошибки.
Аналогичным образом, операнды строковых команд не устанавливают
каких-либо регистров; они просто сообщают Turbo Assembler, как вы
установили SI и/или DI, что позволит Turbo Assembler определить
размер операнда и выполнить контроль ошибок. Далее операнды
строковых команд будут обсуждаться в разделе «Передача операнда
(операндов) строковым командам».

В разделе «Распространенные ошибки при использовании
строковых команд» мы рассмотрим некоторые моменты, которые
необходимо учитывать при работе со строковыми командами.

Многомодульные программы
——————————————————————

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

Создавать многомодульные программы на удивление легко. Turbo
Assembler имеет три директивы, поддерживающие создание подобных
программ: PUBLIC, EXTRN и GLOBAL. Мы изучим их ниже поочередно, но
сначала рассмотрим пример программы, состоящей из двух модулей,
что позволит вам понять контекст, в котором будут обсуждаться
директивы много модульного программирования. Вот главная
программа, MAIN.ASM:

.MODEL small
.STACK 200H
.DATA
String1 DB ‘Hello, ‘,0
String2 DB ‘world’,0dh,0ah,’$’,0
GLOBAL 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 ah,4ch
int 21h ;готово
END ProgramStart

Далее привидится второй модуль программы, SUB1.ASM:

.MODEL small
.DATA
GLOBAL finalString:BYTE
.CODE
;
;Подпрограмма копирует в FinalString сначала одну строку,
;а затем вторую.
;
;Вход:
; DS:AX = указатель первой копируемой строки
; DS:BX = указатель второй копируемой строки
;
;Выход: отсутствует
;
;Разрушаемые регистры: AL, SI, DI, ES
;
PUBLIC ConcatenateStrings
ConcatenateStrings PROC
cld
mov di,SEG FinalString
mov es,di
mov di,OFFSET FinalString
;ES:DI указывает на назначение
mov si,ax ;первая копируемая строка String1Loop:
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 ;готово
ConcatenateStrings ENDP
END

Эти два модуля можно ассемблировать по отдельности, задав:

TASM main

и

TASM sub1

а затем выполнить их компоновку в программу MAIN.EXE:

tlink main+sub1

При запуске программы командой

main

MAIN.EXE выведет на дисплей (как вы наверное, уже догадались)

Hello, world

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

Директива PUBLIC ———————————————-

Действие директивы PUBLIC достаточно просто: она собщает
Turbo Assembler, что связанная с данной директивой метка или
группа меток доступна для других модулей. 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 10000h
TEXT_SYMBOL EQU

Обычно Turbo Assembler при ассемблировании игнорирует,
заглавными или строчными буквами набрано имя, поэтому все общие
метки обычно преобразуются к верхнему регистру. Если же вы
желаете, чтобы заглавные и строчные буквы объявленных общими меток
рассматривались как разные, следует использовать для этого ключи
командной строки Turbo Assembler /ML или /MX для всех модулей,
содержащих или использующих общие метки.

Например, без /ML или /MX остальные модули не сделают
различия между следующими двумя метками:

PUBLIC Symbol1, SYMBOL1

При использовании для различения строчных и заглавных букв
общих и внешних символических имен ключа командной строки /MX вы
должны сами следить, чтобы в директивах PUBLIC и EXTRN эти имена
были набраны правильно. Turbo Assembler сделает доступными другим
модулям указанные символические имена именно в том виде, в котором
они набраны в директивах EXTRN или PUBLIC, а не в том, в котором
они определены или в котором на них сделаны ссылки в других частях
модуля. Например,

PUBLIC Abc
abC Dw

сделает общим имя Abc, а не abC.

Вы можете также задать в директиве PUBLIC язык для каждого
символического имя. Допустимыми языками являются C, PASCAL, BASIC,
FORTRAN, PROLOG и NOLANGUAGE. Тогда перед помещением данного имени
в объектный файл оно будет приведено в форму, приемлемую для
указанного языка. Например, если вы объявите

PUBLIC C myproc

то символическое имя myproc из исходного файла будут
переписано в объектный в виде _myproc, поскольку в соответствии с
соглашениями языка Си, каждле имя должно иметь ведущий символ
подчеркивания. Использование в директиве PUBLIC спецификатора
языка временно переопределяет текущую установку языка (по
умолчанию или выполненную ранее директивой .MODEL).

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

Директива EXTRN ————————————————

В последнем разделе мы сделали метки MemVar, Array1,
ArrayLength, NearProc и FarProc общими при помощи директивы
PUBLIC. Следующий вопрос состоит в том, «Как другие модули
ссылаются на эти метки?»

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

.
.
.
.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, заканчивается двоеточием и
типом. Тип необходим задавать потому, что Turbo Assembler не будет
знать, какого рода метка определяется директивой EXTRN, пока тип
не будет указан явно. За единственным исключением, внешние метки
могут иметь все те же типы, что и метки, объявляемые директивой
LABEL. Существуют следующие типы:

ABS Абсолютное значение

BYTE Переменная данных размером в байт

DATAPTR Ближний или дальний указатель данных, в зависимости
от текущей модели памяти

DWORD Переменная данных размером в двойное слово (4 байта)

FAR Дальняя метка программы (переход при загрузке CS:IP)

FWORD Переменная памяти 6 байт

NEAR Ближняя метка программы (переход с загрузкой
только IP)

PROC Метка процедуры, ближняя или дальняя, в зависимости
от директивы модели памяти .MODEL

QWORD Переманная данных размером в учетверенное слово
(8 байт)

Имя структуры Имя определяемого пользователем типа STRUC

TBYTE Переменная данных 10 байт

UNKNOWN Неизвестный тип

WORD Переменная данных размером в слово (2 байт)

Единственным незнакомым вам типом внешних данных является
ABS; этот тип служит для объявления метки, определенной в исходном
модуле директивами EQU или =; другими словами, метка, являющаяся
простым именем константы, и не связанная с меткой программы или
адресом данных.

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

.
.
.
.CODE
EXTRN FarProc:Near
.
.
.
call FarProc
.
.
.

при

.
.
.
PUBLIC FarProc
FarProc PROC FAR
.
.
.
ret
FarProc EMDP
.
.
.

в другом модуле, то Turbo Assembler сгенерирует ближний вызов
FarProc соответственно типу данных, заданному вами в EXTRN. Такая
программа, разумеется будет работать неверно, поскольку в
действительности FarProc является дальнек процедурой и
заканчивается дальней командой RET.

Как было описано в последнем разделе, Turbo Assembler обычно
не делает различия в именах между строчными и заглавными буквами,
и общие метки в этом случае преобразуются к заглавным буквам. Это
означаает, что обычно ожидается, что внешние метки записаны
заглавными буквами. Если вы хотите, чтобы Turbo Assembler делал
различие между строчными и заглавными метками во внешних метках,
следует использовать для этого ключи командной строки /ML и /MX.

Вы можете также задать в директиве EXTERN язык для каждого
символического имя. Допустимыми языками являются C, PASCAL, BASIC,
FORTRAN, PROLOG и NOLANGUAGE. Тогда перед помещением данного имени
в объектный файл оно будет приведено в форму, приемлемую для
указанного языка. Например, если вы объявите

EXTERN C myproc

то символическое имя myproc из исходного файла будут
переписано в объектный в виде _myproc, поскольку в соответствии с
соглашениями языка Си, каждле имя должно иметь ведущий символ
подчеркивания. Использование в директиве EXTERN спецификатора
языка временно переопределяет текущую установку языка (по
умолчанию или выполненную ранее директивой .MODEL).

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

Директива GLOBAL ———————————————-

Возможно, к этому моменту у вас уже возник вопрос, зачем для
выполнения одной задачи — разделения меток между модулями, —
необходимы две директивы, PUBLIC и EXTRN. Фактически единственная
причина, по которой нужны обе директивы, состоит в совместимости с
другими ассемблерами; Turbo Assembler имеет директиву 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 определены и потому становятся
общими метками, доступными всем прочим модулям. PromptString и
DoReport в этом модуле не определены и потому являются внешними
метками, и предполагается, что они сделаны общими в каком-либо
другом модуле.

Конкретно очень удобно использовать директиву GLOBAL в
включаемом файле. (Включаемые файлы рассматриваются в следующем
разделе). Предположим, что у вас имеется некоторый набор меток,
которые вы желаете сделать доступными для всех остальных модулей
многомодульной программы. Очень хорошо было бы объявить все эти
метки во включаемом файле и затем включить его во все модули
программы. К сожалению, сделать это при помощи PUBLIC и EXTRN
невозможно, поскольку EXTRN не работает в том модуле, в котором
соответствующая метка была определена, а PUBLIC, напротив, будет
работать только в модуле определения метки. Директива же GLOBAL
будет работать во всех модулях, поэтому вы можете создать
включаемый файл и объявить в нем все интересующие вас метки
глобальными, а затем включить этот файл во все модули.

Как и в случае директив PUBLIC и EXTERN, вы можете также
задать в директиве GLOBAL язык для каждого символического имя.
Допустимыми языками являются C, PASCAL, BASIC< FORTRAN, PROLOG и NOLANGUAGE. Тогда перед помещением данного имени в объектный файл оно будет приведено в форму, приемлемую для указанного языка. Например, если вы объявите GLOBAL C myproc то символическое имя myproc из исходного файла будут переписано в объектный в виде _myproc, поскольку в соответствии с соглашениями языка Си, каждле имя должно иметь ведущий символ подчеркивания. Использование в директиве GLOBAL спецификатора языка временно переопределяет текущую установку языка (по умолчанию или выполненную ранее директивой .MODEL). Для использования данного средства иметь в программе действующую директиву .MODEL необязательно. Включаемые файлы ----------------------------------------------------------------- Часто оказывается необходимым вставить один и тот же блок исходного текста ассемблерной программы в несколько различных модулей. Вам может понадобиться, чтобы какие-либо равенства или макросы разделялись разными частями программы или просто повторно использовались в других программах. Также может случиться, что вы напишете длинную программу и не захотите разбивать ее на несколько компонуемых модулей (например, программа для последующей записи в ПЗУ), и при этом она окажется слишком велика для удобной работы с ней в виде единого файла. Директива INCLUDE позволяет выполнить все эти требования. Включаемые файлы редко используются для включения кода, поскольку код может прикомпоновываться в виде отдельных модулей во время работы компоновщика, но если вы так хотите, то допустимо оформлять в виде включаемых файлов и коды. Как только Turbo Assembler встречает директиву INCLUDE, он отмечает ее позицию в текущем ассемблируемом модуле, обращается к диску и находит заданный включаемый файл, а затем начинает его ассемблирование, как если бы его строки находились непосредственно в текущем модуле. По достижении конца включаемого файлаTurbo Assembler возвращается к строке текущего модуля, расположенной непосредственно после этой директивы INCLUDE и продолжает ассемблирование с этого места. Ключевая точка состоит здесь в следующем: текст включаемого файла добуквенно вставляется при ассемблировании текущего ассемблерного модуля в позицию расположения в нем директивы INCLUDE. Например, если файл MAINPROC.ASM содержит . . . .CODE mov ax,1 INCLUDE INCPROG.ASM push ax . . . а INCPROG.ASM содержит mov bx,5 add ax,bx то результат ассемблирования будет эквивалентен: . . . .CODE mov ax,1 mov bx,5 add ax,bx push ax . . . Включаемые файлы могут быть вложенными; другими словами, один включаемый файл может сам содержать внутри себя ссылку на другой включаемый файл. В файле листинга включенные строки легко отличить, поскольку слева от них Turbo Assembler помещает номер, указывающий глубину вложения файлов модулей. (Глубина вложенности включаемых файлов произвольна). Каким образом Turbo Assembler находит включаемые файлы? Если в операнде INCLUDE в имя файла включить имя дисковода или пути доступа к файлу, Turbo Assembler будет искать файл именно там, где указано, и более нигде. Если задать только имя файла, не задавая дисковода или пути, Turbo Assembler сначала просматривает текущую директорию. Если Turbo Assembler на может найти там файл, то он сначала просматривает директории, заданные в ключе командной строки -I, если этот ключ был определен. Например, если задать в командной строке Turbo Assembler: TASM -ic:\include testprog и при этом в TESTPROG.ASM будет иметься строка INCLUDE MYMACROS.ASM то Turbo Assembler сначала будет искать MYMACROS.ASM в текущей директории, и затем, в случае неудачи, в директории C:\INCLUDE. Если ни там, ни там файла MYMACROS.ASM не окажется, то Turbo Assembler выдаст сообщение об ошибке. Между прочим, в спецификации пути директивы INCLUDE можно использовать символ обратной наклонной черты (\). Тем самым обеспечивается совместимость с MASM. Файл листинга ----------------------------------------------------------------- Обычно в результате ассемблирования Turbo Assembler в результате ассемблирования создает только один файл: объектный (.OBJ) файл с тем же именем, что и исходный (.ASM) файл. Однако, если вам понадобится, Turbo Assembler может создать также файл листинга с расширением .LST, для чего в командной строке нужно ввести две дополнительные запятые (или два дополнительных имени файла). Например, если строка TASM hello вызывает ассемблирование HELLO.ASM и создает один объектный файл HELLO.OBJ, то командная строка TASM hello,, приведет к созданию файла листинга HELLO.LST, как и в случае командных строк TASM hello,hello,hello или TASM /L hello Имена объектного файла и/или файла листинга не обязательно должны совпадать с именем исходного файла, но практически очень редко появляется причина, чтобы исходный файл имел одно имя, а объектный файл или файл листинга - другое. Файл листинга в целом представляет собой тот же самый исходный файл, но аннотированный большим количеством информации о результатах ассемблирования. Turbo Assembler включает в него фактический машинный код для каждой исходной команды и смещение в текущем сегменте машинного кода, соответствующего каждой строке. Кроме того, Turbo Assembler обеспечивает таблицы с информацией об используемых в программе метках и сегментах, включая значение и тип каждой метки, а также аттрибуты каждого сегмента. Turbo Assembler может также по вашему требованию сгенерировать таблицу перекрестных ссылок для всех меток, используемых в исходном файле, показывая, в каком месте программы метка была определена и в каких используется. (См. описание опции командной строки /C в главе 3). Сначала мы рассмотрим основное содержание файла листинга - ассемблированный машинный код и смещение для каждой команды. Аннотированный исходный текст программы ----------------------- Ниже приводится файл листинга для первого примера программы, HELLO.ASM: Turbo Assembler Version 2.0 01-18-90 14:31:58 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 + HelloMessage DB 'Hello, world',13,10,12 6 77 6F 72 6C 64 0D 0A + 7 0C 8 = 000F HELLO_MESSAGE_LENGTH EQU $ - HelloMessage 9 000F .CODE 10 0000 B8 0000s mov ax,@Data 11 0003 8E D8 mov ds,ax ;DS указывает на сегмент данных 12 0005 B4 40 mov ah,40h ;функция дос записи на устройство # 13 0007 BB 0001 mov bx,1 ;стандартное назначение вывода 14 000A B9 000F mov cx,HELLO_MESSAGE_LENGTH ;число печатаемых символов 15 000D BA 0000r mov dx,OFFSET HelloMessage ;печатаемая строка 16 0010 CD 21 int 21h ;печать "Hello, world" 17 0012 B4 C4 mov ah,4ch ;функция DOS выхода из программы # 18 0014 CD 21 int 21h ;конец работы программы 19 END Turbo Assembler Version 2.0 01-18-90 14:31:58 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 @WORDSIZE Text 2 HELLOMESSAGE Byte DGROUP:0000 HELLO_MESSAGE_LENGTH Number 000F Groups & Segments Bit Size Align Combine Class DGROUP Group STACK 16 0100 Para Stack STACK _DATA 16 010F Word Public DATA _TEXT 16 0116 Word Public CODE В верхней части каждой страницы находится заголовок, в котором указана версия Turbo Assembler, ассемблировавшая файл, дата и время ассемблирования а также номер страницы в листинге. Файл листинга состоит из двух частей: аннотированного листинга исходного текста программы и таблицы символических имен. Первой помещается ассемблерная программа, которая начинается с заголовка, в котором указывается имя файла с исходным текстом программы. Исходный текст аннотирован информацией о машинных кодах, полученных при его ассемблировании Turbo Assembler. Любые сообщения об ошибках или предупреждения помещаются непосредственно после вызвавшей их появление строки. В файле листинга строки программы помещаются в следующем формате: <глубина><номер строки><смещение><машинный код><исходная строка>

— <глубина> обозначает уровень вложенности включаемых файлов
и макросов в файле листинга.

— <номер строки> это номер строки в файле листинга (за исклю-
чением строк заголовка и титульных строк). Номера строк осо-
бенно полезны для работы с таблицей перекрестных ссылок, со-
здаваемой Turbo Assembler; при использовании этого средства
ссылка к той или иной строке выполняется по этому номеру.
В HELLO.LST директива DOSSEG будет иметь в файле листинга
номер 1, директива .MODEL номер 2 и т.д.

Помните, что номер в поле <номер строки> не является номе-
ром строки в исходном файле. Например, в случае расширен-
ного макроса или включаемого файла поле номера строки полу-
чит приращение, даже если текущая строка в исходном модуле
будет той же. Для того, чтобы перевести номер строки в фай-
ле листинга (например, указанный в таблице перекрестных ссы-
лок) в номер строки исходного файла, нужно по номеру найти
эту строку в листинге и затем отыскать ее (не по номеру,
а на глаз) в исходном файле.

— <смещение> это смещение в текущем сегменте начала машинного
кода, сгенерированного для соответствующей строки ассемблер-
ной исходной программы. Например, HelloMessage начинается
в сегменте данных со смещением 0.

— <машинный код> это фактическая последовательность шестнадца-
тиричных значений байтов и слов, ассемблированная из соот-
ветствующей строки ассемблерной исходной программы. Например,
MOV AX,@Data начинается в сегменте программы со смещением 0.
Информация, помещаемая справа от поля смещения для данной
команды, и есть ассемблированный для нее машинный код, поэ-
тому машинный код команды MOV AX.@Data равен B8 0000s (в ше-
стнадцатиричной записи). 0B8h это команда машинного языка,
загружающая AX константой, а 0000s это константа @Data, за-
гружаемая в AX. (Фактически 0000s это просто метка-заполни-
тель для значения @Data; скоро мы рассмотрим этот вопрос).
Суммарно команда MOV AX,@Data ассемблируется в 3 байта ма-
шинного кода.

Отметим, что в файле листинга указано, что команда, следую-
щая за командой MOV AX,@data, а именно команда MOV DS,AX,
начинается в сегменте программы со смещением 3. Это совер-
шенно понятно, учитывая, что MOV AX,@data начинается со
смещением 0 и имеет длину 3 байта. Машинный код, ассембли-
рованный из MOV DS,AX — 8e D8 — имеет длину 2 байта, поэто-
му следующая команда должна начаться со смещением 5; если
посмотреть в файле листинга, то окажется, что это так и
есть.

— И наконец, <исходная строка> представляет собой просто строку
исходного файла ассемблерной программы, полностьюю с коммен-
тариями. Некоторые строки ассемблерной программы, в котороых
содержатся только комментарии и ничего более, не вызывают
генерирования каких-либо машинных кодов; для таких строк поля
<смещение> и <машинный код> не создаются, но номер строки им
присваивается.

Вспомните, что было сказано о том, что значение 0000s для
@data является только меткой-заполнителем для фактического
значения в команде

mov ax,@data

Это делается потому, что значения сегментов присваиваются не
Turbo Assembler, а компоновщиком, и поэтому Turbo Assembler не
может поместить туда правильное значение. Однако Turbo As- sembler
может дать вам знать, что это значение есть значение сегмента и
будет разрешено компоновщиком, и таким признаком служит буква s,
добавляемая в комец машинного кода, генерируемого для

mov ax,@data

Аналогичным образом, смещение машинного кода,
ассемблированного для

mov dx,OFFSET HelloMessage

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

Ниже приводится полный список обозначений, которые Turbo
Assembler использует для указания характеристик ассемблирования
(таких, как переместимость):

————————————————————
Обозначение Смысл
————————————————————
r Указывает на тип фиксации смещения для симво-
лических имен в модуле.

s Указывает на тип фиксации сегмента для симво-
лических имен в модуле.

sr Указывает на тип фиксации сегмента и смещения
для символических имен в модуле.

e Указывает фиксацию смещения внешнего символи-
ческого имени.

se Указывает фиксацию указателя внешнего символи-
ческого имени.

so Указывает на только сегментную фиксацию.

+ Указывает на то, что объектный код был усечен.
————————————————————

В листинге объектного кода r, s и sr используются для
обозначения типов фиксации символических имен в модуле по
смещению, сегменту и указателю (сегмент плюс смещение). e
указыбает на фиксацию смещения внешнего символического имени, а se
на фиксацию указателя внешнего символического имени. Фиксация
сегмента внешнего символического имени обозначается буквой s, как
и для локальных символических имен. Поле объектного кода может
также содержать в последнем столбце символ +, означающий, что
имеется еще объектный код для вывода, но он усечен.

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

Вы, возможно, заметили, что в файле листинга некоторые
машинные коды выражены байтовыми значениями (двумя
шестнадцатиричными цифрами), а некоторые имеют размер слова. В
этом состоит логический шаблон 8086: когда Turbo Assembler
ассемблирует машинный код размером в слово, как в случае OFFSET
HelloMessage, представляющий собой 16-битовое смещение, то такое
значение показывается с размером в слово. Это полезно, поскольку в
противном случае принятый в 8086 подход хранения слов по методу
«младший байт первым» приведет к реверсированию байтов.

Например, команда:

mov ax,1234h

будет ассемблирована в 3 байта машинных кодов: 0B8h, 034h и
012h, именно в такой последовательности. Если Turbo Assembler
перечислит эти коды ка 3 байта, то они будут иметь вид:

B8 34 12

где байты слова поменяются местами. Чтобы избежать этого,
Turbo Assembler поместит машинный код в виде:

B8 1234

что гораздо более удобочитаемо.

При обсуждении поля <смещение> мы говорили о смещении в
текущем сегменте меток и строк программы. Как узнать, в котором
сегменте находится та или иная метка или строка? Такая информация
хранится в обсуждаемых ниже таблицах листинга.

Таблицы символических имен листинга —————————-

Вторая часть файла листинга начинается с заголовка «Symbol
Table» («Таблица символических имен») и состоит из двух таблиц: в
одной из них описаны метки, используемые в исходном тексте
программы, а в другой используемые сегменты.

Между прочим, если вы не пользуетесь таблицей символических
имен в файле листинга, то можете дать Turbo Assembler команду,
чтобы последний сгенерировал только часть листинга с
аннотированным исходным текстом; для этого служит ключ командной
строки /n.

Таблица меток
————-

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

HELLOMESSAGE BYTE DGROUP:0000

Здесь HELLOMESSAGE это имя мески, или символическое имя; оно
выводится заглавными буквами, поскольку Turbo Assembler
автоматически преобразовывает все символические имена в верхний
регистр, если при помощи ключей командной строки /mx или /ml не
будет задано иное. BYTE представляет собой описание размера данных
элемента, обозначенного именем HelloMessage. DGROUP:0000 это
значение метки HelloMessage, оно означает, что строка, на которую
указывает метка HelloMessage, начинается со смещением 0 в
сегментной группе DGROUP.

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

Аналогичным образом, ProgramStart помещена в таблицу как
метка типа near (ближняя) со значением _TEXT:0000; _TEXT это име
сегмента, определенное директивой .CODE, поэтому Program- Start
имеет первый адрес в сегменте программы. Вы видите, что теперь мы
ответели на ранее поставленный вопрос о том, как найти, к какому
сегменту принадлежит метка, поскольку поле значения в таблице
меток сообщает о том, в каком сегменте находится данная метка.

Другие метки, перечисленные в файле листинга HELLO.LST, это
метки, предопределяемые в Turbo Assembler при использовании
упрощенных сегментных директив. Эти метки все устанавливаются на
текстовые строки и содержат значения типа _TEXT или DGROUP.

Метки могут иметь один из приводимых ниже типов данных:

ABS DWORD NUMBER TBYTE
ALIAS FAR QWORD TEXT
BYTE NEAR STRUCT WORD

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

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

Таблица групп и сегментов
————————-

Другой таблицей в части таблицы символических имен листинга
является таблица групп и сегментов. Сегментные группы, такие как
DGROUP, будут далее называться просто группами, поскольку сами
сегментные группы не имеют собственных аттрибутов, а состоят из
одного или более сегментов. Сегменты, составляющие группу в данном
модуле, перечисляются в таблице групп и сегментов прямо под именем
группы со сдвигом на два столбца, показывающим, что они относятся
к данной группе. В HELLO.LST сегменты STACK и _DATA являются
членами сегментной группы DGROUP.

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

Размер данных всегда равен 16, за исключением сегментов USE32
в программах, ассемблируемых для процессора 80386.

Информацию о сегментах USE32 см. в главе 10.

Размер сегмента задается четырьмя шестнадцатиричными цифрами.
Например, сегмент STACK имеет длину 0200h (512 десятичное) байт.

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

BYTE Сегмент может начинаться с любого адреса.

DWORD Сегмент может начинаться с любого адреса, кратного 4.

PAGE Сегмент может начинаться с любого адреса, кратного 256.

PARA Сегмент может начинаться с любого адреса, кратного 16.

WORD Сегмент может начинаться с любого четного адреса.

В HELLO.LST сегмент STACK начинается с границы параграфа, а
сегменты _DATA и _TEXT выравнены по границе слова.

Более подробную информацию о выравнивании см. в главе 9.

Тип объединения определяет, каким образом сегменты с тем же
именем объединяются с данным сегментом. Например, идентично
названные сегменты с типом объединения PUBLIC сцепляютсяв сегмент
больших размеров, а с типом объединения COMMON образуют один общий
сегмент за счет перекрытий.

И наконец, класс сегмента определяет в целом класс, к
которому принадлежит данный сегмент, например CODE, DATA и STACK.
Компоновщик использует эту информацию для упорядочения сегментов
при их компоновке в программу.

Таблица перекрестных ссылок ————————————

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

Существует два способа, как сообщить Turbo Assembler о
необходимости включить в файл листинга таблицу перекрестных
ссылок. Одним из способов включения в файл листинга перекрестных
ссылок является ключ командной строки /c; например,

TASM /c hello,,

генерирует информацию о перекрестных ссылках и помещает ее в
файл листинга HELLO.LST. Отметим при этом, что одного ключа /c для
этого недостаточно; вы также должны дать Turbo Assembler команду
сгенерировать сам файл листинга, в который информация о
перекрестных ссылках помещается.

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

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

TASM hello,hello,hello,hello

или

TASM hello,,,

Предположим, что мы ассемблируем REVERSE.ASM, второй пример
программы из главы 4, с ключем командной строки /c:

TASM /c reverse,,

Turbo Assembler создаст следующий файл листинга, REVERSE.LST:

REVERSE.ASM
1 DOSSEG
2 .MODEL SMALL
3 .STACK 100h
4 .DATA
5 = 03E8 MAXIMUM_STRING_LENGTH EQU 1000
6 0000 03E8*(??) 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 ds,ax ;установить DS на сегмент данных
13 0005 B4 3F mov ah,3fh ;функция DOS считывания с назначения #
14 0007 BB 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 CD 21 int 21h ;прием строки
20 0012 23 C0 and ax,ax ;были ли считаны какие-либо символы
21 0014 74 1F jz Done ;нет, поэтому конец работы
22 0016 8B C8 mov cx,ax ;поместить длину строки в CX,
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 ;в буфере реверсированной строки
36 0028 E2 F8 loop Reverseloop ;пересылка следующего символа, если он есть
37 002A 59 pop cx ;возврат длины строки
38 002B B4 40 mov ah,40 ;функция DOS записи с назначения #
39 002D BB 0001 mov bx,1 ;стандартное назначение вывода
40 0030 BA 03E8r mov dx,OFFSET ReverseString ;печать данной строки
41 0033 CD 21 int 21h ;печать реверсированной строки
42 Done:
43 0035 B4 4C mov ah,4ch ;функция DOS выхода из программы #
44 0037 CD 21 int 21h ;конец работы программы

45 END ProgramStart

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
STACK 16 0200 Para Stack STACK #3
_DATA 16 07D0 Word Public DATA #2 #4
-TEXT 16 0039 Word Public CODE #2 2 #8 8

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

Для каждого символического имени (метки, группы или сегмента)
в поле перекрестных ссылок приводятся номера всех строк программы,
где имеются ссылки на это имя. Строка, где имя было определено,
помечается префиксом #.

Например, найдем, где определена и где используется метка
MAXIMUM_STRING_LENGTH. В файле листинга сказано, что данная метка
была определена в строке 5; если вы поглядите в первой части файла
листинга, то убедитесь, что это действительно так.

(Заодно обратите внимание на то, что в таблице меток сказано,
что значение MAXIMUM_STRING_LENGTH является числом и равно 03E8h —
десятичное 1000.)

Соответствующее MAXIMUM_STRING_LENGTH поле перекрестных
ссылок также сообщает вам, что на эту метку имеются ссылки (но не
определение метки) в строках 6,7 и 15. По первой части файла
листинга видно, что это так и есть.

Ключ /c разрешает перекрестные ссылки для всего файла. Однако
полный листинг перекрестных ссылок для каждого символического
имени требуется далеко не всегда — тем более что для больших
исходных модулей листинг может получиться весьма большого размера.
Turbo Assembler имеет директивы, позволяющие разрешать и отменять
перекрестные ссылки для конкретных участков исходного файла.

Директива %CREF разрешает создание перекрестных ссылок для
всех последующих строк программы. Директива %NOCREF, напротив,
запрещает создание перекрестных ссылок для всех последующих строк
программы. Любая из этих директив заменяет действие ключа
командной строки /c. Если создание перекрестных ссылок разрешена
для всего исходного модуля, то раздел таблицы символических имен
сообщает о всех строках, в которых были определены все метки,
группы и сегменты. Однако информация о перекрестных ссылках
создается только для тех строк, где на эти метки, группы и
сегменты имеются ссылки.

Например, рассмотрим:

.
.
.
%NOCREF
ProgramStart PROC ;строка 1
.
.
.
jmp LoopTop ;строка 2
.
.
.
%CREF ;строка 3
LoopTop:
.
.
.
loop LoopTop ;строка 4
%NOCREF
mov ax,OFFSET ProgramStart ;строка 5
.
.
.

Строка 1 будет указана как строка с определением (с #)
ProgramStart, даже если она находится области, для которой
перекрестные ссылки отменены, поскольку если хоть где-нибудь в
модуле перекрестные ссылки разрешены, то выводятся строки
определения всех меток. Аналогичным образом, строка 3 будет
указана как строка определения LoopTop.

Строка 4 будет являться для LoopTop строкой перекрестной
ссылки, так как она находится после %CREF, но до %NOCREF. Однако
строка 2 в качестве строки перекрестной ссылки для LoopTop указана
не будет, поскольку в точке ее расположения в программе
перекрестные ссылки запрещены. Подобным же образом для метки
ProgramStart не будет указана строка 5.

Для совместимости с другими ассемблерами даются директивы
.CREF и .XCREF, управляющие перекрестными ссылками аналогично
%CREF и %NOCREF, соответственно.

Управление содержимым и форматом листинга ———————

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

Директивы выбора включаемых в листинг строк
——————————————-

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

%LIST и %NOLIST
—————

Директивы %LIST и %NOLIST это основные директивы выбора
включаемых в листинг строк, которые разрешают и отменяют включение
последующих строк в файл листинга. Например, в случае

.
.
.
%NOLIST
mov ax,1
%LIST
mov bx,2
%NOLIST
add ax,bx
.
.
.

в файл листинга будет выведена только средняя строка, mov
bx,2. По умолчанию выбирается директива %LIST.

%CONDS и %NOCONDS
——————

Директивы %CONDS и %NOCONDS разрешают и отменяют вывод в
листинг условных разделов и операторов, имеющих состояние false.
Обычно вывод таких условных состояний отменен. Например, в случае
программы

.
.
.
%CONDS
IFE IS8086
shl ax,7
ELSE
mov cl,7
shl ax,cl
ENDIF
.
.
.

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

%INCL и %NOINCL
—————

Директивы %INCL и %NOINCL разрешают и отменяют вывод в
листинг строк, включенных в ассемблирование из других файлов при
помощи директивы INCLUDE. Обычно вывод в листинг включенных
текстов разрешен. Например, в случае программы

.
.
.
%NOINCL
INCLUDE HEADER.ASM
%INCL
INCLUDE INIT.ASM
.
.
.

строки, включенные из файла HEADER.ASM, в файл листинга
помещены не будут, а строки, включенные из INIT.ASM, попадут в
файл листинга. (Однако в файл листинга будут включены при этом обе
директивы INCLUDE).

%MACS и %NOMACS
—————

Директивы %MACS и %NOMACS разрешают и отменяют вывод в
листинг текста макрорасширений. Обычно вывод в листинг
макрорасширений разрешен. Например, в случае программы
.
.
.
MAKE_BYTE MACRO VALUE
DB VALUE
ENDM
.
.
.
%NOMACS
MAKE_BYTE 1
$MACS
MAKE_BYTE 2
.
.
.

текст, генерируемый первым расширением макроса MAKE_BYTE, DB
1, в файл листинга не войдет, а для второго расширения MAKE_BYTE,
DB 2, войдет в файл листинга. (Однако в файл листинга будут
включены при этом обе директивы MACRO).

%CTLS и %NOCTLS
—————

Директивы %CTLS и %NOCTLS разрешают и отменяют вывод в
листинг текста самих управляющих директив. Обычно вывод директив
управления листингом отменен. Например, в случае программы

.
.
.
%NOCTLS
%NOINCL
%CTLS
%NOMACS
.
.
.

директива управления листингом %NOINCL в файле листинга не
появится, а директива управления листингом %NOMACS в файл листинга
будет включена.

%UREF и %NOUREF
—————

Директивы %UREF и %NOUREF разрешают и отменяют включение в
таблицу символических имен файла листинга имен, на которые не было
ссылок — другими словами, символических имен, которые были
определены в программе, но нигде далее не используются. Обычно
вывод в листинг таких имен разрешен. Две эти опции вообще имеют
смысл только в том случае, если определен вывод перекрестных
ссылок.

%SYMS и %NOSYMS
—————

%SYMS и %NOSYMS разрешают и отменяют включение в файл
листинга таблиц символических имен. Обычно включение в файл
листинга таблиц символических имен разрешено (что совершенно ясно
из нескольких последних разделов данной главы).

Директивы управления форматом листинга
—————————————

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

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

.
.
.
%TITLE ‘Игровая программа звездных войн’
%SUBTTL ‘Подпрограммы гравитационных эффектов’
.
.
.

то каждая страница аннотированного исходного текста будет
начинаться строками:

Turbo Assembler Version 2.0 1-18-90 21:53:35 Page 1 SPACEWAR.ASM
Игровая программа звездных войн
Подпрограммы гравитационных эффектов

%NEWPAGE заставляет Turbo Assembler начать в файле листинга
новую страницу.

%TRUNC говрит Turbo Assembler о необходимости усечь поля,
превышающие максимальную ширину, а %NOTRUNC говорит Turbo As-
sembler, что поле, превышающее максимально определенную для него
длину, должно переноситься на другую строку. Обычно переполнение
поля приводит к его усечению. Отметим, что умолчанием является
%NOTRUNC.

%PAGESIZE задает высоту страниц генерируемого Turbo As-
sembler файла листинга в строках и ширину в столбцах. Например,

%PAGESIZE 66,132

говорит Turbo Assembler о том, что генерируемые им страницы
должны иметь ширину 132 столбца и высоту 66 строк. Отметим, что
%PAGESIZE не посылает команды размера страницы на принтер; перед
печатью файла листинга следует сделать установку принтера, и уже
затем при помощи директивы %PAGESIZE сообщить Turbo Assembler тот
размер страницы, что установлен на принтере.

Директивы управления шириной поля
———————————

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

<глубина><номер строки><смещение><машинный код><исходная строка>

Ранее мы описали четыре из пяти полей строки; пятое поле,
«глубина», указывает уровень вложенности текущей строки по
включаемым файлам или макросам. Например, если текущая строка
содержится в макросе, который сам был вызван из другого макроса,
то глубина вложенности будет равна 2.

Директива %DEPTH задает ширину в символах поля <глубина>.
Директива %LINUM задает ширину в символах поля <номер строки>.
Директива %PCNT задает ширину поля <смещение>. ((Если вы
рассматриваете это поле как поле «счетчика программы» («program
counter»), то директиву %PCNT запомнить проще). Директива %BIN
задает ширину поля <машинный код>. И наконец, директива %TEXT
задает ширину поля <исходная строка>.

%PUSHLCTL и %POPLCTL
———————

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

Turbo Assembler позволяет решить этот вопрос при помощи
директив %PUSHLCTL и %POPLCTL. %PSHLCTL помещает текущее состояние
управления листингом на внутренний стек, а %POPLCTL снимает его со
стека. (Обе эти директивы позволяют иметь максимум до 16 уровней).
Эти две директивы записывают и восстанавливают только те
управляющие состояния, которые могут иметь значения «разрешено»
или «отменено» (как например, %TRUNC и %NOTRUNC), но не те, что
принимают числовой аргумент (типа %BIN). Например, в следующем
фрагменте состояние управления листингом после выполнения
директивы %POPLCTL становится в точности тем же, что и до
выполнения директивы %PUSHLCTL:

.
.
.
%LIST
%TRUNC
%PUSHLCTL
%NOLIST
%NOTRUNC
%NEWPAGE
.
.
.
%POPLCTL
.
.
.

Прочие директивы управления листингом
————————————-

Turbo Assembler имеет еще несколько директив управления
листингом, которые обеспечивают совместимость с другими
ассемблерами. Сюда входят директивы TITLE, SUBTTL, PAGE, .LIST,
.XLIST, .LFCOND, .SFCOND, .TFCOND, .LALL, .SALL и .XALL.
(Подробное описание этих директив см. в Главе 2 Справочного
руководства).

Сообщения, выдаваемые во время ассемблирования
——————————————————————

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

Директива DISPLAY выводит на экран строку символов, заданную
в ней в кавычках. Директива %OUT выводит на экран строку символов,
не заключенную в кавычки. В остальном эти директивы работают
одинаково. Например, следующий фрагмент:

.
.
.
DISPLAY ‘Это сообщение выдано директивой DISPLAY’
%OUT ‘Это сообщение выдано директивой %OUT’
.
.
.

выводит на экран следующие строки:

Это сообщение выдано директивой DISPLAY
Это сообщение выдано директивой %OUT

Условное ассемблирование исходной программы
——————————————-

Рано или поздно вы обнаружите, что в некоторых случаях может
оказаться полезным организовать ассемблирование одного и того же
исходного модуля на языке ассемблера таким образом, чтобы получить
из него несколько разных версий программы. Например, вам может
понадобиться получить из одного текста две программы: одну
использующую стандартные команды 8086, и одну использующую
преимущества, даваемые мощными командами процессоров 80186 и
80286.

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

Директивы условного ассемблирования Turbo Assembler дают вам
такую возможность, и даже более того. Рассмотрим следующий
фрагмент:

.
.
.
IF IS8086
mov ax,3dah
push ax
ELSE
push 3dah
ENDIF
call GetAdapterStatus
.
.
.

Если метка IS8086 имеет ненулевое значение, то значение
параметра 3dah помещается на стек двухшаговым процессом,
необходимым в случае 8086. Если же, однако, IS8086 равна нулю, то
параметр помещается на стек прямо, при помощи специальной формы
PUSH, которая существует для процессоров 80186 и 80286, но
отсутствует для 8086. Приведенный фрагмент при помощи директив
условного ассемблирования в одном и том же исходном тексте
поддерживает две версии программы, одну для 8086, а другую для
80186 и 80286.

Turbo Assembler поддерживает несколько директив условного
ассемблирования и дает вам при этом возможность самим генерировать
сообщения об ошибках ассемблирования множеством различных
способов. Сначала мы рассмотрим директивы условного
ассемблирования.

Директивы условного ассемблирования —————————

Самыми простыми и наиболее полезными директивами условного
ассемблирования являются директивы IF и IFE, используемые в
сочетании с директивами ENDIF и при необходимости с ELSE. Часто
также используются директивы IFDEF и IFNDEF, а в некоторых
ситуациях могут оказаться полезными директивы IFB, IFNB, IFIDN,
IFDIF, IF1 и IF2.

IF и IFE
———

Директива IF вызывает ассемблирование следующего за ним блока
исходного текста программы (конец этого блока обозначается
соответствующей директивой ELSE или ENDIF), если операнд директивы
IF имеет ненулевое значение. Операнд может являться либо
константой, либо выражением, вычисление которого в результате дает
константу. Например,

.
.
.
IF REPORT_ASSEMBLY_STATUS
DISPLAY ‘Достигнута контрольная точка ассемблирования 1’
ENDIF
.
.
.

выведет на дисплей строку

Достигнута контрольная точка ассемблирования 1

при достижении директивы IF только при ненулевом значении
REPORT_ASSEMBLY_STATUS.

Условная директива IF может заканчиваться либо ENDIF, либо
ELSE. Если конструкция IF заканчивается директивой ELSE, то блок
исходного текста программы, следующий за директивой ELSE, будет
ассемблирован только при нулевом значении операнда директивы IF.
Этот блок должен заканчиваться директивой 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 (INITIAL_ARRAY_VALUE)
ELSE
Array DB ARRAY_LENGTH DUP (?)
ENDIF
ENDIF
.
.
.

блоки IF и IF…ELSE вложены в другой блок IF.

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

.
.
.
IFE 0
.
.
.
ENDIF
.
.
.

Аналогичным IF образом, директива IFE может иметь связанную с
ней директиву ELSE.

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

IFDEF и IFNDEF
—————

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

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

.
.
.
DEFINED_LABEL EQU 0
.
.
.
IFDEF DEFINED_LABEL
DB 0
ENDIF
.
.
.

директива DB будет ассемблирована; однако если удалить
присвоение, в котором устанавливается DEFINED_LABEL (и при
условии, что DEFINED_LABEL более нигде в программе не
установлена), то директива DB ассемблирована не будет. Отметим,
что действие IFDEF от значения, присвоенного DEFINED_LABEL, не
зависит.

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

Вас может заинтересовать, каким образом директивы IFDEF и
IFNDEF применяются. Один способ использования состоит в защите от
попыток повторного определения при помощи EQU одной и той же метки
в сложной программе; если метка уже определена, то директива IFDEF
позволит избежать ее повторного определения и выдать сообщение об
ошибке. Другой способ использования состоит в выборе версии
ассемблируемой программы, во многом похоже на выше описанную
директиву IF; вместо проверки того, равна ли, скажем,
INITIALIZE_ARRAY нулю, или не равна, вы можете просто проверить,
определена ли она вообще.

Один удобный способ выбора версии программы состоит в
использовании ключа командной строки Turbo Asssembler /D. /D
определяет связанную с ним метку и присваивает ей значение. Таким
образом, вы можете, например, задать командную строку типа

TASM /dINITIALIZE_ARRAYS=1 test

которая вызовет ассемблирование программы TEST.ASM с меткой
INITIALIZE_ARRAY, установленной в 1.

Хотя данное средство является, несомненно, полезным, с ним
связана одна потенциальная проблема. Что, если вы понадеетесь на
установку INITIALIZE_ARRAY в командной строке, но затем забудете
задать ключ /D? Кроме того, как быть, если инициализировать
массивы вам требуется только в некоторых специальных случаях и вы
не желаете всякий раз вводить /dINITIALIZE_ARRAYS?

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

.
.
.
IFNDEF INITIALIZE_ARRAYS
INITIALIZE_ARRAYS EQU 0 ;умолчание, если инициализации
;не было
ENDIF
.
.
.

При таком использовании IFNDEF для определения ранее не
определенного символического имени вы получите предупреждение,
означающее, что применена конструкция, зависящая от конкретного
прохода. Если вы выполняете определение символического имени
внутри условного блока IFNDEF, то такое сообщение можно
проигнорировать. Данное сообщение выдается, поскольку Turbo
Assembler не может определить, поместите ли вы в блок команды или
директивы. Если в блоке вы не только определяете символическое
имя, вам может понадобиться многопроходная обработка, задаваемая
опцией /m. Если вы только определяете символическое имя, то
задание многопроходной обработки отменит появление данного
предупреждения.

Прочие директивы условного ассемблирования
——————————————

Для проверки параметров, передаваемых макросам, используются
директивы IFB, IFNB, IFIDN и IFDIF. (Макросы рассматриваются в
главе 9, «Расширенные средства программирования на Turbo
Assembler».) IFB вызывает ассемблирование связанного с ней блока
исходного текста, если заданный в качестве ее операнда параметр
макроса является пустым, а IFNB, напротив, делает это если
параметр не является пустым. IFB и IFNB являются своего рода
эквивалентами директив IFDEF и IFNDEF для параметров макроса.

Рассмотрим, например, макрос TEST, определенный как

;
;Макрос для определения байта или слова
;
;Входы:
; VALUE = значение байта или слова
; DEFINE_WORD = 1 для определения слова и
; = 0 для определения байта
;
;Примечание: если PARM2 не задан, то определяется байт
;
TEST MACRO VALUE, DEFINE_WORD
IFB
DB VALUE ;если параметр PARM2 пустой,
;определить байт
IF DEFINE_WORD
DW VALUE ;если PARM2 не равен нулю,
;определить слово
ELSE
DB VALUE ;если PARM2 равен нулю,
;определить слово
ENDIF
ENDIF
ENDM

Если TEST запустить с параметром

TEST 19

то будет определен байт со значением 19, а если запустить его
как

TEST 19,1

то будет определено слово со значением 19.

IFIDN вызывает ассемблирование связанного с ней блока
программы, если два переданных макросу параметра идентичны, а
IFDIF делает то же самое, если эта пара операндов различны.
Например, следующий макрос, преобразующий байт со знаком в слово
со знаком в AX, не станет выполнять лишние действия по копированию
исходного операнда в AL, если исходный операнд и так находится в
AL:

;
;Макрос для преобразования байта со знаком в 8-битовом
;регистре или именованной ячейки памяти в слово со знаком
;в AX.
;
;Вход:
; SIGNED_BYTE — имя регистра или ячейки памяти, содержащей
; байт со знаком, преобразуемый в слово со
; знаком.
;
MAKE_SIGNED_WORD MACRO SIGNED_BYTE
IFDIFI , ;проверяет, что операнд не AL
mov al,SIGNED_BYTE
ENDIF
cbw
ENDM

IFIDN и IFDIF различают, заглавными или строчными буквами
набраны переданные им аргументы. Аналогичные же им директивы
IFIDNI и IFDIFI считают строчные и заглавные буквы эквивалентными.

Отметим, что операнды директив IFB, IFNB, IFIDN и IFDIF
следует заключать в угловые скобки.

Если вы не разрешали многопроходную обработку при помощи
опции /m, то IF1 всегда рассматривается как true, а IF2 как falce
из-за отсутствия второго прохода. В таком случае, когда Turbo
Assembler встречает в модуле IF1 или IF2, выдается предупреждение
«Pass-dependent construction encountered» («встречена
констрцукция, зависимая от числа проходов»).

При использовании опции командной строки /m если ваш модуль
содержит IF1 или IF2, то автоматически делается два прохода. В
этом случае IF1 дает true для первого прохода, а IF2 дает true для
второго прохода, и выдается сообщение «Module is pass-dependant —
compatibility pass was done» («модуль зависит от числа проходов —
был сделан проход для совместимости»).

Семейство директив ELSEIF
————————-

Каждая директива из семейства IF (IF, IFB, IFIDN и т.д.)
имеет аналог в семействе директив ELSEIF (например, ELSEIF,
ELSEIFB, ELSEIFIDN). Их действие аналогично комбинации директивы
ELSE с одной из директив IF. Эти директивы можно использовать для
улучшения читаемости программы, если для ассемблирования одного
блока исходного текста выполняется проверка по многим условиям или
значениям величин. Рассмотрим следующий фрагмент программы:

IF BUFLENGTH GT 1000
CALL DOBIGBUF
ELSE
IF BUFLENGTH GT 100
CALL MEDIUMBUF
ELSE
IF BUFLENGTH GT 10
CALL SMALLBUF
ELSE
CALL TINYBUFF
ENDIF
ENDIF
ENDIF

Для улучшения читаемости такого текста программы можно
воспользоваться директивами ELSEIF:

IF BUFLENGTH GT 1000
CALL DOBIGBUF
ELSEIF BUFLENGTH GT 100
CALL MEDIUMBUF
ELSEIF BUFLENGTH GT 10
CALL SMALLBUF
ELSE
CALL TINYBUFF
ENDIF

Это приблизительно соответствует операторам case или switch в
Паскале и Си, соответственно. Однако это средство позволяет и
более общие применения, поскольку использовать в одном условном
блоке директивы семейства ELSEIF одного и того же типа не
обязательно. Например, вполне допустим следующий фрагмент:

PUSHREG MACRO ARG
IFIDN ,
PUSH SI
PUSH DI
ELSEIFB
PUSH AX
ENDIF
ENDM

Директивы условного генерирования состояний ошибки ————

Turbo Assembler позволяет устанавливать безусловное или
условное генерирование состояний ошибки при помощи директив
условного генерирования состояний ошибки:

.ERR .ERRB .ERRDIFI .EERRIDNI
.ERR1 .ERRDEF .ERRE .ERRNB
.ERR2 .ERRDIF .ERRIDN .ERRNDEF
.ERRNZ

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

Еще раз рассмотрим список директив условного генерирования
состояний ошибки. Обратите внимание на то, что эти директивы во
многом похожи на директивы условного ассемблирования, и это не
совпадение, поскольку большинство директив условного генерирования
состояний ошибки проверяют наличие одних и тех же условий.
Например, .ERRNDEF генерирует метку, если не определена метка,
заданная в операнде, аналогично директиве IFNDEF, которая
ассемблирует связанный с ней исходный текст при выполнении тех же
условий.

.ERR, .ERR1 и .ERR2
——————-

Когда Turbo Assembler встречает директиву .ERR, генерируется
состояние ошибки. Сама по себе такая функция не очень полезна;
однако в сочетании с директивой условного ассемблирования ее очень
удобно применять.

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

IF (ARRAY_LENGTH GT MAX_ARRAY_LENGTH)
.ERR
ENDIF

Если массив имеет слишком большую длину, Turbo Assembler не
будет ассемблировать блок программы IF, поэтому директива .ERR
ассемблирована не будет, и генерирование ошибки не произойдет.

.ERR1 и .ERR2 делают то же, что и ERR, но только по проходам
1 или 2, соответственно. Если вы не разрешали многопроходную
обработку при помощи опции /m, то ERR1 всегда выводит на дисплей
ошибку, а ERR2 никогда не выводит из-за отсутствия второго
прохода. В таком случае, когда Turbo Assembler встречает в модуле
ERR1 или ERR2, выдается предупреждение «Pass-dependent
construction encountered» («встречена констрцукция, зависимая от
числа проходов»).

При использовании опции командной строки /m если ваш модуль
содержит ERR1 или ERR2, то автоматически делается два прохода. В
этом случае ERR1 выводит на дисплей ошибку для первого прохода, а
ERR2 для второго прохода, и выдается сообщение «Module is
pass-dependant — compatibility pass was done» («модуль зависит от
числа проходов — был сделан проход для совместимости»).

.ERRE и .ERRNZ
—————

Директива .ERRE генерирует ошибку, если ее операнд, который
должен вычисляться как выражение с константами, равен нулю. .ERRE
эквивалентен директиве .IFE, используемой совместно с директивой
.ERR. Например,

.ERRE TEST_LABEL-1

эквивалентно

IFE TEST_LABEL-1
.ERRE
ENDIF

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

Аналогичным образом, директива .ERRNZ генерирует ошибку, если
ее операнды не равны нулю; это эквивалентно тому, как если бы за
директивой IF следовала директива .ERR. ERRNZ можно использовать
для генерирования ошибки при возврате условным выражением значения
true, поскольку оно ненулевое. Например,

.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 пустой. Макрос также
использует директиву .ERRIDN, при помощи которой проверяет, что
PARM 3 не равен DX, так как в этом случае при загрузке PARM1 он
будет затерт. Вот этот макрос:

;
;Макрос складывает две константы, регистра или ячейки памяти
;и помещает результат в DX.
;
;Входы:
; PARM1 — один слагаемый операнд
; PARM2 — второй операнд, прибавляемый к первому
;
ADD_TWO_OPERANDS MACRO PARM1,PARM2,PARM3
.ERRB ;должно быть два параметра
.ERRNB ;…но не три
.ERRIDN , ;вторым параметром
;не может быть DX
mov dx,PARM1
add dx,PARM2
ENDM

Данный макрос также использует .ERRIDN для того, чтобы
убедиться, что PARM2 не равен DX, поскольку в таком случае при
загрузке PARM1 он будет замещен.

Распространенные ошибки
при программировании на языке ассемблера
——————————————————————

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

Вы забыли вернуться в DOS ————————————-

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

Рассмотрим следующий пример программы:

.MODEL small
.CODE
DoNothing PROC NEAR
nop
DoNothing ENDP
END DoNothing

Ваш прошлый опыт программирования мог подсказать вам, что
либо директива ENDP, либо директива END выполняет правильный выход
из программы подобно тому, как это делают } и end. в Си и Паскале,
однако это неверно. Загрузочный код, генерируемый при
ассемблировании и компоновке этой программы, будет состоять из
единственной команды NOP. В ассемблере директива ENDP, как и все
прочие директивы, не ведет к генерированию какого-либо кода; она
просто указывает ассемблеру о том, что текст процедуры DoNothing
кончился. Аналогичным образом, директива END DoNothing сообщает
Turbo Assembler о том, что в этом месте заканчивается текст
данного модуля, и что выполнение программы должно начинаться с
метки DoNothing. Однако в исходном тексте отсутствуют команды,
возвращающие по окончании программы управление DOS; в результате
при запуске данной программы сразу же после выполнения команды NOP
будут выполнены любые случайные команды, расположенные в памяти
после NOP. После этого компьютер зависнет, и вместо возврата в DOS
вы получите необходимость аппаратной перезагрузки системы.

Всего существует несколько способов возврата из ассемблерной
программы в DOS, но рекомендуемым является выполнение функции DOS
4Ch. Следующая версия вышеприведенной программы закончится
правильно:

.MODEL small
.CODE
DoNothing PROC NEAR
nop
mov 4Ch ;функция DOS окончания работы программы
int 21h ;обращение к DOS для окончания работы
DoNothing ENDP
END DoNothing

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

Вы забыли использовать команду RET —————————-

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

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

; Подпрограмма умножения значения на 80.
; Вход: в AX — значение, умножаемое на 80.
; Выход: DX:AX — произведение.
;
MultiplyBy80 PROC NEAR
mov dx,80
mul dx
ret
MultiplyBy80 ENDP

Генерирование неверного типа возврата ————————

Директива 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

Turbo Assembler делает команду RET в данной процедуре
ближней, поскольку LongShiftRight2 является ближней процедурой.
Если изменить директиву PROC на

LongShiftRight2 PROC FAR

то будет сгенерирована дальняя команда возврата (RET).

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

Хорошее решение этой проблемы состоит в том, чтобы обеспечить
в каждой процедуре соответствующую директиву 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
LongShiftRight ENDP
LongShiftRight2 ENDP

как и послесовательные директивы PROC:

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

Вы перепутали расположение операндов —————————

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

mov ax,bx

означала «переслать содержимое AX в BX», то это было бы
эквивалентно сканированию строки слева-направо, а именно так
устроены ассемблеры микропроцессоры многих изготовителей. Однако
фирма Intel при создании 8086 приняла другой подход; для нас
данная строка означает «переслать содержимое BX в AX», а это в
некоторых случаях может привести к ошибке.

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

mov ax,bx

как

ax = bx

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

add bx,(OFFSET BaseTable * 4) + 2

можно представить в виде

bx = (OFFSET BaseTable * 4) + 2

также в соответствии с описанным подходом.

Вы забыли распределить стек
или зарезервировали слишком малый стек ————————

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

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

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

Единственные ассемблерные программы, в которых не должен
распределяться стек, это программы, преобразуемые затем в .COM или
.BIN файлы. .BIN файлы представляют собой программы, жестко
привязанные к конкретному адресу памяти, а .BIN файлы как правило
используются как интерпретированные подпрограммы на Бейсике и
потому используют стек Бейсика. .COM-программы работают со стеком,
который располагается в самом верху выделенного программе сегмента
(и равный максимум 64Кб или менее, если доступная память меньше,
чем 64Кб), поэтому в данном случае максимальный размер стека
просто равен количеству памяти, оставшемуся в кодовом сегменте от
программы. Будьте осторожны, когда размер создаваемых вами .COM
программ приближается к 64Кб, поскольку при этом соответствующим
образом сжимается стек. Помните также, что в случае больших .COM
программ могут возникнуть проблемы, связанные со стеком, если они
выполняются на компьютерах с недостаточной доступной памятью или
при запуске таких программ из оболочки DOS при работе под
управление другой программы.

Этих потенциальных проблем можно избежать в том случае, если
вместо .COM программ создавать .EXE программы и при этом
резервировать некоторое дополнительное пространство стека.

Вы вызвали подпрограмму, которая затирает содержимое
необходимых далее регистров ————————————

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

Для примера рассмотрим следующее:

.
.
.
mov bx,[TableBase] ;BX указывает на базу таблицы
mov ax,[ELEMENT] ;прием элемента
call DivideBy10 ;деление элемента на 10
add bx,ax ;указывает на сответствующий вход
;таблицы
.
.
.
; Подпрограмма деления числа на 10
; Вход: AX — значение, делимое на 10
; Выход: AX — значение, деленное на 10
; DX — остаток значения, деленного на 10
; BX разрушается.
;
DivideBy10 PROC NEAR
mov dx,0 ;подготовить DX:AX как 32-битовое делимое
mov bx,10 ;BX является 16-битовым делителем
div bx
ret
DivideBy10 ENDP

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

.
.
.
mov bx,[TableBase] ;BX указывает на базу таблицы
mov ax,[ELEMENT] ;прием элемента
call DivideBy10 ;деление элемента на 10
add bx,ax ;указывает на сответствующий вход
;таблицы
.
.
.
; Подпрограмма деления числа на 10
; Вход: AX — значение, делимое на 10
; Выход: AX — значение, деленное на 10
; DX — остаток значения, деленного на 10
; BX разрушается.
;
DivideBy10 PROC NEAR
push bx ;сохранить BX
mov dx,0 ;подготовить DX:AX как 32-битовое делимое
mov bx,10 ;BX является 16-битовым делителем
div bx
pop bx ;восстановить исходное значение BX
ret
DivideBy10 ENDP

либо это может быть проделано в вызывающей программе, в обход
вызова DivideBy10:

.
.
.
mov bx,[TableBase] ;BX указывает на базу таблицы
mov ax,[ELEMENT] ;прием элемента
push bx ;сохранить базу таблицы
call DivideBy10 ;деление элемента на 10
pop bx ;восстановить базу таблицы
add bx,ax ;указывает на сответствующий вход
;таблицы
.
.
.
; Подпрограмма деления числа на 10
; Вход: AX — значение, делимое на 10
; Выход: AX — значение, деленное на 10
; DX — остаток значения, деленного на 10
; BX разрушается.
;
DivideBy10 PROC NEAR
mov dx,0 ;подготовить DX:AX как 32-битовое делимое
mov bx,10 ;BX является 16-битовым делителем
div bx
ret
DivideBy10 ENDP

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

.
.
.
mov ax,[ELEMENT] ;прием элемента
call DivideBy10 ;деление элемента на 10
mov bx,[TableBase] ;BX указывает на базу таблицы
add bx,ax ;указывает на сответствующий вход
;таблицы
.
.
.
; Подпрограмма деления числа на 10
; Вход: AX — значение, делимое на 10
; Выход: AX — значение, деленное на 10
; DX — остаток значения, деленного на 10
; BX разрушается.
;
DivideBy10 PROC NEAR
mov dx,0 ;подготовить DX:AX как 32-битовое делимое
mov bx,10 ;BX является 16-битовым делителем
div bx
ret
DivideBy10 ENDP

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

Использование неверной модификации
команды условного перехода ————————————-

Наличие в языке ассемблера нескольких модификаций команды
условного перехода (JE, JNE, JC, JNC, JA, JB, JG и т.д.) дает при
написании программ огромную степень гибкости — и разумеется, при
этом увеличивает возможность неверного выбора типа перехода. Более
того, поскольку для обработки условия перехода в языке ассемблера
требуется как минимум две отдельных строки, одна из которых служит
для выполнения сравнения, а вторая для условного перехода (а для
более сложных условий и большее количество строк), обработка
переходов в языке ассемблера является менее интуитивной и более
зависимой от ошибки, нежели аналогичные функции в Си и Паскале.

* Одна распространенная ошибка состоит в использовании JA, JB,
JAE или JBE для сравнения значений со знаком или, аналогичным
образом, использовании JG, JL, JGE или JLE для сравнения
значений без знака.

* Другая общая ошибка заключается в использовании, скажем, JA
в том месте, где должна была стоять команда JAE. Помните,
что без буквы e в конце команд JAE, JBE, JLE или JGE срав-
нение не будет включать в себя случай, когда два операнда
равны.

* Еще одна общая ошибка происходит при использовании инверс-
ных логических команд, например JS, там, где должна была
стоять команда JNS.

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

.
.
.
;
; если (Length > MaxLength) }
;
mov ax,[Length]
cmp ax,[MaxLength]
jng LengthIsLessThanMax
.
.
.
jmp EndMaxLengthTest
; } иначе {
;
LengthIsLessThanMax:
.
.
.
;
; }
;
EndMaxLengthTest:
.
.
.

Распространенные ошибки, связанные со строковыми командами ——

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

Вы забыли о выходе за границу строки при использовании REP
——————————————————————

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

.
.
.
cld ;устанавливает прямой отсчет для
;строковых команд
mov si,0 ;указыват на смещение 0
lodsb ;чтение байта со смещением 0
.
.
.

SI будет содержать не 0, а 1. Это имеет определенный смысл,
так как следующей команде LODSB, скорее всего, понадобится
выполнить доступ к адресу 1, а еще следующей LODSB — к адресу 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, т.е.
смещение байта, расположенного после того, как был найден нулевой
байт. Для того, чтобы вернуться к указателю на последний символ в
строке, приведенный фрагмент следует записать как:

.
.
.
cld ;устанавливает прямой отсчет для
;строковых команд
les di,[bp+ScanString] ;ES:DI указывает на сканируемую
;строку
mov cx,MAX_STRING_LEN ;проверка до максимальной строки
mov al,0 ;поиск оконечного нуля строки
repne scasb ;выполнение поиска
jne NoMatch ;оканчивающий строку 0 не найден
dec di ;возврат указателя на нулевой
;символ
dec di ;возврат указателя на последний
;символ
NoMatch:
mov di,0 ;возврат нулевого указателя
mov es,di
ret
.
.
.

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

Аналогичная ошибка может возникнуть вследствие
декрементирования CS командами REP SCAS и REP CMPS на один раз
больше, чем вы могли предположить. CX не только декрементируется
один раз на каждый байт, соответствующий условию «повторять пока»
(равно или не равно), но и один раз для байта, для которого это
условие не выполнилось и который вызвал окончание работы команды.
Например, если в последнем примере байт по адресу 2000: 0000
содержал ноль, то после выполнения CX будет содержать значение
MAX_STRING_LEN-1, даже если не будет найдено ни одного ненулевого
символа. Подпрограмма подсчета числа символов в строке должна
выглядеть следующим образом:

; Восвращает длину заканчивающейся нулем строки в байтах.
; Вход: ES:DI — начало строки
; Выход: AX — длина строки, исключая конечный ноль
; ES:DI — указывает на последний байт строки, либо
; 0000:0000, если конечный ноль не найден
;
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 ;конечный ноль в счет не входит
dec di ;указатель на конечный ноль
dec di ;указатель на последний символ
jmp short StringLengthEnd
StringLengthError:
mov di,0 ;возврат нулевого указателя
mov es,di
StringLengthEnd:
pop cx ;восстановить исходное значение CX
ret
StringLength ENDP

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

.
.
.
repz cmpsb
jcxz ArraysAreTheSame
.
.
.

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

.
.
.
repz cmpsb
jc ArraysAreTheSame
.
.
.

Короче говоря, CX следует использовать только как счетчик
байтов, сканируемых командами REP SCAS и REP CMPS, но не как
индикатор равенстава или неравенства сканируемых или сравниваемых
данных.

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

Вы установили нулевое значение CX
для доступа ко всему сегменту сразу
————————————

Любая повторяющаяся строковая команда, выполненная при CX,
равном нулю, не делает ничего. Точка. Это свойство может оказаться
полезным в том смысле, что перед ее выполнением отпадает
необходимость проверки равенства нулю; с другой стороны, иначе не
существует способа получить для строковой команды размером в байт
доступ к каждому байту сегмента. Например, следующий фрагмент
сканирует сегмент, заданный в ES, в поисках первого вхождения в
него буквы A:

.
.
.
cld ;установка прямого счета при поиске
sub di,di ;начало поиска с нулевого смещения
mov al,’A’ ;поиск буквы ‘A’
mov cx,0FFFFh ;сначала сканируются первые 64Кб-1
;байт
repne SCASb ;сканирование первых 64Кб-1 байт
je AFound ;буква ‘A’ найдена
scasb ;не найдена, но остался еще последний
;байт
je AFound ;буква найдена в последнем байте
;в этом сегменте нет буквы ‘A’
.
.
.
AFound: ;DI-1 указывает на
;положение буквы ‘A’
.
.
.

В наборе команд 8086 имеется определенная ассиметрия,
связанная с использованием при счете нулевых значений CX. Если
повторяющиеся строковые команды в случае равенства CX нулю не
выполняются, то команда LOOP в этом случае работает, уменьшая CX
до значения 0FFFFh и выполняя переход на адрес начала цикла. Это
означает, что в одном цикле может быть обработано целиком до 64Кб.
Предыдущий пример, где выполнялось сканирование сегмента,
заданного в ES, в поисках буквы ‘A’, может быть реализован при
помощи команды LOOP как:

.
.
.
cld ;устанавливает счет в прямом направлении
sub di,di ;начало со смещения ноль
mov al,’A’
sub cx,cx ;поиск в 64 Кб памяти сегмента
ASearchLoop:
scasb ;проверка следующего байта
je AFound ;это буква ‘A’
loop ASearchLoop ;в данном сегменте буквы ‘A’ нет
.
.
.
AFound: ;DI-1 указывает на букву ‘A’
.
.
.

С другой стороны, в случае, когда CX равен нулю и если это по
логике программы должно означать «не выполнять никаких действий»,
данное условие следует проверять специально; в противном случае
вместо нуля циклов выполнится 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 подпрограмма FillBytes при нулевом значении
CX вместо того, чтобы оставить память без изменений, заполнит
значением из регистра AL весь сегмент, на который указывает ES.

Неправильная установка флага направления
——————————————————————

При выполнении строковой команды со связанным с ней
указателем или указателями — SI, DI или сразу с ними двумя
выполняется операция инкрементирования (положительного приращения)
или декрементирования (отрицательного приращения). Какая именно из
этих операций будет выполнена, зависит от состояния флага
направления.

Флаг направления можно очистить при помощи команды CLD, и
тогда строковые команды будут инкрементировать счетчик (выполнять
счет в прямом направлении), либо его можно установить при помощи
команды STD, и тогда строковые команды будут декрементировать
счетчик (выполнять счет в обратном направлении). Будучи однажды
очищенным или установленным, флаг направления остается в этом
состоянии до тех пор, пока не будет выполнена следующая команда
CLD или STD, либо пока состояния флагов не будут восстановлены
извлечением из стека при помощи команд POPF или IRET. Разумеется,
удобно иметь возможность запрограммировать один раз значение флага
направления и затем выполнить последовательность строковых команд,
которые все будут работать в одном направлении приращения; однако
в случае ошибочного значения этого флага могут возникнуть трудно
обнаружимые ошибки, приводящие к неадекватной работе строковых
команд, в зависимости от того, как был установлен этот флаг ранее
отработавшими программами.

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

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

Использование невермой модификации
повторяющейся строковой команды сравнения
——————————————

Команда CMPS выполняет сравнение двух областей памяти, а
команда SCAS сравнивает с областью памяти значение сумматора. Если
с данными командами используется префикс REPE, то каждая из них
будет повторяться до тех пор, пока CX не станет равным нулю, либо
пока при сравнении не будет выявлено различие между сравниваемыми
элементами. К сожалению, легко перепутать, какой префикс REP что
делает.

Для того, чтобы запомнить, что именно делает конкретный
префикс REP, нужно умозрительно вставить после слова REP
(«повторять») слово «while» («пока»). Тогда REPE будет иметь для
вас вид «rep while e» или «repeat while equal» («повторять пока
равно»), а REPNE будет иметь вид «rep while ne» или «repeat while
not equal» («повторять пока не равно»).

Вы забыли об умолчаниях сегмента,
принимаемых для строковых команд
———————————

По умолчанию каждая строковая команда использует исходный
сегмент (если он имеется) из DS и сегмент назначения (если он
имеется) из ES. Об этом можно легко забыть и попытаться выполнить,
например, команду STOSB с сегментом данных, поскольку обычно
именно там находятся все данные, обрабатываемые не-строковыми
командами. Аналогичным образом, часто по ошибке пишут:

.
.
.
cld ;счет при поиске в пямом направлении
mov al,0
mov cx,80 ;длина буфера
repe scasb ;найти первый ненулевой символ, если
;он имеется
jz AllZero ;нет ненулевого символа
dec di ;указатель возвращается назад на первый
;ненулевой символ
mov al,[di] ;прием первого ненулевого символа
.
.
.
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:[di] ;прием первого ненулевого символа
.
.
.
AllZero:
.
.
.

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

.
.
.lods es:[SourceArray]
.
.
.

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

.
.
.
stos ds:[DestArray]
.
.
.

(На самом деле Turbo Assembler во время ассемблирования
отметит это как ошибку).

Неправильное преобразование формата операций из байта в слово
————————————————————-

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

.
.
.
mov cx,200 ;количество пересылаемых байтов
.
.
.
shr cx,1 ;преобразование числа байт в число слов
rep movsw ;пересылка за один раз блока данных
.
.
.

работает на 8086 на 50% быстрее, чем

.
.
.
mov cx,200 ;количество пересылаемых байтов
.
.
.
rep movsw ;пересылка за один раз блока данных
.
.
.

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

shr cx,1

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

.
.
.
shr cx,1 ;преобразование счетчика в формат слова
jnc MoveWord ;нечетный байтовый счетчик?
movsb ;да, нечетный,
;поэтому пересылка нечетного байта
MoveWord:
rep movsw ;пересылка четного числа байтов
;по слову за один раз
.
.
.

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

.
.
.
mov cx,200 ;число пересылаемых байтов
.
.
.
rep movsw ;пересылка за один раз блока слов
.
.
.

сотрет 200 байт 9100 слов), непосредственно расположенных за
блоком назначения.

Использование множественных префиксов
————————————-

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

.
.
.
rep movs es:[DesArray],ss:[SourceArray]
.
.
.

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

Если вам совершенно необходимо использовать строковые команды
с множественными префиксами, на время выполнения этих команд
следует отменить прерывания:

.
.
.
cli
rep movs 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 ASCIITable ;указывает на
;таблицу просмотровую
mov al,[CharacterToTranslate] ;прием искомого байта
xlat [LookUpTable] ;поиск байта
.
.
.

просматривает байт по адресу AL в ASCIITable, а не в LookUp-
Table, но ассемблируется нормально, так как все, что XLAT делает с
переданным ей операндом, это проверка того, что он имеет размер
байта, а также поиск переопределенного сегмента. Команда XLAT
всегда просматривает содержимое адреса смещения BX+AL, независмо
от переданных ей операндов.

Вы забыли о нестандартных побочных эффектах ——————-

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

add bx,[Grade]

прибавляет к содержимому BX 16-битовое значение по адресу
Grade и соответствующим образом обновляет состояния флагов
переполнения, знака, вспомогательного переноса, четности и
переноса, которые отображают результат выполнения сложения.
Некоторые же команды вызывают менее очевидные изменения сстояния
процессора. Приведем краткий обзор таких команд.

Стирание регистра при умножении
——————————-

Умножение — в формате чисел 8×8, 16×16 или 32×32 бит, всегда
разрушает содержимое как минимум одного регистра помимо части
сумматора, который используется как исходный опе ранд. Это
неизбежно, если учесть, что результат умножения 8×8 бит может
иметь размер 16 бит, результат умножения 16×16 бит может иметь
размер 32 бита, а результат умножения 32×32 бита может иметь
размер 64 бита. Исходные и конечные операнды операции умножения
показаны в таблице 6.1.

Исходные и конечные операнды команд MUL и IMUL Таблица 6.1
————————————————————-
Размер в байтах Исходный операнд Операнд назначения
исходного Явное Неявное Старший Младший Пример
операнда задание задание
————————————————————-
8×8 рег8* AL AH AL mul dl
16×16 рег16* AX DX AX imul bx
32×32+ рег32++ EAX EDX EAX mul esi

* рег8 может представлять собой AH,AL,BH,BL,CH,CL,DH или DL.
** рег16 может представлять собой AX,BX,CX,DX,SI,DI,BP или SP.
+ Умножение в формате 32×32 процессорами 8086, 8088, 80186,
80188 или 80286 не поддерживается.
++ рег32 может представлять собой EAX,EBX,ECX,EDX,ESI,EDI,EBP
или ESP.
————————————————————

Хотя это кажется достаточно простым, в синтаксисе команд MUL
и IMUL на первый взгляд операнды задаются недостаточно подробно,
так как явно здесь задаются только один из двух исходных операндов
и размер, в котором выполняется операция; обе же части сумматора,
используемого для хранения второго исходного операнда, и регистры,
используемые для помещения туда результата, берутся неявно, по
умолчанию. Недостаточная подробность синтаксиса этих команд может
привести к ошибочному использованию разрушаемых ими регистров в
других местах программы. Например, часто бывает, что программист
точно помнит о том, что результат, скажем, умножения в формате
16×16 бит помещается в регистр AX, но забывает, что при этом
стирается и регистр DX. Нужно учитывать, что при каждом выполнении
команд MUL и IMUL стирается содержимое не только регистров AL, AX
или EAX, но и AH, DX или EDX.

Вы забыли о том, что строковые команды изменяют состояние
нескольких регистров
———————————————————

Строковые команды (MOVS, STOS, LODS, CMPS и SCAS) при
выполнении каждой из них могут влиять на состояния флагов и на
состояния до трех регистров. Как и в случае команды MUL, многие из
воздействий, оказываемых этими командами, не отображаются явно
операндами этих команд. При использовании строковых команд
помните, что при каждом выполнении такой команды выполняется
инкремент, либо декремент регистров SI или DI, либо обоих этих
регистров сразу (в зависимости от состояния фляга направления). В
случае строковых команд с префиксом REP каждое повторение влечет
декрементирование CX как минимум на единицу, и значение CX может
дойти до нуля.

Вы ожидали, что конкретные команды
изменят состояние флага переноса
———————————-

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

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, а затем перед тем, как перейти к участку программы,
в котором обрабатывается это состояние, принудительно установить
его в ноль. С другой стороны, фрагмент

.
.
.
cmp ax,1
mov ax,ax
jg HandlePositive
.
.
.

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

Вы перепутали адрес памяти операнда
с непосредственно значением операнда
————————————

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

На рис.6.1 показано различие между смещением и значением
переменной памяти. Смещение переменной размером в слово MemLoc
равно 5002h, а само значение MemLoc равно 1234h.

|
|
——————-
3000:4FFE | 0001 |
——————-
3000:5000 | 205F | Значение MemLoc
——————- |
| —— | |
Смещение 3000:5002 | |1234| <---|---------- MemLoc ---- | ------ | | ° | | ------------- ------------------- 3000:5004 | 9145 | ------------------- 3000:5006 | 0000 | ------------------- | | Рис.6.1 Переменные памяти: различие между ссылками на смещение и само значение На Рис.6.1 смещение переменной размером слово MemLoc равно константе 5002h, которое может быть получено оператором OFFSET; например, mov bx,OFFSET MemLoc загружает в BX 5002h. Значение 5002h является непосредственно задаваемым операндом, оно встроено в команду и никогда не изменяется. Значение переменной MemLoc равно 1234h, и оно может быть считано из адреса смещения памяти 5002h сегмента данных. Один из сппособов считывания значения переменной состоит в том, чтобы загрузить в один из регистров - BX, SI, DI или BP смещение MemLoc и затем адресовать память, используя этот регистр. Фрагмент mov bx,OFFSET MemLoc mov ax,[bx] загружает в AX значение MemLoc, 1234h. Другим способом значение MemLoc можно загрузить прямо в AX либо при помощи mov ax,MemLoc либо mov ax,[Memloc] Здесь значение 1234h получается как прямой операнд; в команду MOV встроено смещение 5002h, и эта команда загружает в AX значение из адреса 5002h, которое в данном случае равно 1234h. Следовательно, значение 1234h не является постоянно связанным с переменной MemLoc. Например, mov [MemLoc],5555h mov ax,[MemLoc] загрузит в AX не 1234h, а 5555h. Ключевым моментом здесь является то, что если смещение Memloc представляет собой постоянное значение, описывающее фиксированный адрес сегмента данных, то само значение переменной MemLoc это изменяемое число, хранимое в памяти по данному адресу. Команды mov [MemLoc],1 add [Memloc],2 делают в результате значение MemLoc равным 3, тогда как команда add OFFSET Memloc,2 эквивалентна команде add 5002h,2 которая бессмысленна, поскольку сложить одну непосредственную константу с другой невозможно. На удивление часто в пылу программирования забывают написать оператор OFFSET, например: mov si,Memloc когда требуется смещение memloc. На первый взгляд это не кажется ошибкой, а поскольку MemLoc является переменной с размером в слово, данная строка не вызовет при ассемблировании ошибки. Однако, во время выполнения в SI будут загружены данные из переменной MemLoc (значение 1234h на рис.5.1, стр.269 оригинала) вместо смещения MemLoc (5002h на рис.5.1) - что приведет к непредсказуемым результатам. Гарантированного способа избежать этой ошибки не имеется, но вы можете взять себе за правило заключать все ссылки на память в квадратные скобки. Если ссылки на адресные константы будут записываться с префиксом 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:SI равном 1000:FFF9 и преобразовывает строку символов "Testing" с 1000:FFF9 в заглавные буквы: ; Подпрограмма преобразования букв заканчивающейся нулем ; строки символов в заглавные. ; Вход: DS:SI - указатель строки ; ToUpper PROC NEAR mov al,[si] ;прием следующего символа cmp al,0 ;если ноль ... jz ToUpperDone ;... то обработка строки закончена cmp al,'a' ;это строчная буква? jb ToUpperNext ;это не строчная буква cmp al,'z' ja ToUpperNext ;это не строчная буква and al,NOT 20h ;это строчная буква, ;делаем ее заглавной mov [si],al ;указатель на следующий символ jmp ToUpper ToUpperDone: ret ToUpper ENDP Первый байт, адресуемый | относительно 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 Пример циклического перехода в сегменте После того, как ToUpper обработает первые семь символов строки, SI получит положительное приращение от 0FFFFh до 0. (Вспомните, что SI является 16-битовым регистром и потому не может иметь значение, превышающее 0FFFFh). Нулевой байт, который хранится по адресу 20000h, заканчивающий строку, никогда достигнут не будет; вместо этого ToUpper начнет преобразовывать в заглавные буквы совершенно не связанные с данной строкой байты, хранимые начиная с адреса 10000h и не остановится до тех пор, пока не встретит нулевой байт. Впоследствие изменение значений этих байтов может привести к ошибочной работе программы. Часто бывает очень тудно найти такие ошибки, вызванные случайно измененными в части программы, где произошел циклический переход байтами, так как источник ошибки может быть разнесен с местом ее проявления по времени, и может быть не связан с ним в исходном тексте программы. Простого мнемонического правила здесь не имеется; следует только всегда обращать внимание на то, чтобы случайно не выйти за границу сегмента. Кроме того, опасно (хотя бы с точки зрения беспокойства) пытаться выполнить доступ к слову по адресу 0FFFFh; машина зависнет. Вы не смогли сохранить состояние процессора при активации обработчика прерываний ------------------------------------------- Обработчик прерываний это стандартная подпрограмма, получающая управление при аппаратных прерываниях, например прерываниях клавиатуры. обработчики прерываний выполняют множество действий, например запись в буфер нажатий клавиш или обновление состояния системных часов. Прерывание может произойти в любой момент, во время выполнения программы, поэтому обработчик прерываний обязан сохранить регистры и флаги процессора точно в том состоянии, в котором они находились при входе в обработчик прерывания. Если это не выполняется, то программа, работающая на момент прерывания, может неожиданно обнаружить, что состояние процессора непредсказуемым образом изменилось. Например, если выполнялась программа . . . mov ax,[returnvalue] ret . . . то в момент между этими двумя командами могло произойти прерывание. Если обработчик прерываний не сможет сохранить содержимое AX, то значение, возвращаемое вызывающей программе, будет зависеть не от содержимого переменной ReturnValue, а от того, что поместил в AX обработчик прерываний. Следовательно, каждый обработчик прерываний должен в явном виде сохранить содержимое всех используемых им регистров. Хотя допускается явно сохранять при этом только модифицируемые обработчиком регистры, для надежности все же рекомендуется при входе в обработчик поместить в стек все регистры, и извлечь их из стека на выходе. Дело в том, что когда-нибудь вам может понадобиться изменить исходный текст обработчика прерываний таким образом, что он станет модифицировать ранее не затрагиваемые им регистры, и при этом вы можете забыть добавить команды, сохраняющие эти регистры. В обработчике прерываний сохранять флаги не требуется. Во время прерывания флаги помещаются в стек автоматически, а когда обработчик прерываний выполняет для возврата в прерванную программу команду IRET, флаги автоматически восстанавливаются из стека. Следствие из абсолютной необходимости сохранения обработчиком прерываний значений всех регистров таково: при входе в обработчик прерываний не следует делать каких-либо предположений относительно состояний регистров или флагов. Классическим примером здесь служит обработчик прерываний, в котором строковые команды выполнялись бы без первоначального явного задания состояния флага направления. Помните, что в момент прерывания может выполняться программа любого рода, поэтому после сохранения регистров прерванной программы вы должны сразу же явно установить регистры (включая сегментные регистры) и флаги, которые понадобятся вашей программе для дальнейшей работы. Вы забыли задать переопределения групп в операндах и таблицах данных --------------------------------- Концепция сегментной группы проста и полезна: вы задаете, что несколько сегментов принадлежат определенной группе, и тогда компоновщик объединит их в единый сегмент, после чего адресация всех данных в сгруппированных сегментах будет выполняться относительно одного и того же сегментного регистра. На рис.6.3 показаны три сегмента, Seg1, Seg2 и Seg3, сгруппированные в группу GroupSeg; все три сегмента адресуются одновременно, относительно одного сегментного регистра, в который загружен базовый адрес Groupseg. Смещение 0 в GroupSeg = смещению 0 в Seg1 | / ------------------ <------------ | | | | | Seg1 | | | (длина 8К) | Смещение 2000h в GroupSeg | | | = смещению 0 в Seg2 | | | | | ------------------ <------------ | | | GroupSeg | | Seg2 | | | (длина 12К) | Смещение 5000h в GroupSeg | | | = смещению 0 в Seg3 | | | | | ------------------ <------------ | | | | | Seg3 | | | (длина 6К) | | | | \ ------------------ Рис.6.3 Три сегмента, сгруппированные в одну сегментную группу Все три сегмента адресуются одновременно относительно одного и того же сегментного регистра, загруженного базовым адресом GroupSeg. Сегментные группы позволяют логически разделить данные на ряд областей, при переходе между которыми не требуется загружать сегментный регистр. К сожалению, существует несколько проблем, связанных с обработкой сегментных групп в Microsoft Macro Assembler (MASM), поэтому до появления Turbo Assembler сегментные группы были очень неудобным средством языка ассемблера. Однако обойтись без них было нельзя, так как они совершенно необходимы для компоновки ассемблерной программы с программами на языках высокого уровня, таких как Си. Имеющийся в Turbo Assembler режим Ideal разрешает все описанные выше проблемы, связанные с переопределением групп. Это еще одна причина перехода с программирования в стиле MASM на режим Ideal. Одна из проблем, связанных с работой MASM с сегментными группами, связана с тем, что MASM рассматривает все смещения, полученные оператором OFFSET в данном сгруппированном сегменте, как смещение в сегменте, а не в сегментной группе. Например, в случае сегментной группы, показанной на рис.6.3, ассемблер выполнит ассемблирование mov ax,OFFSET Var1 в mov ax,0 так как Var1 имеет в Seg2 смещение 0, даже если в группе GroupSeg она имеет смещение 2000h. Поскольку для сегментных групп имеется тенденция выполнять адресацию именно относительно группы, а не относительно отдельных сегментов, тем самым возникает проблема. Проблема решается за счет использования префикса переопределения группы. Строка mov ax,OFFSET GroupSeg:Var1 правильно выполняет ассемблирование смещения Var1, вычисляя его относительно сегментной группы GroupSeg. MASM имеет еще одну аналогичную проблему, связанную с таблицами данных, используемыми сегментными группами. Как и в случае оператора OFFSET, смещения, ассемблируемые в таблицы данных, генерируются относительно сегментов, а не сегментных групп. Следующий фрагмент содержит пример такой проблемы: Stack SEGMENT WORD STACK 'STACK' DB 512 DUP(?) ;резервируется память для стека ;размером 1/2K Stack ENDS ; ; Определение сегментной группы данных, ; состоящей из Data1 & Data2 ; DGROUP GROUP Data1, Data2 ; ; Первый сегмент в DGROUP ; Data1 SEGMENT WORD PUBLIC 'DATA' Scratch DB 100h DUP(0) ;стираемый буфер 256 байт 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 DGROUP:BufferPtr ;указывает на указатель буфера ;Примечание: для получения правильно- ;го указателя смещения самого буфера ;нужно задавать ;DGROUP:переопределение группы. mov bx,[bx] ;указывает на сам буфер ; ; (Здесь должна находиться программа, работающая с буфером) ; 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 ;указывает на буфер ;Примечание: для получения правильно- ;го смещения нужно задавать ;DGROUP:переопределение группы. Если при использовании сегментных групп в MASM/режим Quirks опустить префиксы переопределения групп, это может привести к серьезным ошибкам, так как в этом случае программа может начать чтение, модификацию или переходы не к тем областям памяти. Можно взять за общее правило не использовать группы в программах на ассемблере MASM/режим Quirks, если в этом нет крайней необходимости. Если же их использование совершрнно обязательно, а также в случае интерфейса ваших ассемблерных модулей с языками высокого уровня постоянно напоминайте сами себе о том, что при задании смещений любых находящихся в группе данных необходимо использовать префиксы переопределения группы. Этими переопределениями пользоваться легко - нужно только не забывать о них. Полезный метод работы с группированными сегментами в MASM/ режим Quirks состоит в том, чтобы вместо LEA использовать MOV OFFSET. Например, lea ax,Var1 имеет тот же эффект, что и mov ax,OFFSET GroupSeg:Var1 и притом не требует использования префикса переопределения группы. Однако LEA занимает на один байт больше и работает несколько медленнее, нежели MOV OFFSET. Между прочим, для сегментных групп проблемы возникают только со смещениями, но не с доступом к памяти. Строки программы типа mov ax,[Var1] не требуют использования префиксом переопределения сегмента.