Как уже отмечалось в гл. 1, присущий процессору алгоритм выполнения программы заставляет его выполнять команды программы друг за другом, в том порядке, как они были описаны в исходном тексте программы и содержатся в выполнимом модуле. Однако часто программисту требуется нарушить этот порядок, заставив процессор обойти некоторый участок программы, перейти на выполнение другой ветви или передать управление подпрограмме, имея в виду после ее завершения вернуться на прежнее место. Все эти операции осуществляются с помощью команд переходов. Переходы разделяются на безусловные, когда передача управления в другую точку программы осуществляется в безусловном порядке, независимо ни от каких обстоятельств, и условные, осуществляемые или не осуществляемые в зависимости от тех или иных условий: результатов сравнения, анализа, поиска и т.п. Безусловные переходы подразделяются на собственно переходы (без возврата в точку перехода) и вызовы подпрограмм (с возвратом после завершения подпрограммы).
Операции переходов и вызовов подпрограмм, помимо их практической ценности, представляют значительный методический интерес, так как затрагивают основу архитектуры процессора - сегментную адресацию памяти. Многочисленные разновидности команд переходов и вызовов обязаны своим существованием не столько потребностям практического программирования, сколько принципиальным архитектурным особенностям процессора. Отчетливое понимание этих особенностей и, соответственно, условий применения и возможностей различных операций переходов необходимо не только при использовании языка ассемблера, но и при программировании на языках высокого уровня, где иногда используется несколько иная терминология, но существо дела остается без изменения.
Безусловные переходы осуществляются с помощью команды jmp, которая может использоваться в 5 разновидностях. Переход может быть:
прямым коротким (в пределах -128... + 127 байтов);
прямым ближним (в пределах текущего сегмента команд):
прямым дальним (в другой сегмент команд);
косвенным ближним (в пределах текущего сегмента команд через ячейку
с адресом перехода);
косвенным дальним (в другой сегмент команд через ячейку с адресом
перехода).
Рассмотрим последовательно структуру программ с переходами разного вида.


Прямой короткий (short) переход. Прямым называется переход, в команде которого в явной форме указывается метка, на которую нужно перейти. Разумеется, эта метка должна присутствовать в том же программном сегменте, при этом помеченная ею команда может находиться как до, так и после команды jmp. Достоинство команды короткого перехода заключается в том, что она занимает лишь 2 байт памяти: в первом байте записывается код операции (EBh), во втором - смещение к точке перехода. Расстояние до точки перехода отсчитывается от очередной команды, т.е. команды, следующей за командой jmp. Поскольку требуется обеспечить переход как вперед, так и назад, смещение рассматривается, как число со знаком и, следовательно, переход может быть осуществлен максимум на 127 байт вперед или 128 байт назад. Прямой короткий переход оформляется следующим образом:

code segment

jmp short go ;КодЕВ dd


go:

code ends

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

tasm /m2 p,p,p

то описатель short можно опустить, так как ассемблер сам определит, что расстояние до точки перехода укладывается в короткий переход, даже если метка go расположена после строки с командой jmp. При использовании транслятора MASM указание описателя short обязательно (если метка go расположена после команды jmp). Здесь проявляются незначительные различия ассемблеров разных разработчиков.
В комментарии указан код команды; dd (от displacement, смещение) обозначает байт со смещением к точке перехода от команды, следующей за командой jmp.
При выполнении команды прямого короткого перехода процессор прибавляет значение байта dd к младшему байту текущего значения указателя команд IP (который, как уже говорилось, всегда указывает на команду, следующую за выполняемой). В результате в IP оказывается адрес точки перехода, а предложения, находящиеся между командой jmp и точкой перехода, не выполняются. Между прочим, конструкция с прямым переходом вперед часто используется для того, чтобы обойти данные, которые по каким-то причинам желательно разместить в сегменте команд.


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

code segment

jmp go ;КодЕ9 dddd

go:

code ends

Метка go может находиться в любом месте сегмента команд, как до, так и после команды jmp. В коде команды dddd обозначает слово с величиной относительного смещения к точке перехода от команды, следующей за командой jmp.
При выполнении команды прямого ближнего перехода процессор должен прибавить значение слова dddd к текущему значению указателя команд IP и сформировать тем самым адрес точки перехода. Что представляет собой смещение ddddl Какая это величина, со знаком или без знака? Если рассматривать смещение как величину без знака, то переход будет возможен только вперед, что, конечно, неверно. Если же смещение является величиной со знаком, то переход возможен не более, чем на полсегмента вперед или на полсегмента назад, что тоже неверно. В действительности, рассматривая вычисление адреса точки перехода, следует иметь в виду явление оборачивания, суть которого можно кратко выразить такими соотношениями:

FFFFh+0001h=0000h


0000h-0001h=FFFFh

