Иногда программам может потребоваться доступ к данным, адрес которых известен относительно текущего счетчика программв - регистра PC (Program Counter). Распространенным примером подобного вида адресации является чтение данных из литерального пула. Пулы литералов часто используются компиляторами и ассемблерами для хранения некоторых постоянных данных в конце блока кода. Поскольку расстояние между литералом и инструкцией, которая обращается к нему, фиксировано, его можно загрузить через адрес инструкции плюс некоторое фиксированное смещение. Поскольку адрес, к которому осуществляется доступ, вычисляется относительно адреса текущей инструкции (а он хранится в регистре PC), то данный тип адресации и называется адресацией относительно PC.
Другой распространенной ситуаций использования этого вида адресации представляет обращение к адресу глобальной переменной. В этом случае ассемблер может вычислить смещение от текущей инструкции (которая будет в регистре PC при выполнении инструкции) до адреса метки. А инструкции загрузки адреса этой метки обычно неявно преобразуются ассемблером в инструкции загрузки относительно PC.
Для загрузки константного значения или метки инструкция LDR использует специальный синтаксис
LDR Xn, =label
В статье про инструкцию LDR уже упоминалась загрузка глобальных переменных, которые по сути являются метками, на которые проецируются данные:
.global _start _start: ldr x1, =num // загружаем в X1 адрес метки num ldr x0, [x1] // Х0=22 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num: .quad 22
В данном случае в регистр Х1 загружаем адрес метки (глобальной переменной) num.
Причем таким образом мы можем загружать адрес вообще любой метки. Например:
.global _start _start: ldr x1, =exit // загружаем в X1 адрес метки num mov x0, x1 // Х0=0x8 exit: mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux
Здесь также определена метка exit
, по адресу которой располагается инструкция mov x8, #93
.
Подобный синтаксис может быть полезен, когда надо загрузить константу, однако она слишком большая (больше 12 байт), чтобы быть загруженной в регистр с помощью инструкции
MOV. Например, если мы попробуем в рамках одной инструкции MOV
поместить в регистр число 0x1234cdef
, то на этапе компиляции мы столкнемся
с ошибкой, которая говорит, что нельзя скопировать непосредственый операнд одной инструкцией. Однако мы можем ее загрузить ее в рамках одной инструкции LDR
.global _start _start: ldr x0, =0x1234cdef // Х0=0x1234cdef mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux
Таким образом, в регистре Х0 окажется число 0x1234cdef. Если мы посмотрим на вывод дизассемблера утилиты objdump, то мы увидим, что для литерала 0x1234cdef создается специальное поле после кода программы:
Disassembly of section .text: 0000000000000000 <_start>: 0: 58000080 ldr x0, 10 <_start+0x10> 4: d2800ba8 mov x8, #0x5d // #93 8: d4000001 svc #0x0 c: 00000000 udf #0 10: 1234cdef and w15, w15, #0xf0f0f0f0 14: 00000000 udf #0
Здесь мы видим, что непосредственно сам литерал 0x1234cdef
хранится в программе по относительному адресу 10. Дизассемблер может попытаться рассматривать это число
как код инструкции, например, в случае выше дизассемблер некорректно транслировал это число в инструкцию and w15, w15, #0xf0f0f0f0
. Но в реальности это просто числовой литерал.
И инструкция LDR в начале программы загружает этот литерал, используя его относительный адрес <_start+0x10>
.
Причем ассемблер может сгруппировать литералы и отбросить дубликаты. Например:
.global _start _start: ldr x0, =0x1234cdef ldr x1, =0x1234cdef ldr x2, =0x1234ffff mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux
Здесь в регистры Х0 и Х1 загружается одно и то же число - 0x1234cdef, а в Х2 - число 0x1234ffff. Если мы посмотрим на вывод дизассемблера:
Disassembly of section .text: 0000000000000000 <_start>: 0: 580000c0 ldr x0, 18 <_start+0x18> 4: 580000a1 ldr x1, 18 <_start+0x18> 8: 580000c2 ldr x2, 20 <_start+0x20> c: d2800ba8 mov x8, #0x5d // #93 10: d4000001 svc #0x0 14: 00000000 udf #0 18: 1234cdef and w15, w15, #0xf0f0f0f0 1c: 00000000 udf #0 20: 1234ffff .inst 0x1234ffff ; undefined 24: 00000000 udf #0
то мы увидим, что в конце программы сохраняются два литерала - это и есть пул литералов (literal pool). При этом дубликаты отсутствуют.
В качестве параметра инструкция LDR может принимать число, кратное 4, которое указывает на смещение в битах относительно текущего адреса памяти. В качестве смещения выступает непосредственный операнд. Например:
.global _start _start: ldr x0, #12 // обращаемся по адресу, который находится от текущего на 12 байт вперед mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .quad 12 num2: .quad 22
Здесь в регистр X0 будут загружаться данные, которые располагаются на 12 байт от текущего (от адреса данной инструкции LDR). Каждая инструкция имеет 32 бита или 4 байта. Всего в программе до
секции .data
3 инструкции, то есть как минимум 3 * 4 = 12 байт. Это в общем случае, потому что в реальности ассемблер может добавлять дополнительные нулевые байты.
Но в данном частном случае после 12 байт будет располагаться секция .data
и, в частности, переменная num1
, в которой определяются 8-байтовое число 12.
Поэтому в регистр X0 будет загружено число 12.
Стоит отметить, что загружается именно само значение, а не адрес.
Если мы перейдем на 20 байта от адреса инструкции:
ldr x0, #20
то в регистр X0 будет загружено число num2.