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

Последнее обновление: 23.09.2023

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

VALUE(BASEREG, IDXREG, MULTIPLIER)

Фактичеси адрес состовляется на основе 4 компонентов:

  • VALUE: некоторое фиксированное значение - смещение.

  • BASEREG: базовой регистр, который хранит базовый адрес

  • IDXREG: индексный регистр

  • MULTIPLIER: некоторое число-множитель, на который умножается значение в индексном регистре. Может принимать значения 1, 2, 4 или 8

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

address = VALUE + BASEREG + IDXREG * MULTIPLIE

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

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

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

(%reg)

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

$number  # адрес переменной number

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

.globl _start

.data
num: .quad 5

.text
_start:
    movq $num, %rbx     # помещаем в RBX адрес переменной num
    movq (%rbx), %rdi   # помещаем в RDI значение по адресу из RBX
    movq $60, %rax
    syscall

Здесь в программе сначала получаем в регистр RBX адрес переменной num:

movq $num, %rbx

То есть в RBX будет именно адрес переменной, а не ее значение.

Затем в регистр RDI помещаем значение, которое располагается по адресу из RBX:

movq (%rbx), %rdi

То есть в RDI фактически окажется значение переменной num.

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

Инструкция leaq

В качестве альтернативы для загрузки адреса переменной можно использовать инструкцию leaq:

.globl _start

.data
num: .quad 5

.text
_start:
    leaq num, %rbx     # помещаем в RBX адрес переменной num
    movq (%rbx), %rdi   # помещаем в RDI значение по адресу из RBX
    movq $60, %rax
    syscall

Первый операнд инструкции leaq - переменная, адрес которой надо загрузить. Второй операнд - регистр, в который надо загрузить.

Арифметика адресов

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

.globl _start

.data
nums: .quad 11, 12, 13, 14, 15, 16

.text
_start:
    movq $nums, %rbx     # помещаем в RBX адрес переменной nums
    addq $8, %rbx       # прибавляем к адресу в RBX 8 байт
    movq (%rbx), %rdi   # помещаем в RDI значение по адресу из RBX (RDI = 12    )
    movq $60, %rax
    syscall

Здесь в регистр RBX помещен адрес переменной-массива nums. Фактически это адрес первого элемента в этом массиве - числа 11. Поскольку массив состоит из чисел типа .quad, то каждое из этих чисел занимает 8 байт. Соответственно каждое последующее число размещено от предыдущего на расстоянии в 8 байт. Поэтому если к адресу первого элемента прибавить 8 байт, то мы получим адрес второго элемента, что собственно и происходит в строке:

addq $8, %rbx

В итоге в регистре RDI будет располагаться второй элемент массива nums - число 12. Но важно не путать: выражение (%rbx) представляет данные по адресу в RBX, а %rbx - сам адрес. При этом адрес фактически представляет 64-разрядное число.

Аналогичным образом можно перемещаться на большое количество элементов. Например, переместимся к четвертому элементу массива nums:

.globl _start

.data
nums: .quad 11, 12, 13, 14, 15, 16

.text
_start:
    movq $nums, %rbx     # помещаем в RBX адрес переменной nums
    addq $24, %rbx       # прибавляем к адресу в RBX 24 байта - $3 * $8 = $24
    movq (%rbx), %rdi   # помещаем в RDI значение по адресу из RBX (RDI = 14)
    movq $60, %rax
    syscall

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

.globl _start

.data
num1: .quad 5
num2: .quad 6
nums: .quad 11, 12, 13, 14, 15, 16

.text
_start:
    movq $nums, %rbx     # помещаем в RBX адрес переменной nums
    subq $16, %rbx       # вычитаем от адресу в RBX 16 байт
    movq (%rbx), %rdi   # помещаем в RDI значение по адресу из RBX (RDI = 5)
    movq $60, %rax
    syscall

В данном случае переходим от адреса переменной nums назад на 16 байт - к переменной num1.

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

.globl _start

.data
nums: .quad 1, 2, 3, 4, 5, 6
count: .quad 6      # количество элементов в массиве nums
.text
_start:
    movq $nums, %rbx     # помещаем в RBX адрес массива nums
    movq count, %rcx    # помещаем в RCX количество элементов в массиве nums
    movq $0, %rdi       # будущая сумма элементов массива
main_loop:
    addq (%rbx), %rdi   # складываем  значение по адресу из RBX с числом в RDI
    addq $8, %rbx       # перемещаемся к следующему элементу массива
    subq $1, %rcx       # отнимаем 1 от значения в RCX
    jnz main_loop       # если счетчик RCX не равен 0, то переходим обратно к main_loop

    # loopq main_loop     # альтернативный способ вместо предыдущих 2 инструкций

    movq $60, %rax
    syscall

