Скачиваний:
98
Добавлен:
03.06.2014
Размер:
5.64 Mб
Скачать

Форматы команд

363

а самый длинный — все возможные случаи.JVM содержит команду ILOAD, использующую 8-битный индекс для определения локальной переменной, которую нужно поместить в стек. Мы также показали, как префикс WIDE позволяет использовать тот же код операции для определения любого из первых 65 536 элементов во фрейме локальных переменных. Для команды WIDE ILOAD требуется 4 байта: 1 — для WIDE, 1 — для ILOAD и 2 — для 16-битного индекса. Такое разделение объясняется тем, что большинство команд ILOAD используют одну из первых 256 локальных переменных. Префикс WIDE нужен для универсальности, применимости к любым ситуациям, и используется он редко.

Но разработчики машины JVM пошли еще дальше. Так как параметры процедуры передаются в первые несколько слов фрейма локальных переменных, команда ILOAD чаще всего использует элементы фрейма локальных переменных с невысокими индексами. Разработчики JVM решили, что стоит назначить отдельные 1-байтные коды операций для каждой из возможных комбинаций. Команда ILOAD_O помещает в стек локальную переменную 0. Эта команда полностью эквивалентна 2-байтной команде ILOAD 0, за исключением того, что она занимает один байт вместо двух. Точно также команды ILOAD_1, ILOADJ? и IL0AD_3 (коды операций OxlB, OxlC и OxlD соответственно) помещают в стек локальные переменные 1, 2 и 3. Отметим, что локальную переменную 1, например, можно загрузить одним из трех способов: ILOAD_1, ILOAD 1 и WIDE ILOAD 1.

Многие команды имеют варианты, подобные этим. Существуют специальные тоъшвдл, полностью эквивалентные BIPUSH, для значений 0,1, 2, 3,4, 5, а также-1. Есть, кроме того, особые команды для записи переменных из стека в первые 4 слова пространства локальных переменных.

Отметим, что эти варианты повлекли за собой некоторые убытки. Только 256 различных команд могут определяться в одном байте. Поскольку 4 из этих 256 команд решили отвести на загрузку первых четырех локальных переменных, число команд уменьшилось на 4. Эти команды вместе с основной командой ILOAD в сумме составляют 5 команд. Префикс WIDE тоже использует одно из 256 возможных значений (а это даже не команда, а просто префикс), но он применяется к различным кодам операций.

Для спецификации загрузки операндов из набора констант разработчики использовали немного другой метод, поскольку они ожидали различия в распределении адресов. Они предоставили две версии команды: LDC и LDC_W. Вторая форма команды (она была включена в IJVM) может вызывать любое из 65 536 слов в наборе констант. В первой форме требуется только однобайтный индекс, но такая команда может вызывать только одно их первых 256 слов. Вызов любого из первых 256 слов можно осуществить с помощью 2-байтной команды, а вызов любого слова — с помощью 3-байтной команды. На эти 2 варианта требуется 2 кода из 256 кодов операций. Набор команд был бы более простым и регулярным, если бы разработчики выбрали ту же технологию, которую они использовали для команды ILOAD, то есть использовали бы префикс WIDE, а не команду LDC_W. Однако в этом случае для вызова констант из верхних 256 слов потребовалось бы 4 байта, а не 3.

Технология объединения кодов операций и индексов в один байт, а также размещения 256 доступных байтов в соответствии с частотой их использования была впервые предложена автором данной книги в 1978 году, но нашла применение только спустя два десятилетия [145].

3 6 4 Глава 5. Уровень архитектуры команд

Адресация

Разработка кодов операций является важной частью архитектуры команд. Однако значительное число битов программы используется для того, чтобы определить, откуда нужно брать операнды, а не для того, чтобы узнать, какие операции нужно выполнить. Рассмотрим команду ADD, которая требует спецификации трех операндов: двух источников и одного пункта назначения. (Термин «операнд» обычно используется применительно ко всем трем элементам, хотя пункт назначения — это место, где сохраняется результат.) Так или иначе команда ADD должна сообщать, где найти операнды и куда поместить результат. Если адреса памяти 32-битные, то спецификация этой команды требует помимо кода операции еще три 32-битных адреса. Адреса занимают гораздо больше бит, чем коды операции.

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

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

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

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

