Глава 5. Элементы программы на языке ассемблера.


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

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

Тем не менее, в этой главе затрагиваются лишь некоторые
аспекты языка ассемблера, а новые для вас вопросы этого языка
рассматриваются в главах 6, «Дополнительные сведения о
программировании на Turbo Assembler» и 9, «Расширенные средства
программирования на Turbo Assembler».

Компоненты и структура программы на языке ассемблера
——————————————————————

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

.MODEL small ;ближние модели программы и данных
.STACK 200h ;размер стека 512 байт
.DATA ;начало сегмента данных
DisplayString DB 13,10 ;пара «возврат каретки/перевод строки»
;для начала новой строки
ThreeCHARS DB 3 DUP (?) ;память для трех символов,
;введенных с клавиатуры
DB ‘$’ ;хвостовой символ «$», сообщающий
;DOS, когда остановить печать
;DisplayString при выполнении
;функции 9
.CODE ;начало кодового сегмента
Begin:
mov ax,@data
mov ds,ax ;указывает DS на сегмент данных
mov bx,OFFSET ThreeChars ;указывает на ячейку памяти
;для первого символа
mov ah,1 ;функция DOS ввода с клавиатуры
int 21h ;прием следующей введенной клавиши
dec al ;вычесть 1 из кода символа
mov [bx],al ;сохранить модифицированный символ
inc bx ;указывает на ячейку дла хранения
;следующего символа
int 21h ;прием следующей введенной клавиши
dec al ;вычесть 1 из кода символа
mov [bx],al ;сохранить модифицированный символ
mov dx,OFFSET DisplayString ;указывает на строку
;модифицированных символов
mov ah,9 ;функция DOS печати строки
int 21h ;печеть модифицированных символов
mov ah,4ch ;Функция DOS выхода из программы
int 21h ;конец программы
END Begin ;директива отметки конца исходной
;программы и обозначения начала вы-
;полнения при запуске программы.

Данная программа содержит упрощенные сегментные директивы
.MODEL, STACK, .DATA и .CODE, а также директиву END. Сегментные
директивы, упрощенные или стандартные, необходимы в любой
ассемблерной программе для определения и управления использованием
сегментов, а директива END всегда заканчивает программу на
ассемблере. В данной главе будут рассмотрены как сегментные
директивы, так и директива END, и кроме того, еще некоторые
директивы.

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

mov [bx],al

и
inc dx

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

Если вас заинтересовало, что делает приведенная выше
программа, введите ее, наберите IBM, и программа ответит вам

HAL

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

Резервированные слова
——————————————————————

Резервированные слова, или ключевые слова Turbo Assembler,
используются исключительно самим ассемблером; вы не можете
использовать их в качестве ваших собственных имен в директивах
присвоения, меток или процедур. Вы должны рассматривать
резервированные слова как некоторые блоки, из которых строится
программа на языке ассемблера. Слова, приводимые в таблице 5.1,
включают операции (+, *, -, +), директивы (.386, ASSUME, MASM,
QUIRKS) и предопределенные символические имена (??time, ??version,
@WordSize), которые работают как предопределенные директивы
присвоения, а также алиасы.

Резервированные слова TASM Таблица 5.1
——————————————————————
: @datasitze @filename NAME .RADIX
: ??date ??filename NE RECCORD
= DB FWORD NEAR REPT
? DD GE %%NEWPAGE .SALL
[] %DEPTH GLOBAL %NOCONDS SEG
() DISPLAY GT %NOCTLS .SEQ
+ DOSSEG HIGH NOEMUL .SFCOND
— DP IDEAL %NOINGL SHL
* DQ IF NOJUMPS SHORT
. DT IF1 %NOLIST SHR
.186 DUP IF2 NOLOCALS SIZE
.286 DW IFB %NOMACS SIZESTR
.286C DWORD IFDEF NOMASM51 SMALL
.286P ELSE IFDIF NOMULTERRS SMART
.287 ELSEIF IFDIFI NOSMART STACK
.386 EMUL IFE %NOYIMS .STACK
.386C END IFIDN NOT STRUC
.387 ENDIF IFIDNI NOTHING SUBSTR
.8086 ENDM IFNB %NOTRUNC SUBTTL
.8087 ENDP IFNDEF NOWARN %SUBTTL
ALIGN ENDS %INCL OFFSET %SYMS
.ALPHA EQ INCLUDE OR SYMTYPE
AND EQU INCLUDELIB ORG %TABSIZE
ARG ERR INSTR %OUT TBYTE
ASSUME .ERR IRP P186 %TEXT
%BIN .ERR1 IRPC P286 .TFCOND
BYTE .ERR2 JUMPS P286N THIS
CATSTR .ERRB LABEL P287 ??time
@code .ERRDEF .LALL P386 TITLE
CODESEG ERDIF LARGE P386N %TITLE
@CodeSize ERRDIFI LE P386P %TRUNC
COMM ERRE LENGTH P387 TYPE
COMMENT ERRIDN .LECOND P8086 .TYPE
%CONDS ERRIDNI %LINUM P8087 UDATASEG
.CONST ERRIFNB %LIST PAGE UFARDATA
CONST ERRIFNDEF .LIST %PAGESIZE UNION
@Cpu ERRNB LOCAL PARA UNKNOWN
%CREF ERRNDEF LOCALS %PCNT USES
.CREF ERRNZ LOW PN087 ??version
%CREFALL EVEN LT %POPLCTL WARN
%CREFREF EVENDATA MACRO PROC WIDTH
%CREFUREF EXITM %MACS PTR WORD
%CTLS EXTRN MASK PUBLIC @WordSize
@curseg FAR MASM PURGE .XALL
@data FARDATA MASM51 @PUSHLCTL .XCREF
.DATA @fardata MOD PWORD .XLIST
.DATA? .FARDATA MODEL QUIRKS XOR
DATAPTR @fardata? .MODEL QWORD
DATASEG .FARDATA? MULTERRS RADIX

——————————————————————

Формат строки
——————————————————————

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

<метка><команда/директива>,<операнды><;комментарий>

где <метка> это произвольное символическое имя; <команда/ директива> это либо мнемоническое обозначение команды, либо
директива; <операнды> могут представлять собой комбинацию из нуля,
одной, двух (и иногда более) констант, адресов памяти, адресов
регистров и текстовых строк, что определяется соответствующей
командой или директивой; <комментарий> участвует в строке как
необязательный компонент.

Символ обратной наклонной черты (\) может располагаться
практически в любой точке программы в качестве символа продолжения
строки. Для разбиения строк или идентификаторов он служить не
может. Этот символ означает: «прочесть в данной точке следующую
строку и продолжить обработку». Это позволяет записывать строки
программы более естественным образом, комментируя каждую из них
произвольным образом. Например,

foo mystructure \ ;Начало заполнения структуры
<0, \ ;Первым идет нулевое значение 1, \ ;Единичное значение 2> ;Значение два и конец структуры

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

ifdif <123\>,<456\>

не распознает два включенных в него символа продолжения строки.

comment \
.
.

начинает блок комментария, а не определяет ближнее (near)
символическое имя COMMENT.

Символ продолжения строки также не распознается внутри
макроопределений. Однако, он распознается при расширении макроса.

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

Метки ———————————————————

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

.MODEL small
.STACK 200h
.DATA
FactorialValue DW ?
Factorial DW ?
.CODE
FiveFactorial PROC
mov ax,@data
mov ds,ax
mov [FactorialValue],1
mov [Factorial],2
mov cx,4
FiveFactorialLoop:
mov ax,[FactorialValue]
mul [Factorial]
mov [FactorialValue],ax
inc [Factorial]
loop FiveFactorialLoop
ret
FiveFactorial ENDP
END

Метки FactorialValue и Factorial эквивалентны адресам двух
16-битовых переменных; они используются далее в программе для
обращения к этим двум переменным. Метка FiveFactorial это имя
подпрограммы (функции или процедуры), в которой содержится часть
программы, которую могут вызывать другие части программы. И
наконец, метка FiveFactorialLoop эквивалентна адресу команды

mov ax,[FactorialValue]

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

Метки могут состоять из следующих символов:

A-Z a-z _ @ $ ? 0-9

В режиме MASM (этот вопрос рассматривается в главе 12) первым
символом в метке может являться точка (.). Цифры 0-9 не могут
являться первыми символами метки. Одинарные символы $ или ? имеют
специальное значение и поэтому не могут служить символическими
именами, определяемые пользователем.

Каждая метка может быть определена только один раз; таким
образом, метки в программе должны быть уникальными. (Из этого
правила существуют и исключения; например, специальные метки,
определяемые дитективой =, а также локальные метки в макросах и
подпрограммах режима Ideal.) Метки могут быть любое число раз
использованы в качестве операндов.

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

.
.
jmp DoAddition
.
.
.
DoAddition:
add ax,dx
.
.
.

следующая команда, выполняемая после команды ветвления JMP,
которая задает переход на метку DoAddition, это ADD AX,DX.
Приведенный выше фрагмент в точности эквивалентен фрагменту

.
.
jmp DoAddition
.
.
.
DoAddition: add ax,dx
.
.
.

Отметим, что в приведенном выше примере команда ADD AX,DX
сильно сдвинута вправо из-за длины метки DoAddition, что уменьшает
читаемость текста программы.

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

.
.
.
jmp DoAddition
.
.
.
DoAddition: mov dx,[MemVar]
add ax,dx
.
.
.

вам пришлось отделить DoAddition от ADD AX,DX и затем
добавить новый текст. И напротив, если бы метка DoAddition стояла
в строке одна (как в предыдущем примере), вам достаточно было бы
просто добавить после DoAddition новую строку.

Все имена директив приводятся в главе 3 Справочного
руководства; а регистры 8086 перечислены в Главе 4.

Метка не может совпадать с каким-либо из встроенных
символических имен, используемых в выражениях. В число таких
резервируемых имен входят имена регистров (AX, BX и т.д.), а также
операторы, используемые в выражениях (PTR, BYTE, WORD и т.д). В
качестве имен меток нельзя также использовать любые директивы
IFxxx или .ERRxxx. Некоторые другие резервируемые символические
имена Turbo Assembler могут использоваться лишь в конкретном
контексте, сюда входят имена NAME, INCLUDE и COMMENT, которые
могут применяться в качестве имен элементов структуры, а не как
символические имена общего назначения (дополнительные сведения о
структурах приводятся в главе 10).

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

bx DW 0
PTR:

неприемлемы, поскольку BX это имя регистра, а PTR это
оператор выражения. Однако метка

Old_BX DW 0

вполне годится.

Ниже приводятся примеры допустимых имен меток:

MainLoop
calc_long_sum
Error0
iterate
Draw$Dot
Dalay_100_milliseconds

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

.
.
.
LoopTop:
mov al,[si]
inc si
and al,al
jz Done
jmp LoopTop
Done: ret
.
.
.

метки LoopTop и Done определены с двоеточиями, тогда как
ссылки на эти метки двоеточой не содержат.

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

Имена меткок должны быть осмысленными. Сравните

.
.
.
cmp al,’a’
jb NotALowerCaseLetter ;»НеБукваНижнегоРегистра»
cmp al,’z’
ja NotALowerCaseLetter
sub al,20h ;преобразование к верхнему
;регистру
NotALowerCaseLetter:
.
.
.

и

.
.
.
cmp al,’a’
jb P1
cmp al,’z’
ja P1
sub al,20h ;преобразование к верхнему
;регистру
P1:
.
.
.

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

Мнемонические команды и директивы
——————————————————————

Ключевым полем в строке ассемблерной программы является поле
<команда/директива>. Это поле может содержать либо мнемоническую
команду, либо директиву; эти два понятия сильно отличаются друг от
друга.

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

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

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

Большая часть данного руководства посвящена изучению
различных мнемонических команд и директив Turbo Assembler, каждая
из которых также рассматривается в главе 3 Справочного
руководства. Существует несколько директив, которые совершенно
обязательны в каждой создаваемой вами программе, особенно
сегментные директивы, которые будут рассмотрены далее в этой
главе, в разделе «Сегментные директивы». Одной из таких
обязательных директив является рассматриваемая ниже директива END.

Директива END

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

Директива END служит хорошим примером директивы, не ведущей к
генерированию какого-либо загрузочного кода. Например,

.MODEL small
.STACK 200h
.CODE
ProgramStart:
mov ah,4ch
int 21h
END ProgramStart

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

Безусловно, вы обратили внимание на то, что в одной строке с
директивой END находится слово ProgramStart. Помимо обозначения
конца программы, в директиве END при желании можно также указать
на точку, откуда должно начинаться выполнение программы при ее
запуске. По какой-либо причине у вас может возникнуть
необходимость, чтобы выполнение начиналось не с первой команды
.EXE-файла; в таких случаях используется данное свойство директивы
END. Предположим, что вы запускаете на выполнение программу,
ассемблированную и скомпонованную из следующего исходного
текста(DELAY.ASM):

.MODEL small
.STACK 200h
.CODE
Delay:
mov cx,0
DelayLoop:
loop DelayLoop
ret

ProgramStart:
call Delay ;пауза длительностью, необходимой для
;выполнения 64К циклов
mov ah,4ch
int 21h
END ProgramStart

Выполнение начинается не с первой строки искодной программы,
MOV CX,0 в метке Delay. Вместо этого работа программы начнется с
команды CALL Delay в метке ProgramStart, как указано в директиве
END.

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

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

Операнды ——————————————————

Мнемонические команды и директивы сообщают Turbo Assemb- ler,
что он должен делать. Операнды, напротив, сообщают Turbo
Assembler, какие регистры, параметры, ячейки памяти и т.д. связаны
с теми или иными командами или директивами. Сама по себе команда
MOV ничего не означает; нужны операнды, которые сообщили бы Turbo
Assembler, откуда взять пересылаемое значение и куда его
поместить.

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

Совершенно очевидно, что делает команда с одним операндом:
она обрабатывает этот операнд. Например,

push ax

помещает на стек AX. Команды без операндов еще более ясны.
Однако, как обстоит дело в случае команды с двумя операндами, один
из которых это исходный операнд, а другой это операнд назначения?
Например, когда 8086 выполняет команду

mov ax,bx

то из какого регистра выполняется считывание, а какой регистр
принимает пересылаемое значение?

Вам может показаться, что на английском языке эта команда
эквивалентна записи «Переслать содержимое регистра AX в регистр
BX», однако это неверно. Напротив, команда MOV пересылает
содержимое регистра BX в AX. Чтобы облегчить восприятие этой
команды, мысленно подставьте вместо запятой между двумя операндами
знак равенства и рассматривайте строку этой команды как оператор
присвоения на языке Си (или Паскаль). Тем самым команда MOV примет
для вас вид

ax = bx;

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

Операнды — регистры
——————-

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

Ниже дается несколько примеров команд с операндами —
регистрами:

mov di,ax
push di
xchg ah,dl
ror dx,cl
in al,dx
inc si

Операнды — регистры могут чередоваться с другими видами
операндов:

mov al,1
add [Basecount],cx
cmp si,[bx]

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

.
.
.
mov cx,1
mov dx,2
sub dx,cx
.
.
.

CX устанавливается равным 1, DX равным 2, затем содержимое CX
вычитается из DX, а разность, равная 1, записывается назад в DX.
Самым правым операндом команды SUB является регистр CX, который
поэтому авляется исходным регистром; DX это самый левый операнд,
поэтому он одновременно служит вторым исходным регистром и
регистром назначения. Между прочим, словами предыдущейкоманды SUB
словами можно выразить как «Вычесть CS из DX». Беря такую
формулировку за основу, можно выразить рассмотренную команду SUB
на языке Си как

dx -= cx;

На Паскале это будет выглядеть как

dx := dx-cx;

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

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

.
.
.
CountByFourLoop:
.
.
.
dec si
dec si
dec si
dec si
jnz CountByFourLoop
.
.
.

однако гораздо проще сделать так:

.
.
.
CountByFourLoop:
.
.
.
sub si,4
jnz CountByFourLoop
.
.
.