Если последовательно увеличивать содержимое какого-либо регистра или ячейки памяти, то, достигнув верхнего возможного предела FFFFh, число "перевалит" через эту границу, станет равным нулю и продолжит нарастать в области малых положительных чисел (1, 2, 3, и т.д.). Точно так же, если последовательно уменьшать некоторое положительное число, то оно, достигнув нуля, перейдет в область отрицательных (или, что то же самое, больших беззнаковых) чисел, проходя значения 2, 1, 0, FFFFh, FFFEh и т.д.
Таким образом, при вычислении адреса точки перехода смещение следует считать числом без знака, но при этом учитывать явление оборачивания. Если команда jmp находится где-то в начале сегмента команд, а смещение имеет величину порядка 64 К, то переход произойдет вперед, к концу сегмента. Если же команда находится в конце сегмента команд, а смещение имеет ту же величину порядка 64 К, то для определения точки перехода надо двигаться по сегменту вперед, дойти до его конца и продолжать перемещаться от начала сегмента по-прежнему вперед, пока не будет пройдено заданное в смещении число байтов. Для указанных условий мы попадем в точку, находящуюся недалеко от команды jmp со стороны меньших адресов.
Итак, с помощью команды ближнего перехода (команда jmp без каких-либо спецификаторов) можно перейти в любую точку в пределах данного сегмента команд. Для того, чтобы перейти в другой сегмент команд, следует воспользоваться командой дальнего перехода.


Прямой дальний (far), или межсегментный переход. Этот переход позволяет передать управление в любую точку любого сегмента. При этом, очевидно, предполагается, что программный комплекс включает несколько сегментов команд. Команда дальнего перехода имеет длину 5 байт и включает, кроме кода операции EAh, еще и полный адрес точки перехода, т.е. сегментный адрес и смещение. Транслятору надо сообщить, что этот переход - дальний (по умолчанию команда jmp транслируется, как команда ближнего перехода). Это делается с помощью описателя far ptr, указываемого перед именем точки перехода.

codel segment


assume CS: codel ;Сообщим транслятору, что это сегмент команд

jmp far ptr go ;Код EA dddd ssss

codel ends


code2 segment


assume CS : code2 ; Сообщим транслятору, что это сегмент команд

gо:

code2 ends

Метка go находится в другом сегменте команд этой двухсегментной программы. В коде команды ssss - сегментный адрес сегмента code2, a dddd - смещение точки перехода go в сегменте команд code2.
Заметим, что при наличии в программе нескольких сегментов команд, каждый из них необходимо предварять директивой ассемблера assume СS:имя_сегмента, которая сообщает транслятору о том, что начался очередной сегмент команд. Это поможет транслятору правильно обрабатывать адреса меток, встречающихся в этом сегменте.
Освоив применение команд дальних переходов, мы получили возможность создавать программы любой длины. Действительно, предусмотрев в конце каждого программного сегмента команду дальнего перехода на начато следующего, мы можем включить в программный комплекс любое число сегментов по 64 Кбайт. Единственное ограничение - чтобы они все поместились в памяти. В действительности так, конечно, не делают. Разумнее дополнительные сегменты команд заполнить подпрограммами и вызывать их из основного сегмента (или друг из друга) по мере необходимости. Однако и в этом случае команды вызовов подпрограмм должны быть дальними. Разновидности подпрограмм и команд их вызова будут рассмотрены ниже.
Все виды прямых переходов требуют указания в качестве точки перехода программной метки. С одной стороны, это весьма наглядно; просматривая текст программы, можно сразу определить, куда осуществляется переход. С другой стороны, такой переход носит статический характер - его нельзя настраивать по ходу программы. Еще более серьезный недостаток прямых переходов заключается в том, что они не дают возможность перейти по известному абсолютному адресу, т.е. не позволяют обратиться ни к системным средствам, ни вообще к другим загруженным в память программам (например, резидентным). Действительно, программы операционной системы не имеют никаких меток, так как метка - это атрибут исходного текста программы, а программы операционной системы транслировались не нами и присутствуют в компьютере только в виде выполнимых модулей. А вот адреса каких-то характерных точек системных программ определить можно, хотя бы из векторов прерываний. Для обращения по абсолютным адресам надо воспользоваться командами косвенных переходов, которые, как и прямые, могут быть ближними и дальними.


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

code segment

jmp DS:go_addr ;Код FF 26 dddd

go: ; Точкаперехода

code ends


data segment

go_addr dw go ;Адресперехода (слово)

data ends

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

mov go_addr, offset gol


mov go_addr, offset go2


mov go_addr, offset go3

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

jmp DS:go_addr


jmp word ptr go_addr
jmp go_addr  

 

