Для обращения к данным по определенному адресу мы можем использовать один из способов адресации:
Обращение по адресу в базовом регистре.
LDR Xt, [Xn]
В данном случае Xn
называется базовым регистром. Это собственно тот тип адресации, который рассматривался в предыдущих статьях.
Адресация со смещением
LDR Xt, [Xn, <offset>]
Здесь <offset>
представляет смещение относительно адреса в базовом регистре Xn. Смещение может представляет константное значение или динамически вычисляемое значение.
Оно может быть положительным и отрицательным. Фактический адрес, к которому будет идти обращение, будет равен адресу в базовом регистре плюс смещение.
Преиндексная адресация
LDR Xt, [Xn, <offset>]!
При преиндексной адресации сначала изменяется адрес в базовом регистре: к нему прибавляется смещение. То есть Xn = Xn + <offset>
.
И именно по этому адресу будет идти обращение. То есть в отличие от предыдущего виде адресации здесь происходит изменение адреса в базовом регистре.
Постиндексная адресация
LDR Xt, [Xn], <offset>
При постиндексной адресации сначала идет обращение по адресу в базом регистре (получение или сохранение данных), затем обновляется адрес в базовом регистре - к нему прибавляется смещение. То есть в отличие от предыдущего вида адресации здесь обращение к памяти идет до изменения адреса в базовом регистре.
Обращение по адресу относительно регистра PC
Рассмотрим сначала адресацию со смещением.
При данном типе адресации для получения фактического адреса к адресу базового регистра прибавляется смещение в байтах. В качестве спещения могут применяться:
Положительные и отрицательные константы - непосредственные операнды
LDR Xt, [Xn, #imm]
Регистры
LDR Xt, [Xn, Xm] LDR Xt, [Xn, Wm]
Регистры с применением сдвига или расширения знаком/нулем
LDR Xt, [Xn, Xm, <оператор_сдвига> #imm] LDR Xt, [Xn, Wm, <оператор_расширения> {#imm}]
Если в качестве смещения применяется непосредственный операнд, то он должен быть размером не более 12 бит. Для инструкций LDR/STR
,
если первый операнд - 32-разрядный регистр W0-W30, то смещение должно быть кратно 4, а если первый операнд - 64-разрядный регистр
Х0-Х30, то смещение должно быть кратно 8. При этом смещение долдно быть в диапазоне 0-4095.
Используем непосредственый операнд в качестве смещения:
.global _start _start: ldr x1, =nums // загружаем в X1 адрес nums ldr x0, [x1, 16] // смещение равно 16 байтам, Х0=13 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data nums: .quad 11, 12, 13, 14, 15, 16, 17, 18
Здесь в регистр Х1 загружен адрес массива nums, который состоит их 8-байтных чисел. Далее обращаемся по адресу на 16 байт вперед относительно адреса в регистре Х1:
ldr x0, [x1, 16] // смещение равно 16 байтам, Х0=13
Регистр Х1 хранит адрес массива nums, точнее самого первого его элемента (число 11). Прибавив к этому адресу 16 байт, получим адрес второго элемента в массиве nums - числа 13. И именно это число будет загружено в регистр Х0.
Смещения могут представлять вычисляемые выражения. Например:
.global _start _start: ldr x1, =nums // загружаем в X1 адрес nums ldr x0, [x1, 3*8] // смещение равно 24 байтам, Х0=14 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data nums: .quad 11, 12, 13, 14, 15, 16, 17, 18
В данном случае обращаемся к 4-му элементу (элементу с индексом 3 или nums[3]). Поскольку каждый элемент занимает 8 байт, то смещение представляет результат выражения
3*8
(то есть 24 байта). Использование таких выражений может быть проще в плане наглядности, так как мы видим и индекс элемента - 3 и размер элемента - 8.
Если смещение отрицательное, то фактически происходит вычитание смещения от адреса из базового регистра, то есть мы как-бы двигаемся в памяти назад. Например:
.global _start _start: ldr x1, =nums // загружаем в X1 адрес nums ldr x0, [x1, -8] // смещение равно 8 байтам, Х0=125 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data number: .quad 125 nums: .quad 11, 12, 13, 14, 15, 16, 17, 18
Здесь смещение равно -8, то есть относительно адреса переменной nums смещаемся назад на 8 байт. А по этому адресу располагается 8-байтная переменная number, равная 125. Поэтому в регистре Х0 будет загружено число 125.
Если мы попробуем дизассемблировать предыдущую скомпилированную программу, то получим следующий вывод:
Disassembly of section .text: 0000000000000000 <_start>: 0: 58000081 ldr x1, 10 <_start+0x10> 4: f85f8020 ldur x0, [x1, #-8] 8: d2800ba8 mov x8, #0x5d // #93 c: d4000001 svc #0x0
Здесь мы видим, что вместо второй инструкции LDR
используется инструкция LDUR. Эта инструкция во многоим аналогична LDR
,
только может принимать смещения, которые не являются кратными 4 или 8. Однако в этом случае смещения должны быть не более 9 бит и находиться в диапазоне от -256 до 255. Существует набор
подобных инструкций, которые принимают смещения, не кратные 4 и 8:
LDUR
: загружает данные размером целевого регистра (аналог LDR
)
LDURB
: загружает 1 байт (аналог LDRB
)
LDURSB
: загружает 1 байт со знаком (аналог LDRS
)
LDURH
: загружает полслова - 2 байта (аналог LDRH
)
LDURSH
: загружает полслова со знаком (аналог LDRSH
)
LDURW
: загружает слово - 4 байта (аналог LDRW
)
LDURSW
: загружает слово со знаком (аналог LDRSW
)
STUR
: сохраняет данные размером целевого регистра (аналог STR
)
STURB
: сохраняет 1 байт (аналог STRB
)
STURH
: сохраняет полслова (аналог STRH
)
Все эти инструкции принимают смещение в диапазоне от -256 до 255. И если инструкциям LDR/STR
передается или отрицательное смещение или смещение не кратное 4 или 8,
то дизассемблер транслирует их в соответствующие инструкции LDUR/STUR
В качестве смещения также может применяться значение регистра:
LDR Wt, [Xn, Xm] LDR Xt, [Xn, Xm]
В таком случае регистр часто выступает в качетсве индекса для обращения к каким-то частям данным. Например:
.global _start _start: mov x2, 2 // Х2-регистр-индекс - обращаемся к nums[2] ldr x1, =nums // загружаем в X1 адрес nums ldrb w0, [x1, x2] // эквивалентно ldr x0, [x1 + x2] mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data nums: .byte 11, 12, 13, 14, 15, 16, 17, 18
Здесь определен массив однобайтовых чисел nums. Допустим, мы хотим обратиться к третьему его элемента (nums[2]), то есть к числу 13 в данном случае. Каждое число в массиве nums находится друг от друга на расстоянии 1 байта (так как числа однобайтовые). Соответственно чтобы получить адрес тертьего элемента, надо к адресу начала массива nums (адресу его первого элемента) прибавить 2 байта.
В качестве регистра-индекса выступает регистр X2 - в него загружаем индекс искомого элемента:
mov x2, 2 // Х2-регистр-индекс - обращаемся к nums[2] ldr x1, =nums // загружаем в X1 адрес nums
В регистр Х1 загружаем адрес массива nums, то есть регистр Х1 будет базовым регистром. Затем используем индекс в регистре Х2 и базовый адрес в регистре Х1, с помощью инструкции ldrb (нам нужно получить 1 байт) обращаемся к элементу по нужному адресу:
ldrb w0, [x1, x2] // эквивалентно ldr x0, [x1 + x2]
К регистру-смещению можно применять стандартные операции сдвига - LSL/LSR
:
LDR Wt, [Xn, Xm, LSL #2] // адрес = Xn + (Xm*4) LDR Xt, [Xn, Xm, LSL #3] // адрес = Xn + (Xm*8)
Если регистру-смещение представляет 32-разрядный регистр W0-W30, то также к нему можно применять операции расширения знаком и нулем UXTW/SXTW/SXTX
:
LDR Wt|Xt, [Xn, Wm, UXTW {#imm}] LDR Wt|Xt, [Xn, Wm, SXTW {#imm}] LDR Wt|Xt, [Xn, Wm, SXTX {#imm}]
В качестве примера рассмотрим следующую программу:
.global _start _start: mov x2, 3 // Х2-регистр-индекс - обращаемся к nums[3] ldr x1, =nums // загружаем в X1 адрес nums ldr x0, [x1, x2, lsl 3] // эквивалентно ldr x0, [x1, x2*8] mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data nums: .quad 11, 12, 13, 14, 15, 16, 17, 18
В секции данных определен массив nums, который содержит числа размером 8 байт. То есть каждое последующее число находится от предыдущего на расстоянии 8 байт. Допустим, мы хотим получить четвертое число в этом массиве (в данном случае число 14). Оно будет находится от начала массива на расстоянии 3 * 8 = 24 байта. То есть если к адресу массива nums прибавить 24 байта, то мы получим адрес nums[3], то есть четвертого числа.
Для получения нужного нам числа сначала помещаем в регистр Х2 индекс числа - 3:
mov x2, 3 // Х2-регистр-индекс - обращаемся к nums[3] ldr x1, =nums // загружаем в X1 адрес nums
То есть регистр Х2 будет выполнять роль смещения. Затем загружаем в регистр Х1 адрес массива nums. Таким образом, Х1 будет выполнять роль базового регистра. Далее, используя сдвиг смещения, получаем нужный нам элемент:
ldr x0, [x1, x2, lsl 3]
Сдвиг влево на 3 эквивалентен умножению на 8. То есть финальный адрес будет равен x1 + x2*8
.