В качестве операндов — констант могут также использоваться
символы, поскольку каждый символ имеет свое определенное числовое
значение. Например, поскольку символу A соответствует десятичное
значение 65, то следующие две команды будут эквивалентны:

.
.
.
sub al,’A’
sub a1,65
.
.
.

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

Из двух операндов константа никогда не может быть крайней
правой, поскольку ясно, что константа не может являться операндом
назначения. Однако операнд — константу можно использовать
практически во всех случаях, когда вообще исходный операнд может
являться значением. 8086 накладывает на использование констант
некоторые ограничения; например, вы не можете поместить константу
на стек непосредственно (это ограничение существует только для
8086/8088). Чтобы поместить в стек значение 5, вы должны
организовать две команды:

.
.
.
mov ax,5
push ax
.
.
.

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

Выражения
———

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

Например, фрагмент

.
.
.
MemVar DB 0
NextVar DB ?
.
.
.
mov ax,SEG MemVar
mov ds,ax
mov bx,OFFSET MemVar+((3*2)-5)
mov BYTE PTR [bx],1
.
.
.

использует оператор SEG для загрузки постоянного значения
сегмента, в котором помещается MemVar, в регистр AX, а затем
копирует это значение из AX в DS. Далее эта программа содержит
сложное выражение, включающее в себя операторы *, +, — и OFFSET,
которое дает в результате значение OFFSET MemVar+1, котрое
представляет собой ничто иное, как адрес NextVar. И наконец, при
помощи оператора BYTE PTR программа выбирает при записи константы
1 по адресу ячейки памяти, на которую указывает BX, то есть
NextVar, выбирается режим работы по байтам.

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

Turbo Assembler может вычислять выражения, состоящие из
констант, при ассемблировании программы, поскольку значения
констант всегда известны. Для Turbo Assembler OFFSET MemVar + 2
это то же самое, что 5 + 2; поскольку все части этого выражения не
меняются и хорошо определены к моменту ассемблирования, все
выражение в целом решается и заменяется одним единственным
значением константы.

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

<>,(),[],LENGTH,MASK,SIZE,WIDTH
.(селектор элемента структуры)
HIGH,LOW
+,- (унарные)
:(перекрытие сегмента)
OFFSET,PTR,SEG,THIS,TYPE
*,/,MOD,SHL,SHR
+,- (бинарные)
EQ,GE,GT,LE,LT,NE
NOT
AND
OR,XOR
LARGE,SHORT,SMALL,.TYPE

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

Операнды — метки
—————-

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

.
.
.
MemWord DW 1
.
.
.
mov al,SIZE MemWord
.
.
.

пересылает в AL число 2, равное размеру в байтах (SIZE)
переменной памяти MemWord. В данном контексте метка может
участвовать в выражении, как показано в прошлом разделе.

Метки могут также служить операндами назначения в командах
CALL и JMP.

Например, в

.
.
.
cmp ax,100
ja IsAbove100
.
.
.
IsAbove100:
.
.
.
команда JA в случае, если AX больше 100, выполняет переход по
адресу, заданному операндом IsAbove100. И снова в таком качестве
метки используются как константы, задавая адреса памяти, по
которым выполняются переходы.

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

.
.
.
TempVar DW ?
.
.
.
mov [TempVar],ax
sub ax,[TempVar]
.
.
.

неизменно оставляет AX равным нулю, так как первая команда
записывает значение, хранящееся в AX, в переменную памяти TempVar,
а вторая команда вычитает из AX значение, хранимое в TempVar.

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

Режимы адресации памяти
————————

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

.
.
.
Assets DW ?
Debts DW ?
.
.
.
mov ax,[Debts]
sub [Assets],ax
.
.
.

Адресация памяти не вызывает особых вопросов. Предположим, у
вас имеется строка символов CharString, содержащая буквы
ABCDEFGHIJKLM, которая расположена в сегменте данных, начиная со
смещения 100, как показано на рис.5.1.

.
.
.
——————
99? ? ?
——————
CharString —> 100? ‘A’ ?
——————
101? ‘B’ ?
——————
102? ‘C’ ?
——————
103? ‘D’ ?
——————
104? ‘E’ ?
——————
105? ‘F’ ?
——————
106? ‘G’ ?
——————
107? ‘H’ ?
——————
108? ‘I’ ?
——————
109? ‘J’ ?
——————
110? ‘K’ ?
——————
111? ‘L’ ?
——————
112? ‘M’ ?
——————
113? 0 ?
——————
114? ? ?
——————
.
.
.
Рис.5.1 Ячейка памяти строки символов CharString

Как прочитать девятый символ этой строки, I, который хранится
по адресу 108? В Си можно сделать это в виде

C = CharString[8];

В Паскале это выглядит как

C := CharString[9];

Каким же образом сделать то же самое в ассемблере?
Разумеется, непосредственная ссылка на имя CharString не решит
задачу, поскольку имя CharString совпадает лишь с символом A.

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

.
.
.
.DATA
CharString DB ‘ABCDEFGHIJKLM’,0
.
.
.
.CODE
.
.
.
mov ax,@Data
mov ds,ax
mov al,[Charstring+8]
.
.
.

В данном случае это то же самое, что и

mov al,[100+8]

поскольку CharString начинается со смещения 100. Turbo
Assembler рассматривает все, помещенное в квадратные скобки, как
адрес, поэтону смещение CharString и 8 в сумме используются в
качестве адреса памяти. Команду можно поэтому переписать в более
эффективной форме:

mov al,[108]

что и показано на рис.5.2.

.
.
.
——————
99? ? ?
——————
CharString —> 100? ‘A’ ?
——————
101? ‘B’ ?
——————
102? ‘C’ ?
——————
103? ‘D’ ?
—————— ————
104? ‘E’ ? AL? ?
—————— ————
105? ‘F’ ? °
—————— ?
106? ‘G’ ? ?
—————— ?
107? ‘H’ ? ?
—————— ?
CharString+8 -> 108? ‘I’ ?————-
——————
109? ‘J’ ?
——————
110? ‘K’ ?
——————
111? ‘L’ ?
——————
112? ‘M’ ?
——————
113? 0 ?
——————
114? ? ?
——————
.
.
.
Рис.5.2 Адресация строки символов CharString

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

Следующий пример также загружает девятый символ строки
CharString в регистр AL:

.
.
.
mov bx,OFFSET CharString+8
mov al,[bx]
.
.
.

В данном примере BX служит указателем на девятый символ.
Первая команда загружает BX значением смещения CharString
(запомните, что оператор OFFSET возвращает значение смещения для
метки), плюс 8. (Данное выражение, состоящее из вычисления
смещения OFFSET и сложения, расчитывается Turbo Assembler во время
ассемблирования.) Во второй команде в AL загружается содержимое
памяти со смещением, указываемым BX, как показано на рис.5.3.

.
.
.
——————
99? ? ?
——————
CharString —> 100? ‘A’ ?
——————
101? ‘B’ ?
——————
102? ‘C’ ?
——————
103? ‘D’ ?
—————— ————
104? ‘E’ ? AL? ?
—————— ————
105? ‘F’ ? °
—————— ?
106? ‘G’ ? ?
—————— ?
107? ‘H’ ? ?
——— —————— ?
BX ? 108 ?-> 108? ‘I’ ?————-
——— ——————
109? ‘J’ ?
——————
110? ‘K’ ?
——————
111? ‘L’ ?
——————
112? ‘M’ ?
——————
113? 0 ?
——————
114? ? ?
——————
.
.
.
Рис.5.3 Адресация CharString при помощи BX

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

mov ax,[bx] ;загрузка AX из ячейки памяти со смещением,
;указываемым BX

и

mov ax,bx ;загрузка AX содержимым BX

это две совершенно разные команды.

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

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

.
.
.
mov bx,OFFSET CharString ;указатель начала строки
FindLastCharLoop:
mov al,[bx] ;прием следующего символа строки
cmp al,0 ;это нулевой байт?
je FoundEndOfString ;да, возврат на символ назад
inc bx ;указатель на следующий символ
jmp FindLastCharLoop ;переход на проверку
;следующего символа
FoundEndOfString:
dec bx ;указатель на один символ назад
mov al,[bx] ;прием последнего символа строки
.
.
.

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

BX это не единственный регистр, который может работать как
указатель памяти. BP, SI и DI также используются в этом качестве,
а также произвольно задаваемые константы и метки. В общем виде
операнд обращения к памяти выглядит следующим образом:

[базовый регистр+индекс+регистр+смещение]

или

[базовый регистр+индекс] [регистр+смещение]

где базовый регистр это BX или BP, индексный регистр это SI
или DI, а смещение это 16-битовая константа, которое может
включать в себя выражения и метки. Эти три значения складываются
8086 между собой всякий раз при выполнении команды, использующей
операнд — память. Каждый из трех компонентов операнда памяти
указывается произвольно, но хотя бы один из трех должен
обязательно присутствовать (иначе ведь вообще никакого адреса не
получится!). В другом формате элементы операнда — памяти имеют
несколько иной вид:

BX SI
или + или + Смещение
BP DI
(база) (индекс)

В результате задать адрес памяти можно 16 способами:

— [смещение] — [bp+смещение]
— [bx] — [bx+смещение]
— [si] — [si+смещение]
— [di] — [di+смещение]
— [bx+si] — [bx+si+смещение]
— [bx+di] — [bx+di+смещение]
— [bp+si] — [bp+si+смещение]
— [bp+di] — [bp+di+смещение]

где, как и ранее, «смещение» это любым образом полученная
16-битовая константа.

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

.
.
.
.DATA
CharString DB ‘ABCDEFGHIJKLM’,0
.
.
.
.CODE
mov ax,@Data
mov ds,ax
.
.
.
mov si,OFFSET CharString+8
mov al,[si]
.
.
.
mov bx,8
mov al,[CharString+bx]
.
.
.
mov bx,OFFSET CharString
mov al,[bx+8]
.
.
.
mov si,8
mov al,[CharString+si]
.
.
.
mov bx,OFFSET CharString
mov di,8
mov al,[bx+di]
.
.
.
mov si,OFFSET CharString
mov bx,8
mov al,[si+bx]
.
.
.
mov bx,OFFSET CharString
mov si,7
mov al,[bx+si+1]
.
.
.
mov bx,3
mov si,5
mov al,[bx+Charstring+si]
.
.
.

Верьте или не верьте, но все эти команды действительно
адресуют одну и ту же ячейку памяти, [CharString]+8.

С приведенным примером связано несколько интересных вопросов.
Во-первых, вы должны понимать, что знак плюс (+) внутри квадратных
скобок имеет специальное значение. Во время ассемблирования Turbo
Assembler складывает все константы внутри квадратных скобок,
поэтому

mov [10+bx+1+si+100],cl

фактически принимает вид

mov [bx+si+111],cl

Далее, во время действительного выполнения этой команды (при
запуске программы) операнды, адресующие память, динамически
складываются 8086. Если BX содержит 25, а SI содержит 52, то
содержимое CL при выполнении команды MOV будет помещено в ячейку
памяти с адресом 25+52+11=188. Ключевым моментом здесь яляется то,
что во время выполнения команды именно 8086 складывает между собой
содержимое базового регистра, индексного регистра и смещение.
Говоря иначе, Turbo Assembler складывает во время ассемблирования
константы, а 8086 складывает во время фактического выполнения
команды значения базового и/или индексного регистров и/или
смещение.

Вы, должно быть, заметили, что ни в одном из последних
примеров мы не использовали регистр BP. Дело в том, что BP
работает несколько иначе, чем BX. Вспомните, что BX используется
как смещение относительно начала сегмента данных, а BP
используется как смещение в стековом сегменте. Это значит, что BP
не может нормальным образом использоваться служить для адресации
CharString, которая находится в сегменте данных (это дополнительно
о сегментах).

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

Фактически BP можно использовать для адресации сегмента
данных, а BX, SI и DI можно использовать для адресации стекового
сегмента, сегмента программы или дополнительного сегмента при
помощи префиксов замены сегмента. Эти префиксы рассматриваются в
главе 9; однако по большей части они вам не понадобятся, и пока
что мы будем игнорировать существование этого механизма.

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

mov al,[MemVar]

и

mov al,MemVar

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

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

Вам также встретятся формы записи адресации памяти следующего
вида:

mov al,CharString[bx]

и даже

mov al,CharString[bx][si]+1

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

mov al,[CharString+bx+si+1]

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

Комментарии —————————————————

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

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

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

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

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

mov [bx],al ;записать модифицированный символ

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

mov [bx],al

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

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

.
.
.
mov ah,1 ;функция DOS ввода с клавиатуры
int 21h ;обращение к DOS для приема следующего
;нажатия клавиши
.
.
.

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

.
.
.
mov ah,1
int 21h ;прием следующей нажатой клавиши
.
.
.

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

.
.
.
;
;Генерирование байта контрольной суммы для буфера передачи
;
mov bx,OFFSET TransferBuffer
mov cx,TRANSFER_BUFFER_LENGTH
sub al,al ;очистить сумматор контрольной суммы
Checksum:
add al,[bx] ;сложить с текущим значением байта
inc bx ;установить указатель на следующий байт
loop Checksum
.
.
.

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

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

;
;Функция, возвращающая байт контрольной суммы буфера данных
;
;Вход:
; DS:BX — указатель на начало буфера
; CX — длина буфера
;Выход:
; AL — контрольная сумма буфера
;
;Регистры, содержимое которых разрушается:
; BX, CX
;
;Примечание: длина буфера не должна превышать 64 Кб,
;и буфер не должен пересекать границы сегмента.
;
Checksum PROC NEAR
sub al,al ;очистить сумматор для контрольной
;суммы Checksum:
add al,[bx] ;добавить значение текущего байта
inc bx ;установить указатель на следующий
;байт
loop Checksum
ret
Checksum ENDP

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

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

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

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

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

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

Подробное описание сегментных директив дается в главе 9,
«Расширенные средства программирования на Turbo Assembler».

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

Основные упрощенные сегментные директивы это .STACK, .CODE,
.DATA, .MODEL и DOSSEG. В этом разделе мы разделим их на две
группы и начнем с директив .STACK, .CODE и .DATA.

.STACK, .CODE и .DATA
———————

Директивы .STACK, .CODE и .DATA определяют сегменты стека,
программы и данных, соответственно. Директива .STACK определяет
размер стека. Например,

.STACK 200h

определяет размер стека равным 200h (512) байт. Это все, что
вы должны указать касательно стека; обеспечьте в вашей программе
наличие директива .STACK, и Turbo Assembler обеспечит все
остальные действия по организации стека. 200h это достаточный
размер стека для обычных программ, хотя программы с интенсивным
использованием стека — например, рекурсивные программы, могут
потребовать распределить стеку больший размер памяти.

Информацию об исключениях при использовании директивы .STACK
см. в разделе «Что делать, если вы забыли распределить стек или
выделили слишком маленький стек» в главе 6.

Директива .CODE отмечает начало программного сегмента для
вашей программы. Вам могло показаться, что с точки зрения Turbo
Assembler очевидно, что все команды вашей программы должны
принадлежать программному сегменту. Действительно, это так, но
(при работе со стандартными сегментными директивами) Turbo As-
sembler позволяет вам иметь несколько программных сегментов, и
тогда .CODE сообщает Turbo Assembler конкретно, который из
программных сегментов будет содержать данные команды. Определение
программного сегмента выполняется еще проще, чем определение
стекового сегмента, поскольку .CODE не имеет операндов. Например,

.
.
.
.CODE
sub ax,ax ;установить сумматор в нуль
mov cx,100 ;число выполняемых циклов
.
.
.

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

.
.
.
.DATA
TopBoundary DW 100
Counter DW ?
ErrorMessage DB 0dh,0ah,’***Error***’,0dh,0ah,’$’
.
.
.

Однако это был пример прямого определения сегмента данных
Сложность с директивой .DATA (собственно говоря, вы убедитесь, что
она не так уж и серьезна) состоит в том, что прежде чем вы
получите доступ к ячейкам памяти сегмента, определяемого
директивой .DATA, вы должны явно загрузить в регистр сегмента DS
символическое имя @data. Поскольку сегментный регистр может быть
загружен из любого регистра общего назначения или из любой ячейки
памяти, но не может быть загружен константой, сегментный регистр
DS как правило загружается последовательностью из двух команд