В первом варианте, использованном в приведенном выше фрагменте, указано, через какой сегментный регистр надлежит обращаться к ячейке go_addr, содержащей адрес перехода. Здесь допустима замена сегмента, если сегмент с ячейкой go_addr адресуется через другой сегментный регистр, например, ES или CS.
Во втором варианте подчеркивается, что переход осуществляется через ячейку размером в одно слово и, следовательно, является ближним. Ячейка go_addr могла быть объявлена с помощью директивы dd и содержать полный двухсловный адрес перехода, требуемый для реализации дальнего перехода. Однако ею можно воспользоваться и для ближнего перехода. Описатель word ptr перед именем ячейки с адресом перехода засташшет транслятор считать, что она имеет размер 1 слово (независимо от того, как она была объявлена), и что переход, следовательно, является ближним.
Наконец, возможен и самый простой, третий вариант, который совпадает по форме с прямым переходом, но, тем не менее, является косвенным, так как символическое обозначение go_addr является именем поля данных, а не программной меткой. В этом варианте предполагается, что сегмент, в котором находится ячейка go_addr, адресуется по умолчанию через регистр DS, хотя, как и во всех таких случаях, допустима замена сегмента. Тип перехода (ближний или дальний) определяется, исходя из размера ячейки go_addr. Однако этот вариант не всегда возможен. Для его правильной трансляции необходимо, чтобы транслятору к моменту обработки предложения

jmp go_addr

было уже известно, что собой представляет имя go_addr. Этого можно добиться двумя способами. Первый - расположить сегмент данных до сегмента команд, а не после, как в приведенном выше примере. Второй - заставить транслятор обрабатывать исходный текст программы не один раз, как он это делает по умолчанию, а несколько. Число проходов для транслятора TASM можно задать при его вызове с помощью ключа /m#, где # - требуемое число проходов. В нашем случае достаточно двух проходов.
В приведенных примерах адрес поля памяти с адресом точки перехода задавался непосредственно в коде команды косвенного перехода. Однако этот адрес можно задать и в одном из регистров общего назначения (ВХ, SI или DI). Для приведенного выше примера косвенного перехода в точку go, адрес которой находится в ячейке go_addr в сегменте данных, перс-ход с использованием косвенной регистровой адресации может выглядеть следующим образом:

mov BX, offset go_addr ;В ВХ смещение поля с адресом перехода


jmp [BX] ;Переход в точку gо

Особенно большие возможности предоставляет методика косвенного перехода с использованием базово-индексной адресации через пары регистров, например, [BX][SI] или [BX][DI]. Этот способ удобен в тех случаях, когда имеется ряд альтернативных точек перехода, выбор которых зависит от некоторых условий. В этом случае в сегменте данных создается не одно поле с адресом, а таблица адресов переходов. В базовый регистр ВХ загружается адрес этой таблицы, а в один из индексных регистров - определенный тем или иным способом индекс в этой таблице. Переход осуществляется в точку, соответствующую заданному индексу. Структура программы, использующий такую методику, выглядит следующим образом:

code segment


mov BX, off set go_tbl ;Загружаем в ВХ базовый адрес таблицы


mov SI, 4 ;Вычисленное каким-то


;образом смещение в таблице


jmp [BX] [SI] ;Если индекс =4, переход в точку goЗ

gol: ;1-я точка перехода

gо2 : ;2-я точка перехода

gоЗ: ;3-я точка перехода

code ends


data segment


go_tbl label word ;Таблица адресов переходов


gol_addr dw gol ;Адрес первой альтернативной
;точки перехода


go2_addr dw go2 ;Адрес второй альтернативной
;точки перехода


go3_addr dw доЗ ;Адрес третьей альтернативной
;точки перехода


data ends

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

mov BX, off set gol jmp BX

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


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

codel segment


assume CS:codel,DS:data

jmp DS:go_addr ; Код FF 2E dddd

codel ends


code2 segment


assume CS:code2

go: ;Точка перехода в другом сегменте команд

code2 ends


data segment

go_addrdd go ;Двухсловный адрес точки перехода

data ends

Точка перехода go находится в другом сегменте команд этой двухсегментной программы. В коде команды dddd обозначает относительный адрес слова go_addr в сегменте данных. Ячейка go_addr объявляется директивой dd (define double, определить двойное слово) и содержит двухсловный адрес точки перехода; в первом слове содержится смещение go в сегменте команд codel, во втором слове сегментный адрес codel. Оба компонента адреса перехода могут быть вычислены и помещены в ячейку go_addr по ходу выполнения программы.
Как и в случае ближнего косвенного перехода, ассемблер допускает различные формы описания дальнего косвенного перехода через ячейку сегмента данных:

jmp DS:go_addr ;Возможна замена сегмента


jmp dword ptr go_addr ;Если поле go_addr объявлено
;операторами dw


jmp go_addr ;Характеристики ячейки должны
;быть известны

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

mov BX,offset go_addr


jmp [BX]

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