Заметка основана на конспектах из книги Programming with 64-bit ARM Assembly Language.
Регистры
X0-X30
– Регистры общего назначения.X31
,SP
,XZR
– Указатель стека или нулевой регистр, в зависимости от контекста.X30
,LR
– Ссылочный регистр. При вызове функции этот регистр используется для хранения адреса возврата. Не рекомендуется его использовать для чего-либо другого.PC
– Счётчик инструкций. Хранит адрес по которому в памяти расположена выполняемая в данный момент инструкция.
Мы не всегда хотим использовать все 64 бита данных регистра,
иногда нам достаточно 32 бит. Поэтому существуют удобные
32-битные регистры W0-W30
и WZR
. Когда мы их используем, то
верхние 32 бита соответствующего 64-битного регистра
устанавливаются в 0.
Формат инструкций
Все инструкции имеют длину 32 бита. Остальное нам пока не очень важно для практических целей (если только мы не собираемся писать дизассемблер).
Тривиальная программа
Рассмотрим “привет мир”.
X0-X2
– Параметры системных вызовов.X16
– Номер системного вызова.
; сообщаем линковщику где начинается программа
.global _start
; устанавливаем нужное выравнивание
.align 2
_start:
// печатаем
MOV X0, #1 ; куда печатать: 1 = stdout
ADR X1, helloworld ; что печатать
MOV X2, #13 ; длина нашей строчки
MOV X16, #4 ; номер системного вызова "write"
SVC #0x80 ; делаем системный вызов
// выходим
MOV X0, #0 ; код 0
MOV X16, #1 ; номер системного вызова "exit"
SVC #0x80 ; просим ядро завершить программу
.data
helloworld: .ascii "Hello World!\n"
Вот так можно собрать:
as -arch arm64 -o HelloWorld.o HelloWorld.s
ld -o HelloWorld HelloWorld.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start -arch arm64
Базовые инструкции
MOV/MOVK/MOVN
Существует несколько форм инструкций перемещения данных:
MOVK XD, #imm16{, LSL #shift}
MOV XD, #imm16{, LSL #shift}
MOV XD, XS
MOV XD, operand2
MOVN XD, operand2
Здесь:
XD
иXS
– Какие-то любые регистры.#imm16
(immediate value) – Любой 16-битный числовой литерал.operand2
– Про него ниже.
Рассмотрим их по очереди.
MOVK (move keep)
Загружает 16-битное число в одну из 4-х позиций регистра, не
трогая остальные 48 бит. Например, если мы хотим загрузить число
0x1234FEDC4F5D6E3A
в регистр X2
, то мы можем это сделать
так:
MOV X2, #0x6E3A
MOVK X2, #0x4F5D, LSL #16
MOVK X2, #0xFEDC, LSL #32
MOVK X2, #0x1234, LSL #48
В этом примере выше первая инструкция MOV
это алиас MOVZ
.
Инструкция MOVZ
ведёт себя идентично MOVK
за исключением
того, что она обнуляет остальные 48 бит.
MOV (из регистра в регистр)
Копирует содержимое регистра X2
в регистр X1
:
MOV X1, X2
Что такое operand2
Все ARM-инструкции, которые работают с данными, имеют
опциональный параметр operand2
. Он может быть в 3 формах:
- Регистр и сдвиг
MOV X1, X2, LSL #1 ; логический сдвиг влево MOV X1, X2, LSR #1 ; логический сдвиг вправо MOV X1, X2, ASR #1 ; арифметический сдвиг вправо MOV X1, X2, ROR #1 ; сдвиг вправо (вращение)
- Регистр и расширение. Операции расширения позволяют нам
достать байт, половину или целое слово из второго регистра.
С инструкцией
MOV
расширения использовать нельзя, поэтому приведём пример сADD
:Если можно было догадаться что делают сдвиги, то с; X2 = X1 + SXTB(X0) ADD X2, X1, X0, SXTB
SXTB
и другими расширениями уже не так очевидно. Сейчас не будем на этом детально останавливаться. Надеюсь, что сможем изучить как работают расширения позже. - Число и сдвиг. Мы уже встречали эту форму, когда
разбирались с
MOVK
.MOV X1, 0xAB00, LSL #16
Попробуем MOV
на практике:
.global _start
.align 2
_start:
; помещаем число 0x1234FEDC4F5D6E3A в регистр X2
MOV X2, #0x6E3A
MOVK X2, #0x4F5D, LSL #16
MOVK X2, #0xFEDC, LSL #32
MOVK X2, #0x1234, LSL #48
; копируем содержимое W2 в W1
MOV W1, W2
; пробуем мнемоники сдвигов и вращений
LSL X1, X2, #1 ; логический сдвиг влево
LSR X1, X2, #1 ; логический сдвиг вправо
ASR X1, X2, #1 ; арифметический сдвиг вправо
ROR X1, X2, #1 ; сдвиг вправо (вращение)
; выходим
MOV X0, #0 ; код 0
MOV X16, #1 ; номер системного вызова "exit"
SVC #0x80 ; просим ядро завершить программу
Пора начать использовать мейк-файлы:
OBJS = mov.o
ifdef DEBUG
DEBUGFLGS = -g
else
DEBUGFLGS =
endif
LDFLAGS = -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _start -arch arm64
%.o : %.s
as $(DEBUGFLGS) $< -o $@
all: mov
mov: $(OBJS)
ld -o mov $(LDFLAGS) $(OBJS)
clean:
rm *.o mov
А теперь посмотрим что получилось objdump
‘ом:
objdump -s -d -M no-aliases mov.o
Покажет нам следующее:
mov.o: file format mach-o arm64
Contents of section __TEXT,__text:
0000 42c78dd2 a2eba9f2 82dbdff2 8246e2f2 B............F..
0010 e103022a 41f87fd3 41fc41d3 41fc4193 ...*A...A.A.A.A.
0020 4104c293 000080d2 300080d2 011000d4 A.......0.......
Disassembly of section __TEXT,__text:
0000000000000000 <ltmp0>:
0: 42 c7 8d d2 mov x2, #28218
4: a2 eb a9 f2 movk x2, #20317, lsl #16
8: 82 db df f2 movk x2, #65244, lsl #32
c: 82 46 e2 f2 movk x2, #4660, lsl #48
10: e1 03 02 2a orr w1, wzr, w2
14: 41 f8 7f d3 lsl x1, x2, #1
18: 41 fc 41 d3 lsr x1, x2, #1
1c: 41 fc 41 93 asr x1, x2, #1
20: 41 04 c2 93 extr x1, x2, x2, #1
24: 00 00 80 d2 mov x0, #0
28: 30 00 80 d2 mov x16, #1
2c: 01 10 00 d4 svc #0x80
Обратим внимание, например, что MOV X0, #0
была транслирована в ORR W1, WZR, W2
.
ADD/ADC
Эти инструкции складывают второй и третий параметры и сохраняют
результат сложения в первый: Xd = Xs + Operand2
, где Xd
и
Xs
могут совпадать.
ADD{S} Xd, Xs, Operand2
Рассмотрим несколько примеров.
-
Сложение с константой. Число может быть до 12 бит (0-4095).
; X2 = X1 + 4000 ADD X2, X1, #4000
-
Со сдвигом влево.
; X2 = X1 + 0x20000 ADD X2, X1, #0x20, LSL 12
-
Простое сложение 2-х регистров.
; X2 = X1 + X0 ADD X2, X1, X0
-
Сложение регистра со сдвинутым значением в другом регистре.
; X2 = X1 + (X0 * 4) ADD X2, X1, X0, LSL 2
SUB/SBC
SUB{S} Xd, Xs, Operand2
Память
Для чтения и записи памяти используются инструкции STR
и LDR
соответственно.
LDR X1, =num ; загружает адрес num в регистр X1
LDR X3, =loc ; загружает адрес loc в регистр X3
LDR X2, [X3] ; загружает слово по адресу X3 в X2
.data
num .BYTE 0x12
loc .QUAD 0x123456789ABCDEF0
Функции и стек
Стек растёт вниз, SP
указывает на его вершину и всегда
выровнен на 16 байт (это важно). У нас есть инструкции STR
и
STP
для размещения содержимого регистров в стеке, а также
LDR
и LDP
для считывания данных из стека в регистры.
Чтобы скопировать в стек значение из регистра X5
, в котором
хранится число 1022
:
STR X5, [SP, #-16]!
Чтобы загрузить в регистр X4
значение из вершины стека, где
лежит число 1022
:
LDR X4, [SP], #16