.
.
.
mov ax,@data
mov dx,ax
.
.
.

(Вместо AX здесь можно было использовать любой регистр общего
назначения). Приведенная последовательность устанавливает DS как
указатель на сегмент данных, начинающийся с директивы .DATA.

Следующая программа выводит на экран текст, хранящийся в
DataString (DSLYSTR.ASM на диске):

.MODEL small
.STACK 200h
.DATA
DataString DB ‘Этот текст находится в сегменте данных’
.CODE
ProgramStart:
mov bx,@Data
mov ds,bx ;устанавливает DS на сегмент .DATA
mov dx,OFFSET DataString ;DS указывает на
;смещение DataString в сегменте
;.DATA
mov ah,9 ;функция DOS печати строки
int 21h ;обращение к DOS для печати строки
mov ah,4ch ;функция DOS конца программы
int 21h ;обращение к DOS для выхода из
;программы
END ProgramStart

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

Здесь вы также можете поинтересоваться, почему собственно
требуется загружать именно DS, а не CS или SS. Или, скажем, ES?

Ответ состоит в том, что загружать CS в явном виде никогда не
требуется, поскольку DOS делает это за вас при запуске программы.
Кроме того, если бы CS не был уже установлен к моменту выполнения
первой команды программы, то 8086 не знал бы, откуда брать эту
первую команду и программа никогда бы не пошла. Сейчас это может
быть для вас не очевидным, но поверьте нам — CS при запуске
программы устанавливается автоматически, и делать это явно вам
никогда не понадобится.

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

DS работает совсем иначе. Если CS содержит указатель на
команды программы, а SS на стек, то DS указывает на данные.
Программы не манипулируют командами или стеком напрямую — но
всегда прямо работают с данными. И более того, программе в любой
момент могут понадобиться данные, находящиеся в нескольких
различных сегментах; помните, что 8086 позволяет осуществлять
доступ к любой ячейке памяти в пределах 1 мегабайта, но только в
блоках по 64 Кб (относительно сегментного регистра) за каждое
обращение.

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

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

Сегмент ES загружается так же, как DS. Часто в использовании
ES никакой необходимости нет, но если вам требуется доступ к
памяти в сегменте, на который указывает ES, вы должны сперва
загрузить ES этим сегментом. Например, следующая программа
загружает ES сегментом .DATA, а затем загружает через ES символ
для печати из этого сегмента:

.MODEL small
.Stack 200h
.DATA
OutputChar DB ‘B’
.CODE
ProgramStart:
mov dx,@Data
mov es,dx ;установка ES на сегмент .DATA
mov bx,OFFSET OutputChar ;BX указывает на сме-
;щение для OutputChar
mov al,es:[bx] ;прием символа для вывода из се-
;гмента, указываемого в ES
mov ah,2 ;функция DOS вывода на дисплей
int 21h ;вызов DOS для печати символа
mov ah,4ch ;функция DOS конца программы
int 21h ;обращение к DOS для выхода из
;программы
END ProgramStart

Отметим, что ES загружается последовательностью из двух
команд:

.
.
.
mov dx,@Data
mov es,dx
.
.
.

так же, как и ранее DS.

Конечно, в приведенном примере не было конкретной причины для
использования ES вместо DS, и то, что был взят сегмент ES,
означало необходимость использования префикса ES: (это будет
рассмотрено в главе 10). Однако существует множество случаев,
когда удобно иметь ES установленным на один сегмент, а DS на
другой, в частности при работе со строковыми командами.

DOSSEG
——

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

Подробное описание DOSSEG см. в главе 3 Справочного
руководства.

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

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

.MODEL
——

Директива .MODEL задает модель памяти ассемблерного модуля,
использующего упрощенные сегментные директивы. Отметим, что
ближнее ветвление (переход) задается при помощи загрузки только
регистра IP, дальнее — регистров как CS, так и IP. Аналогичным
образом, обращение к ближним данным задается только смещением, а к
дальним задается полным адресом сегмент: смещение. Короче говоря,
«дальнее» обращение означает использование полного 32-битового
адреса сегмент:смещение, а «ближнее» — только 16-битового
смещения.

Существуют следующие модели памяти:

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

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

medium Программа может занимать более 64 Кб, а данные этой
программы должны размещаться в одном отдельном сег-
менте 64 Кб. Используется дальняя адресация программы
и ближняя адресация данных.

compact Программа должна размещаться в одном сегменте разме-
ром 64 Кб, а данные программы могут занимать более
64 Кб. Используется ближняя адресация программы и
дальняя адресация данных. Ни один массив данных не
может по размеру превышать 64 Кб.

large И программа, и данные могут превышать по размеру
64 Кб, но ни один массив данных не может по размеру
превышать 64 Кб. Адресация программы и данных даль-
няя.

huge И программа, и данные могут превышать по размеру
64 Кб, и массивы данных могут по размеру превышать
64 Кб. Адресация программы и данных дальняя. Ука-
затели на элементы массивов дальние.

Отметим, что с точки зрения ассемблера, модели large и huge
идентичны. Модель huge не поддерживает массивы данных свыше 64 Кб
автоматически.

Редко какие ассемблерные программы требуют более 64 Кб памяти
для программы или для данных, поэтому большинству прикладных
программ годится модель small. Модель small следует использовать
всегда, когда это возможно, поскольку использование дальней
адресации программы (модели medium, large и huge) замедляют работу
программы, а дальняя адресация данных (модели compact, large и
huge) на языке ассемблера труднее программируется.

Описанные здесь модели памяти соответствуют моделям памяти,
используемым в Turbo C (и многих других компиляторах для PC).
Всякий раз при компоновке ассемблерного модуля с модулем языка
высокого уровня следует задавть правильную директиву .MODEL.
Директива .MODEL обеспечит соответствие имен ассемблерных
сегментов используемым в языках высокого уровня, а также
соответствие меток типа PROC, в которых именуются подпрограммы,
процедуры и функции, типу адресации по умолчанию — ближней или
дальней — используемому в языках высокого уровня.

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

.MODEL small
STACK 200h
.DATA
MemVar DW 0
.
.
.
.CODE
ProgramStart:
mov ax,@data
mov ds,ax
mov ax,[MemVar]
.
.
.
mov ah,4ch
int 21h
END ProgramStart

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

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

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

.FARDATA позволяет определить дальний сегмент данных, то есть
сегмент данных, не являющийся стандартным сегментом @data,
разделяемым всеми модулями. .FARDATA позволяет модулю определить
свой собственный сегмент данных, размером до 64 Кб. Если задана
директива .FARDATA, то @fardata это имя дальнего сегмента данных,
задаваемого этой директивой, подобно тому, как @data это имя
сегмента данных, задаваемого директивой .DATA.

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

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

При использовании упрощенных сегментных директив доступны
некоторые полезные предопределенные метки.

— @FileName это имя ассемблируемого файла.

— @CurSeg это имя ассемблируемого втекущий момент Turbo
Assembler сегмента.

— @CodeSize равна 0 для моделей памяти с ближней адресацией
программы (tiny,small и compact), и 1 для моделей памяти с
дальней адресацией программы (medium, large и huge).

— Аналогичным образом, @DataSize равна 0 для моделей памяти
с ближней адресацией сегментов данных (tiny, small и medium),
1 для моделей памяти compact и large, и 2 для модели huge.

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

Далее приводится фрагмент программы из предыдущего раздела,
но на этот раз будут использованы стандартные сегментные директивы
SEGMENT, ENDS и ASSUME:

DGROUP GROUP _DATA, STACK
ASSUME CS:_TEXT, DS:_DATA, SS:STACK
STACK SEGMENT PARA STACK ‘STACK’
DB 200h DUP (?)
STACK ENDS
_DATA SEGMENT WORD PUBLIC ‘DATA@
MemVar DW 0
.
.
.
_DATA ENDS
_TEXT SEGMENT WORD PUBLIC ‘CODE’
ProgramStart:
mov ax,_DATA
mov ds,ax
mov ax,[MemVar]
.
.
.
mov ah,4ch
int 21h
_TEXT ENDS
END ProgramStart

Теперь вы знаете, почему упрощенные сегментные директивы
называются «упрощенными»! Однако, многое из того, что делают
упрощенные сегментные директивы, предназначено для упрощения
компоновки ассемблерных модулей с языками высокого уровня и для
автономных программ на ассемблере не требуется. Ниже приводится
программа Hello, world с использованием стандартных сегментных
директив:

Stack SEGMENT PARA STACK ‘STACK’
DB 200h DUP (?)
Stack ENDS

Data SEGMENT WORD ‘DATA’
HelloMessage DB ‘Hello, world’,13,10,’$’
Data ENDS

Code SEGMENT WORD ‘CODE’
ASSUME CS:Code, DS:Data
ProgramStart:
mov ax,Data
mov ds,ax ;установка DS на сегмент данных
mov dx,OFFSET HelloMessage ;DS:DX указывает на
;сообщение hello
mov ah,9 ;функция DOS печати строки #
int 21h ;печать строки hello
mov ah,4ch ;функция DOS конца программы #
int 21h ;конец программы
Code ENDS
END ProgramStart

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

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

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

Директива SEGMENT определяет начало сегмента. Метка,
сопровождающая директиву SEGMENT, это имя сегмента; например,

Cseg SEGMENT

определяет начало сегмента с именем Cseg. В директиве SEGMENT
можно при желании определить некоторые аттрибуты сегмента, в том
числе задать выравнивание по границе байта, слова, двойного слова,
параграфа (16 байт) или страницы (256 байт) памяти. В число прочих
аттрибутов входит способ объединения сегмента с другими сегментами
с тем же именем и классом сегмента.

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

Директива ENDS определяет конец сегмента. Например,

Cseg ENDS

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

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

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

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

Data1 SEGMENT WORD ‘DATA’
Var1 DW 0
Data1 ENDS
.
.
.
Data2 SEGMENT WORD ‘DATA’
Var2 DW 0
Data2 ENDS

Code SEGMENT WORD ‘CODE’
ASSUME CS:Code
ProgramStart:
mov ax,Data1
mov ds,ax ;установка DS на Data1
ASSUME DS:Data1
mov ax,[Var2] ;попытка загрузить Var2 в AX —
;это вызовет ошибку, т.к. доступ
;к Var2 из сегмента Data1
;невозможен
.
.
.
mov ah,4ch ;функция DOS конца программы
int 21h ;конец программы
Code ENDS
END ProgramStart

Turbo Assembler обнаружит в приведенном примере ошибку,
поскольку в ней имеется попытка доступа к переменной памяти Var2,
когда DS установлен на сегмент Data1, тогда как адресация Var2,
пока DS не будет установлен на Data2, невозможен.

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

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

mov al,[bx]

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

Если сегментный регистр в текущий момент не указывает ни на
один имеющий имя сегмент, то вы можете использовать с директивой
ASSUME имя NOTHING (ничто), чтобы сообщить Turbo Assembler эту
информацию. Например,

.
.
.
mov ax,0b800h
mov ds,ax
ASSUME DS:NOTHING
.
.
.

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

.
.
.
ColorTextSeg SEGMENT AT 0B800h
ColorTextMemory LABEL BYTE
ColorTextSeg ENDS
.
.
.
mov ax,ColorTextSeg
mov ds,ax
ASSUME DS:ColorTextSeg
.
.
.

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

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

.
.
.
Data1 SEGMENT WORD ‘DATA’
Var1 DW 0
Data1 ENDS

Data2 Segment WORD ‘DATA’
Var2 DW 0
Data2 Ends

