Глава 9. Расширенные средства программирования на Turbo Assembler.


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

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

Префиксы переопределения сегментов
——————————————————————

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

.
.
.
mov bx,10h
mov si,5
mov ax,[bx+si+1]
.
.
.

загружает слово, хранящееся со смещением 16h в сегменте, на
который указывает DS, в регистр AX. Иными словами, в AX
загружается содержимое адреса памяти DS:0016.

Исключением из этого правила, состоящего в выполнении
загрузки из сегмента, на который указывает DS, является то, что
строковые команды STOS и MOVS выполняют запись в сегмент, на
который указывает ES, а строковые команды SCAS и CMPS берут
исходные операнды из сегмента, на который указывает ES. (Один из
исходных операндов CMPS это сегмент данных, а другой — это
дополнительный сегмент.)

Другое исключение состоит в том, что любой операнд памяти,
включая BP, выполняет доступ к сегменту, на который указывает SS.
Например,

.
.
.
mov bp,1000h
mov al,[bp+6]
.
.
.

загружает в AL содержимое ячейки памяти SS:10006.

Однако, предположим, что вы желаете обратиться к адресу
сегмента CS, как к операнду памяти; это бывает полезно для
переходов по таблицам, особенно в многосегментных программах. Или
предположим, что вы желаете обратиться к адресу в стеке с помощью
BX, либо адресу в DS с помощью BP, либо к адресу в ES с помощью
не-строковой команды. Можете ли вы это сделать?

Да, можете. Для того, чтобы многие команды могли обращаться к
сегменту по вашему выбору, существуют префиксы переопределения
сегмента. Например,

.
.
.
mov bx,100h
mov cl,ss:[bx+10h]
.
.
.

загрузит в CL содержимое смещения 110h стекового сегмента, а

.
.
.
mov bp,200h
mov si,cs:[bp+1]
.
.
.

загрузит в SI содержимое сегмента 201h кодового сегмента.

В целом, все, что от вас требуется для того, чтобы та или
иная команда могла обратиться к сегменту, не являющемуся сегментом
по умолчанию, это поместить перед операндом памяти в этой команде
префикс переопределения сегмента -CS:, DS:, ES: или SS:.

Префиксы переопределения сегмента иногда не называют
«префиксами» из-за того, что они предшествуют в строке командам
операндам памяти. Однако фактически префикс преопределения
сегмента именно представляет собой байт, предшествующий команде,
модифицирующий ее действие, как префикс REP, рассмотренный выше в
главе 5, представляющий собой также байт, предшествующий команде.
Например, когда 8086 встречает байты команды

A0 00 00

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

mov al,[0]

то он загружает в AL содержимое смещения 0 сегмента данных.
Однако,поскольку значение «ES:префикс переопределения сегмента»
равно 26h, то когда 8086 встречает байты

26 А0 00 00

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

mov al,es:[0]

он загружает в AL содержимое смещения 0 дополнительного
сегмента, а не сегмента данных.

Альтернативная форма ——————————————

Turbo Assembler поддерживает альтернативную форму префикса
переопределения сегмента., в которой этот префикс помещается в
отдельной строке. В этой форме для переопределения сегмента на CS:
используется отдельная строка префикса SEGCS, для DS: — SEGDS, для
ES: — SEGES, и для SS: — SEGSS. Каждый из этих префиксов выполняет
переопределение строки только для следующей за ним строки
программы, и не распространяется на последующие. Например,
следующий фрагмент записывает содержимое DX со смещением 999h в
дополнительный сегмент:

.
.
.
mov si,999h
seges
mov [si],dx
.
.
.

Альтернативная форма полезна в тех случаях, когда требуется
указывать префиксы переопределения сегментов для команд, не
имеющих операндов, например LODSB. Следующий фрагмент загружает AL
из SS:SI:

.
.
.
segss
lodsb
.
.
.

Случаи, когда префиксы переопределения сегмента не действуют —-

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

lods es:[ByteVar]

годится для загрузки AL из ES:SI, но

stos ds:[VyteVar]

не сработает. Если вы попытаетесь переопределить доступ
строковой команды к дополнительному сегменту, как в приведенном
выше примере, то Turbo Assembler укажет на недопустимость такого
действия. Однако, если вы используете префикс SEGCS или подобный
ему для создания переопределения сегмента, Turbo As- sembler не
будет знать, для какой команды вы выполняете переопределение, и не
выдаст в таком случае сообщение об ошибке. Например,

.
.
.
segds
stosb
.
.
.

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

Аналогичным образом, имейте в виду, что префиксы
переопределения сегмента не действуют на обращения к стеку.
Команды помещения в стек всегда выполняются со стековым сегментом,
и команды извлечения также всегда выполняются со стековым
сегментом. Например, команда типа:

.
.
.
segcs
push [bx]
.
.
.

использует префикс переопределения сегмента для выбора
сегмента, из которого должно извлекаться значение, помещаемое этой
командой в стек; Оно записывается, как и обычно, в смещение SP-2
стекового сегмента. Аналогичным образом, команды всегда
извлекаются из сегмента, на который указывает CS.

Как правило, следует всегда избегать одновременного
использования префиксов переопределения сегмента с префиксами REP,
поскольку здесь могут возникнуть проблемы в случае прерываний
команды, в которой оба эти префикса используются. (Подробности см.
в главе 6.)

Доступ к нескольким сегментам ———————————

Префиксы переопределения сегмента полезны в случае
необходимости доступа к нескольким сегментам. Такая необходимость
может возникнуть , например, в том случае, если вам нужно
обратиться к данным, хранимым и в стеке, и в сегменте данных, что
обычно происходит, когда стек используется для динамически
распределяемых переменных, а сегмент данных — для статических
переменных. Другая возможность состоит в том, что программа может
просто иметь объем данных свыше 64Кб, и вследствие этого
необходимость одновременного доступа к нескольким сегментам.

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

.
.
.
mov ax,SEG StringToConvert
mov es,ax
mov di,OFFSET StringToConvert
;ES:DI указывает на преобразуемую
;строку
cld ;устанавливает для команды STOSB
;режим инкрементирования счетчика
ConvertLoop:
mov al,es:[di] ;прием следующего символа
and al,al ;это конец строки?
jz ConvertLoopDone
;да, конец работы
cmp al,20h ;требуется ли преобразование?
jnb SaveChar ;нет, сохранить символ
mov al,’ ‘ ;сдельть его пробелом
SaveChar:
stosb ;сохранить данный символ и устано-
;вить указатель на следующий
jmp ConvertLoop ;перейти к проверке следующего
ConvertLoopDone:
stosb ;завершить строку нулем
.
.
.

Локальные метки
——————————————————————

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

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

.
.
.
Sub1 PROC
sub ax,ax
IntCountLoop:
add ax,[bx]
inc bx
inc bx
loop IntCountLoop
ret
Sub1 ENDP
.
.
.
Sub2 PROC
sub ax,ax
mov dx,ax
LongCountLoop:
add ax,[bx]
adc dx,[bx+2]
add bx,4
loop LongCountLoop
ret
Sub2 ENDP
.
.
.

Если два раздела программы выполняют аналогичные функции,
часто случается, что они содержат одинаковые метки. Например,
каждая из процедур Sub1 и Sub2 содержит метку, обозначающую начало
цикла счета.

Если меток во всей программе немного, легко обеспечить, чтобы
все они были отличны друг от друга. Однако, в больших программах
необходимость избегать одинаковых названий меток может
представлять собой большое неудобство. Кроме того, обычной
практикой является взять работающую подпрограмму, скопировать ее
блоком в свою программу и модифицировать, сделав из нее другую
подпрограмму. Здесь возникает проблема, поскольку легко забыть о
том, что необходимо везде поменять названия меток, и программа
может ошибочно выполнить передачу управления на метку в старой
подпрограмме. Например, если вы скопировали и модифицировали Sub1,
сделав из нее Sub2, то вы вполне можете непреднамеренно в конце ее
оставить:

.
.
.
Sub2 PROC
sub ax,ax
mov dx,ax
LongCountLoop:
add ax,[bx]
adc dx,[bx+2]
add bx,4
loop IntCountLoop
ret
Sub2 ENDP
.
.
.

в результате чего переход будет сделан в середину Sub1 — что
вызовет потенциально опасные для работы системы последствия.

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

В этом заключается суть локальных меток. Локальные метки, по
умолчанию обычно начинающиеся двумя символами @@, имеют область
определения, ограниченную диапазоном команд между двумя
не-локальными метками. (Не-локальные метки это те, которые при
помощи директивы PROC, а также метки, заканчивающиеся двоеточием и
не начинающиеся двумя символами @@). Что касается Turbo Assembler,
то локальные метки для него даже не существуют вне диапазона,
ограниченного двумя не-локальными метками.

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

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

.
.
.
LOCALS
Sub1 PROC
sub ax,ax
@@CountLoop:
add ax,[bx]
inc bx
inc bx
loop @@CountLoop
ret
Sub1 ENDP
.
.
.
Sub2 PROC
sub ax,ax
mov dx,ax
@@CountLoop:
add ax,[bx]
adc dx,[bx+2]
add bx,4
loop @@CountLoop
ret
Sub2 ENDP
.
.
.

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

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

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

.
.
.
LOCALS
cmp al,’A’
jnz @@P1
jmp HandleA
@@P1:
cmp al,’B’
jnz @@P2
jmp HandleB
@@P2:
cmp al,’C’
jnz @@P3
jmp HandleC
@@P3:
.
.
.

Благодаря локальным меткам можно не беспокоиться о том, нет
ли где-нибудь еще в данной программе меток типа P1.

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

.
.
.
Sub1 PROC NEAR
.
.
.
LOCALS
@@CountLoop:
add ax,[bx]
jnz NotZero
inc dx
NotZero:
inc bx
inc bx
loop @@CountLoop
.
.
.

Дело здесь в том, что имеется не-локальная метка NotZero,
которая находится между ссылокой на локальную метку @@CountLoop в
команде LOOP и самим определением @@CountLoop. Область определения
локальной переменной распространяется исключительно до ближайшей
не-локальной метки, поэтому когда Turbo Assembler будет
ассемблировать команду LOOP, то локальная метка @@CountLoop не
будет найдена.