0ESTINATI0N=S0URSEl+S0URSE2.

двухадресную команду можно сократить до формы

REGISER2-REGISTER2+S0URSE1.

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

Адресация 365

ков разные предпочтения. В Pentium II, например, используются двухадресные команды, а в UltraSPARC II — трехадресные.

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

Итак, мы перешли оттрехадресной команды ADD кдвухадресной, азатем к одноадресной. Что же остается? Ноль адресов? Да. В главе 4 мы увидели, как машина IJVM использует стек. Команда IADD не имеет адресов. Входные и выходные операнды не показываются явным образом. Ниже мы рассмотрим стековую адресацию более подробно.

Способы адресации

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

Непосредственная адресация

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

MOV R1 4

Рис. 5.12. Команда с непосредственнымадресомдля загрузки константы 4 в регистр 1

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

3 6 6 Глава 5. Уровень архитектуры команд

Прямая адресация

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

Регистровая адресация

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

Такой способ адресации называют регистровой адресацией. В архитектурах с загрузкой с запоминанием, например UltraSPARC II, практически все команды используют исключительно этот способ адресации. Он не используется только в том случае, когда операнд перемещается из памяти в регистр (команда LOAD) или из регистра в память (команда STORE). Даже в этих командах один из операндов является регистром — туда отправляется слово из памяти или оттуда перемещается слово в память.

Косвенная регистровая адресация

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

Чтобы понять, почему может быть полезно использование разных слов при каждом выполнении команды, представим себе цикл, который проходит по 1024-эле- ментному одномерному массиву целых чисел для вычисления суммы элементов в регистре R1. Вне этого цикла какой-то другой регистр, например R2, может указывать первый элемент массива, а еще один регистр, например R3, может указывать первый адрес после массива. Массив содержит 1024 целых числа по 4 байта каждое. Если массив начинается с А, то первый адрес после массива будет А+4096. Типичная программа ассемблера, выполняющая это вычисление для двухадресной машины, показана в листинге 5.1.

Адресация

3 6 7

Листинг 5.1. Программа на ассемблере для вычисления суммы элементов массива

 

MOVRl.#0

накопление суммы

в R1.

изначально 0

 

MOV

R2,#A

;R2=aflpec

массива А

 

 

MOV

R3,#A+4096

;R3=aflpec

п е р в о г о слова после А

LOOP:

ADD

R1.(R2)

получение операнда через регистр R2

 

ADD

R2,#4

увеличение R2 на одно слово(4байта)

 

CMP

R2.R3

;проверка

на

завершение

 

BLT

LOOP

:если

R2<R3.

продолжать цикл

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

Интересно отметить, что само тело цикла не содержит каких-либо адресов памяти. В четвертой команде используется регистровая и косвенная адресация. В пятой команде применяется регистровая и непосредственная адресация, а в шестой — два раза регистровая. Команда BLT могла бы использовать адрес памяти, однако более привлекательным является определение адреса с помощью 8-битно- госмещения, связанного с самой командой BLT. Таким образом, полностью избегая адресов памяти, мы получили короткий и быстрый цикл. Кстати, эта программа предназначена для Pentium II, только мы переименовали команды и регистры и для упрощения понимания изменили запись.

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

ADDR1.A

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

ADD R1.A+4

и так далее до завершения цикла.

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

Индексная адресация

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

3 6 8 Глава 5. Уровень архитектуры команд

В машине IJVM при доступе к локальной переменной используется указатель ячейки памяти (LV) в регистре плюс небольшое смещение в самой команде, как показано на рис. 4.14, а. Есть и другой способ: указатель ячейки памяти в команде и небольшое смещение в регистре. Чтобы показать, как это работает, рассмотрим следующий пример. У нас есть два одномерных массива А и В по 1024 слова в каждом. Нам нужно вычислить А, И Bi для всех пар, а затем соединить все эти 1024 логических произведения операцией ИЛИ, чтобы узнать, есть ли в этом наборе хотя бы одна пара, не равная нулю. Один из вариантов — поместить адрес массива А в один регистр, а адрес массива В — в другой регистр, а затем последовательно перебирать элементы массивов, аналогично тому, как мы делали в предыдущей программе (см. листинг 5.1). Такая программа, конечно же, будет работать, но ее можно усовершенствовать, как показано в листинге 5.2.