Code SEGMENT WORD ‘CODE’
ASSUME CS:Code
ProgramStart:
mov ax,Data1
mov ds,ax ;установка DS на Data1
ASSUME DS:Data1
mov ax,Data2
mov es,ax ;установка ES на Data2
ASSUME ES:Data2
mov ax,[Var2 ;загрузка Var2 в AX — Turbo As-
;sembler заставит 8086 выполнять
;загрузку относительно ES, т.к.
;доступ к Var2 через DS невозможен
.
.
.
mov ah,4ch ;функция DOS конца программы
int 21h ;конец программы
Code ENDS
END ProgramStart

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

mov ax,[Var2]

таким образом, чтобы обратиться к Var2 не через сегментный
регистр DS, а через сегментный регистр ES.

Префиксы переопределения сегмента и стандартные сегментные
директивы в целом рассматриваются в главе 9.

Практически происходит следующее: две директивы ASSUME
сообщили Turbo Assembler, что DS установлен на сегмент Data1, а ES
на сегмент Data2. Когда команда MOV пытается обратиться к Var2,
которая находится в сегменте Data2, Turbo Assembler справедливо
заключает, что через DS доступ к Var2 невозможен; однако
обратиться к Var2 можно через ES. Следовательно, Turbo Assembler
вставит перед командой MOV специальный код, который называется
префиксом переопределения сегмента, который сообщит 8086 о том,
что тому следует вместо сегментного регистра DS использовать ES.

Что это означает для вас? Это означает, что если вы при
помощи директив тщательно определите для Turbo Assembler текущие
установки DS и ES, то он сможет помочь вам, автоматически проверяя
допустимость доступа к именованным переменным памяти и даже в
некоторых случаях автоматически изменяя назначение сегмента на
правильное.

Сравнение упрощенных и стандартных сегментных директив ———

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

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

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

Если вы пишете большие автономные программы со многими
сегментами и смешанным программированием (с ближними и дальними
программными ссылками и/или ближними и дальними ссылками на данные
в одной программе), вам понадобятся стандартные сегментные
директивы, поскольку только в этом случае вы получаете полные
возможности управления типом сегмента, выравниванием,
наименованием и способом объединения сегментов.

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

Распределение данных
——————————————————————

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

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

Биты, байты и основания системы счисления ———————

Фундаментальной единицей хранения данных в компьютере
является бит. Бит может принимать значения либо 1, либо 0. Сам по
себе бит не играет большой роли. 8086 напрямую с битами не
работает; фактически минимальной единицей информации, с которой он
имеет дело, является байт, который состоит из восьми битов.

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

2 в степени 0: 1
2 в степени 1: 2
2 в степени 2: 4
2 в степени 3: 8
2 в степени 4: 16
2 в степени 5: 32
2 в степени 6: 64
2 в степени 7: +128

255

Это значит, что байт может хранить одно значение в диапазоне
0-255.

Каждый из 8-битовых регистров 8086 (AL, AH, BL, BH, CL, CH,
DL и DH) хранит ровно 1 байт. Каждая из более чем 1,000, 000
адресуемых ячеек памяти также может хранить ровно 1 байт.

Набор символов PC (включающий в себя буквы верхнего и нижнего
регистров, цифры 0-9,специальные графические, научные и
иностранные символы, а также знаки пунктуации и прочие символы)
суммарно состоит из 256 символов. Число звучит знакомо, не так ли?
Из этого следует, что набор символов для PC был разработан таким
образом, чтобы 1 байт мог хранить 1 символ.

Итак, теперь вы знаете, что такое байт, то есть мимимальная
адресуемая в 8086 единица памяти, в которой может храниться один
символ, либо одно число без знака в диапазоне от 0 до 255, либо
одно число со знаком от -127 до +128. Однако, байт не годится для
многих задач программирования на ассемблере, как то хранение целых
чисел типа integer, чисел с плавающей точкой и указателей на
адреса хранения в памяти.

Следующей, более крупной единицей хранения 8086 является
16-битовое слово. Слово в два раза превышает размер байта (16
бит). Фактически слово хранится в памяти в виде двух
последовательно расположенных байтовых ячеек; адресное
пространство памяти 8086 можно рассматривать как 500,000 с лишним
слов. Каждый из 16-битовых регистров 8086 (AX, BX, CX, DX, SI, DI,
BP, SP, CS, DS, ES, SS, IP и флаговый регистр) может хранить одно
слово. Слово содержит 16-битовое двоичное число. Максимально
возможное 16-битовое число в двоичной системе складывается из:

2 в степени 0: 1
2 в степени 1: 2
2 в степени 2: 4
2 в степени 3: 8
2 в степени 4: 16
2 в степени 5: 32
2 в степени 6: 64
2 в степени 7: 128
2 в степени 8: 256
2 в степени 9: 512
2 в степени 10: 1024
2 в степени 11: 2048
2 в степени 12: 4096
2 в степени 13: 8192
2 в степени 14: 16384
2 в степени 15: +32768
——
65535

Это также и максимальная величина целого (типа integer) числа
без знака — это не совпадение, поскольку целые имеют длину 16 бит.
Целые со знаком (они могут лежать в диапазоне от -32768 до +32767)
также хранятся в виде слов.

Поскольку размер слова 16 бит, то слова могут адресовать
любое смещение заданного сегмента, поэтому размер слова
достаточен, чтобы использовать его в качестве указателя памяти.
Вспомните, что указателями памяти служат регистры BX, BP, SI и DI,
также имеющие размер в одно слово.

Величины, хранимые в 32-битовых (4-байтовых) единицах памяти,
называются «двойное слово» (doubleword или dword). Хотя 8086 не
позволяет прямо манипулировать 32-битовыми целыми, такие команды
как ADC и SUB дают возможность выполнять 32-битовые целочисленные
арифметические операции при посредством двух последовательных
16-битовых операции. Двойные слова поддерживают работу с целыми
без знака в диапазоне от 0 до 4,294,967,295 и с целыми, имеющими
знак, в диапазоне от -2,147,483,648 до +2,147,483,647.

8086 может загружать указатель сегмент:смещение из двойного
слова в сегментный регистр и регистр общего назначения командами
LDS или LES, но на этом прямая поддержка операций с двойными
словами и заканчивается. В виде двойных слов также
хранятся числа с плавающей точкой одинарной точности. (Они
-38
занимают 4 байта и могут представлять числа порядка от 10
38
до 10 .)

Для хранения каждого числа с плавающей точкой двойной
точности требуется полные 8 байт. Такие 64-битовые единицы
памяти называются «учетверенное слово» (quadword). 8086 не
имеет встроенных средств поддержки учетверенных слов. Однако,
числовой сопроцессор 8087 в качестве своего базового типа
данных использует именно учетверенные слова. (Числа двойной
-308 308
точности могут лежать в диапазоне от 10 до 10 и име-
ют точность представления до 16 значащих цифр.)

Turbo Assembler поддерживает еще один размер данных для
временных (промежуточных) значений с плавающей точкой, элемент
данных размером 10 байт. Такой 10-байтовый размер элемента памяти
может также использоваться для хранения упакованных
двоично-десятичных значений (CD), в случае которых каждый байт
содержит две десятичных цифры.

Стоит отметить, что при записи в память слов и двойных слов
первым идет младший байт. То есть, как показано на рис. 4.4, если
в памяти начиная с адреса 0 записано значение длиной в одно слово,
то биты 7-0 этого слова будут находиться по адресу 0, а биты 15-8
— по адресу 1, как показано на Рис.5.4 (WordVar содержит значение
199Fh. DwordVar содержит значение 12345678h).

————————
WordVar —> 0 ? 9Fh ?
————————
1 ? 19h ?
————————
2 ? ? ?
————————
3 ? ? ?
————————
4 ? ? ?
————————
DwordVar —> 5 ? 78h ?
————————
6 ? 56h ?
————————
7 ? 34h ?
————————
8 ? 12h ?
————————
9 ? ? ?
————————
? ?

Рис.5.4 Хранение в памяти WordVar и DwordVar

Аналогичным образом, если значение размером в двойное слово
записано по адресу 5, то биты 7-0 хранятся по адресу 5, биты 15-8
хранятся по адресу 6, биты 23-16 хранятся по адресу 7, а биты
31-24 хранятся по адресу 8. Это может показаться несколько
странным, но именно таким образом работают все процессоры
семейства iAPx86.

Десятичная, двоичная, восьмиричная и шестнадцатиричная
формы записи
——————————————————————

Теперь, когда вам известны типы данных, с которыми работает
язык ассемблера, следующим вопросом является «Каким образом должны
быть представлены величины?» Десятичное представление (в системе
счисления с основанием 10) чисел является самым легким, поскольку
в этой форме мы имеем дело с числами всю жизнь. действительно,
легко записать:

mov cx,100 ;установить счетчик цикла равным 100

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

Если так, то представляется более логичным использовать в
ассемблерных программах двоичную форму записи. Вы можете сообщить
Turbo Assembler, что вводимое число является двоичным, указав в
конце его букву b. (Конечно, число при этом должно состоять только
из нулей и единиц, поскольку двоичная запись использует только эти
две цифры). Например, десятичное 5 в двоичной записи будет
выглядеть как 101b.

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

Например, последний фрагмент в двоичной записи примет вид:

mov cx,1100100b ;установить счетчик цикла равным 100
;десятичному

Чтение и запись двоичных значений длиной слово и двойное
слово затруднены еще более.

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

————————————————————
Десятичная Двоичная Восьмиричная Шестнадцатиричная
————————————————————
0 0 0 0
1 1 1 1
2 10 2 2
3 11 3 3
4 100 4 4
5 101 5 5
6 110 6 6
7 111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 A
11 1011 13 B
12 1100 14 C
13 1101 15 D
14 1110 16 E
15 1111 17 F
16 10000 20 10
17 10001 21 11
18 10010 22 12
19 10011 23 13
20 10100 24 14
21 10101 25 15
22 10110 26 16
23 10111 27 17
24 11000 30 18
25 11001 31 19
26 11010 32 1A
. . . .
. . . .
256 100000000 400 100
. . . .
. . . .
4096 1000000000000 10000 1000
. . . .
. . . .
65536 10000000000000000 200000 10000
————————————————————

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

Восьмиричная, или в системе счисления с основанием 8, запись
чисел использует цифры 0-7, и каждая из них соответствует группе
из трех бит. На рис.5.5 показано, как двоичное число 001100100b
(100 десятичное) может быть разбито на группы по три бита, образуя
эквивалентное восьмиричное значение 144o.

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

Двоичное 000 100 100
— — —
? ? ?
——- ? ——-
? ? ?
Восьмиричное 1 4 4

Рис.5.5 Преобразование двоичного 001100100 (десятичное
100) в восьмиричное 144

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

mov cx,144o ;установить счетчик цикла равным 100
;десятичному

Восьмиричная запись очень полезна и широко распространена в
некоторых областях вычислительной техники. Однако по большей части
программисты IBM PC используют не восьмиричную, а
шестнадцатиричную (в системе счисления с основанием 16) форму
записи чисел.

Каждая шестнадцатиричная цифра может принимать одно из 16
значений. Ниже приводится пример счета в этой системе:

0 1 2 3 4 5 6 7 8 9 A B C D E F 10 …

Буквы после цифры 9 это ни что иное, как дополнительные
шестнадцатиричные цифры A-F. (Можно использовать для их записи и
строчные буквы a-f). Хотя использовать в качестве цифр букв может
показаться вам странным, выбора у вас нет, поскольку требуется 16
цифр, а традиционных десятичных цифровых символов имеется только
10. На рис.5.6 показано, как двоичное число 01100100b (100
десятичное) может быть разбито на группы по четыре бита, образуя
эквивалентное шестнадцатиричное значение 64h.

Двоичное 0110 0100
—- —-
? ?
——— ———
? ?
Шестнадцатиричное 6 4

Рис.5.6 Преобразование двоичного 01100100 (десятичное
100) в шестнадцатиричное 64

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

mov cx,64h ;установить счетчик цикла равным 100
;десятичному

Шестнадцатиричные числа обозначаются суффиксом h. Кроме того
шестнадцатиричные числа должны начинаться с цифры 0-9, поскольку
шестнадцатиричное число вроде BAD4 может быть воспринято
ассемблером как метка BAD4h. Ниже приводится пример, в котором
одновременно имеются и шестнадцатиричное число 0BAD4h и метка
BAD4h:

.
.
.
.DATA
BAD4h DW 0 ;метка BAD4h
.
.
.
.CODE
mov ax,0BAD4h ;загружает в AX шестнадцатиричную
;константу (ведущий о указывает на
то, что это константа)
.
.
.
mov ax,0BAD4h ;загружает AX содержимым переменной
;памяти BAD4h (отсутствие ведущего
;0 указывает на то, что это метка
.
.
.

В общем виде, числовой константой может являться только
операнд, начинающийся с цифры 0-9.

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

1.1
-12.45
1.oE12
252.123E-6

Turbo Assembler преобразовывает форму записи мантисса/
экспонента в двоичный вид, следуя формату с плавающей точкой. Если
вы хотите, то можете задавать числа с плавающей точкой
непосредственно в формате IEEE или Microsoft binary, задавая число
в шестнадцатиричном виде и помещая в конце числа суффикс r.

Действительные числа могут быть использованы только с
директивами DD, DQ и DT, которые мы рассмотрим ниже. Если вы
предпочитаете использовать суффикс r, то вы должны точно задать
максимальное количество шестнадцатиричных знаков для
инициализируемого вами типа данных (плюс, при необходимости,
ведущий 0); например,

DD 40000000r ;2.0 (длиной ровно 8)
DQ 0C014CCCCCCCCCCCCr ;-5.2 (длина 16, плюс ведущий 0)
DT 4037D529AE9E86000000r ;1.2E17 (длина ровно 20)

В целом, гораздо проще пользоваться формой записи «мантисса/
экспонента».

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

И наконец, можно использовать символьные константы,которые
обозначаются одинарными или двойными кавычками. Значение
символьной константы равно ее ASCII-коду. Например, во всех
приводимых ниже строках в AL загружается символ A:

mov al,65
mov al,41h
mov al,’A’
mov al,»A»

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

mov ax,1001b
add cx,5bh
sub [Count],177o
and al,1
mov al,’A’

Значения с плавающей точкой допустимы только в DD, DQ и DT;
BCD (двоично-десятичные) — только в DT.

Выбор системы счисления по умолчанию
————————————

По большей части вам, вероятнее всего, понадобится, чтобы по
умолчанию была установлена десятичная система счисления, хотябы
потому, что такая запись является для вас наиболее знакомой.
Однако, в некоторых случаях бывает удобнее, чтобы по умолчанию,
без суффикса, была принята другая форма записи — в этом случае и
применяется директива .RADIX. (Radix означает «основание системы
счисления»).

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

.RADIX 16

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

.
.
.
.RADIX 16 ;выбирает систему счисления по умолчанию
;с основанием 16, или шестнадцатиричную
mov ax,100 ;=100h, или 256 десятичное
.RADIX 10 ;выбирает систему счисления по умолчанию
;с основанием 10, или десятичную
sub ax,100 ;-100 десятичное,
;то есть 256-100 = 156 десятичное
.RADIX 2 ;выбирает систему счисления по умолчанию
;с основанием 2, или двоичную
add ax,100 ;+100b, или 4 десятичное, результат равен
;156 + 4 = 160 десятичное
.
.
.

.RADIX позволяет выбрать в качестве основания системы
счисления 2, 8, 10 или 16.

Операнд директивы .RADIX всегда задается десятичный,
независимо от установленной на текущий момент системы записи чисел
по умолчанию; другими словами, одна директива .RADIX не влияет на
операнд следующей директивы .RADIX.

Существует потенциальная проблема, которую следует
рассмотреть в связи с директивой .RADIX. Независимо от
установленной по умолчанию формы записи значения, задаваемые в DD,
DQ и DT, считаются десятичными, если противное не было определено
явно при помощи суффикса. Это означает, что

.
.
.
.RADIX 16
DD 1E7
.
.
.

1E7 это 10 в седьмой степени, умноженное на 1, а не 1E7h.
Фактически лучше помещать в конце шестнадцатиричных значений
суффикс h даже при использовании директивы .RADIX. Почему ?
Вспомните, что существуют допустимые суффиксы b и d, задающие
двоичные и десятичные значения, соответственно. К сожалению, b и d
это также и допустимые шестнадцатиричные цифры. Если действует
директива .RADIX 16, как Turbo Assembler воспримет числа 129D и
101B?

В таких случаях Turbo Assembler всегда обращает внимание на
допустимые суффиксы, поэтому 129D будет воспринято как 129
десятичное, а 101B как 101 двоичное, или 5 десятичное. Это значит,
что даже при действующей директиве .RADIX 16 любое
шестнадцатиричное число, заканчивающееся b или d, должно иметь в
конце и суффикс h. С учетом этого гораздо проще использовать этот
суффикс при записи любых шестнадцатиричных чисел, а это в свою
очередь означает, что использовать директиву .RADIX 16 практически
бесполезно.

Инициализированные данные ————————————-

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

Директивы определения данных, DB, DW, DD, DF, DP, DQ и DT
позволяют определение переменных памяти различного размера
следующим образом:

DB 1 байт
DW 2 байта = одно слово
DD 4 байта = одно двойное слово
DF,DP 6 байт = одно слово дальнего указателя (386)
DQ 8 байт = одно учетверенное слово
DT 10 байт

Например,

.
.
.
.DATA
ByteVar DB ‘Z’ ;1 байт
WordVar DW 101b ;2 байта (1 слово)
DwordVar DD 2BFh ;4 байта (1 двойное слово)
QwordVar DQ 307o ;8 байт (1 учетверенное слово)
TwordVar DT 100 ;10 байт
.
.
.
mov ah,2 ;функция DOS вывода на дисплей #
mov dl,[ByteVar];выводимый на дисплей символ
int 21h ;обращение к DOS для вывода
;символа на дисплей
.
.
.
add ax,[WordVar]
.
.
.
add WORD PTR [DwordVar],ax
adc WORD PTR [DwordVar+2],dx
.
.
.

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

Инициализация массивов
———————-

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

SampleArray DW 0, 1, 2, 3, 4

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

.
.
.
—————
? ? ?
—————
SampleArray —> ? 0 ?
—————
? 1 ?
—————
? 2 ?
—————
? 3 ?
—————
? 4 ?
—————
? ? ?
—————
.
.
.
Рис.5.7 Премер пятиэлементного массива

Что делать, если вы захотели определить массив, который не
помещается в одной строке? Следует просто добавить еще несколько
строк; при этом использовать метку с директивами определения
данных не требуется. Например,

.
.
.
SquaresArray DD 0, 1, 4, 9, 16
DD 25, 36, 49, 64, 81
DD 100, 121, 144, 169, 196
.
.
.
создает массив из элементов размером двойное слово с именем
SquaresArray, состоящий из квадратов первых 15 целых чисел.

Turbo Assembler позволяет определять блоки памяти,
инициализированные в заданное значение оператором DUP. Например,

BlankArray DW 100h DUP (0)

создает массив BlankArray, состоящий из 256 (десятичных)
слов, инициализированных в ноль. Аналогично

ArrayOfA DB 92 DUP (‘A’)

создает массив из 92 байт, каждый из которых инициализирован
символом ‘A’.

Инициализация строк символов
—————————-

Каким образом создаются строки символов? Символы являются
допустимыми операндами директив определения данных, поэтому строка
символов может быть определена следующим образом:

String DB ‘A’,’B’,’C’,’D’

Однако все эти символы вводить не обязательно, т.к. Turbo
Assembler позволяет удобную сокращенную форму:

String DB ‘ABCD’

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

HelloString DB ‘Hello, world’,0dh,0ah,0

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

.MODEL small
.STACK 200h
.DATA
String1 DB ‘Line1′,’$’
String2 DB ‘Line2′,’$’
String3 DB ‘Line3′,’$’
.CODE
ProgramStart:
mov ax,@Data
mov ds,ax
mov ah,9 ;функция DOS печати строки #
mov dx,OFFSET String1 ;печатаемая строка
int 21h ;обращение к DOS для печати
mov dx,OFFSET String2 ;печатаемая строка
int 21h ;обращение к DOS для печати
mov dx,OFFSET String3 ;печатаемая строка
int 21h ;обращение к DOS для печати
mov ah,4ch ;функция DOS конца программы
int 21h
END ProgramStart

печатает на выходе следующее:

Line1Line2Line3

Однако, если добавить в конце каждой строки пару возврата
каретки/перевода строки,

String1 DB ‘Line1′,0dh,0ah,’$’
String2 DB ‘Line2′,0dh,0ah,’$’
String3 DB ‘Line3′,0dh,0ah,’$’

то выход программы будет иметь вид:

Line1
Line2
Line3

Инициализация выражениями и метками
————————————

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

TestVar DW ((924/2)+1)

и метки:

.
.
.
.DATA
Buffer DW 16 DUP (0)
BufferPointer DW Buffer
.
.
.

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

mov ax,OFFSET Buffer

и

mov ax,[BufferPointer]

загрузят в AX одно и то же значение, смещение Buffer.

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

.
.
.
.DATA
WordArray DW 50 DUP (0)
WordArrayEnd LABEL WORD
WordArrayLength DW (WordArrayEnd — WordArray)
.
.
.

Если вам нужно вычислить длину WordArray не в байтах, а в
словах, вы можете просто разделить длину в байтах на два:

WordArrayLengthWords DW (WordArrayEnd — WordArray)/2

Неинициализированные данные
—————————

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

.
.
.
mov cx,10 ;число считываемых символов
mov bx,OFFSET KeyBuffer
;символы будут записаны в
;KeyBuffer
GetKeyLoop:
mov ah,1 ;функция DOS ввода с клавиатуры #
int 21h ;прием следующей нажатой клавиши
mov [bx],al ;запись символа
inc bx ;установка указателя на адрес, в
;который запишется следующий символ
loop GetKeyLoop
.
.
.

При определении KeyBuffer его можно было инициализировать при
помощи

KeyBuffer DB 10 DUP (0)

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

Вопросительный знак сообщает TurboAssembler, что вы
резервируете ячейку памяти без ее инициализации. Например,
правильнее определить KeyBuffer в последнем примере так:

KeyBuffer DB 10 Dup(?)

В этой строке резервируется 10 байтов, начиная с метки
KeyBuffer, но в какое-либо конкретное значение эти байты не
устанавливаются.

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

Именованные ячейки памяти
————————-

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

LABEL позволяет задать как имя метки, так и ее тип без
определения каких-либо данных. Например, ниже дается следующий
способ определения массива KeyBuffer, о котором шла речь в прошлом
примере:

.
.
.
KeyBuffer LABEL BYTE
DB 10 DUP (?)
.
.
.

LABEL позволяет определять типы меток, включая:

BYTE PWORD FAR
WORD QWORD PROC
DWORD TBYTE UNKNOWN
FWORD NEAR

BYTE, WORD, DWORD, FWORD, PWORD, QWORD и TBYTE не нуждаются в
объяснении и обозначают 1-, 2-, 4-, 6-, 8- и 10-байтовые элементы
данных, соответственно. Ниже приводится пример инициализации
переменной памяти в виде пары байтов, но доступ к ним производится
как к слову.

.
.
.
.DATA
WordVar LABEL WORD
DB 1,2
.
.
.
.CODE
.
.
.
mov ax,[WordVar]
.
.
.

При выполнении такой программы в AL загружается 1 (первый
байт WordVar), а в AH загружается 2.

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

.
.
.
.CODE
.
.
.
FarLabel LABEL FAR
NearLabel LABEL NEAR
mov ax,1
.
.
.
jmp FarLabel
.
.
.
jmp NearLabel
.
.
.

первая команда JMP определяет дальний переход (при котором
загружаются как CS, так и IP), поскольку этот переход задан к
метке, описанной как дальняя (FAR), а второй команда JMP
определяет ближний переход (при котором загружается только IP),
поскольку он задан к ближней (NEAR) метке.

Отметим, что и FarLabel, и NearLabel описывают один и тот же
адрес, адрес команды MOV, но позволяют выполнять переход
по-разному.

При использовании упрощенных сегментных директив PROC
представляет собой удобный способ определения метки
соответствующего размера, а именно ближней или дальней, для
текущей модели памяти программы. Когда задана модель памяти tiny,
small или compact, LABEL PROC это то же самое, что и LABEL NEAR;
при модели medium, large или huge LABEL PROC это то же самое, что
и LABEL FAR. Это означает, что в случае изменения модели памяти вы
можете автоматически изменить также и конкретные метки.

Например, в

.MODEL small
.
.
.
.CODE
.
.
.
EntryPoint LABEL PROC
.
.
.

EntryPoint задана как ближняя (near), но если изменить модель
памяти на large, то EntryPoint станет дальней (far). Обычно вместо
директивы LABEL для определения точек входа, которые должны
изменить свой тип при изменении модели памяти, следует
использовать директиву PROC (рассматриваемую в разделе
«Подпрограммы» на стр.174 оригинала); однако иногда в одной
подпрограмме возникает необходимость в нескольких точках входа, и
тогда наряду с PROC используется и LABEL.

И наконец, рассмотрим конструкцию LABEL UNKNOWN. UNKNOWN
обозначает, что вы не знаете точно, в качестве которого типа
данных будет использоваться эта метка. Если вы знакомы с Си, то
UNKNOWN покажется вам аналогом типа void в Си. Как пример,
иллюстрирующий применение UNKNOWN, представьте, что у вас есть
переменная памяти TempVar, обращение к которой иногда выполняется
как к байту, а иногда как к слову. Следующий пример показывает
такой случай с использованием LABEL UNKNOWN:

.
.
.
.DATA
TempVar LABEL UNKNOWN
DB ?,?
.
.
.
.CODE
.
.
.
mov [TempVar],ax
.
.
.
add dl,[TempVar]
.
.
.

Пересылка данных
——————————————————————

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

MOV это команда пересылки данных в 8086. Фактически имя этой
команды (MOV) несколько не соответствует смыслу выполняемых ей
действий и имя COPY (скопировать) более подошло бы ей, поскольку в
действительности эта команда записывает копию исходного операнда в
операнд назначения без разрушения содержимого исходного операнда.
Например:

.
.
.
mov ax,0
mov bx,9
mov ax,bx
.
.
.

сначала записывает в AX константу 0, затем записывает в BX
константу 9 и наконец копирует содержимое BX в AX, как показано
ниже.

После mov ax,0: ———————
AX ? 0 ?
———————
———————
BX ? ? ?
———————
После mov bx,9: ———————
AX ? 0 ?
———————
———————
BX ? 9 ?
———————
После mov ax,bx: ———————
AX ? 9 ?
———————
———————
BX ? 9 ?
———————

Отметим, что число 9 не пересылается из BX в AX, а
практически копируется из BX в AX.

Команда MOV может принять любую имеющую смысл пару операндов,
но не может иметь операндом сегментный регистр. (Эта ситуация
обсуждается ниже, в разделе «Доступ к сегментным регистрам»).
Исходным (стоящим справа) операндом команды MOV могут являться:

— константа

— выражение, при вычислении дающее константу

— регистр общего назначения

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

В качестве операнда назначения MOV (стоящего в команде слева)
может служить либо регистр общего назначения, оибо ячейка памяти.

Выбор размера данных ——————————————

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

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

.
.
.
mov al,1 ;размер равен байту
mov dx,si ;размер равен слову
mov bx,[di] ;размер равен слову
mov bp+si+2],al ;размер равен байту
.
.
.

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

.
.
.
.DATA
TestChar DB ?
TempPointer DW TestChar
.
.
.
.CODE
.
.
.
mov [TestChar],’A’
mov [TempPointer],0
.
.
.

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

mov [bx],1

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

Turbo Assembler предоставляет возможность гибкого определения
размера данных в виде операторов WORD PTR или BYTE PTR. WORD PTR
заставляет Turbo Assembler рассматривать данный операнд памяти как
имеющий размер в слово, а BYTE PTR — как имеющий размер в байт,
независимо от того размера, который мог быть определен ранее.
Например, в предыдущем примере можно было поместить в слово, на
которое указывает BX, значение 1 с размером в слово, указав:

mov WORD PTR [bx],1

или же поместить в байт, на который указывает BX, значение 1
с размером в байт, указав:

mov BYTE PTR [bx],1

Отметим, что WORD PTR и BYTE PTR применительно к регистрам
значения не имеют, поскольку регистры всегда имеют фиксированный
размер; в этом случае WORD PTR и BYTE PTR игнорируются.
Аналогичным образом, WORD PTR и BYTE PTR игнорируются
применительно к константам, размер которых всегда равен размеру
операнда назначения.

WORD PTR и BYTE PTR могут иметь и другое использование,
состоящее в том, чтобы временно выбирать разные размеры данных для
именованных переменных памяти. Чем это средство может быть
полезно? Рассмотрим следующее:

.
.
.
.DATA
Source1 DD 12345h
Source2 DD 54321h
Sum DD ?
.
.
.
.CODE
.
.
.
mov ax,WORD PTR [Source1] ;прием младшего
;слова Source1
mov dx,WORD PTR [Source1+2] ;прием старшего
;слова Source1
add ax,WORD PTR [Source2] ;сложить с младшим
;словом Source2
adc dx,WORD PTR [Source2+2] ;сложить со старшим
;словом Source2
mov WORD PTR [Sum],ax ;записать младшее
;слово суммы
mov WORD PTR [Sum+2],dx ;записать старшее
;слово суммы
.
.
.

Переменные, с которыми работает данный пример, являются
длинными целыми, или двойными словами. Однако, 8086 не может прямо
выполнять сложение в формате двойных слов, что вынуждает разбивать
сложение на серию операций в формате слова. WORD PTR позволяет
доступ к Source1, Source2 и Sum как к словам, даже если сами эти
переменные определены как имеющие размер в двойное слово.

Поскольку операторы FAR PTR и NEAR PTR не оказывают прямого
влияния на размер данных, в этом они аналогичны WORD PTR и BYTE
PTR. FAR PTR приводит к тому, что метка — цель перехода или
вызова, будет рассматриваться как дальняя метка, и при переходе
или вызове будут загружены регистры как CS, так и IP. Напротив,
NEAR PTR привeдeт к тому, что метка будет рассматриваться как
ближняя, и при переходе или вызове будет загружен только регистр
IP.

Данные со знаком и без знака
—————————-

Как числа со знаком, так и числа без знака представляют собой
последовательности двоичных разрядов. Различие между ними
определяется именно вами, программистом, а не самим 8086.
Например, значение 0FFFFh может быть равно либо 65,535, либо -1, в
зависимости от того, как вы в своей программе зададите его
интерпретацию. Откуда взялось, что 0FFFFh равно -1? Добавьте к
этому числу 1:

.
.
.
mov ax,0ffffh
add ax,1
.
.
.

и вы увидите, что результат равен 0, что очевидно из сложения
-1 и 1.

Команда ADD работает независимо от того, рассматриваете ли вы
операнды как числа со знаком или без знака. Предположим, вы
собираетесь вычесть из 0FFFFh 0 следующим образом:

.
.
.
mov ax,0ffffh
sub ax,1
.
.
.

Результат будет равен 0FFFEh, то есть либо 65,534 (в случае
числа без знака), либо -2 (в случае числа со знаком).

Если изложенное показалось вам непонятным, обратитесь к
рекомендованной в конце книге литературе, из которой вы сможете
больше узнать об арифметических операциях в дополнениях до двух,
посредством которых 8086 обрабатывает числа со знаком. К
сожалению, объем данной книги не позволяет привести сведения об
арифметических операциях со знаками, хотя этот вопрос
исключительно важен для программиста, работающего на языке
ассемблера. Теперь же вам достаточно знать, что команды ADD, SUB,
ADC и SBB одинаково хорошо выполняются как с числами, имеющими
знак, так и с числами без знака, и никаких специальных команд для
сложения и вычитания чисел со знаком не требуется. Знак имеет
значение при умножении и делении, как будет показано ниже; он
также играет роль при преобразованиях между различными размерами
данных и при выполнении условных переходов.

Преобразования размеров данных
——————————

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

Сначала рассмотрим преобразование слова в байт. Это несложно;
для этого просто отбрасывается старший байт слова. Например,

.
.
.
mov ax,5
mov bl,al
.
.
.

преобразовывает слово со значением 5, находящееся в AX, в
байт, находящийся в BL. Конечно, при этом вы должны быть уверены,
что преобразуемое число не превысит по размеру байт; пытаться
преобразовать число 100h в байт при помощи

.
.
.
mov dx,100h
mov al,dl
.
.
.

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

При преобразовании байта без знака в слово произойдет
обнуление старшего байта слова. Например,

.
.
.
mov cl,12
mov al,cl
mov ah,0
.
.
.

преобразует байт без знака с значением 12, находящийся в CL,
в слово без знака с значением 12 в AX.

Преобразование байта со знаком в слово несколько сложнее,
поэтому 8086 имеет для этого специальную команду CBW. CBW
преобразовывает байт со знаком из AL в слово со знаком в AX. В
следующем фрагменте байт со знаком, находящийся в DH и равный -1,
преобразовывается в слово, находящееся в DX и равное -1:

.
.
.
mov dh,-1
mov al,dh
cbw
mov dx,ax
.
.
.

8086 также имеет специальную команду CDW для преобразования
слова со знаком, находящегося в AX, в двойное слово со знаком в
DX:AX (при этом старшее слово помещается в DX). В приводимом далее
примере слово со знаком из AX, равное +10,000, преобразовывается в
двойное слово, равное +10,000, в DX:AX:

.
.
.
mov ax,10000
cwd
.
.
.

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

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

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

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

.
.
.
DataSeg DW @Data
.
.
.
.CODE
.
.
.
mov ax,@Data
mov es,ax
.
.
.
mov es,[DataSeg]
.
.
.

Желательно было бы сделать это иначе, хотя и невозможно:

mov es,@Data ;это не будет работать !

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

.
.
.
mov ax,cs
mov ds,ax
.
.
.

так и

.
.
.
push cs
pop ds
.
.
.

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

Следует отметить, что ограничения на использование сегментных
регистров существуют не только для команды MOV; большинство команд
вообще не воспринимает сегментные регистры как операнды.
Сегментные регистры можно помещать (push) на стек и извлекать
(pop) оттуда, но и только; с ними не работают команды сложения,
вычитания, логические операции или операции сравнения.

Передача данных на стек и обратно ——————————

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

mov ax,[bp+4]

загружает AX содержимым слова, имеющего в стековом сегменте
смещение BP+4. (Доступ к стеку через BO рассматривается в главе
2).

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

.
.
.
mov ax,1
push ax
pop bx
.
.
.

помещает значение из AX (равное 1) на вершину стека, а затем
снимает 1 с вершины стека и записывает в BX.

Обмен данными ————————————————-

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

xcng ax,dx

поменяет местами содержимое AX и DX, что эквивалентно

.
.
.
push ax
mov ax,dx
pop dx
.
.
.

Ввод/вывод —————————————————-

Итак, мы обсудили пересылку данных для констант, регистров и
ячеек памяти адресного пространства 8086. Как вы помните, 8086
имеет и второе, независимое ядресное пространство, известное как
адресное пространство ввода/вывода (I/O). В качестве каналов
управления и данных к аппаратным устройствам, как то дисководы,
дисплейные адаптеры, клавиатуры и принтеры, используется 65,536
адресов ввода/вывода, или портов.

Большинство команд 8086, включая MOV, может работать только с
операндами адресного пространства памяти. К адресам портов I/O
могут обращаться только две команды, IN и OUT.

IN копирует значение из выбранного порта I/O в AL или AX.
Адрес порта I/O, используемый как исходный операнд, может
выбираться одним из двух следующих способов. Если адрес порта I/O
менее 256 (100h), то такой адрес можно задавать как часть команды;
например,

in al,41h

копирует байт из порта I/O 41h в AL.

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

.
.
.
mov dx,41h
in al,dx
.
.
.

Зачем усложнять вопрос и использовать как указатель ввода/
вывода DX? С одной стороны, если адрес порта ввода/вывода больше
255, вы обязаны использовать DX. С другой стороны, благодаря DX вы
плучаете большую гибкость адресации портов I/O; например,
указатель порта I/O может быть загружен в DX и передан
подпрограмме.

Не ошибитесь в синтаксисе команды IN; AL и AX это единственно
допустимые здесь имена регистров. Подобным же образом
единственными возможными исходными операндами могут являться
константы менее 256 и DX. Нельзя использовать команды типа:

in bh,si ;это не работает

OUT работает аналогично команде IN, за исключением того, что
AL и AX это исходные операнды, о операндом назначения служит порт
I/O, на который указывает DX или константа менее 256. Следующий
фрагмент устанавливает порт I/O 3B4h равным 0Fh:

.
.
.
mov dx,3b4h
mov al,0fh
out dx,al
.
.
.

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

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

Арифметические операции —————————————

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

Дело обстоит и так, и не так. Хотя программное обеспечение,
идущее на 8086, безусловно имеет мощную математику, сам 8086 не
имеет развитых математических средств. Для начинающих сообщаем,
что 8086 не имеет команд для поддержки арифмртических операций с
плавающей точкой любого рода (с числами типа 5.2 или 1.03E17, а не
целыми числами) или трансцедентных функций; это задачи дла
сопроцессора 8087. Это не означает, что программы для 8086 не
обрабатывают числа с плавающей точкой; разумеется, электронные
таблицы идут на PC и без сопроцессора 8087. Однако программы для
8086 выполняют такие операции при помощи длительных
последовательностей сдвигов, сложений и проверок, а не в одной
быстро выполняющейся команде, как это делается в случае 8087.

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

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

Сложение и вычитание
———————

Во многих приведенных примерах программ вам уже встречались
команды ADD и SUB. Их действие практически совпадает с тем,
которое вы ожидаете от них интуитивно. Команда ADD складывает
складывает содержимое исходного (правого) операнда с содержимым
операнда назначения и помещает результат обратно в операнд
назначения. SUB работает аналогичным образом, за исключением того,
что исходный операнд вычитается из операнда назначения.

Например,

.
.
.
.DATA
BaseVal DW 99
Adjust DW 10
.
.
.
.CODE
.
.
.
mov dx,[BaseVal]
add dx,11
sub dx,[Adjust]
.
.
.

сначала загружает значение 99 из BaseVal в DX, а потом
складывает его с константой 11 и помещает результат 110 в DX, и
наконец вычитает из DX значение 10, находящееся в Adjust. конечный
результат 100 хранится в DX.

32-битовые операнды
——————-

Команды ADD и SUB работают только с 8- или 16-битовыми
операндами. Если вам требуется произвести сложение или вычитание,
скажем, 32-битовых операндов, то операция разбивается на
последовательность операций ADD или SUB с отдельными словами.

При сложении двух операндов 8086 записывает состояние флага
переноса (бит C флагового регистра), обозначающее, должен ли быть
сделан перенос для операнда назначения, то есть превысил ли
результат сложения максимально допустимое для данного операнда
значение. Вы знакомы с концепцией переноса в десятичной
арифметике; если сложить числа 90 и 10, то получится перенос в
третий разряд:

90
+ 10
—-
100

Теперь рассмотрим сложение двух шестнадцатиричных значений:

FFFF
+ 1
——
10000

Младшее слово результата будет равно нулю, а перенос равен 1,
поскольку результат, 10000h, не может быть выражен 16 битами.

Команда ADC похожа на команду ADD, за исключением того, что
она учитывает состояние флага переноса (который считается
установленным в предыдущей операции сложения). При сложении двух
величин, превышающих слово, нужно сперва сложить командой ADD
младшие (наименее значащие) слова этих величин, а затем сложить
оставшиеся слова одной или более командами ADC, причем самые
старшие слова должны быть сложены последними. К примеру,
приводимый ниже фрагмент складывает значение размером двойное
слово, хранимое в CX:BX, с двойным словом в DX:AX:

.
.
.
add ax,bx
adc dx,cx
.
.
.

а следующий складывает учетверенное слово из DoubleLong1 а
учетверенным словом из DoubleLong2:

.
.
.
mov ax,[DoubleLong1]
add [DoubleLong2],ax
mov ax,[DoubleLong1+2]
adc [DoubleLong2+2],ax
mov ax,[DoubleLong1+4]
adc [DoubleLong2+4],ax
mov ax,[DoubleLong1+6]
adc [DoubleLong2+6],ax
.
.
.

SBB работает во многом аналогично ADC. Когда команда SBB
выполняет вычитание, она учитывает, был ли сделан заем во время
предыдущей операции вычитания. Например, в следующем фрагменте
двойное слово , хранящееся в CX:BX, вычитается из двойного слова,
хранимого в DX:AX:

.
.
.
sub ax,bx
sbb dx,cx
.
.
.

В обеих командах ADC и SBB вы должны точно быть уверены, что
флаг переноса с момента последнего сложения или вычитания не
изменялся, так как тогда будет утеряно состояние переноса/ заема,
хранимое флагом переноса. Например, следующий пример приведет к
неправильному сложению CB:BX и DX:AX:

.
.
.
add ax,bx ;сложение младших слов
sub si,si ;установка SS в 0 (со сбросом флага переноса)
adc dx,cx ;сложение старших слов…
;такая программа будет работать неправильно,
;поскольку теряется состояние флага переноса
;от первого сложения!
.
.
.

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

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

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

Например, следующий фрагмент заполняет 10-байтовый массив
TempArray числами 0,1,2,3,4,5,6,7,8,9:

.
.
.
.DATA
TempArray DB 10 DUP(?)
FillCount DW ?
.
.
.
.CODE
.
.
.
mov al,0 ;первое помещаемое в TempArray значение
mov bx,OFFSET TempArray ;BX указывает на TempArray
mov [FillCount],10 ;число заполняемых элементов
FillTempArrayLoop:
mov [bx],al ;установка текущего элемента TempArray
inc bx ;указывает на следующий элемент TempArray
inc al ;следующее помещаемое в массив значение
dec [FillCount] ;уменьшение счетчика подлежащих
;заполнению элементов
jnz FillTempArrayLoop ;если не все элементы мас-
;сива заполнены, то переход
;к следующему элементу
.
.
.

Почему же понадобилось использовать, скажем,

inc bx

вместо

add bx,1

если обе эти команды выполняют одно и то же действие? Дело в
том, что если команда ADD имеет размер 3 байта, то INC только 1
байт и выполняется быстрее. Действительно, две команды INC дадут
более компактный код по сравнению с обычным сложением числа 2 с
регистром размером в слово. (Команды инкремента и декремента с
байтовыми регистрами и переменными памяти занимают 2 байта — что
тем не менее короче, нежели сложение и вычитание.)

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

Умножение и деление
——————-

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

Команда MUL умножает два 8- или 16-битовых сомножителя без
знака, давая 16- или 32-битовое произведение. Сначала рассмотрим
умножение 8-битового числа на 8-битовое число.

Один из сомножителей команды MUL при умножении 8-бит x 8 бит
должен находиться в регистре AL; второй может являться любым
8-битовым е_регистром общего назначения или операндом памяти. MUL
всегда помещает 16-битовое произведение в AX.

Например,

.
.
.
mov al,25
mov dh,40
mul dh
.
.
.

умножает содержимое AL на содержиное DH, а результат
умножения, равный 1000, в AX. Отметим, что команда MUL имеет
только один операнд; второй сомножитель всегда записан в AL (или в
AX в случае умножения 16 бит x 16 бит).

Команда MUL для умножения 16-бит x 16 бит работает
аналогично; один сомножитель должен находиться в AX, а другой
может быть в любом 16-битовом регистре общего назначения или
переменной памати. MUL помещает 32-битовое произведение в DX:AX,
причем младшие (наименее значащие) 16 битов помещаются в регистр
AX, а старшие (наиболее значащие) 16 бит — в DX. Например,

.
.
.
mov ax,1000
mul ax
.
.
.

загрузит в AX число 1000, а затем возведет его в квадрат,
поместив результат, 1,000,000 в DX:AX.

В отличие от сложения и вычитания, для умножения важно,
являются ли операнды величинами со знаком или без знака,
вследствие чего имеется еще одна команда, IMUL, предназначенная
для уумножения 8- или 16-битовых сомножителей со знаками. За
исключением обработки знака, IMUL аналогична MUL. Фрагмент

.
.
.
mov al,-2
mov ah,10
imul ah
.
.
.

помещает в AX значение -20.

8086 позволяет делить 32-битовое значение на 16-битовое или
16-битовое на 8-битовое, с некоторыми ограничениями. Сначала
рассмотрим деление 16-бит / 8 бит.

При делении 16 бит / 8 бит без знака делимое должно
находиться в AX. 8-битовый делитель может являться любым 8-битовым
регистром общего назначения или переменной памяти. DIV всегда
помещает 8-битовое частное в AL, а 8-битовый остаток в AH.
Например,

.
.
.
mov ax,51
mov dl,10
div dl
.
.
.

запишет частное, равное 5 (51, деленное на 10), в AL , а 1
(остаток от деления 51 на 10) в AH.

Отметим, что частное является 8-битовым числом. Это значит,
что результат от деления 16 бит / 8 бит не должно превышать 255.
Если частное слишком велико, то генерируется прерывание 0
(прерывание деления на ноль). Фрагмент

.
.
.
mov ax,0ffffh
mov bl,1
div bl
.
.
.

генерирует прерывание деления на ноль. (Очевидно, что
прерывание деления на ноль также генерируется, если делитель равен
нулю).

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

.
.
.
mov ax,2
mov dx,1 ;загрузка в DX:AX числа 10002h
mov bx,10h
div bx
.
.
.

помещает 1000h (частное от деления 10002h на 10h) в AX, и 2
(остаток от деления 10002 на 10h) в DX.

И опять, частное может являться только 16-битовым числом,
поэтому результат деления 32 бит / 16 бит не должен превышать
числа 0FFFFh, или 65,535; в противном случае генерируется
прерывание деления на ноль.

Как и для умножения, при делении учитывается, имеют ли
используемые операнды знак. DIV работает с операндами без знака, а
IDIV работает с операндами, имеющими знак. Например,

.
.
.
.DATA
TestDivisor DW 100
.
.
.
.CODE
.
.
.
mov ax,-667
cwd ;установка DX:AX в значение -667
idiv [TestDivisor]
.
.
.

записывает в AX -6, а в DX -67.

Изменение знака
—————

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

.
.
.
mov ax,1 ;установка AX равным 1
neg ax ;изменение знака AX на противоположный;
;значение AX становится равным -1
mov bx.ax ;копирование AX в BX
neg bx ;изменение знака BX на противоположный;
;значение BX становится равным -1
.
.
.

записывает в AX -1, а в BX 1.

Логические операции ——————————————-

Turbo Assembler поддерживает полный набор команд, выполняющих
логические операции, включая AND (И), OR (ИЛИ), XOR (исключающее
ИЛИ) и NOT (НЕ). Эти команды очень полезны при манипулировании
отдельными битами байта или слова, а также для выполнения операций
алгебры логики.

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

and ax,dx

выполняет логическое И с битом 0 регистра AX и битом 0
регистра DX как с исходными битами, и использует бит 0 в AX как
назначения для результата операции, и проделывает то же самое с
битом 1, битом 2 и т.д., кончая битом 15.

Работа логических команд 8086 AND, OR и XOR Таблица 5.2
————————————————————
Исходный бит A Исходный бит B A AND B A OR B A XOR B
————————————————————
0 0 0 1 0
0 1 0 1 1
1 0 0 1 1
1 1 1 0 0
————————————————————

Команда AND объединяет два операнда в соответствии с
правилами, приведенными в таблице 5.2, устанавливая каждый бит
назначения в 1 только в том случае, если оба соответствующих
исходных бита равны 1. AND позволяет вам выделить конкретный бит
или принудительно установить нужный бит равным 0. Например,

.
.
.
mov dx,3dah
in al,dx
and al,1
.
.
.

выделяет бит 0 байта состояния цветного графического адаптера
(CGA). В приведенном фрагменте AL устанавливается в 1, если
обновление памяти дисплея с CGA может быть выполнено, не вызывая
«снега» на дисплее, и в 0 в противном случае.

Команда OR объединяет два операнда в соответствии с
правилами, приведенными в таблице 5.2, устанавливая каждый бит
назначения в 1 только в том случае, если хотя бы один из исходных
битов равен 1. OR позволяет принудительно установить нужный бит
(биты) равным 1. Например,

.
.
.
mov ax,40h
mov ds,ax
mov bx,10h
or WORD PTR [bx],0030h
.
.
.

принудительно устанавливает биты 5 и 4 слова флагов
аппаратного обеспечения BIOS равными 1, тем самым заставляя BIOS
поддерживать работу с монохромным дисплейным адаптером.

Команда XOR объединяет два операнда в соответствии с
правилами, приведенными в таблице 5.2, устанавливая каждый бит
назначения в 1 только в том случае, если один из исходных битов
равен 0, а другой равен 1. XOR позволяет изменять значение нужного
бита в байте на противоположное. Например,

.
.
.
mov al,01010101b
xor al,11110000b
.
.
.

устанавливает AL равным 10100101b, или A5h. Смысл здесь
состоит в том, что когда с содержимым AL выполняется операция
исключающего ИЛИ (XOR) с числом 11110000b, или 0F0h, биты со
значением 1 в числе 0f0h изменяют на противоположное значения
соответствующух битов в AL, тогда как биты со значением 0
оставляют соответствующие биты в AL без изменений. В результате
все значения битов старшего полубайта в AL изменятся, а биты
младшего полубайта не изменятся.

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

xor ax,ax

И наконец, команда NOR просто изменяет состояние каждого бита
операнда на противоположное, как если бы с исходным операндом было
выполнено исключающее или с чеслом 0FFh. Рассмотрим такой пример:

.
.
.
mov bl,10110001b
not bl ;изменяет содержимое BL на 01001110b
xor bl,0ffh ;изменяет содержимое BL обратно на
;10110001b
.
.
.

Операции сдвига и циклического сдвига ————————-

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

Команда SHL (сдвиг влево, также называется SAL) перемещает
значение каждого бита исходного операнда влево, или в направлении
наиболее значащего бита. На рис.4.8 показано, как хранимое в AL
число 10010110b (96h или 150 десятичное) сдвигается влево командой
SHL AL,1. Результатом выполнения этой команды будет значение
00101100b (2Ch или 44 десятичное), помещаемое назад в AL. Флаг
переноса при этом будет установлен в 1.

Флаг
переноса AL
———————————————
— | — — — — — — — — |
| | <--- | |1|<-|0|<-|0|<-|1|<-|0|<-|1|<-|1|<-|0| |<-- 0 --- | --- --- --- --- --- --- --- --- | -------------------------------------------- бит 7 6 5 4 3 2 1 0 Рис.5.8 Пример сдвига влево Наиболее значащий бит числа сдвигается вовне операнда, попадая во флаг переноса, а в наименее значащий бит помещается 0. Где применяется сдвиг влево? Наиболее часто SHK используется для быстрого умножения на число, являющееся некоторой степенью числа 2, поскольку каждый сдвиг командой SHL эквивалентен умножению операнда на 2. Например, в следующем фрагменте содержимое DX умножается на 16: . . . shl dx,1 ;DX = 2 shl dx,1 ;DX = 4 shl dx,1 ;DX = 8 shl dx,1 ;DX = 16 . . . Умножение при помощи команды SHL выполняется намного быстрее, нежели командой MUL. Вы уже, наверное, заметили, что команда SHL в предыдущем примере имеет второй операнд, равный числу 1. Этоы операнд указывает, что сдвиг должен выполняться на 1 бит. К сожалению 8086 не поддерживает в качестве значения числа битов, на которое происходит сдвиг, никаких числовых констант, кроме 1. Однако для указания этой величины может служить регистр CL; например, . . . mov cl,4 shl dx,cl . . . умножает DX на 16, как и предыдущий фрагмент. Если существует сдвиг влево, то логично предположить, что существует и сдвиг вправо, и это действительно так - при этом сдвигов вправо имеется два. SHR (сдвиг вправо) во многом похожа на SHL: эта команда сдвигает биты операнда вправо, либо на 1 разряд, либо на число разрядов, указанное в регистре CL, наименее значащий бит помещается во флаг переноса, а в наиболее значащий бит операнда помещается 0. SHR позволяет быстро выполнать деление на число, являющееся степенью числа 2. SAR (арифметический сдвиг вправо) работает аналогично SHR, за исключением того, что в SAR наиболее значащий бит операнда сдвигается на следующий бит вправо, а затем возвращается обратно на свое место. На рис.5.9 показано, как хранимое в AL число 10010110b (96h или -106 десятичное со знаком) сдвигается вправо командой SAR AL,1. Результатом выполнения этой команды будет значение 11001011b (0CBh или -53 десятичное со знаком), помещаемое назад в AL. Флаг переноса при этом будет установлен в 0. Флаг AL переноса ---------------------------------------------- | --- | | ? | | | --- | --- --- --- --- --- --- --- | --- | |1|--> |0|->|0|->|1|->|0|->|1|->|1|->|0| |—> | |
| — — — — — — — — | —
———————————————-
бит 7 6 5 4 3 2 1 0

Рис.5.9 Пример команды SAR (арифметического сдвига
вправо)

Благодаря этому сохраняется знак операнда, что позволяет
исполльзовать команду SAR в операциях деления на степени числа два
величин со знаком. Например,

.
.
.
mov bx,-4
sar bx,1
.
.
.

помещает в регистр BX число -2.

Имеется также четыре команды циклического сдвига: ROR, ROL,
RCR и RCL. Команда ROR аналогична команде SHR, за исключением
того, что наименее значащий сдвиг, помимо флага переноса,
помещается еще и в наиболее значащий бит. На рис.5.10 показано,
как значение 10010110b (96h или 150 десятичное), находящееся в AL,
циклически сдвигается командой ROR AL,1. Результатом операции
является число 01001011b (04Bh или 75 десятичное), помещаемое
обратно в AL. Флаг переноса устанавливается равным 0.

Флаг
AL переноса
———————————————
| |
| |
| — — — — — — — — | —
—->| |1|->|0|->|0|->|1|->|0|->|1|->|1|->|0| |—>| |
| | — — — — — — — — | | —
| ——————————————— |
|бит 7 6 5 4 3 2 1 0 |
—————————————————

Рис.5.10 Пример команды ROR (циклического сдвига вправо)

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

.
.
.
mov si,49f1h
mov cl,4
ror si,cl
.
.

оставляет 149Fh в SI, но перемещает биты 3-0 в позиции 15-12,
7-4 в позиции 3-0 и т.д.

Команды RCR и RCL имеют несколько другой характер. RCR
аналогична сдвигу вправо, при котором наиболее значащий бит
берется из флага переноса. На рис.5.11 показано, как значение
10010110b (96h или 150 десятичное), находящееся в AL, циклически
сдвигается вправо командой RCR AL,1 через флаг переноса, в котором
находилось исходное значение 1. Результатом операции является
число 11001011b (0CBh или 203 десятичное), помещаемое обратно в
AL. Флаг переноса устанавливается равным 0.

Флаг
AL переноса
———————————————
| |
| |
| — — — — — — — — | —
—->| |1|->|0|->|0|->|1|->|0|->|1|->|1|->|0| |—>|1|
| | — — — — — — — — | | —
| ——————————————— |
|бит 7 6 5 4 3 2 1 0 |
—————————————————

Рис.5.11 Пример команды RCR (циклического сдвига вправо
через флаг переноса)

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

.
.
.
shl ax,1 ;бит 15 AX сдвигается во флаг переноса
rcl dx,1 ;флаг переноса сдвигается в бит 0 регистра DX
shl ax,1 ;бит 15 AX сдвигается во флаг переноса
rcl dx,1 ;флаг переноса сдвигается в бит 0 регистра DX
.
.
.

Как и в командах сдвига, в командах циклического сдвига
операнд может быть сдвинут либо на 1 бит, либо на число битов,
задаваемое в CL.

Циклы и переходы
——————————————————————

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

.
.
.
mov ax,[BaseCount]
add ax,4
.
.
.
push ax
.
.
.

вы вполне можете быть уверены, что команда ADD будет
выполнена непосредственно после MOV, а PUSH несколько позднее.

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

Безусловные переходы ——————————————

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

.
.
.
mov ax,1
jmp AddTwoToAX
AddOneToAX:
inc ax
jmp AXIsSet
AddTwoToAX:
add ax,2
AXIsSet:
.
.
.

AX будет содержать значение 3, а команды ADD и JMP после
метки AddOneToAX не выполняются никогда. Здесь команда

jmp AddTwoToAX

заставляет 8086 ускановить указатель команд IP равным
смещению метки AddTwoToAX, и следующая выполняемая команда будет
тем самым

add ax,2

Иногда в команде JMP применяется оператор SHORT. Обычно JMP
использует для указания на метку назначения 16-битовое смещение;
оператор SHORT говорит Turbo Assembler, что необходимо
использовать вместо него 8-битовое смещение, чем экономится 1 байт
на каждой метке. Например, следующий фрагмент будет на 2 байта
короче предыдущего:

.
.
.
mov ax,1
jmp SHORT AddTwoToAX
AddOneToAX:
inc ax
jmp SHORT AXIsSet
AddTwoToAX:
add ax,2
AXIsSet:
.
.
.

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

Команда JMP может быть также использована для перехода к
другому сегменту программы, загружая в одной команде и CS, и IP.
Например,

.
.
.
CSeg1 SEGMENT
ASSUME CS:Cseg1
.
.
.
FarTarget LABEL FAR
.
.
.
Cseg1 ENDS
.
.
.
Cseg2 SEGMENT
ASSUME CS:Cseg2
.
.
.
jmp FarTarget ;это дальний переход
.
.
.
Cseg2 ENDS
.
.
.

выполняет дальний переход.

При желании вы можете при помощи оператора FAR PTR указать,
что метка должна рассматриваться как дальняя; например

.
.
.
jmp FAR PTR NearLabel
nop
NearLabel:
.
.
.

выполняет дальний переход к NearLabel, даже если NearLabel
находится в том же программном сегменте, что и команда JMP.

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

.
.
.
mov ax,OFFSET TestLabel
jmp ax
.
.
.
TestLabel:
.
.
.

выполнит переход к TestLabel, также как и

.
.
.
.DATA
JumpTarget DW TestLabel
.
.
.
.CODE
.
.
.
jmp [JumpTarget]
.
.
.
TestLabel:
.
.
.

Условные переходы ———————————————

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

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

.
.
.
mov ah,1 ;функция DOS ввода с клавиатуры
int 21h ;прием следующей нажатой клавиши
cmp al,’A’ ;была ли нажата клавиша «A»?
je AWasTyped ;да, переход к специальной обра-
;ботке этой ситуации
mov [TempByte],al ;нет, запись символа в память
.
.
.
AWasTyped:
push ax ;поместить символ на стек
.
.
.

Сначала эта программа при помощи функции DOS примет нажатие
клавиши. Затем в команде CMP сравнит, является ли введенный символ
символом A. Команда CMP, как и команда SUB, сама никаких действий
не выполняет; ее назначение состоит в том, чтобы сравнить два
оператора, не изменяя их значений. Однако, как и SUB, CMP
устанавливает флаги. Поэтому в приведенном примере флаг нуля
устанавливается равным 1 только если AL содержит символ A.

Теперь мы подощли к тому, чтобы понять смысл примера. JE
представляет собой команду условного перехода, который выполняется
только в том случае, если флаг нуля равен 1. В противном случае
выполняется следующая после JE команда, в данном случае команда
MOV. В приведенном примере флаг нуля будет установлен только в
случае нажатия клавиши A, и только тогда 8086 выполнит переход к
команде, обозначаемой меткой AWasTyped, т.е. команде PUSH.

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

В таблице 5.3 сведены все команды условного перехода.

Команды условного перехода Таблица 5.3
————————————————————
Имя Описание Проверяемый флаг
————————————————————
JB/JNAE Переход если ниже/
Переход если не выше или равно CF=1

JAE/JNB Переход если выше или равно/
Переход если не ниже CF=0

JBE/JNA Переход если ниже или равно/
Переход если не выше CF=1 или ZF=1

JA/JNBE Переход если выше/
Переход если не ниже или равно CF=0 и ZF=0

JE/JZ Переход если равно ZF=1
JNE/JNZ Переход если не равно ZF=0
JL/JNLE Переход если меньше/
Переход если не больше или равно SF<>OF

JGE/JNL Переход если больше или равно/
Переход если не меньше CF=OF

JLE/JNG Переход если меньше или равно/
Переход если не больше ZF=1 или ZF<>OF

JG/JNLE Переход если больше/
Переход если не меньше или равно ZF=0 или SF=OF

JP/JPE Переход при успешной проверке четности/
Переход при четности PF=1

JNP/JPO Переход при неуспешной проверке четности/
Переход при нечетности PF=о

JS Переход если есть знак SF=1

JNS Переход если нет знака SF=0

JC Переход если есть перенос CF=1

JNC Переход если нет переноса CF=0

JO Переход если есть переполнение OF=1

JNO Переход если нет переполнения OF=0

CF=флаг переноса; SF=флаг знака; OF=флаг переполнения;
ZF=флаг нула; PF=флаг четности
————————————————————

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

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

Например, Turbo Assembler не может ассемблировать

.
.
.
JumpTarget:
.
.
.
DB 1000 DUP (?)
.
.
.
dec ax
jnz JumpTarget
.
.
.

поскольку JumpTarget находится от команды JNZ на расстоянии
свышв 1000 байт. В этом случае данный фрагмент должен выгладеть
так:

.
.
.
JumpTarget:
.
.
.
DB 1000 DUP (?)
.
.
.
dec ax
jz SkipJump
jmp JumpTarget
SkipJump:
.
.
.

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

Циклы ———————————————————

Одной из программных конструкций, организуемой при помощи
команд условного перехода, является цикл. Цикл это просто
некоторый блок программы, заканчивающийся условным переходом,
позволяющим повторять этот блок снова и снова, пока не будет
выполнено условие выхода из цикла. Вы наверняка знакомы с
конструкциями цикла, такими как for и while в Си, while и repeat в
Паскале и FOR в Бейсике.

Для чего служат циклы? Они предназначены для манипулирования
массивами, проверки состояния портов ввода/вывода до тех пор, пока
не будет обнаружено конкретное состояние, очистки блоков памяти,
чтения с клавиатуры строк символов, вывода строк символов на экран
и т.д. Цикл представляет собой базовое средство решения задач, в
которых требуется какое-либо повторяэщееся действие. В этом
качестве циклы используются очень часто, настолько часто, что в
8086 предусмотрено несколько специальных команд для организации
циклов: LOOP, LOOPE, LOOPNE и JCXZ.

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

.
.
.
.DATA
TestString: DB ‘This is a test…’
.
.
.
.CODE
.
.
.
mov cx,17
mov bx,OFFSET TestString
PrintStringLoop:
mov dl,[bx] ;прием следующего символа
inc bx ;указывает на последующий символ
mov ah,2 ;функция DOS вывода на дисплей
int 21h ;вызов DOS для печати символа
dec cx ;реверсивный счет длины строки
jnz PrintStringLoop ;переход к следующему сим-
;волу, если таковой еще остался
.
.
.

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

loop PrintStringLoop

делает то же, что и команды

dec cx
jzn PrintStringLoop

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

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

LOOPE делает то же самое, что и LOOP, за исключением того,
что LOOPE выйдет из цикла (не станет выполнять обратный переход к
началу цикла) либо если счетчик CX станет равным нулю, либо если
станет равным 1 флаг нуля. (Вспомните, что флаг нуля
устанавливается в 1, если результат последней арифметической
операции был равен 0, либо при неравенстве двух операндов
последней операции сравнения). Аналогичным образом, команда LOOPNE
закончит цикл либо если счетчик CX станет равным нулю, либо если
станет равным 0 флаг нуля.

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

.
.
.
.DATA
KeyBuffer DB 128 DUP (?)
.
.
.
.CODE
.
.
.
mov cx,128
mov bx,OFFSET KeyBuffer
KeyLoop:
mov ah,1 ;функция DOS ввода с клавиатуры #
int 21h ;считывание следующего символа
mov [bx],al ;записать клавишу
inc bx ;установить указатель на следующую
;клавишу
cmp al,0dh ;была нажата клавиша Enter?
loopne KeyLoop;если нет, то переход к приему сле-
;дующей клавиши, если число введенных
;символов не равно максимальному
.
.
.

Команда LOOPE также исвестна под именем LOOPZ, а LOOPNE под
именем LOOPNZ, подобно тому как JE также называют JZ.

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

.
.
.
jcxz SkipLoop ;если CX=0, ничего не происходит
ClearLoop:
mov BYTE PTR [si],0
;установка следующего байта
;равным 0
inc si ;установка указателя на следующий
;обнуляемый байт
loop ClearLoop ;переход к следующему символу,
;если цикл не закончен
SkipLoop:
.
.
.

Зачем в случае равенства CX нулю опускается весь цикл? Дело в
том, что если он будет выполнен с CX, равным 0, то произойдет
декрементирование CX, и он станет равным 0FFFFh, после чего
команда LOOP выполнит переход к метке назначения. Затем этот цикл
выполнится еще 65,535 раз! Вам же было нужно, чтобы установка CX в
0 означала отсутствие какого-либо обнуления памяти, а вовсе не
обнуление 65,536 байт. JCXZ позволяет быстро и эффективно
проверить данное условие.

С командами организации циклов связано два интересных
момента. Во-первых, вам следует знать, что команды организации
циклов, как и команды условного перехода, могут выполнять переход
только к метке, лежащей в пределах 128 байт относительно места
расположения самой команды, до нее или после. Для циклов величиной
более 128 байт нужно применять метод «условного перехода через
безусловный переход», описанный в предыдущем разделе, «Условные
переходы» (см.стр.167 оригинала). Во-вторых, что ни одна из команд
организации циклов никаким способом не воздействует на состояние
флагов. Это значит, что

loop LoopTop

это не точно то же самое, что

dec cx
jzn LoopTop

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

sub cx,1
jnz LoopTop

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

Подпрограммы
——————————————————————

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

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

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

Как работает подпрограмма ————————————-

Фундаментальные основы работы подпрограммы показаны на
рис.5.12.

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

. .
. .
|————|(В IP |————|
1000 | mov al,1 |загружается —>DoCalc 1110 | shl al,1 |
|————|1110, а 1007 | |————|
1002 | mov bl,3 |помещается | 1112 | add al,bl |
|————|на стек) | |————|
1004 | call Docalc|————-| 1114 | and al,7 |
|————| |————|
1007 | mov ah,2 |<------------- 1116 | add al,'0' | |------------|(Значение | |------------| 1009 | int 21h |1007 снима- --------- 1118 | ret | |------------|ется с вершины стека |------------| . и загружается в IP) . . . Рис.5.12 Работа подпрограммы После того, как подпрограмма выполнила свою задачу, она выполняет команду RET, которая снимает со стека адрес, помещенный на него исходной командой PUSH, и загружает его в IP. Затем выполнение вызывающей программы продолжается с команды, следующей после выполненной команды CALL. Например, следующая программа печетает три строки: Hello, world! Hello, solar system! Hello, universe! при помощи подпрограммы PrintString: .MODEL small .STACK 200h .DATA WorldMessage DB 'Hello, world!',0dh,0ah,0 SolarMessage DB 'Hello, solar system!',0dh,0ah,0 UniverseMessage DB 'Hello, universe!',0dh,0ah,0 .CODE ProgramStart PROC NEAR mov ax,@Data mov ds,ax mov bx,OFFSET WorldMessage call PrintString ;печать Hello, world! mov bx,OFFSET SolarMessage call PrintString ;печать Hello, solar system! mov bx,OFFSET UniverseMessage call PrintString ;печать Hello, universe! mov ah,4ch ;функция DOS выхода из программы int 21h ;...выполнено ProgramStart ENDP ; ;Подпрограмма печати на экран строки символов, оканчиваю- ;щейся нулем ; ;Ввод: ; DS:BX - указатель печатаемой строки ; ;Регистры, содержимое которых разрушается: AX, BX ; PrintString PROC NEAR PrintStringLoop: mov dl,[bx] ;прием следующего символа строки and dl,dl ;значение символа равно нулю? jz EndPrintString ;если да, то обработка строки ;закончена inc bx ;указатель на следующий символ mov ah,2 ;функция DOS вывода на дисплей int 21h ;вызов DOS для печати символа jmp PrintString ;печать следующего символа, если ;таковой имеется EndPrintString: ret PrintString ENDP END ProgramStart Здесь нужно отметить две вещи. Во-первых, в PrintString печатаемая строка не зашивается жестко, она печатает то, что будет передано ей вызывающей программой через указатель в BX. Во-вторых, в качестве "скобок", отмечающих начало и конец PrintString, использованы две новые директивы, PROC и ENDP. Директива PROC служит для обозначения начала процедуры. Метка, связанная с данной директивой, в данном случае Print- String, это имя процедуры, как если бы было записано PrintString LABEL PROC Однако директива PROC делает несколько больше: она определяет, должны ли в этой процедуре использоваться ближние или дальние команды RET. Рассмотрим следствия из данного утверждения. Вспомните, что при переходе на ближнюю метку IP загружается новым значением, тогда как в случае перехода на дальнюю метку загружаются как CS, так и IP. Если команда CALL содержит ссылку на дальнюю метку, загружаются и CS, и IP, как для перехода. Следовательно, при дальнем вызове значения CS и IP требуется поместить на стек; в противном случае команде RET не хватит информации для правильного возврата в вызывающую программу. Это можно себе представить так: если при дальнем вызове были загружены CS и IP, но помещен на стек только IP, то при возврате с вершины стека будет снят только IP. Результатом RET будет пара CS:IP, состоящая из CS вызываемой процедуры и IP вызывающей процедуры, что явно бессмысленно. Что же происходит, когда при вызове дальней метки на стек помещаются как CS, так и IP. Откуда Turbo Assembler узнает, какого типа возврат, дальний или ближний, должен быть сгенерирован в данной подпрограмме? Одним из способов заключается в задании типа каждого возврата в явном виде, например командами RETN (ближний возврат) и RETF (дальний возврат). Однако лучшее решение дают директивы PROC и ENDP. Директива ENDP служит для того, чтобы отметить конец подпрограммы, начатой процедурой PROC. Конкретная процедура ENDP обозначает конец подпрограммы, начинающейся PROC и той же самой метки. Например, . . . TestSub PROC NEAR . . . TestSub ENDP . . . отмечает начало и конец подпрограммы TestSub. Фактически PROC и ENDP не ведут к генерированию какихлибо кодов; это директивы, а не инструкции. Единственное их действие заключается в управлении типом команд RET, используемых в данной процедуре. Если процедуре PROC задан операнд NEAR, то все команды RET между директивой PROC и соответствующей директивой ENDP ассемблируются с ближним возвратом. Если же, напротив, процедуре PROC задан операнд FAR, то все команды RET этой процедуры ассемблируются с дальним возвратом. Таким образом, например, для изменения типа всех команд RET фрагмента TestSub достаточно изменить директиву PROC на TestSub PROC FAR В целом везде, где это возможно, нужно стараться использовать подпрограммы с ближними вызовы, поскольку дальние вызовы дают больший размер и медленнее ближних, и дальние возвраты также медленнее ближних. Однако в случае необходимости иметь более 64 Кб кода программы дальние подпрограммы необходимы. В случае использования упрощенных сегментных директив директиву PROC лучше брать без каких-либо операндов вообще, как например TestSub PROC Если TurboAssembler встречает такую директиву, он автоматически делает процедуру ближней или дальней, в зависимости от того, какая модель памяти выбрана директивой .MODEL. (По умолчанию устанавливается модель памяти small). Программам с моделями памяти tiny, small и compact соответствуют ближние вызовы, а с моделями medium, large и huge - дальними. Например, в . . . .MODEL small . . . TestSub PROC . . . TestSub вызывается по ближней ссылке, а . . . .MODEL LARGE . . . TestSub PROC . . . по дальней. Передача параметров ------------------------------------------- Вызывающая программа часто передает подпрограмме некоторую информацию. Например, в предыдущей приведенной программе подпрограмме PrintString через регистр BX был передан указатель. Этот процесс известен как "передача параметров", а сами параметры конкретно задают подпрограмме выполняемые ей действия. Имеется два наиболее часто применяемых способа передачи параметров: через регистры и через стек. Передача через регистры используется в чисто ассемблерной программе, а передача через стек в языках самого высокого уровня, включая Паскаль и Си, а также в ассемблерных подпрограммах, вызываемых из этих языков. Передача параметров через регистры весьма проста - значения параметров перед вызовом подпрограммы просто помещаются в соответствующие регистры. Каждая подпрограмма может иметь свои собственные требования к параметрам, хотя гораздо проще установить некоторые соглашения относительно параметров и следовать им, что позволит избежать несоответствий. Например, вы можете взять за правило, что первый параметр-указатель передавался в регистре BX, второй в SI и т.д. При использовании метода передачи через регистры не забывайте тщательно комментировать каждую подпрограмму, описывая, какие именно параметры эта программа ожидает и в какие регистры они должны быть помещены. Передача параметров через стек несколько более сложна и сравнительно с передачей через регистры обеспечивает большую гибкость. Если вы решили использовать передачу параметров через стек, вам вероятно понадобится следовать соглашениям, принятым в вашем любимом языке высокого уровня, что облегчит компоновку ассемблерной подпрограммы с программой на данном языке. В главах 7 и 8 приводятся подробные описания соглашений по передаче параметров Turbo C и Turbo Pascal, с примерами программ на ассемблере. Возврат значений ---------------------------------------------- Подпрограммы часто возвращают в вызывающую программу какие-либо полученные в них значения. В случае ассемблерных подпрограмм, которые будут затем вызываться из программ на языках высокого уровня, обязательно должны соблюдаться соглашения о возврате значений, принятые в соответствующих языках. Например, функция, вызываемая из Си, должна возвращать 8- и 16-битовые значения (типов char, int и ближние указатели near) в AX, а 32-битовые значения (типа long и дальние указатели far) в DX: AX. В главах 7 и 8 настоящей книги приводятся подробные описания соглашений о возвращаемых значениях для Turbo C и Turbo Pascal. В чисто ассемблерной программе вам предоставляется полная свобода относительно того, каким образом вы будете возвращать значения; в этом случае их можно помещать в любые регистры. Фактически подпрограммы могут даже возвращать во флаговом регистре, информацию о состоянии подпрограммы, устанавливая флаги переноса или нуля. Однако и здесь лучше установить и придерживаться некоторых соглашений. Одно из них состоит в том, чтобы 8-битовые значения возвращать через регистр AL, а 16-битовые через AX; тем самым вы приобретете привычку не оставлять в AX ценную информацию, поскольку она может быть изменена при вызове. Главная проблема с использованием в ассемблере значений, возвращаемых из подпрограмм, состоит в том, что во время возврата информации подпрограммы могут разрушить информацию, которая важна для вызывающей программы. В ассемблере очень легко написать вызов подпрограммы, забыв о том, что подпрограмма возвращает значение, например, в SI (или что она просто меняет значение в SI); тем самым будет сделана ошибка программы, которую нелегко найти. Поэтому лучше всего сводить число возвращаемых из подпрограммы через регистры значений к минимуму - желательно, чтобы таких значений было не более одного,- а остальные значения передавать, помещая их в ячейки памяти, на которые указывают переданные указатели, как это делается в Си и Паскале. Сохранение регистров ------------------------------------------ Сохранение правильных значений регистров при вызове подпрограммы в целом представляет собой главную проблему программирования на ассемблере. В современных языках высокого уровня подпрограмма обычно не имеет возможности модифицировать переменные вызывающщей программы, если в последней такая возможность не объявлена явно. В ассемблере дело обстоит иначе, поскольку там переменные вызывающей программы часто хранятся в регистрах, используемых далее подпрограммой. Например, если подпрограмма модифицирует регистр, который до ее вызова был установлен вызывающей программой и используется последней после выхода из подпрограммы, то в этом месте получится ошибка. Одно из решений этой проблемы состоит в том, чтобы в каждой подпрограмме, после входа в нее, все используемые ей регистры были помещены на стек, а затем, после выхода из вызываемой программы, были восстановлены снятием их с вершины стека. К сожалению, этот метод занимает много времени и приводит к увеличению размера кода. Другая возможность состоит в том, чтобы взять за правило, что вызывающая программа не должна ожидать от подпрограммы сохранности регистров, а сама должна позаботиться о них. Однако такой путь непривлекателен, поскольку большая часть причин использования языка ассемблера сводится именно к его способности эффективного использования регистров. Говоря кратко, в языке ассемблера существует противоречие между скоростью работы программы и простотой программирования. Если вы собираетесь использовать ассемблер, то вам потребуется получение быстродействующего и компактного кода, а это означает, что вам придется проявить особое внимание к сохраннию регистров и отсутствию конфликтных обращений к регистрам со стороны вызывающих и вызываемых программ. Лучший подход состоит в тщательном комментировании каждой подпрограммы относительно того, содержимое которых регистров она разрушает, после чего при каждом использовании команды CALL вы сможете обратиться к этим комментариям. Исключительно внимательный подход к проблеме сохранности регистров при одновременной эффективности их использования является составной частью хорошего стиля программирования на языке ассемблера. Языки высокого уровня делают это за вас сами, но опять же напоминаем, что они не в состоянии обеспечить такие же быстродействующие и компактные программы, как те, что можно писать на языке ассемблера. Пример программы на языке ассемблера ----------------------------------------------------------------- Сведем воедино информацию, полученную вами в двух последних программах, в виде полезного примера программы. Этот пример, WCOUNT.ASM, подсчитывает число слов в файле и выводит результат подсчета на экран. ; ; Программа подсчитывает число слов в файле. Между ними нахо- ; дятся разделители, в число которых входят пробелы, метки та- ; буляции, символы возврата каретки и прогона строки. ; ; Использование: wc <имя_файла.расширение ; ;выбор стандартного упорядочения сегментов .MODEL small ;программа и данные помещаются ;в 64К .STACK 200h ;размер стека 512 К .DATA Count DW 0 ;служит для счета слов InWhitespace DW ? ;устанавливается в 1, если пос- ;ледний считанный символ это ;разделитель TempChar DB ? ;временная память, используемая ;в GetNextCharacter Result DB 'Word count: ', 5 DUP (?) ;строка с сообщением о счетчике CountInsertEnd Label BYTE ;используется для нахождения конца ;области, где хранится строка со ;значением счетчика DB 0dh,0ah,'$' ;функция DOS #9 ожидет, что строка ;символов оканчивается знаком дол- ;лара .CODE ProgramStart: mov ax,@Data mov ds,ax ;DS указывает на сегмент .DATA mov [InWhitespace],1 ;предположим, что мы нахо- ;димся на разделителе, поскольку ;первый же обнаруженный не-разде- ;литель будет означать начало ;слова CountLoop: call GetNextCharacter ;прием следующего проверя- ;емого символа jz CountDone ;...если такой имеется call IsCharacterWhitespace ;это разделитель? jz IsWhitespace ;да cmp [IsWhitespace],0 ;символ не является разде- ;лителем - или в текущий ;момент мы на разделителе? jz CountLoop ;мы не на разделителе, и символ ;не является разделителем, поэтому ;с данным символом все inc [Count] ;мы на разделителе, а символ не ;является разделителем, это зна- ;чит, что найдено начало нового ;слова mov [InWhitespace],0 ;отметить, что мы уже не ;на разделителе jmp CountLoop ;переход к обработке следующего ;символа ; ;мы закончили подсчет - переходим к печати результатов ; CountDone: mov ax,[Count] ;число, преобразуемое в строку mov bx,OFFSET CountInsertEnd-1 ;указывает на конец строки, в ;которую помещается число mov cx,5 ;число преобразуемых цифр call ConvertNumberToString ;делает число строкой mov bx,OFFSET Result ;печать счетчика mov ah,4ch ;функция DOS конца программы # int 21h ;конец программы ; ;Подпрограмма приема следующего символа со стандартного ввода ; ;Вход: отсутствует ; ;Выход: ; AL = символ, если он имеется ; флаг Z = 0 (NZ), если символ имеется, ; = 1 (Z) , если обнаружен конец файла ; ;Разрушаемые регистры: AH, BX, CX, DX ; GetNextCharacter PROC mov ah,3fh ;функция DOS чтения из файла # mov bx,0 ;назначение стандартного ввода mov cx,1 ;чтение одного символа mov dx,OFFSET TempChar ;помещение символа в TempChar int 21h ;прием следующего символа jc NoCharacterRead ;если DOS сообщает об ошибке, ;его нужно рассматривать как ;конец файла cmp [TempChar],1ah ;это был символ Control-Z? ;(отмечает конец некоторых файлов) jne NotControlZ ;нет NoCharacterString: sub ax,ax ;отмечает, что в NotControlZ ни ;один символ не считан and ax,ax ;установка флага Z т.о., чтобы он ;отражал, был ли считан символ ;(NZ) или найден конец файла (Z). ;Отметим, что функция DOS #3fh ус- ;танавливает число считанных сим- ;волов mov al,[TempChar] ;возврат считанного символа ret ;выполнено GetNextCharacter ENDP ; ; Подпрограмма сообщает, является ли считанный символ ; разделителем ; ; Вход: ; AL = проверяемый символ ; ; Выход: ; флаг Z = 0 (NZ) если символ не является разделителем ; = 1 (Z) если символ является разделителем ; ; Разрушаемые регистры: none ; IsCharacterWhitespace PROC cmp al,' ' ;пробел? jz EndIsCharacterWhitespace ;тогда это разделитель cmp al,09h ;метка табуляции? jz EndIsCharacterWhitespace ;тогда это разделитель cmp al,0dh ;возврат каретки? jz EndIsCharacterWhitespace ;тогда это разделитель cmp al,0ah ;прогон строки? jz EndIsCharacterWhitespace ;тогда это разделитель ;и возвращается Z; ;иначе это не разде- ;литель, поэтому воз- ;вращается значение ;NZ, устанавливаемое ;cmp EndIsCharacterWhiteSpace: ret IsCharacterWhitespace ENDP ; ; Подпрограмма преобразования двоичного числа в текстовую ; строку. ; ; Вход: ; AX = преобразуемое число ; DS:BX = указатель на конец строки, в которую помещается ; преобразуемое число ; CX = число преобразуемых знаков ; ; Выход: отсутствует ; ; Разрушаемые регистры: AX, BX, CX, DX, SI ; ConvertNumberToString PROC mov si,10 ;используется для деления на 10 ;ConvertLoop: sub dx,dx ;преобразование AX в двойное слово ;в DX:AX div si ;деление числа на 10. Остаток на- ;ходится в DX - это одноразрядное ;десятичное число. Число/10 нахо- ;дится в AX add dl,'0' ;преобразование остатка в текстовый ;символ mov [bx],dl ;помещение данной цифры в строку dec bx ;указатель адреса следующей наибо- ;лее значащей цифры loop ConvertLoop ;переход к следующей цифре, если ;таковая имеется ret ConvertNumberToString ENDP ; ; Подпрограмма печати строки символов на дисплее ; ; Вход: ; DS:BX = указатель печатаемой строки ; ; Выход: отсутствует ; ; Разрушаемые регистры: отсутствуют ; PrintString PROC push ax push dx ;сохраняемые данной подпрограммой ;регистры mov ax,9 ;функция DOS печати строки # mov dx,bx ;указатель DS:DX на печатаемую строку int 21h ;вызов DOS для печати строки pop dx ;восстановить измененные регистры pop ax ret PrintString ENDP END ProgramStart Программу WCOUNT.EXE следует запускать по приглашению DOS, предварительно переназначив стандартный ввод на файл, для которого вы желаете выполнить подсчет. Например, для подсчета количества слов в файле WCOUNT.ASM нужно ввести по подсказке DOS: wcount