Здесь реализован цикл аля do-while. В регистр RCX помещаем значение переменной count, которая хранит количество элементов массива. Благодаря этому мы можем узнать, когда массив заканчивается. В регистре RDI будет храниться сумма элементов массива, по умолчанию в регистре 0.

После метки main_loop идет тело цикла. Мы складываем значение по адресу из RBX со значение в регистре RDI и результат помещаем обратно в RDI:

addq (%rbx), %rdi

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

addq $8, %rbx

Затем уменьшаем значение счетчика - регистра RCX на 1:

subq $1, %rcx

Если RCX не равен нулю, то есть в массиве еще есть элементы для перебора, то переходим обратно к метке main_loop и повторяем действия цикла:

jnz main_loop

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

loopq main_loop

Косвенная адресация со смещением

Смещение позволяет нам сместиться на некоторое количество байтов вперед или назад относительно адреса в базовом регистре:

.globl _start

.data
num1: .quad 45
nums: .quad 11, 12, 13, 14, 15, 16

.text
_start:
    movq $nums, %rbx     # помещаем в RBX адрес массива nums
    movq 8(%rbx), %rdi   # RDI =  значение по адресу (RBX+8)

    movq $60, %rax
    syscall

Выражение 8(%rbx) прибавляет к адресу в RBX 8 байт. Затем по полученному адресу извлекается значение и помещается в регистр RDI.

Смещение также может быть отрицательным:

movq 8(%rbx), %rdi      # RDI = значение по адресу (RBX-8)

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

Индексный регистр и множитель

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

.globl _start

.data
nums: .byte 11, 12, 13, 14, 15, 16

.text
_start:
    xorq %rax, %rax             # обнуляем RAX
    movq $nums, %rbx            # в RBX адрес массива nums, RBX - базовый регистр
    movq $3, %rsi               # RSI - индексный регистр    
    movb (%rbx, %rsi), %al     # в AL число по адресу (RBX + RSI)
    movq %rax, %rdi

    movq $60, %rax
    syscall

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

Регистр RBX выполняет роль базового регистра - в него загружается адрес переменной nums.

В качестве индексного регистра применяется регистр RSI - в него помещается число 3:

movq $3, %rsi

То есть мы собираемся обратиться к элементу с индексом 3. Поскольку индексация начинается с нуля (то есть первый элемент имеет индекс 0). То, обращаясь по индексу 3, мы собираемся обратиться к 4-му элементу в массиве nums.

Далее собственно производим обращение и помещаем байт по адресу (RBX + RSI) в 8-битовый регистр AL (так как загружаем 1 байт):

movb (%rbx, %rsi), %al

То есть в RDI будет число 14.

Просто индексный регистр удобно использовать для перемещения по массивам байтов, но это не работает, если каждый элемент больше 1 байта. И в этом случае применяется множитель, на который умножается значение индексного регистра. Множитель может иметь значения 1, 2, 4 или 8 - в соответствии с размером элемента в массиве. Например:

.globl _start

.data
nums: .quad 111, 112, 113, 114, 115, 116

.text
_start:
    movq $nums, %rbx            # в RBX адрес массива nums, RBX - базовый регистр
    movq $2, %rsi               # RSI - индексный регистр    
    movq (%rbx, %rsi, 8), %rdi  # в RDI число по адресу (RBX + RSI * 8)

    movq $60, %rax
    syscall

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

movq (%rbx, %rsi, 8), %rdi

предписывает положитель в RDI число, которое находится по адресу RBX + RSI*8, то есть к адресу в RBX прибавляется 3 * 8 = 24 байта.

Имя переменной как смещение

В качестве смещения можно использовать имя переменной:

.globl _start

.data
nums: .quad 11, 12, 13, 14, 15, 16

.text
_start:
    movq $2, %rsi               # RSI - индексный регистр    
    movq nums(, %rsi, 8), %rdi  # в RDI число по адресу ($nums + RSI * 8)

    movq $60, %rax
    syscall

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

Например, используем этот способ адресации и получим сумму элементов массива nums:

.globl _start

.data
nums: .quad 1, 2, 3, 4, 5, 6
count: .quad 6      # количество элементов в массиве nums
.text
_start:
    movq count, %rcx    # помещаем в RCX количество элементов в массиве nums
    movq $0, %rdi       # будущая сумма элементов массива
    movq $0, %rsi       # индексный регистр
main_loop:
    addq nums(,%rsi, 8), %rdi   # складываем значение по адресу из  (nums+rsi*8) с числом в RDI
    incq %rsi           # прибавляем 1 - получаем индекс следующего элемента
    loopq main_loop     # если счетчик RCX не равен 0, то переходим обратно к main_loop

    movq $60, %rax
    syscall

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850