Заметка основана на конспектах из книги Programming with 64-bit ARM Assembly Language.

Регистры Link to heading

  • X0-X30 – Регистры общего назначения.
  • X31, SP, XZR – Указатель стека или нулевой регистр, в зависимости от контекста.
  • X30, LR – Ссылочный регистр. При вызове функции этот регистр используется для хранения адреса возврата. Не рекомендуется его использовать для чего-либо другого.
  • PC – Счётчик инструкций. Хранит адрес по которому в памяти расположена выполняемая в данный момент инструкция.

Мы не всегда хотим использовать все 64 бита данных регистра, иногда нам достаточно 32 бит. Поэтому существуют удобные 32-битные регистры W0-W30 и WZR. Когда мы их используем, то верхние 32 бита соответствующего 64-битного регистра устанавливаются в 0.

Формат инструкций Link to heading

Все инструкции имеют длину 32 бита. Остальное нам пока не очень важно для практических целей (если только мы не собираемся писать дизассемблер).

Тривиальная программа Link to heading

Рассмотрим “привет мир”.

  • 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

Базовые инструкции Link to heading

MOV/MOVK/MOVN Link to heading

Существует несколько форм инструкций перемещения данных:

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) Link to heading

Загружает 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 (из регистра в регистр) Link to heading

Копирует содержимое регистра X2 в регистр X1:

MOV X1, X2

Что такое operand2 Link to heading

Все ARM-инструкции, которые работают с данными, имеют опциональный параметр operand2. Он может быть в 3 формах:

  1. Регистр и сдвиг
          MOV X1, X2, LSL #1 ; логический сдвиг влево
          MOV X1, X2, LSR #1 ; логический сдвиг вправо
          MOV X1, X2, ASR #1 ; арифметический сдвиг вправо
          MOV X1, X2, ROR #1 ; сдвиг вправо (вращение)
    
  2. Регистр и расширение. Операции расширения позволяют нам достать байт, половину или целое слово из второго регистра. С инструкцией MOV расширения использовать нельзя, поэтому приведём пример с ADD:
          ; X2 = X1 + SXTB(X0)
          ADD X2, X1, X0, SXTB
    
    Если можно было догадаться что делают сдвиги, то с SXTB и другими расширениями уже не так очевидно. Сейчас не будем на этом детально останавливаться. Надеюсь, что сможем изучить как работают расширения позже.
  3. Число и сдвиг. Мы уже встречали эту форму, когда разбирались с 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 Link to heading

Эти инструкции складывают второй и третий параметры и сохраняют результат сложения в первый: Xd = Xs + Operand2, где Xd и Xs могут совпадать.

ADD{S} Xd, Xs, Operand2

Рассмотрим несколько примеров.

  1. Сложение с константой. Число может быть до 12 бит (0-4095).

          ; X2 = X1 + 4000
          ADD X2, X1, #4000
    
  2. Со сдвигом влево.

          ; X2 = X1 + 0x20000
          ADD X2, X1, #0x20, LSL 12
    
  3. Простое сложение 2-х регистров.

          ; X2 = X1 + X0
          ADD X2, X1, X0
    
  4. Сложение регистра со сдвинутым значением в другом регистре.

          ; X2 = X1 + (X0 * 4)
          ADD X2, X1, X0, LSL 2
    

SUB/SBC Link to heading

SUB{S} Xd, Xs, Operand2

Память Link to heading

Для чтения и записи памяти используются инструкции STR и LDR соответственно.

LDR X1, =num ; загружает адрес num в регистр X1
LDR X3, =loc ; загружает адрес loc в регистр X3
LDR X2, [X3] ; загружает слово по адресу X3 в X2

.data
num .BYTE 0x12
loc .QUAD 0x123456789ABCDEF0

Функции и стек Link to heading

Стек растёт вниз, SP указывает на его вершину и всегда выровнен на 16 байт (это важно). У нас есть инструкции STR и STP для размещения содержимого регистров в стеке, а также LDR и LDP для считывания данных из стека в регистры.

Чтобы скопировать в стек значение из регистра X5, в котором хранится число 1022:

STR X5, [SP, #-16]!

Чтобы загрузить в регистр X4 значение из вершины стека, где лежит число 1022:

LDR X4, [SP], #16