Л и с т и нг 5 . 2 . Программа на языке ассемблера для вычисления операции ИЛИ от (Ai И Bi) для массива из 1024 элементов

MOV Rl,#0

;собирает результаты выполнения ИЛИ в R1.

MOV R2.#0

:R2= л от текущего

произведения A [ i ]

И B [ i ]

MOV R3.#4096

;R3=nepBoe

ненужное

значение

индекса

 

LOOP: MOV R4.A(R2)

;R4-A[i]

 

 

 

 

AND R4,B(R2)

;R4=A[l] И

B [ i ]

 

 

 

OR R1.R4

 

 

 

 

 

ADO R2.#4

И-1+4

 

 

 

 

CMP R2.R3

;нужно ли

продолжать?

 

 

BLT LOOP

;если R2<R3, мы не

закончили

и нужно

продолжать

Здесь нам требуется 4 регистра:

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

2.R2 — индекс i, который используется для перебора элементов массива.

3.R3 — константа4096. Это самое маленькоезначение i, которое не используется.

4.R4 — временный регистр для хранения каждого произведения.

После инициализации регистров мы входим в цикл из шести команд. Команда напротив LOOP вызывает элемент Ai в регистр R4. При вычислении источниказдесь используется индексная адресация. Регистр (R2) и константа (адрес элемента А) складываются, и полученный результат используется для обращения к памяти. Сумма этих двух величин поступает в память, но не сохраняется ни в одном из видимых пользователем регистров. Запись

MOV R4.ACR2)

означает, что для определения пункта назначения используется регистровая адресация, где R4 — это регистр, а для определения источника используется индексная адресация, где А — это смещение, a R2 — это регистр. Если А имеет значение, скажем, 124300, то соответствующая машинная команда будет выглядеть так, как показано на рис. 5.13.

MOV

R4

R2

124300

Рис. 5.13. Возможное представление команды MOV R4, A{R2)

Адресация 369

Во время первого прохождения цикла регистр R2 принимает значение 0 (поскольку регистр инициализируется таким образом), поэтому нужное нам слово АО находится в ячейке с адресом 124300. Это слово загружается в регистр R4. При следующем прохождении цикла R2 принимает значение 4, поэтому нужное нам слово А1 находится в ячейке с адресом 124304 и т. д.

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

Относительная индексная адресация

В некоторых машинах применяется способ адресации, при котором адрес вычисляется путем суммирования значений двух регистров и смещения (смещение факультативно). Такой подход называется относительной индексной адресацией. Один из регистров — это база, а другой — это индекс. Такая адресация очень удобна при следующей ситуации. Вне цикла мы могли бы поместить адрес элемента А в регистр R5, а адрес элемента В в регистр R6. Тогда мы могли бы заменить две первые команды цикла LOOP на

LOOP: MOV R4,(R2+R5)

AND R4,(R2+R6)

Было бы идеально, если бы существовал способ адресации по сумме двух регистров без смещения. С другой стороны, даже команда с 8-битным смещением была бы большим достижением, поскольку мы оба смещения могли бы установить на 0. Однако если смещения всегда составляют 32 бита, тогда мы ничего не выиграем, используя такую адресацию. На практике машины с такой адресацией обычно имеют форму с 8-битным и 16-битным смещением.

Стековая адресация

Мы уже говорили, что очень желательно сделать машинные команды как можно короче. Конечный предел в сокращении длины адреса — это команды без адресов. Как мы видели в главе 4, безадресные команды, например IADD, возможны при наличии стека. В этом разделе мы рассмотрим стековую адресацию более подробно.

Обратная польская запись

В математике существует древняя традиция помещать оператор между операндами (х+у), а не после операндов (ху+). Форма с оператором между операндами называется инфиксной записью. Форма с оператором после операндов называется

постфиксной или обратной польской записью в честь польского логика Я. Лукасевича (1958), который изучал свойства этой записи.

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