Можно заменить обычный префикс локального символического
имени (@@) на любые другие два символа, которыми вы желаете
начинать локальные символические имена. Для этого они передаются в
качестве аргумента директиве LOCALS:

LOCALS__

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

Изменив префикс локальных символических имен таким способом,
вы тем самым автоматически разрешаете их использование, как если
бы директива LOCALS не имела аргумента. Кроме того, если вы в
дальнейшем отменили использование локальных символических имен
директивой NOLOCALS, то Turbo Assembler запомнит заданный вами
префикс переопределения сегмента. Это позволяет вам просто
использовать затем LOCALS без аргументов, восстановив тем самым
локальные символические имена с префиксом, заданным ранее.

Автоматическое определение размера перехода
——————————————————————

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

Разумеется, сегодня такие команды условного перехода также
имеются, и в них есть как хорошие, так и плохие стороны. Иногда
команды условного перехода 8086 позволяют получить очень
компактный код (поскольку они имеют длину всего 2 байта), однако в
некоторых случаях они могут дать и громоздкий, неэффективный код,
так как если адрес назначения условного перехода находится слишком
далеко, чтобы его можно было задать 1-байтовым смещением, то для
этого могут понадобиться 5-байтовые последовательности команд
типа:

.
.
.
jnz NotZero
jmp IsZero
NotZero:
.
.
.

