Для обращения к данным по определенному адресу применяется косвенная адресация. Ассемблер 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:
.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