Программирование для ARM64. Часть 1

Заметка основана на конспектах из книги 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 формах:

  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

Эти инструкции складывают второй и третий параметры и сохраняют результат сложения в первый: 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

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