И что еще хуже, нельзя расчитать заранее, «достанет» ли тот
или иной условный переход до заданной метки, в результате чего
приходится либо пытаться либо непосредственно переходить на эту
метку, что несет в себе риск ошибки ассемблирования, либо
программировать условный переход в обход безусловного перехода, а
на это тратится 3 байта, и кроме того, замедляется выполнение.
Также очень раздражает и часто случается, что, добавив в цикл одну
или две команды, вы начинаете получать сообщение об ошибке
Relative jump out of range (Переход по относительному смещению
лежит вне допустимого диапазона».

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

Как работает средство автоматического определения размера
перехода? Рассмотрим следующий фрагмент:

.
.
.
JUMPS
RepeatLoop:
jmp SkipOverData
DB 100h DUP (?)
SkipOverData:
.
.
.
dec dx
jnz RepeatLoop
.
.
.

Ясно, что команда JNZ в конце цикла не достанет RepeatLoop,
поскольку между ними лежит свыше 256 байт. Так как была задана
директива JUMPS, ошибки ассемблирования не произойдет. Фактически
Turbo Assembler ассемблирует данный фрагмент эквивалентно
следующему:

.
.
.
RepeatLoop:
jmp SkipOverData
DB 100h DUP (?) ;временное хранение данных в CS
SkipOverData:
.
.
.
dec dx
jz $+5
jmp RepeatLoop
.
.
.

автоматически используя в конце цикла вместо JNZ команды JZ и
JMP.

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

.
.
.
JUMPS
RepeatLoop:
add BYTE PTR [bx],1
inc bx
dec dx
jnz RepeatLoop
.
.
.

Как было отмечено выше, средство Turbo Assembler для
автоматического определения размера перехода не решает всех
проблем 8086, связанных с условными переходами. Turbo Assembler
всегда очень точно выполняет автоматическое определение размера
перехода в обратном направлении (перехода к меткам, стоящим в
тексте программы выше данной команды перехода), редко когда
расходуя лишний байт или команду.

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

Из последующего материала вам станет ясно, почему
автоматическое определение размера прямых переходов не во всех
случаях приводит к генерированию оптимального кода. Когда Turbo
Assembler встречает команду условного перехода, выполняющую ссылку
на метку в прямом направлении, он не может знать, как далеко от
текущей позиции находится данная метка; кроме того, к этому
моменту Turbo Assembler вообще еще не встретил этой метки. Если
средство автоматического определения размера включено, то Turbo
Assembler в случае, когда метка назначения находится достаточно
близко и может быть прочитана непосредственно, генерирует условный
переход (2-байтовую команду), и условный переход в обход
безусловного перехода (2-байтовая команда, за которой следует
3-байтовая команда) в противном случае. К сожалению, в тот момент,
когда Turbo Assembler встречает команду прямого условного
перехода, он еще не знает, должен ли он использовать 2-байтовую
команду, или же 5-байтовую пару команд.

Тем не менее, Turbo Assembler обязан выбрать какой-либо
размер команды именно в этот момент, чтобы знать, где
ассемблировать последующие команды. Следовательно, Turbo Assembler
обязан сделать такой выбор, который будет более безопасен, и
зарезервировать 5 байт для пары условного/безусловного переходов.
Далее, если Turbo Assembler, встретив метку назначения,
определяет, что 2-байтовой команды условного перехода будет
достаточно, то ассемблирована будет команда условного перехода,
после которой будет вставлено три пустых команды NOP, чтобы
заполнить 5 зарезервированных байт.

Предположим, что Turbo Assembler ассемблирует следующее:

.
.
.
JUMPS
jz DestLabel
inc ax
.
.
.

Если JZ не может непосредственно достать до DestLabel, то
Turbo Assembler ассемблирует эквивалент следующего:

.
.
.
jnz $+5 ;длина 2 байта
jmp DestLabel ;длина 3 байта
inc ax
.
.
.

С другой стороны, если JZ может достать до DestLabel
непосредственно, то Turbo Assembler ассемблирует следующее:

.
.
.
jz DestLabel ;длина 2 байта
nop ;каждая команда NOP имеет длину 1 байт
nop
nop
inc ax
.
.
.

Главное здесь заключается в том, что на каждый автоматически
определяемый условный переход в прямом направлении Turbo Assembler
тратит 5 байт, поэтому в тех случаях, когда такая команда достает
до метки назначения, в код вставляется три команды NOP. Эти три
команды NOP занимают место и требуют время на их выполнение (в
8086 на выполнение каждой такой команды затрачивается 3 цикла
процессора). Следовательно, в тех случаях, когда вопросы размера
кода и скорости его выполнения особенно важны для вас,
рекомендуется осторожно подходить к использованию средства
автоматического определения размера условного перехода в прямом
направлении, либо использовать опцию многопроходной обработки.

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

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

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

.
.
.
LoopTop:
.
.
.
lodsb
cmp al,80h
NOJUMPS
jb SaveByteValue
neg al
SaveByteValue:
stosb
.
.
.
dec
dec dx
JUMPS
jnz LoopTop
.
.
.

Здесь мы явно задали 2-байтовый условный переход в прямую
сторону к SaveByteValue, но предоставили Turbo Assembler самому
выбрать лучший код для обратного перехода к LoopTop.

Прямые ссылки к коду и данным
——————————————————————

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

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

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

.
.
.
jmp DestLabel
.
.
.
DestLabel:
.
.
.

Когда Turbo Assembler встречает строку

jmp DestLabel

он еще не встречал определения метки DestLabel;
следовательно, Turbo Assembler не знает, является ли метка
DestLabel ближней или дальней, а если метка ближняя, то он не
знает, лежит ли она в пределах 1-байтового смещения, или же это
число должно быть задано как полное 2-байтовое смещение.
Следовательно, Turbo Assembler вынужден сделать предположение
относительно природы метки DestLabel, прежде чем он сможет
продолжить ассемблирование.

Turbo Assembler мог предположить, что метка DestLabel
является дальней и зарезервировать 5 байтов для дальней команды
перехода JMP; однако, большинство переходов представляет собой
3-байтовые ближние переходы, а тратить на каждую прямую ближнюю
ссылку лишние 2 байта жалко. И напротив, Turbo Assembler мог
предположить, что DestLabel может быть достигнута при помощи
1-байтового смещения, и зарезервировать 2 байта для команды
короткого перехода JMP SHORT; проблема здесь в том, что многие
переходы не являются короткими, и если Turbo Assembler резервирует
только 2 байта, то в случае ближнего или дальнего перехода
возникнет ошибка.

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

С другой стороны, если прямая ссылка оказалась короткой,
Turbo Assembler ассемблирует короткий переход, вставляет команды
NOP для заполнения трех байтов, которые были зарезервированы для
этого перехода, и тем самым теряется байт. Например, Turbo
Assembler ассемблирует следующее:

.
.
.
jmp DestLabel
DestLabel:
.
.
.

в

.
.
.
jmp DestLabel
nop
DestLabel:
.
.
.

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

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

Если Turbo Assembler знает, какого рода переход должен быть
сделан — SHORT, NEAR или FAR — то генерируется наиболее
эффективный код. Следовательно, для коротких прямых переходов
хорошо использовать оператор SHORT (и разумеется, для дальних
прямых переходов требуется задавать FAR PTR.)

Команды перехода — это не единственные команды, где следует
избегать прямых ссылок; прямые ссылки к данным могут также иметь
следствием неэффективность кода. Рассмотрим следующий пример:
.
.
.
.CODE
.
.
.
mov bl,Value
.
.
.
Value EQU 1
.
.
.

Когда Turbo Assembler встречает команду MOV, он не может
знать, является ли Value меткой равенства, или же переменной
памяти. Если Value — это переменная памяти, то требуется 4-
байтовая команда, а если метка равенства, то (используемая в
качестве константы), то 2-байтовая команда.

Как всегда, Turbo Assembler, чтобы иметь возможность
продолжить ассемблирование, предполагает худший случай и поэтому
резервирует для команды MOV 4 байта. Затем, если ассемблер дойдет
до метки Value и обнаружит, что это не переменная памяти, а метка
равенства, Turbo Assembler должен вернуться к команде MOV и
сделать ее 2-байтовой, с операндом-константой, и вставить две
команды NOP, чтобы заполнить третий и четвертый байты, ранее
зарезервированные. Отметим, что ни одна из них не была бы
вставлена, если бы метка Value была определена до команды MOV,
поскольку тогда Turbo Assembler знал бы, что Value не является
переменной памяти.

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

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

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

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

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

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

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

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

Блоки повторения ————————————————

Блоки повторения начинаются директивой REPT и заканчиваются
директивой ENDM. Участок программы, заключенный в блоке
повторения, ассемблируется число раз, заданное в качестве операнда
директивы REPT. Например,

.
.
.
REPT 10
DW 0
ENDM
.
.
.

генерирует такой же код, как и

.
.
.
DW 0
DW 0
DW 0
DW 0
DW 0
DW 0
DW 0
DW 0
DW 0
DW 0
.
.
.

Данное средство не кажется слишком мощным, тем более что

DW 10 DUP (0)

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

.
.
.
IntVal = 0
REPT 10
DW IntVal
IntVal =IntVal+1
ENDM
.
.
.

Тем самым будет сгенерировано следующее:

.
.
.
DW 0
DW 1
DW 2
DW 3
DW 4
DW 5
DW 6
DW 7
DW 8
DW 9
.
.
.

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

Замечательное применение REPT состоит в генерировании таблиц,
используемых для быстрого выполнения умножения или деления.
Например, следующий код очень быстро умножает число со значением
от 0 до 99 (хранимое в BX) на 10 и помещает результат в AX.

.DATA
TableOfMultiplesOf10 LABEL WORD
BaseVal = 0
REPT 100
DW BaseVal
BaseVal = BaseVal+10
ENDM
.
.
.
.CODE
.
.
.
shl bx,1 ;подготовка к просмотру в таблице
;элементов размером в слово
mov ax,[TableOfMuliplesOf10+bx]
;просмотр результата умножения
;на 10
.
.
.

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

Это означает, что любой допустимый ассемблерный код, включая
команды, можно поместить в блок повторения. Например, следующее
сгенерирует код, выполняющий деление 32-битового значения без
знака в DX:AX на 16:

.
.
.
REPT 4
shr dx,1
rcr ax,1
ENDM
.
.
.

Блоки пповторения могут быть вложенными. Например, следующий
фрагмент сгенерирует 10 команд NOP:

.
.
.
REPT 5
REPT 2
nop
ENDM
ENDM
.
.
.

Блоки повторения и переменные параметры
—————————————

Имеется два способа передачи каждому проходу блока повторения
переменного параметра, а именно IRP и IRPC.

IRP подставляет первый элемент в списке в качестве параметра
для первого повторения блока, второй элемент для второго
повторения и т.д., до конца списка. Например,

.
.
.
IRP PARM,<0,1,4,9,16,25>
DB PARM
ENDM
.
.
.

генерирует

.
.
.
DB 0
DB 1
DB 4
DB 9
DB 16
DB 25
.
.
.

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

.
.
.
IRPC TEST_CHAR,azklq
cmp al,’&TEST_CHAR&’
jz EndCompare
ENDM
EndCompare:
.
.
.

В данном примере для выполнения оценки параметра блока
повторения TEST_CHAR используется знак амперсенд (&), даже в
кавычках. Этот знак является оператором макросов, который работает
в блоках повторения, поскольку фактически блоки повторения
представляют собой разновидность макросов. Другие средства
макросов, например директивы LOCAL и EXITM, также работают в
блоках повторения. Ниже мы рассмотрим макросы.

Макросы ——————————————————-

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

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

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

Следующий фрагмент использует макрос MULTIPLY_BY_4, которая
умножает значение из AX на 4 и записывает результат в DX:AX:

.
.
.
MULTIPLY_BY_4 MACRO
sub dx,dx
shl ax,1
rcl dx,1
shl ax,1
rcl dx,1
ENDM
.
.
.
mov ax,[MemVar]
MULTIPLY_BY_4
mov WORD PTR [Result],ax
mov WORD PTR [Result+2],dx
.
.
.

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

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

.
.
.
MultiplyBy4 PROC
sub dx,dx
shl ax,1
rcl dx,1
shl ax,1
rcl dx,1
ret
MultiplyBy4 ENDP
.
.
.
mov ax,[MemVar]
call MultiplyBy4
mov WORD PTR [Result],ax
mov WORD PTR [Result+2],dx
.
.
.

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

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

Какого рода гибкость обеспечивают макросы? Гибкость макросов
ограничивается исключительно вашим воображением, поскольку макросы
могут принимать параметры и содержать директивы условного
ассемблирования. Параметры макросов задаются в виде операндов
директивы MACRO. Например, VALUE и LENGTH — это параметры макроса
FILL_ARRAY, определенной следующим образом:

.
.
.
FILL_ARRAY MACRO VALUE,LENGTH
REPT LENGTH
DB VALUE
ENDM
ENDM
.
.
.

При запуске макроса передаваемые ей параметры должны быть
помещены в качестве операндов. Например, FILL_ARRAY можно
запустить в виде:

.
.
.
ByteArray LABEL BYTE
FILL_ARRAY 2,9
.
.
.

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

.
.
.
ByteArray LABEL BYTE
FILL_ARRAY 2,9
.
.
.

вызывает ассемблирование следующего кода:

.
.
.
ByteArray LABEL BYTE
REPT 9
DB 2
ENDM
.
.
.

Значения фактических параметров, задаваемых при запуске
макроса, подставляются вместо формальных параметров в определении
макроса, поэтому различные коды макроса получаются просто
изменением фактических параметров, заданных при запуске макроса.
Например, если вы желаете инициализировать ByteArray длиной 8
байтов значением 0FFh, а ByteArray2 длиной 100 байтов значением 0,
вам нужно просто записать:

.
.
.
ByteArray LABEL BYTE
FILL_ARRAY 0fff,8
ByteArray2 LABEL BYTE
FILL_ARRAY 0,100h
.
.
.

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

.
.
.
PUSH_WORD_REG MACRO RLETTER
push RLETTERx
ENDM
.
.
.

Turbo Assembler не может знать, является ли строка RLETTER,
встроенная в RLETTERx, именем формального параметра, или же частью
операнда PUSH, и поэтому предполагает, что это часть операнда. При
этом помещение в стек RLETTERx не произойдет, если у вас нет
переменной памяти с этим именем, поэтому желаемый результат
помещения в стек регистра не будет достигнут в любом случае.

Решение этой проблемы состоит в том, чтобы заключить имя
формального параметра в пару амперсендов (&&). Встретив в мак росе
некоторый текст, заключенный между амперсендами, Turbo Assembler
сначала проверяет, не является ли этот текст именем формального
параметра, и если является, то подстанавливает значение этого
параметра.

(Если не является, то Turbo Assembler игнорирует амперсенды.)

Например, следующее расширение PUSH_WORD_REG,

.
.
.
Push_WORD_REG MACRO RLETTER
push &RLETTER&x
ENDM
.
.
.
PUSH_WORD_REG b
.
.
.

ассемблируется в

push bx

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

PUSH_WORD_REG MACRO REGISTER
push REGISTER
ENDM
.
.
.

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

Вложенность макросов
———————

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

.
.
.
PUSH_WORD_REG MACRO REGISTER
push REGISTER
ENDM
.
.
.
PUSH_ALL_REGS MACRO
IRP REG,
PUSH_WORD_REG REG
ENDM
ENDM
.
.
.

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

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

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

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

.
.
.
MULTIPLY MACRO FACTOR
;
; Проверка коэффициента FACTOR на предмет, не является ли он
; одной из 16 возможных степеней двух
;
IS_POWER_OF_TWO = 0
COUNT = 15
POWER_OF_TWO = 8000h
REPT 16
IF POWER_OF_TWO EQ FACTOR
IS_POWER_OF_TWO = 1 ;фактор есть степень двух
IXITM
ENDIF
COUNT = COUNT-1
POWER_OF_TWO = POWER_OF_TWO SHR 1
ENDM
IF IS_POWER_OF_TWO
sub dx,dx
REPT count
shl ax,1
rcl dx,1
ENDM
ELSE
mov dx,FACTOR
mul dx
ENDIF
ENDM
.
.
.

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

MULTIPLY 10

ассемблируется в

.
.
.
mov dx,10
mul dx
.
.
.

а код

MULTIPLY 8

ассемблируется в

.
.
.
sub dx,dx
shl ax,1
rcl dx,1
shl ax,1
rcl dx,1
shl ax,1
rcl dx,1
.
.
.

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

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

Прекращение расширения при помощи директивы EXITM
————————————————-

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

Shiftn MACRO OP,N
Count = 0
REPT N
shl OP,N
Count = Count + 1
IF Count GE 8 ;допустимо не более 8
EXITM
ENDIF
ENDM

Определение меток в макросах
—————————-

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

.
.
.
DO_DEC MACRO
jcxz SkipLabel
dec cs
SkipLabel:
ENDM
.
.
.
DO_DEC
.
.
.
DO_DEC
.
.
.

К счастью, Turbo Assembler дает простое решение этой проблемы
в виде директивы LOCAL. Использование в данном макросе директивы
LOCAL ограничивает область определения данной метки только этим
макросом. Например, для того, чтобы последний пример мог быть
успешно ассемблирован, директива LOCAL может быть использована
следующим образом:

.
.
.
DO_DEC MACRO
LOCAL SkipLabel
jcxz SkipLabel
dec cs
SkipLabel:
ENDM
.
.
.

Если LOCAL используется в макросе, то она должна стоять
непосредственно вслед за директивой MACRO. Одной директивой LOCAL
можно объявить локальными несколько меток; также можно
использовать несколько директив LOCAL:

.
.
.
TEST_MACRO MACRO
LOCAL LoopTop,LoopEnd,SkipInc
LOCAL NoEvent,MacroDone
.
.
.
ENDM
.
.
.

Имена, фактически присваиваемые локальным меткам, имеют вид:

??XXXX

где XXXX — это шестнадцатиричное число от 0 до 0FFFFh.
Следовательно, вы не должны присваивать своим собственным меткам
имена, начинающиеся с ??, так как это может привести к конфликту
между ними и локальными метками, генерируемыми Turbo Assembler.

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

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

Существует несколько директив условного ассемблирования,
специально предназначенных для использования в макросе; сюда
входят директивы IFDIF, IFIDN, IFDIFI, IFIDNI, IFB и IFNB. Имеется
также несколько директив условного генерирования состояния ошибки,
которые предназначены для использования в макросах, включая
ERRDIF, ERRIDN, ERRDIFI, ERRIDNI, ERRB и ERRNB.

Информацию об этих директивах см. в главе 6 данного
руководства и в главе 3 Справочного руководства.

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

& Операция подстановки
<> Операция литеральной текстовой строки
! Операция символа в кавычках
% Операция оценки выражения
;; Отмена комментария

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

Эта и другие специальные операции более подробно
рассматриваются в главе 2 Справочного руководства.

Сложные структуры данных
——————————————————————

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

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

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

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

В главе 11 вы узнаете об улучшенных средствах режима Ideal
более подробно.

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

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

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

Те из вас, кто знаком с языком Си, увидят, что директива
STRUC аналогична оператору struct в Си.

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

CLIENT STRUC
NAME DB ‘Здесь находится имя ….’
AGE DW ?
INCOME DD ?
CLIENT ENDS

Структура CLIENT имеет три поля: NAME, в котором содержится
имя клиента длиной до 20 символов; AGE, в котором содержится
возраст, хранимый в виде 16-битового значения, и INCOME, в котором
содержится доход, хранимый в виде 32-битового значения.

Структуру CLIENT можно использовать следующим образом:

.
.
.
CLIENT STRUC
NAME DB ‘Здесь находится имя ….’
AGE DW ?
INCOME DD ?
CLIENT ENDS
.
.
.
.DATA
MisterBark CLIENT <'John Q. Bark',32,10000>
.
.
.
.CODE
.
.
.
mov ax,[MisterBark.Age]
mov bx,OFFSET MistrBark
mov ax,WORD PTR [bx.INCOME]
mov dx,WORD PTR [bx.INCOME+2]
.
.
.

Данный пример следует рассмотреть внимательно. Во-первых,
отметим, что определение структуры заканчивается директивой ENDS.
Это та же самая директива, которая заканчивает определения
сегментов. Допускается вложение определений структур в определения
сегментов. Например, ниже показано определение структуры в
сегменте данных:

.
.
.
_Data STRUCT WORD PUBLIC ‘DATA’
.
.
.
Test STRUCT
.
.
.
Test ENDS
.
.
.
_Data ENDS
.
.
.

Во-вторых, отметим, что переменная MisterBark типа структуры
CLIENT создается таким образом, как если бы существовал новый тип
данных с именем CLIENT, и фактически именно этот тип вы и создали,
определив структуру CLIENT. Действительно, использовав для
структуры CLIENT операцию SIZE, вы получите число 26, то есть
размер структуры.

При создании MisterBark в угловых скобках объявлению
передается три параметра. Эти параметры становятся исходными
значениями соответствующих полей MisterBark; строка символов ‘John
Q. Bark’ — это исходное значение поля NAME, 32 — это исходное
значение поля AGE, а 10,000 — это исходное значение поля INCOME.

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

MisterBark CLIENT <>

не инициализирует никаких полей MisterBark, а

MisterBark CLIENT <,,19757>

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

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

В следующем примере при создании MisterBark исходное значение
задается только для одного поля MisterBark — поля NAME. Однако,
при определении структуры CLIENT задается исходное значение для
поля AGE, поэтому оно и будет являться значением, назначенным полю
AGE MisterBark. Для поля INCOME нигде никакого значения не
назначалось, и поэтому поле INCOME инициализируется в 0. Вот этот
пример:

.
.
.
CLIENT STRUC
NAME DB ‘Здесь находится имя ….’
AGE DW 21
INCOME DD ?
CLIENT ENDS
.
.
.
.DATA
MisterBark CLIENT <'John Q. Bark'>
.
.
.

Результатом выполнения данного фрагмента явится то, что поле
NAME будет инициализировано значением ‘John Q. Bark’, поле AGE
будет инициализировано значением 21, а поле INCOME будет
инициализировано значением 0. Отметим, что исходное значение поля
NAME, заданное при создании MisterBark, переопределит исходное
значение, заданное при определении структуры CLIENT.

Инициализация массивов или структур может выполняться при
помощи операции DUP. Например,

Clients CLIENT 52 DUP (<>)

создает массив Clients, состоящий из 52 структур типа CLIENT,
каждая из которых инициализируется значениями по умолчанию.

Если вернуться к исходному примеру структуры, то вы увидите
там новую операцию — операцию «точка» (.). Эта операция фактически
представляет собой дополнительную форму операции «плюс» для
адресации памяти; таким образом, все приводимые ниже строки
выполняют одно и то же:

.
.
.
mov ax,[bx.AGE]
mov ax,[bx].AGE
mov ax,[bx+AGE]
mov ax,[bx]+AGE
.
.
.

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

Поля структуры, определенные директивой STRUCT, фактически
представляют собой метки, равные смещению этих полей в структуре.
С учетом ранее сделанного определения CLIENT и MisterBark
следующие две строки эквивалентны:

.
.
.
mov [MisterBark.AGE],ax
mov [MisterBark+20],ax
.
.
.

а также будет вполне допустимо следующее:

.
.
.
AGE_FIELD EQU 20
.
.
.
mov [MisterBark+Age_FIELD],ax
.
.
.

Достоинства и недостатки использования директивы STRUC
——————————————————

Зачем нужна директива STRUC? С одной стороны, поля структуры
обеспечивают типизирование данных; Turbo Assembler знает, что в
первом примере переменная MisterBark.AGE — это переменная размером
в слово, поскольку там AGE является элементом структуры, однако во
втором примере MisterBark+AGE не содержит встроенного размера.

С другой стороны, гораздо проще изменить элемент структуры,
нежели смещения-константы или даже набор директив равенства.
Например, если вы решили, что поле NAME должно иметь длину в 30
символов, все, что для этого нужно сделать, это изменить в
определении CLIENT строку для поля NAME. Если бы вы использовали
директивы равенства, вам бы пришлось вручную вычислить и изменить
смещения полей AGE и INCOME; для больших структур эта работа
достаточно трудоемка.

И наконец, STRUC упрощает создание и инициализацию структур
данных.

Короче говоря, директива STRIC является удобным и облегчающим
сопровождение средством создания и доступа к структурам данных. С
другой стороны, структуры данных в азыке ассемблера не имеют такой
защищенности от возможных ошибок, как структуры данных в Си.
Например, когда вы используете в качестве указателя на структуру
данных регистр, Turbo Assembler не может знать, содержит ли этот
регистр указатель на допустимую структуру данных этого типа. В
следующем примере в BX загружено значение 0, но Turbo Assembler не
знает, находится ли в адресе смещения 0 допустимая структура
данных CLIENT:

.
.
.
mov bx,0
mov dx,[bx.AGE]
.
.
.

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

Уникальность имен полей структуры
———————————

Неприятным следствием из того факта, что имена полей
структуры в действительности представляют собой метки, является
то, что имена полей структуры обязаны быть уникальными в исходном
модуле программы. Например, если вы определили в данном исходном
модуле структуру CLIENT, вы не имеете права иметь где-либо в этом
модуле еще одну метку с именем INCOME, даже в другой структуре.
INCOME — это просто метка со значением 22, и конечно, иметь в
одном и том же исходном модуле две метки с одинаковым именем не
разрешается. Следующий фрагмент вызовет ошибку вследствие попытки
повторного определения AGE:

.
.
.
CLIENT STRUC
NAME DB ‘Здесь находится имя ….’
AGE DW ?
INCOME DD ?
CLIENT ENDS
.
.
.
AGE EQU 21
.
.
.

Вложенность структур
———————

Структуры могут быть вложенными; например, в

.
.
.
.DATA
.
.
.
AGE_STRUC STRUC
YEARS DW ?
MONTHS DW ?
AGE_STRUC ends
.
.
.
CLIENT STRUC
NAME DB ‘Здесь находится имя ….’
AGE AGE_STRUC <>
INCOME DD ?
CLIENT ENDS
.
.
.
.CODE
.
.
.
mov dx,[MisterBark.AGE.MONTHS]
mov si,OFFSET MisterBark
mov cx,[si.AGE.Years]
.
.
.

структура AGE_STRUC в качестве поля AGE вложена в структуру
CLIENT, и в переменной MisterBark типа структуры CLIENT через поле
AGE делаются ссылки к полям MONTHS и YEARS.

Инициализация структур
———————-

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

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

.
.
.
TEST STRUC
TEXT DB 30 DUP (‘ ‘)
TEST ENDS
.
.
.
Tstruc TEST <'Test string'>
.
.
.

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

.
.
.
TEST STRUC
TEXT DB ‘Здесь находится строка …….’
TEST ENDS
.
.
.
Tstruc TEST <'Test string'>
.
.
.

В-третьих, в то время, как можно определить несколько
элементов данных как принадлежатие одному полю структуры,
инициализировать можно самое большее один элемент на каждое поле,
при создании экземпляра данной структуры. Например, в следующем
фрагменте при создании TestStruc первый байт поля A
инициализируется в 1, а первый байт поля B инициализируется в 2,
тогда как второй байт каждого поля оставлен равным значению по
умолчанию 0FFh (пробел):

.
.
.
T STRUC
A DB 0ffh,0ffh
B DB 0ffh,0ffh
T ENDS
.
.
.
TestStruc T <1,2>
.
.
.

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

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

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

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

TEST_REC RECORD FLAG:1,FLAG2:1,FLAG3:1,TVAL:12

В данном примере определено три флага с именами FLAG1, FLAG2
и FLAG3, а также поле данных с именем TVAL. Число после двоеточия
задает для каждого поля его размер в битах; каждый из флагов имеет
размер в один бит; TVAL имеет размер 12 бит.

Каким образом поля располагаются в записи? Здесь имеются
сложности. Первое поле, FLAG1, является самым левым (самым
значащим) битом записи. Второе поле, FLAG2, является вторым по
значимости битом записи, и т.д., пока не дойдет до TVAL, которое
заканчивается наименее значащим битом записи. Однако, данная
запись имеет длину всего 15 бит, оставляя один бит в слове
незанятым. (Записи всегда имеют длину точно 8 или 16 бит.) Правило
состоит в том, что в байте или слове запись всегда выравнена по
правому краю.

Как было сказано, это довольно сложно. Ниже приводится
пример, позволяющий прояснить этот вопрос. Запись типа TEST_REC
определена в строке

TRec TEST_REC <1,0,0,52h>

Здесь мы создали переменную TRec типа записи TEST_REC.
Значения в угловых скобках представляют собой исходные значения
соответствующих полей записи, поэтому поле FLAG1 переменной TRec
инициализируется в 1, поля FLAG2 и FLAG3 инициализируются в 0, а
поле TVAL инициализируется в 52h. На рис. 9.1 показаны
расположения и исходные значения четырех полей переменной TRec:

не FLAG1 FLAG3
исполь- | FLAG2 | TVAL
зуется | | | |
—?——?——?——?————?————
TRec | 0 | 1 | 0 | 0 | 52h |
————————————————
Бит 15 14 13 12 11 0

Рис.9.1 Расположения и исходные значения полей TRec

Если общий размет записи (общая сумма длин всех полей) равен
8 битам или менее, запись хранится в байте; в противном случае
запись хранится в слове. Записи длиной свыше 16 бит не
поддерживается, за исключением режима ассемблирования 80386; в
этом случае допускается разотать с записями длиной до 32 бит.

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

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

Например,

TEST_REC RECORD FLAG:1=1,FLAG2:1=0,FLAG3:1,TVAL:12=0fffh

задает значения по умолчанию: 1 для FLAG1, 0 для FLAG2 и
0FFFh для TVAL, а для FLAG3 явного задания значения по умолчанию
не было. Значение по умолчанию любого поля, для которого оно явно
не задавалось, равно 0, поэтому значение по умолчанию для FLAG3
равно 0.

Поэтому, учитывая следующее определение TEST_REC и создание
TRec:

.
.
.
.DATA
.
.
.
TEST_REC RECORD FLAG1:1=1,FLAG2:1=0,FLAG3:1,TVAL:12=0fffh
.
.
.
TRec TEST_REC <,1,,2>
.
.
.

поля записи будут инициализированы следующим образом:

— FLAG1 инициализирован в 1
— FLAG2 инициализирован в 1
— FLAG3 инициализирован в 0
— TVAL инициализирована в 2

Общее значение переменной-записи TRec равно 6002h. Отметим,
что исходные значения, заданные при создании переменной-записи,
переопределяют исходные значения, заданные при определении типа
записи.

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

TRecArray TEST_REC 90 DUP (<1,1,1,0>)

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

Доступ к записям
—————-

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

mov al,[TRec.FLAG2] ; это не работает !!!

однако это неверно. 8086 может работать только с 8- или 16-
битовыми операндами памяти, а способа загрузить, скажем, 1-
битовое поле в регистр, не существует. Единственное, что вы можете
сделать с полями записи, это определить их размер в байтах,
определить, на сколько битов их нужно сдвинуть для выравнивания по
правому краю и затем сгенерировать маски, которые позволят
выделить отдельные биты. Другими словами, хотя сам 8086 и не
позволяет вам работать непосредственно с полями записи, Turbo
Assembler поддерживает манипулирование полями записи при помощи
таких команд, как AND и SHR.

Значение данного поля записи представляет собой число битов,
на которое вы должны сдвинуть запись для выравнивания этого поля
по правому краю (т.е., чтобы поместить бит 0 поля в позицию бита 0
записи). Например,

.
.
.
mov al,FLAG1
mov ah,TVAL
.
.
.

загрузит в AL 14, а в AH 0, поэтому

.
.
.
mov ax,[TRec]
mov cl,FLAG1
shr ax,cl
.
.
.

выравнивает по правому краю поле FLAG1 записи TRece в AX.

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

mov ax,TEST_REC <1,1,1,0fffh>

загрузит в AX 7FFFh, то есть значение, получаемое при
создании записи типа TEST_REC с исходными значениями
<1,1,1,0FFFh>. Следует принимать во внимание различие между
загрузкой в AX типа записи TEST_REC, как это было сделано в
последнем примере, и загрузкой в AX переменной-записи TRec, как в
следующем примере

.
.
.
TEST_REC RECORD FLAG1:1=1,FLAG2:1=0,FLAG3:1,TVAL:12=0fffh
.
.
.
TRec TEST_REC <,1,,2>
.
.
.
.CODE
.
.
.
mov ax,[TRec]
.
.
.

где в AX загружается 6002h, значение переменной TRec.

Операция WIDTH
—————

Операция WIDTH возвращает размер записи или поля записи в
битах. Например, следующая строка запишет в AL 15, число битов в
записи TEST_REC:

mov al,WIDTH TEST_REC ;размер записи TEST_REC в битах

а следующая последовательность строк запишет 1, ширину
каждого из полей флагов, в AL, AH и BL, и 12, ширину поля TVAL, в
BH:

.
.
.
mov al,WIDTH FLAG1
mov ah,WIDTH FLAG2
mov bl,WIDTH FLAG3
mov bh,WIDTH TVAL
.
.
.

Операция MASK
————-

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

mov ax,MASK TEST_REC

запишет в AX 7FFFh, а

.
.
.
mov ax,MASK TEST_REC
mov dx,[TRec]
and dx,ax
.
.
.

запишет значение записи TRec в DX, маскируя тем самым бит 15,
не являющийся частью записи TRec.

Операция MASK еще более полезна для выделения конкретного
поля записи. Следующий фрагмент позволяет определить, установлен
ли флаг в поле FLAG3 записи TRec:

.
.
.
mov ax,[TRec]
and ax,MASK FLAG3
jz Flag3NotSet
.
.
.

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

.
.
.
mov ax,[TRec]
test ax,MASK FLAG3
jz Flag3NotSet
.
.
.

Операцию MASK также полезно использовать для манипулирования
полями записи в сочетании с командами сдвига; это будет
продемонстрировано вам вкратце.

Зачем использовать записи
————————-

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

EQ_FLAG RECORD NUMDISK:2,VIDEO:2,RSRVD:2,MATHCHIP:1,AREDISKS:1

где

— NUMDISKS — число установленных дисководов для гибких дисков,
минус 1.

— VIDEO — указывает на активный в текущий момент дисплей-
ный адаптер.

— RSRVD — поле, резервируемое разными микрокомпьютерами
IBM для различных целей.

— MATHCHIP — равно 1, если установлен математический сопро-
цессор , например 8087.

— AREDISKS — равно 1, если установлен хоть один дисковод
для гибких дисков.

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

;
; Возвращает текущее значение поля дисплейного адаптера
; переменной флагов оборудования BIOS
;
; Вход: отсутствует
;
; Выход:
; AL = 0 если в текущий момент не выбран ни один
; дисплейный адаптер
; = 1 если в текущий момент выбран цветной дисплей 40×25
; = 2 если в текущий момент выбран цветной дисплей 80×25
; = 3 если в текущий момент выбран монохромный дисплей
; 80×25
;
; Разрушаемые регистры: AX,CL,ES
;
EQ_FLAG RECORD NUMDISKS:2,VIDEO:2,RSRVD:2,MATHCHIP:1,AREDISKS:1
;
GetBIOSEquipmentFlag PROC
mov ax,40h
mov es,ax ;ES указывает на сегмент данных BIOS
mov al,es:[10h] ;прием младшего байта флагов
;оборудования
and al,MASK VIDEO ;выделение поля дисплейного адаптера
mov cl,VIDEO ;прием числа байтов, на которое поле
;дисплейного адаптера должно быть
;сдвинуто вправо, чтобы стать вырав-
;ненным по правому краю
shr al,cl ;выравнивание по правому краю поля
;дисплейного адаптера
ret
GetBIOSEquipmentFlag ENDP

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

;
; Устанавливает значение поля дисплейного адаптера
; переменной флагов оборудования BIOS
;
; Вход: AL = 0 если в текущий момент не выбран ни один
; дисплейный адаптер
; = 1 если в текущий момент выбран цветной дисплей 40×25
; = 2 если в текущий момент выбран цветной дисплей 80×25
; = 3 если в текущий момент выбран монохромный дисплей
; 80×25
;
; Выход: отсутствует
;
;
; Разрушаемые регистры: AX,CX,ES
;
EQ_FLAG RECORD NUMDISKS:2,VIDEO:2,RSRVD:2,MATHCHIP:1,AREDISKS:1
;
SetBIOSEquipmentFlag PROC
mov ax,40h
mov es,ax ;ES указывает на сегмент данных BIOS
mov cl,VIDEO ;прием числа байтов, на которое пере-
;данное значение должно быть
;сдвинуто влево, чтобы выравниться с
;полем дисплейного адаптера
shl al,cl ;выравнивание значения
mov al,es:[10h] ;прием младшего байта флагов
;оборудования
and ah,NOT MASK VIDEO
;очистить поле дисплейного адаптера
and al,MASK VIDEO ;убедиться в допустимости нового
;значения поля дисплейного адаптера
or al,ah ;вставить новое значение поля дис-
;плейного адаптера в переменную
;флагов оборудования
mov es:[10h],al ;установка нового флага оборудования
ret
SetBIOSEquipmentFlag ENDP

В данном разделе мы обсудили версию директивы RECORD для
режима MASM. Версия директивы RECORD для режима Ideal несколько
отличается от версии для режима MASM; информацию о режиме Ideal
см. в главе 11.

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

Директива UNION дает возможность обращаться к заданному
адресу памяти как более чем к одному типу данных. Директива UNION
аналогична оператору Си union.

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

.
.
.
FLEX_COUNT UNION
COUNT8 DB ?
COUNT16 DW ?
FLEX_COUNT ENDS
.
.
.

Отметим, что как и в случае директивы STRUC, объявление UNION
должно заканчиваться директивой ENDS.

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

.
.
.
.DATA
Counter FLEX_COUNT
.
.
.
.CODE
.
.
.
mov [Counter.COUNT16],0ffffh
LoopTop:
.
.
.
dec [Counter.COUNT16]
jnz LoopTop
.
.
.
mov [Counter.COUNT8],255
ShortLoopTop:
.
.
.
dec [Counter.COUNT8]
jnz LoopTop
.
.
.

Как и в случае директивы STRUC, для ссылки на поля
объединения используется операция «точка»; вместо нее также можно
использовать операцию «плюс». Ссылка на переменную посредством
полей объединения эквивалентна переопределения ее типа. Предыдущий
пример эквивалентен следующему:

.
.
.
.DATA
Counter DW ?
.
.
.
.CODE
.
.
.
mov WORD PTR [Counter],0ffffh
LoopTop:
.
.
.
dec WORD PTR [Counter]
jnz LoopTop
.
.
.
mov BYTE PTR [Counter],255
ShortLoopTop:
.
.
.
dec BYTE PTR [Counter]
jnz LoopTop
.
.
.

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

Объединения позволяют вложенность в них других объединений и
структур. Например, следующее объединение позволяет доступ к
4-байтовой переменной памяти либо как к указателю сегмент:смещение
размером в двойное слово, либо как к переменной смещения размером
в слово и к переменной сегмента, также размером в слово:

.
.
.
SEG_OFF STRUC
POFF DW ?
PSEG DW ?
SEG_OFF ENDS
.
.
.
PUNION UNION
DPTR DD ?
XPTR SEG_OFF <>
PUNION ENDS
.
.
.
.CODE
.
.
.
mov [bx.XPTR.POFF],si
mov [bx.XPTR.PSEG],ds
.
.
.
les di,[bx.DPTR]
.
.
.

Как и в случае STRUC и RECORD, имена полей, определенных
директивой UNION, это обычные метки, без ограничения области
определения. Следовательно, имена полей объединения должны быть
уникальны в содержащем их исходном модуле.

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

Сегментные директивы
——————————————————————

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

Вспомните, что упрощенные сегментные директивы проще в
использовании, но менее мощны по сравнению со стандартными
сегментными директивами. В следующих разделах мы рассмотрим
стандартные сегментные директивы SEGMENT, GROUP и ASSUME.

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

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

Полная форма директивы SEGMENT имеет вид:

имя SEGMENT выравнивание комбинирование использование ‘класс’

где поля «выравнивание», «комбинирование», «использование»
и «класс» — опции. Далее мы поочередно рассмотрим каждое
из этих полей.

Поля «имя» и «выравнивание»
—————————

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

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

— BYTE использует адрес следующего доступного байта.

— DWORD использует адрес следующего двойного слова.

— PAGE использует адрес следующей страницы (выравнивание по
границе 256 байтов).

— PARA использует адрес следующего параграфа (выравнивание
по границе 16 байтов).

— WORD использует адрес следующего слова.

Если тип выравнивания не задан явно, используется
выравнивание по границе параграфа.

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

Поле «комбинирование»
———————

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

AT PUBLIC
COMMON STACK
MEMORY VIRTUAL
PRIVATE

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

Тип комбинирования AT вызывает помещение начала сегмента в
конкретный адрес памяти. Фактически никакого кода не генерируется;
вместо этого сегменты типа AT используются в качестве шаблона для
доступа к таким областям памяти, как сегмент данных ROM BIOS или
дисплейная память. Например,

.
.
.
VGA_GRAPHICS_MEMORY SEGMENT AT 0A000h
BitMapStart LABEL BYTE
VGA_GRAPHICS_MEMORY ENDS
.
.
.
mov ax,VGA_GRAPHICS_MEMORY
mov es,ax
ASSUME ES:VGA_GRAPHICS_MEMORY
mov di,OFFSET BitMapStart
mov cx,08000h
sub ax,ax
cld
rep stosw
.
.
.

очищает графический экран VGA.

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

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

Тип комбинирования MEMORY — это то же самое, что и тип
комбинирования PUBLIC.

Тип комбинирования STACK говорит компоновщику о необходимости
конкатенации всех сегментов с данным именем в один сегмент и
построении выполняемого .EXE-файла таким образом, чтобы SS:SP при
запуске программы устанавливались как указатель на конец данного
сегмента. Это специализированный тип комбинирования, используемый
исключительно в отношении стека.

Тип VIRTUAL определяет специальный тип сегмента, который
рассматривается в качестве общей области памяти и может быть
присоединен к другому сегменту во время компоновки. Сегмент
VIRTUAL считается присоединенным к объемлющему сегменту. Директива
ASSUME рассматривает сегмент VIRTUAL как часть порождающего его
сегмента; в остальном это обычный сегмент. Компоновщик
рассматривает виртуальный сегмент как общую область, комбинируемую
для всех сегментов. Это позволяет разделение статических данных,
входящих в несколько модулей из включаемых файлов.

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

Поля «использование» и «класс»
——————————

Поле «использование» директивы SEGMENT существует
исключительно в ассемблере для 80386; информацию об использовании
данного поля приводится в главе 10.

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

Размер, тип, имя и вложенность сегментов
—————————————-

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

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

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

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

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

.
.
.
DataSeg SEGMENT PARA PUBLIC ‘DATA’
.
.
.
DataSeg2 SEGMENT PARA PRIVATE ‘FAR_DATA’
.
.
.
DataSeg2 ENDS
.
.
.
DataSeg ENDS
.
.
.

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

.
.
.
TEST MACRO
.
.
.
TestSeg SEGMENT WORD PRIVATE ‘FAR_DATA’
.
.
.
TestSeg ENDS
.
.
.
ENDM
.
.
.

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

Упорядочение сегментов —————————————-

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

— Сегменты класса CODE.

— Сегменты, не имеющие класс CODE, и и не являются частью
DGROUP.

— Сегменты, являющиеся частью DGROUP, в следующем порядке:

— Сегменты, не имеющие классов STACK или BSS.

— Сегменты класса BSS.

— Сегменты класса STACK.

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

Остается вопрос: как упорядочены сегменты, если вы не
компонуете ваш модуль с модулем на языке высокого уровня и не
пользуетесь директивой DOSSEG? В большей части случаев у вас не
возникает необходимости знать ответ на этот вопрос, но если уж
такая необходимость возникла, то вот он. (Этот ответ несколько
сложнее, чем вы могли думать.)

При отсутствии явного задания упорядочения, например при
помощи DOSSEG, компоновщик группирует вместе все сегменты данного
класса, где класс сегмента задается полем «класс» директивы
SEGMENT. Эти группы сегментов помещаются в .EXE-файл просто в той
последовательности, в какой их встречает компоновщик; первый
встреченный компоновщиком при загрузке .OBJ-файлов класс сегментов
помещается в .EXE-файл первым, второй — следующим и т.д. Это
означает, что последовательность компонуемых .OBJ- файлов влияет
на конечную последовательность расположения сегментов в
.EXE-файле.

Теперь сегменты упорядочены по классам. Каким образом
выполняется их упорядочение внутри каждого класса? И опять в
.EXE-файл они будут помещены в той последовательности, в которой
их встретит компоновщик. Одним из факторов здесь является
последовательность компонуемых .OBJ-файлов; другим фактором
является порядок следования сегментов в каждом .OBJ-файле. Turbo
Assembler предоставляет вам два варианта, касающихся задания
порядка расположения файлов в .OBJ-файле.

Директива .SEQ сообщает Turbo Assembler о том, что сегменты
должны помещаться в .OBJ-файл в той последовательности, в которой
они находятся в исходном файле программы. При таком способе
упорядочения на последовательность расположения сегментов в
.EXE-файле может оказать влияние последовательность их
расположения в исходном файле. Это режим работы Turbo As- sembler
по умолчанию, поэтому последовательное упорядочение сегментов
будет выполняться даже при опущенной дитективе .SEQ, если только
не была использована директива .ALFA.

Директива .ALFA сообщает Turbo Assembler о том, что сегменты
должны помещаться в .OBJ-файл в алфавитном порядке. При
упорядочении сегментов по алфавиту последовательность расположения
сегментов в исходном файле на последовательность их расположения в
.EXE-файле не влияет. Этот режим упорядочения является умолчанием
для некоторых более старых ассемблером, поэтому в некоторых
случаях для правильной работы программы может оказаться
необходимым использовать данную директиву.

Итак, теперь сегменты упорядочены по классу, а также внутри
класса по порядку следования. Можно управлять порядком
расположения сегментов в классе как задавая последовательность
компоновки .OBJ-файлов, так и при помощи директив .SEQ и .ALFA.
Если выбрана директива .SEQ, то порядок расположения сегментов в
соответствующем исходном модуле влияет на порядок их расположения
в .EXE-файле.

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

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

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

Предположим, у вас имеется программа, которая обращается к
данным в двух сегментах. Обычно всякий раз для адресации
следующего сегмента нужно загрузить сегментный регистр и выполнить
новую операцию ASSUME; это занимает много времени и доставляет
неудобство. Гораздо проще объединить сегменты в одну группу с
именем DataGroup, загрузить в DS начало DataGroup, назначить при
помощи ASSUME регистр DS на DataGroup, после чего в любое время
можно иметь доступ к любому сегменту. Ниже приводится пример
программы:

.
.
.
DataGroup GROUP DataSeg1,DataSeg2
.
.
.
DataSeg1 SEGMENT PARA PUBLIC ‘DATA’
MemVar1 DW 0
DataSeg2 ENDS
.
.
.
mov ax,DataGroup
mov ds,ax
ASSUME DS:DataGroup
.
.
.
mov ax,[MemVar1]
mov [MemVar2],ax
.
.
.

Зачем вам может понадобиться использовать группы, если того
же результата можно добиться гораздо проще при помощи
единственного имени сегмента и типа комбинирования PUBLIC.
Фактически в чисто ассемблерных программах нет такой необходимости
в группах, хотя, безусловно, при желании бы можете ими
пользоваться. Группы прежде всего служат для для интерфейса
ассемблерного кода с языками высокого уровня. В частности, группа
DGROUP используется языками высокого уровня, позволяя доступ к
стеку, инициализированным ближним данным и сегментам констант
относительно единственного сегментного регистра.

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

Однако, если вы компонуете данный модуль с модулем на языке
высокого уровня или используете где-либо в программе директиву
DOSSEG, то нет необходимости заботиться о том, чтобы все сегменты
DGROUP находились вместе; в обоих случаях компоновщик
автоматически делает все сегменты в DGROUP смежными.

Сегменты группы должны помещаться в 64Кб, однако они не
обязаны быть непрерывными при компоновке. Между сегментами,
составляющими группу, могут находиться не входящие в группу
сегменты.

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

.
.
.
mov ax,DGROUP
mov ds,ax
ASSUME DS:Stack ;даст неправильное смещение
.
.
.

Вместо этого следует записать:

.
.
.
mov ax,DGROUP
mov ds,ax
ASSUME DS:DGROUP
.
.
.

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

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

(Дополнительную информацию см. в разделе «Вы забыли указать
переопределение группы в операндах и таблицах данных» главы 5.)

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

Директива ASSUME позволяет сообщить Turbo Assembler, на какую
группу или сегмент указывает данный сегментный регистр. Отметим,
что это не то же самое, что фактически загрузить сегментный
регистр так, чтобы он указывал на данный сегмент; последняя задача
выполняется отдельно, при помощи команды MOV. Назначение ASSUME
состоит в том, чтобы позволить Turbo Assem- bler проверить
допустимость сделанных вами ссылок к памяти и при необходимости
автоматически вставить в адреса памяти префиксы переопределения
сегментов.

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

Другие директивы ASSUME для различных сегментных регистров
могут быть вставлены в любом исходном модуле так часто, как это
бывает необходимо. Сегмент, назначенный любому сегментному
регистру, может быть изменен в любой момент. Любые назначения для
каждого сегмента или для всех сегментов могут быть изменены одной
директивой ASSUME.

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

Ниже приводится пример использования директивы ASSUME:

Stack SEGMENT PARA STACK ‘STACK’
DB 512 DUP (0)
Stack ENDS
TGROUP GROUP DataSeg1,DataSeg2
DataSeg1 SEGMENT PARA PUBLIC ‘DATA’
.
.
.
DataSeg1 ENDS
DataSeg2 SEGMENT PARA PUBLIC ‘DATA’
.
.
.
DataSeg2 ENDS
DataSeg3 SEGMENT PARA PUBLIC ‘DATA’
MemVar DW 0
.
.
.
DataSeg3 ENDS
.
.
.
CodeSeg SEGMENT PARA PUBLIC ‘DATA’
ASSUME CS:CodeSeg,DS:TGROUP,SS:Stack,ES:NOTHING
ProgramStart:
mov ax,TGROUP
mov ds,ax
ASSUME DS:TGROUP
.
.
.
mov ax,SEG MemVar ;то же, что и в DataSeg3
mov es,ax
ASSUME ES:SEG MemVar
.
.
.
push ds
pop es
mov ax,CodeSeg
mov ds,ax
ASSUME DS:CodeSeg,ES:TGROUP
.
.
.
CodeSeg ENDS
END ProgramStart

Если директива ASSUME относится к группе, то заданный
сегментный регистр указывает на начало группы. Однако, если
директива ASSUME относится к сегменту, который является частью
группы, то сегментный регистр указывает не на начало группы, а на
начало сегмента. Это может вызвать проблемы, поскольку сегментные
регистры обычно указывают на начало групп, а не составляющих их
сегментов. Например, следующий фрагмент загрузит в AX неверный
адрес памяти, так как DS указывает на начало TGROUP, а оператор
ASSUME некорректно обозначает, что DS указывает на начало
DataSeg2:

.
.
.
TGROUP GROUP DataSeg1,DataSeg2
DataSeg1 SEGMENT PARA PUBLIC ‘DATA’
.
.
.
DataSeg1 ENDS
DataSeg2 SEGMENT PARA PUBLIC ‘DATA’
MemVar DW 0
DataSeg2 ENDS
.
.
.
CodeSeg SEGMENT PARA PUBLIC ‘CODE’
ASSUME CS:CodeSeg
.
.
.
mov ax,TGROUP
mov ds,ax
ASSUME DS:DataSeg2 ;неверно!!! (должно быть TGROUP)
mov ax,[MemVar] ;загрузит память с неправильным
;смещением, относительно DataSeg2
;вместо TGROUP
.
.
.

При использовании упрощенных сегментных директив в целом не
обязательно указывать ASSUME, поскольку Turbo Assembler
автоматически генерирует соответствующие назначения сегментов.
Однако, если вы измените какие-либо сегментные регистры и будете
пользоваться упрощенными сегментными директивами, то вам придется
выполнить соответствующие директивы ASSUME. Например, следующий
фрагмент устанавливает DS как указатель на сегмент .DATA, сегмент
.CODE, сегмент .FARDATA и наконец, снова на сегмент .DATA:

.
.
.
.DATA
.
.
.
.FARDATA
.
.
.
.CODE
mov ax,@Data
mov ds,ax
ASSUME DS:@Data
.
.
.
mov ax,@Code
mov ds,ax
ASSUME DS:@Code
.
.
.
mov ax,@FarData
mov ds,ax
ASSUME DS:@FarData
.
.
.
mov ax,@Data
mov ds,ax
ASSUME DS:@Data
.
.
.

Как было сказано выше, директива ASSUME может заставить Turbo
Assembler вставлять при обращениях к адресам памяти пре фиксы
переопределения сегментов всякий раз, когда Turbo Assem- bler
(работая на основе выданной вами директивы ASSUME) считает, что
это необходимо для доступа к данной переменной памяти. Например,
Turbo Assembler поместит префикс ES: в команды, обращающиеся в
следующем фрагменте к переменной MemVar, так как директива ASSUME
непавильно указывает на то, что DS не может достать сегмент, в
котором находится MemVar:

.
.
.
DataSeg SEGMENT PARA PUBLIC ‘DATA’
MemVar DB ?
.
.
.
DataSeg ENDS
.
.
.
CodeSeg SEGMENT PARA PUBLIC ‘CODE’
ASSUME CS:CodeSeg,DS:NOTHING,ES:DataSeg
.
.
.
mov ax,DataSeg
mov ds,ax
mov es,ax
mov [MemVar],1
.
.
.

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

Упрощенные сегментные директивы ———————————

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

Сегменты и сегментные группы, создаваемые директивами .CODE,
.DATA, .DATA?, .STACK, .CONST, .FARDATA и .FARDATA?, зависят от
модели памяти, выбранной при помощи директивы .MODEL. (Вспомните,
что модели памяти мы рассматривали в главе 5.) Следующие таблицы
показывают соотношения между моделями памяти и сегментами,
создаваемыми упрощенными сегментными директивами:

Сегменты по умолчанию и типы
для модели памяти Tiny Таблица 9.1
—————————————————————-
Директива Имя Выравнивание Комбинирование Класс Группа
—————————————————————-
.CODE _TEXT WORD PUBLIC ‘CODE’ DGROUP
.FARDATA FAR_DATA PARA private ‘FAR_DATA’
.FARDATA? FAR_BSS PARA private ‘FAR_BSS’
.DATA _DATA WORD PUBLIC ‘DATA’ DGROUP
.CONST CONST WORD PUBLIC ‘CONST’ DGROUP
.DATA? _BSS WORD PUBLIC ‘BSS’ DGROUP
.STACK* STACK PARA STACK ‘STACK’ DGROUP

* Если задано FARSTACK, то STACK не предполагается в DGROUP.
—————————————————————-

Сегменты по умолчанию и типы
для модели памяти Small Таблица 9.2
—————————————————————-
Директива Имя Выравнивание Комбинирование Класс Группа
—————————————————————-
.CODE _TEXT WORD PUBLIC ‘CODE’
.FARDATA FAR_DATA PARA private ‘FAR_DATA’
.FARDATA? FAR_BSS PARA private ‘FAR_BSS’
.DATA _DATA WORD PUBLIC ‘DATA’ DGROUP
.CONST CONST WORD PUBLIC ‘CONST’ DGROUP
.DATA? _BSS WORD PUBLIC ‘BSS’ DGROUP
.STACK* STACK PARA STACK ‘STACK’ DGROUP

* Если задано FARSTACK, то STACK не предполагается в DGROUP.
—————————————————————-

Сегменты по умолчанию и типы
для модели памяти Medium Таблица 9.3
—————————————————————-
Директива Имя Выравнивание Комбинирование Класс Группа
—————————————————————-
.CODE имя _TEXT WORD PUBLIC ‘CODE’
.FARDATA FAR_DATA PARA private ‘FAR_DATA’
.FARDATA? FAR_BSS PARA private ‘FAR_BSS’
.DATA _DATA WORD PUBLIC ‘DATA’ DGROUP
.CONST CONST WORD PUBLIC ‘CONST’ DGROUP
.DATA? _BSS WORD PUBLIC ‘BSS’ DGROUP
.STACK* STACK PARA STACK ‘STACK’ DGROUP

* Если задано FARSTACK, то STACK не предполагается в DGROUP.
—————————————————————-

Сегменты по умолчанию и типы
для модели памяти Compact Таблица 9.4
—————————————————————-
Директива Имя Выравнивание Комбинирование Класс Группа
—————————————————————-
.CODE _TEXT WORD PUBLIC ‘CODE’
.FARDATA FAR_DATA PARA private ‘FAR_DATA’
.FARDATA? FAR_BSS PARA private ‘FAR_BSS’
.DATA _DATA WORD PUBLIC ‘DATA’ DGROUP
.CONST CONST WORD PUBLIC ‘CONST’ DGROUP
.DATA? _BSS WORD PUBLIC ‘BSS’ DGROUP
.STACK* STACK PARA STACK ‘STACK’ DGROUP

* Если задано FARSTACK, то STACK не предполагается в DGROUP.
—————————————————————-

Сегменты по умолчанию и типы
для моделей памяти Large и Huge Таблица 9.5
—————————————————————-
Директива Имя Выравнивание Комбинирование Класс Группа
—————————————————————-
.CODE имя_TEXT WORD PUBLIC ‘CODE’
.FARDATA FAR_DATA PARA private ‘FAR_DATA’
.FARDATA? FAR_BSS PARA private ‘FAR_BSS’
.DATA _DATA WORD PUBLIC ‘DATA’ DGROUP
.CONST CONST WORD PUBLIC ‘CONST’ DGROUP
.DATA? _BSS WORD PUBLIC ‘BSS’ DGROUP
.STACK* STACK PARA STACK ‘STACK’ DGROUP

* Если задано FARSTACK, то STACK не предполагается в DGROUP.
—————————————————————-

Сегменты по умолчанию и типы
для модели памяти Turbo Pascal (TPASCAL) Таблица 9.6
—————————————————————-
Директива Имя Выравнивание Комбинирование
—————————————————————-
.CODE CODE BYTE PUBLIC
.DATA DATA WORD PUBLIC
—————————————————————-

В последних главах вы, вероятно, отметили, что программы,
использующие упрощенные сегментные директивы, не нуждаются в
директивах ASSUME, GROUP или ENDS. Директива .MODEL автоматически
выполняет соответствующие соответствующие директивы ASSUME для
выбранной модели памяти, назначая сегменты, указанные в предыдущих
таблицах. Как показано в этих же таблицах, .MODEL также выполняет
определение группы DGROUP.

Что же касается ENDS, то начало нового сегмента с упрощенной
сегментной директивой — например, .CODE или .DATA — автоматически
заканчивает текущий сегмент, если таковой имелся.

Теперь рассмотрим наиболее неясные упрощенные сегментные
директивы: .DATA?, .CONST, .FARDATA и .FARADATA?. Директива
.FARDATA единственная из них, которая может понадобиться в чисто
ассемблерной программе; остальные служат исключительно для
согласования использования сегментов с языками высокого уровня.

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

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

Директива .FARDATA используется для создания дальнего
сегмента данных, уникального для данного исходного модуля; таким
образом, этот сегмент, не разделяемый другими модулями. Этот
сегмент называется FAR_DATA, но имеет тип комбинирования PRIVATE,
поэтому он не комбинируется ни с одним другим сегментом. .FARDATA
позволяет определить до 64К области локальных данных в каждом
модуле. Разумеется, если вы используете .FARDATA, вы должны
установить сегментный регистр как указатель на этот сегмент
следующим образом:

.MODEL SMALL
.DATA
InitValue DW 0
.FARDATA
MemArray DW 100 DUP (?)
.CODE
.
.
.
mov ax,@Data
mov ds,ax
mov ax,[InitValue]
mov ax,@FarData
mov es,ax
ASSUME ES:@FarData;
mov di,OFFSET MemArray
mov cx,100
cld
rep stosw
.
.
.

Отметим, что предопределенная метка @FarData содержит имя
сегмента, определенного директивой .FARDATA.

Так как сегмент, определенный при помощи .FARDATA, не
разделяется с каким-либо другим модулем (как, например, в случае
сегмента, определенного при помощи .DATA), вы можете при помощи
директивы GLOBAL разделить конкретные переменные в сегменте
.FARDATA с другими модулями. Например, следующий фрагмент делает
MemVar доступным другим модулям:

.MODEL SMALL
.FARDATA
GLOBAL MemVar:WORD
MemVar DW 0
.
.
.

Затем другой модуль может обратиться к MemVar следующим
образом:

.MODEL SMALL
GLOBAL MemVar:WORD
.DATA
.
.
.
.CODE
.
.
.
mov ax,SEG MemVar
mov ds,ax
ASSUME DS:SEG MemVar
mov ax,[MemVar]
.
.
.

Отметим, что объявление MemVar как GLOBAL должно находиться
до объявления любого сегмента. Это необходимо потому, что
объявление данной переменной как global должно выполняться либо
внутри сегмента переменной, либо вне всех сегментов вообще.
Поскольку, по определению, ни один модуль не может разделять
сегмент .FARDAT с другим модулем, объявление MemVar должно
выполняться вне всех сегментов.

Директива .FARDATA? во многом аналогична .FARDATA, за
исключением того, что он создает сегмент private с именем FAR_BSS.
Сегменты FAR_BSS используются в языках высокого уровня для
неинициализированных дальних данных. Если вы не реализуете
интерфейс с языком высокого уровня, вам ничто не мешает определить
ваши неинициализированные дальние данные в сегменте, определенном
.FARDATA и отказаться от .FARDATA?. Действительно, сегмент
.FARDATA дает вам дополнительные 64Кб дальней области памяти,
однако если вам действительно требуется более 64Кб дальней памяти,
уникальной для данного модуля, вам вероятно, так или иначе
придется использовать стандартные сегментные директивы.

Если вы используете .FARDATA?, то предопределенная метка
@FarData? содержит имя сегмента, определенного .FARDATA, который
может быть использован в директивах ASSUME и при загрузке
сегментных регистров.

Пример многосегментной программы ——————————

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

Вот этот пример:

;
; Программа, демонстрирующая использование нескольких
; кодовых сегментов и сегментов данных
;
; Считывает с консоли строку, записывает ее в один сегмент
; данных, копирует строку в другой сегмент данных, преобра-
; зовывая ее в процессе копирования в символы нижнего ре-
; гистра, а затем печатает строку на консоль. Использует
; функции из другого кодового сегмента для чтения, печати
; и копирования строки.
;
Stack SEGMENT PARA STACK ‘STACK’
DB 512 DUP (?)
Stack ENDS
MAX_STRING_LENGTH EQU 1000
SourceDataSeg SEGMENT PARA PRIVATE ‘DATA’
InputBuffer DB MAX_STRING_LENGTH DUP (?)
SourceDataSeg ENDS
DestDataSeg SEGMENT PARA PRIVATE ‘DATA’
OutputBuffer DB MAX_STRING_LENGTH DUP (?)
DestDataSeg ENDS
SubCode SEGMENT PARA PRIVATE ‘CODE’
ASSUME CS:SubCode
;
; Подпрограмма считывает строку с консоли. Конец строки
; обозначается возвратом каретки, который преобразовывается
; в пару возврат каретки/перевод строки, поэтому при печа-
; ти будет выполняться переход к следующей строке. Добав-
; ляется 0, завершающий строку.
;
; Вход:
; ES:DI — адрес хранения строки
;
; Выход: отсутствует
;
; Разрушаемые регистры: AX,DI
;
GetString PROC FAR
GetStringLoop:
mov ah,1
int 21h ;прием следующего символа
stosb ;запись его
cmp al,13 ;это символ возврата каретки?
jnz GetStringLoop ;нет это еще не он
mov BYTE PTR es:[di],10
mov BYTE PTR es:[di+1],0 ;строка заканчивается воз-
;вратом каретки и 0
ret
GetString ENDP
;
; Подпрограмма копирования строки с преобразованием ее
; к нижнему регистру
;
; Вход:
; DS:SI — копируемая строка
; ES:DI — адрес помещения скопированной строки
;
; Выход: отсутствует
;
; Разрушаемые регистры: AL, SI, DI
;
CopyLowercase PROC FAR
CopyLoop:
lodsb
cmp al,’A’
jb NotUpper
cmp al,’Z’
ja NotUpper
add al,20h ;если это символ верхнего регистра,
;преобразование к нижнему регистру
NotUpper:
stosb
and al,al ;это был завершающий строку 0?
jnz CopyLoop ;нет, копирование другого символа
ret
Copylowercase ENDP
;
; Подпрограмма вывода строки на консоль
;
; Вход:
; DS:SI — строка, выводимая на дисплей
;
; Выход: отсутствует
;
; Разрушаемые регистры: AH, DL, SI
;
DisplayString PROC FAR
DisplayStringLoop:
mov dl,[si] ;прием следующего символа
and dl,dl ;это был завершающий строку 0?
jz DisplayStringDone ;да, работа завершена
inc si ;указатель на следующий символ
mov ah,2
int 21h ;вывод символа на дисплей
jmp DisplayStringLoop
DisplayStringDone:
ret
DisplayString ENDP
SubCode ENDS
Code SEGMENT PARA PRIVATE ‘CODE’
ASSUME CS:Code,DS:NOTHING,ES:NOTHING,SS:Stack
ProgramStart:
cld ;инкрементирование строковой командой
;ее регистров-указателей
;
;Считывание строки с консоли в InputBuffer
;
mov ax,SourceDataSeg
mov es,ax
ASSUME ES:SourceDataSeg
mov di,OFFSET InputBuffer
call GetString ;считывание строки с консоли и запись
;ее в ES:DI
;
; Печать символа перевода строки для перехода на следующую
; строку
;
mov ah,2
mov dl,10
int 21h
;
; Копирование строки из InputBuffer в OutputBuffer с преоб-
; разованием ее в процессе копирования к нижнему регистру
;
push es
pop ds
ASSUME DS:SourceDataSeg
mov ax,DestDataSeg
mov es,ax
ASSUME ES:DestDataSeg
mov si,OFFSET InputBuffer ;копирование из DS:SI…
mov di,OFFSET OutputBuffer ;…в ES:DI
call CopyLowercase ;…с преобразованием к
;нижнему регистру
;
; Вывод на дисплей строки в нижнем регистре
;
push es
pop ds
ASSUME DS:DestDataSeg
mov si,OFFSET OutputBuffer
call DisplayString ;вывод строки из DS:SI на консоль
;
; Готово
;
mov ah,4ch
int 21h
Code ENDS
END ProgramStart

Отметим, что в данном примере подпрограммы следуют до главной
программы. Это делается для того, чтобы избежать прямых ссылок,
поскольку подпрограммы и главная программа находятся в разных
кодовых сегментах. Если бы первой шла главная программа, то при
каждом вызове подпрограммы вам пришлось бы указывать
переопределения сегмента FAR PTR, поскольку Turbo Assembler не
может автоматически ассемблировать дальние переходы в прямую
сторону. Однако, при данном способе организации программы все
вызовы подпрограмм имеют вид обратных ссылок, поэтому Turbo
Assembler имеет возможность автоматически генерировать дальние
вызовы этих подпрограмм.

В остальном программа организована исключительно
последовательно. Подпрограммы используют полные указатели данных
типа сегмент:смещение, а главная программа при необходимости
устанавливает DS и ES на разные сегменты данных. Отметим
использование строковых команд для копирования строки и
преобразования ее к нижнему сегменту; поскольку LODS по умолчанию
использует DS, а STOS — ES, то эти команды идеально подходят для
программ, которым нужно одновременно осуществлять доступ к двум
сегментам.