3 7 0 Глава 5. Уровень архитектуры команд

стеками. В-третьих, инфиксные операторы имеют приоритеты, которые произвольны и нежелательны. Например, мы знаем, что axb+c значит (axb)+c, а не ах(Ь+с), поскольку произвольно было определено, что умножение имеет приоритет над сложением. Но имеет ли приоритет сдвиг влево над логической операцией И? Кто знает? Обратная польская запись устраняет такие недоразумения.

Существует несколько алгоритмов для превращения инфиксных формул в обратную польскую запись. Ниже изложена переделка идеи Э. Дейкстры. Предположим, что формула состоит из следующих символов: переменных, двухоперандных операторов +, -, *, /, а также левой и правой скобок. Чтобы отметить конец формулы, мы будем вставлять символ -L после последнего символа одной формулы и перед первым символом следующей формулы.

Калифорния

А

X

- ( - В

-

+

- С J - )

4-

 

 

 

О О О

О О ^ О О ^

О О

0 0

О О

Нью-Йорк

Железнодорожная

стрелка

Техас

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

На рис. 5.14 нарисована железная дорога из Нью-Йорка в Калифорнию с развилкой, ведущей в Техас. Каждый символ формулы представлен одним вагоном. Поезд движется на запад (налево). Перед развилкой каждый вагон должен останавливаться и узнавать, должен ли он двигаться прямо в Калифорнию, или ему нужно по пути заехать в Техас. Вагоны, содержащие переменные, всегда направляются Калифорнию и никогда не едут в Техас. Вагоны, содержащие все прочие символы, должны перед вхождением на развилку узнавать о содержимом ближайшего вагона, отправившегося в Техас.

В таблице на рис. 5.15 показана зависимость ситуации от того, какой вагон отправился последним в Техас и какой вагон находится у развилки. Первый 1 всегда отправляется в Техас. Числа соответствуют следующим ситуациям:

1.Вагон на развилке направляется в Техас.

2.Последний вагон, направившийся в Техас, разворачивается и направляется вКалифорнию.

3.Вагон, находящийся на развилке, и последний вагон, отправившийся в Техас, угоняются и исчезают (то есть оба удаляются).

Адресация 371

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

5.Остановка. Произошла ошибка. Изначальная формула была некорректно сбалансирована.

 

Вагон на развилке

 

1

+

Р

х

/

(

)

4

1

1

1

1

1

5

2

2

2

1

1

1

2

2

2

2

1

1

1

2

2

2

2

2

2

1

2

2

2

2

2

2

1

2

5

1

1

1

1

1

3

Рис. 5.15. Алгоритм преобразования инфиксной записи в обратную польскую запись

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

Таблица 5.5. Некоторые примеры инфиксных выражений и их эквиваленты в обратной польской записи

Инфиксная запись

Обратная польская запись

А+ВхС

АВСх+

АхВ+С

АВхС+

AxB+CxD

А Вх С Dx+

(A+B)/(C-D)

А В+С D-/

АхВ/С

АВхС/

((А+В) xC+D)/(E+F+G)

A B+CxD+ E F + G +/

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

Вычисление формул в обратной польской записи

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

3 7 2 Глава 5. Уровень архитектуры команд

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

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

Втаблице 5.6 показано вычисление выражения

(8+2x5)/(1+3x2-4)

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

825х+132х+4-/

В таблице мы ввели команды умножения и деления IMULи IDIV. Число на вершине стека — это правый операнд (а не левый). Это очень важно для операций деления и вычитания, поскольку порядок операндов в данном случае имеет значение (в отличие от операций сложения и умножения). Другими словами, команда I0IV определяется следующим образом: сначала в стек помещается числитель, потом знаменатель, и тогда выполнение операции дает правильный результат. Отметим, что преобразовать обратную польскую запись в код (I)JVM очень легко: нужно просто просканировать формулу в обратной польской записи и выдавать одну команду с каждым символом. Если символ является константой или переменной, нужно выдавать команду помещения этой константы или переменной в стек. Если символ является оператором, нужно выдавать команду для выполнения данной операции.

Способы адресации для команд перехода

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

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

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

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