Для загрузки данных применяется инструкция LDR. Она загружает в регистр адрес данных и затем использует этот адрес для загрузки в регистр реальных данных. Одна из ее форм:
LDR Xd, label
Здесь Xd
- регистр, в который загружается адрес, а label - метка (например, глобальная переменная), данные по адресу которой загружаются.
Простейший пример с загрузкой числа:
.global _start _start: ldr x0, number // загружаем в X0 число number // выход из программы mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data number: .word 15 // определяем слово, которое равно 15
В данном случае в регистр X0 загружается число number, то есть число 15.
Подобным образом можно загружать и другие данные. Например, загрузим байт:
.global _start _start: ldr x0, num // X0 = 11 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num: .byte 11
Стоит отметить, что для прямой загрузки данных с помощью этой инструкции необходимо, чтобы данные были выровнены по границе, кратной 2. Например, в следующем случае при компиляции мы получим ошибку:
.global _start _start: ldr x0, num2 // ! Ошибка mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 num2: .byte 12 num3: .byte 13 num4: .byte 14
Здесь мы пытаемся загрузить в регистр Х0 число num2. По умолчанию секция .data
выравнивается по четной границе, то есть кратной 2. Поэтому число num2 располагается по нечетному
адресу. Чтобы все-таки загрузить число num2 в регистр Х0, надо установить выравнивание по границе, кратной 2 (то есть как минимум по 2)
.global _start _start: ldr x0, num2 // X0 = 12 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num1: .byte 11 .align 2 // теперь num2 выравнено по 2 байтам num2: .byte 12 .align 2 num3: .byte 13 .align 2 num4: .byte 14
Распространенным сценарием применения инструкции LDR представляет загрузка данных по определенному адресу, причем инструкция LDR
может применяться как
для получения адреса данных, так и для получения самих данных по этому адресу.
Для получения адреса метки применяется следующий синтаксис:
LDR Xn, =label
После знака "равно" (=) указывается метка, адрес которой мы хотим получить.
Далее для загрузки данных, которые располагаются по определенному адресу, применяется следующая форма инструкции LDR :
LDR{type} Xt, [Xa]
Для обращения по адресу, который хранится в регистре, используются квадратные скобки - [Xa]
. Регистр Xa еще называется базовым регистром, потому что
он хранит базовый адрес, по которому будет идти обращение. Справа от названия инструкции опционально может указываться суффикс type
, который уточняет тип
загружаемых данных и который может принимать следующие значения
B: беззнаковый байт
SB: байт со знаком
H: беззнаковые полслова (16 бит)
SH: полслова со знаком (16 бит)
Например, загрузим число:
.global _start _start: ldr x1, =num // загружаем адрес метки num в Х1 ldr x0, [x1] // загружаем данные по адресу из Х1 в регистр Х0 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data num: .quad 11
Таким образом, загрузка переменной num из секции .data
разбивается на два этапа. Сначала в регистр X1 загружается адрес метки
num
:
LDR X1, =num
Затем в регистр X0 загружаем собственно данные, которые хранятся по адресу в X1.
LDR X0, [X1]
Квадратные скобки [X1]
указывают на непрямой доступ к памяти. Это означает загрузку данных, сохраненных по адресу,
на который указывает регистр X1. То есть в X2 загружаются НЕ данные регистра X1, а данные, на которые указывает регистр X1.
Стоит учитывать, что инструкция LDR загружает данные размером, равным разрядности регистра. То есть в примере выше инструкция будет пытаться загрузить по адресу из [X1] 64 байта. Это может приводить к избыточной загрузке данных. Например:
.global _start _start: ldr x1, =n1 // загружаем в X1 адрес числа n1 ldr x0, [x1] // загружаем в X0 значение, которое находится по адресу из X1 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data n1: .byte 1 n2: .byte 2 n3: .byte 3 n4: .byte 4
Здесь в регистр X1 загружается адрес числа n1. Далее загружаем в регистр X0 данные по адресу из X1, то есть по сути само число n1. Но число n1 представляет собой 1 байт, а регистр
X0 - 64-разрядный, поэтому инструкция LDR
будет пытаться загрузить 8 байт. Соответственно в регистр X0 будут загрузжены значения всех четырех однобайтных переменных n1, n2, n3, n4.
В итоге в регистре X0 будет число 0x0000000004030201
- то есть все числа будут последовательно располагаться в регистре.
В каких-то ситуациях это может быть преимуществом. Например, нам надо загрузить массив из 8 или менее однобайтовых чисел. И мы можем разом загрузить его в регистр:
.global _start _start: ldr x1, =nums // загружаем в X1 адрес числа nums ldr x0, [x1] // X0 = 0x0807060504030201 mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data nums: .byte 1, 2, 3, 4, 5, 6, 7, 8
Но это может быть не удобно, когда нам надо загрузить только одно число и гарантировать, что больше в регистре кроме этого числа ничего нет. И для этой цели как раз можно использовать специальные разновидности инструкции:
LDRB: загрузка беззнакового байта
LDRSB: загрузка байта со знаком
LDRH: загрузка беззнаковых полслова (16 бит)
LDRSH: загрузка полслова со знаком (16 бит)
Эти инструкции загружают число в 32-х разрядный регистр W0-W30. S-версия (signed/со знаком) при загрузке данных будет распространять знак на остальную часть регистра.
Например, нам надо загрузить всего лишь один байт, и для этого применим инструкцию LDRB:
.global _start _start: ldr x1, =n1 // загружаем в X1 адрес числа n1 ldrb w0, [x1] // X0 = 0x01 - загружаем только 1 байт mov x8, #93 // устанавливаем функцию Linux для выхода из программы svc 0 // Вызываем функцию Linux .data n1: .byte 1 n2: .byte 2 n3: .byte 3 n4: .byte 4
Для загрузки 32 бит инструкции LDR в качестве первого операнда передается 32-разрядный регистр W0-W30:
ldr w0, [x1]
Подобным образом можно загружать и другие данные, например, строку, которую потом можно вывести на консоль:
.global _start _start: // вывод строки на консоль MOV X0, #1 // 1 = StdOut - стандартный поток вывода LDR X1, =message // помещаем в регистр X1 адрес метки message MOV X2, #18 // длина строки MOV X8, #64 // функция Linux для вывода в поток SVC 0 // вызываем функцию Linux // завершение программы MOV X0, #0 // помещаем в регистр X0 число 1 MOV X8, #93 // номер функции Linux для выхода из программы - 93 SVC 0 // вызываем функцию и выходим из программы .data message: .ascii "Hello METANIT.COM\n"
Здесь инструкция LDR
загружает в регистр X1 адрес строки "Hello METANIT.COM\n". В данном случае используем системную функцию 64 в Linux, которая пишет в поток переданные данне.
В регистр X0 передается номер стандартного потока вывода (для печати на консоль) - число 1. Кроме того, в регистр X2 помещается длина строки в символах. Если все символы представляют
латинские символы, то каждый символ расценивается как однобайтный символ. Если используется кириллица, то каждый символ идет как двухбайтный символ. В примере выше все символы латинские,
соответственно длина строки - 18. В итоге при выполнении программы на консоль будет выведена строка "Hello METANIT.COM"