Режимы адресации

Урок разбирает режимы адресации — разные способы команды сказать, где взять операнд.

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

Зачем разные режимы

Команде ADD нужны операнды, но где они? Иногда это константа (5), иногда значение регистра, иногда ячейка памяти по вычисляемому адресу. Режимы адресации дают гибкость: с их помощью одной командой обходят массив, обращаются к полю структуры, работают со стеком. Они напрямую отражаются в синтаксисе ассемблера (квадратные скобки, смещения).

Основные режимы

РежимПримерГде операнд
НепосредственныйADDI R1, R1, 5значение 5 прямо в команде
РегистровыйADD R1, R2, R3в регистрах R2, R3
ПрямойLOAD R1, [1000]в памяти по адресу 1000
Косвенный регистровыйLOAD R1, [R2]в памяти по адресу из R2
Базовый со смещениемLOAD R1, [R2+8]память по (R2 + 8)
ИндексныйLOAD R1, [R2+R3]память по (R2 + R3)

Как работает под капотом: вычисление эффективного адреса

Самый частый режим — базовый со смещением: адрес = база (регистр) + константа. Так читают поля структуры (база — указатель, смещение — позиция поля) и элементы массива. Промоделируем работу режимов над «памятью»:

memory = {1000: 11, 1004: 22, 1008: 33, 1012: 44}
regs = {"R2": 1000, "R3": 8}

def load(mode, **kw):
    if mode == "immediate":
        return kw["value"]                       # операнд = константа
    if mode == "register":
        return regs[kw["reg"]]                    # значение регистра
    if mode == "direct":
        return memory[kw["addr"]]                 # память по адресу
    if mode == "indirect":
        return memory[regs[kw["reg"]]]            # память по адресу из регистра
    if mode == "base_offset":
        return memory[regs[kw["reg"]] + kw["off"]]# база + смещение
    if mode == "indexed":
        return memory[regs[kw["base"]] + regs[kw["idx"]]]

print("immediate 5      :", load("immediate", value=5))
print("register R2      :", load("register", reg="R2"))
print("direct  [1008]   :", load("direct", addr=1008))
print("indirect [R2]    :", load("indirect", reg="R2"))
print("base+off [R2+8]  :", load("base_offset", reg="R2", off=8))
print("indexed [R2+R3]  :", load("indexed", base="R2", idx="R3"))

Вывод:

immediate 5      : 5
register R2      : 1000
direct  [1008]   : 33
indirect [R2]    : 11
base+off [R2+8]  : 33
indexed [R2+R3]  : 33

Адресация и массивы

Обход массива — это базовая адресация в цикле: база — начало массива, смещение растёт на размер элемента. Так одна команда LOAD с режимом [base + i*size] достаёт любой элемент. Индексный режим (база + индекс-регистр) делает это ещё удобнее.

Глубже в тему

Режимы адресации — это, по сути, маленький язык вычисления адресов, встроенный прямо в команду. Зачем такая роскошь? Потому что подавляющее большинство обращений к памяти в реальных программах укладываются в несколько типичных шаблонов: доступ к полю структуры (база указателя плюс фиксированное смещение поля), проход по массиву (база плюс растущий индекс), работа со стеком (база — указатель стека, смещение — позиция локальной переменной). Дав аппаратуре пару универсальных режимов, проектировщик позволяет одной командой LOAD выразить любой из этих случаев, не плодя десятки специализированных опкодов. Базовый режим со смещением неслучайно называют рабочей лошадкой: именно он покрывает доступ и к полям, и к локальным переменным.

Здесь полезно увидеть прямую связь с языками высокого уровня. Когда вы в C пишете p->field, компилятор почти наверняка превратит это в базовую адресацию: значение указателя p ложится в регистр-базу, а смещение поля внутри структуры известно на этапе компиляции и становится константой. Выражение arr[i] для массива четырёхбайтовых int превращается в индексную адресацию [база + i*4] — и вот тут кроется частая ловушка: смещение считается в байтах, а не в элементах, поэтому индекс приходится домножать на размер элемента. Понимание этого объясняет, почему обход массива с шагом по памяти эффективнее, чем хаотичные прыжки по указателям: первый укладывается в один компактный режим адресации и дружит с кэшем.

Историческая перспектива показывает, как именно тут разошлись RISC и CISC. Машины вроде VAX доводили идею режимов до крайности: автоинкремент (адрес используется и тут же увеличивается — удобно для строк), двойная косвенность (адрес адреса), масштабируемый индекс. Это делало ассемблер выразительным, но превращало вычисление эффективного адреса в многоступенчатый процесс, который было трудно вписать в один такт конвейера. RISC сознательно отказался от изобилия, оставив обычно только регистровый, непосредственный и базовый со смещением. Богатые режимы переложили на компилятор: то, что в VAX делал один сложный режим, в MIPS компилятор разворачивает в две-три простые команды. Это классический пример переноса сложности из аппаратуры в программное обеспечение.

Режимы адресации также напрямую влияют на безопасность и корректность программ. Косвенная адресация через регистр — это разыменование указателя, и если в регистре мусор, обращение уйдёт в недопустимую область памяти, вызвав ошибку защиты (segmentation fault). Индексная адресация без проверки границ — корень переполнений буфера, классической уязвимости. Поэтому современные защищённые языки добавляют проверки границ перед такими обращениями, а аппаратура (через виртуальную память и теги указателей вроде ARM MTE) старается ловить выход за пределы. Видно, что простой на первый взгляд вопрос «где взять операнд» тянет за собой целую цепочку решений — от удобства компилятора до защиты от атак.

Частые ошибки

  • Путать значение и адрес. Регистровый режим даёт значение регистра; косвенный — значение из памяти по адресу в регистре.
  • Забывать про размер элемента. Смещение в массиве считается в байтах: для 4-байтовых int индекс умножают на 4.
  • Думать, что все ISA имеют все режимы. RISC обычно ограничивается несколькими (чаще базовый+смещение); богатый набор режимов — черта CISC.

Итог

  • Режим адресации определяет, где находится операнд: в команде, регистре или памяти.
  • Базовый со смещением (база + константа) — рабочая лошадка для структур и массивов.
  • RISC экономит на режимах; CISC предлагает их богатый набор.
Проверьте себя
1. Где находится операнд при непосредственном (immediate) режиме адресации?
AВ памяти по адресу
BПрямо в самой команде как константа
CВ регистре
DВ стеке
2. Как вычисляется адрес при базовом режиме со смещением [R2+8]?
AБерётся ровно 8
BСкладываются значение регистра R2 и константа-смещение 8
CБерётся значение R2
DПеремножаются R2 и 8
3. Чем регистровый режим отличается от косвенного регистрового?
AНичем
BРегистровый даёт значение самого регистра, косвенный — значение из памяти по адресу в регистре
CКосвенный быстрее
DРегистровый работает только с константами