Skip to content

Files

Latest commit

db723de · Oct 1, 2021

History

History
249 lines (175 loc) · 21.9 KB

README.md

File metadata and controls

249 lines (175 loc) · 21.9 KB

Архитектура AArch64 (armv8)

Кросс-компиляция и запуск программ на x86

Процесс сборки программ, предназначенных для другой процессорной архитектуры или операционной системы называется кросс-компиляцией.

Для этого необходимо специальная версия компилятора gcc, предназначенного для другой платформы. Во многих дистрибутивах существуют отдельные пакеты компилятора для других платформ, включая различные варианты ARM. Готовую сборку компилятора для armv8 можно взять из проекта Linaro: http://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/aarch64-linux-gnu/.

Полные названия команд gcc имеют вид триплетов:

ARCH-OS[-VENDOR]-gcc
ARCH-OS[-VENDOR]-g++
ARCH-OS[-VENDOR]-gdb

и т. д.

где ARCH - это имя архитектуры: i686, x86_64, arm, aarch64, ppc и т.д.; OS - целевая операционная система, например linux, win32 или darwin; а необязательный фрагмент триплета VENDOR - соглашения по бинарному интерфейсу, если их для платформы существует несколько, например для ARM это может быть gnu (стандартное соглашение Linux) или none (без операционной системы, просто голое железо).

Выполнение программ, предназначенных для других архитектур, возможно только интерпретацией инородного набора команд. Для этого предназначены специальные программы - эмуляторы.

Архитектуры arm/aarch64, как и многие другие архитектуры, поддерживает эмулятор QEMU.

Эмулировать можно как компьютерную систему целиком, по аналогии с VirtualBox, так и только набор команд процессора, используя при этом окружение хост-системы Linux.

Команды QEMU имеют вид:

qemu-ARCH
qemu-system-ARCH

где ARCH - это имя эмулируемой архитектуры. Команды, в названии которых присутствует system, запускают эмуляцию компьютерной системы, и для их использования необходимо установить операционную систему.

Команды без system требуют в качестве обязательного аргумента имя выполняемого файла для ОС Linux, и эмулируют только набор команд процессора в пользовательском режиме, выполняя "инородный" исполняемых файл так, как будто это обычная программа.

Поскольку большинство программ, скомпилированных для Linux и другой процессорной архитектуры,, подразумевают использование стандартной библиотеки Си, необходимо использовать именно версию glibc для для нужной архитектуры. Минимальное окружение с необходимыми библиотеками можно взять из проекта Linaro (см. ссылку выше), и скормить его qemu с помощью опции -L ПУТЬ_К_SYSROOT.

Пример компиляции и запуска:

# в предположении, что компилятор распакован в /opt/aarch64-gcc,
# а sysroot - в /opt/aarch64-sysroot

# Компилируем
> /opt/aarch64-gcc/bin/aarch64-linux-gnu-gcc -o program hello.c

# На выходе получаем исполняемый файл, который не запустится
> ./program
bash: ./program: cannot execute binary file: Exec format error

# Но мы можем запустить его с помощью qemu-aarch64
> qemu-aarch64 -L /opt/aarch64-sysroot ./program
Hello, World!

У команд qemu-* предусмотрена возможность запуска в режиме отладки, - в этом случае возможно взаимодействие с qemu точно так же, как и с gdbserver:

# Компилируем с отладочной информацией
> /opt/aarch64-gcc/bin/aarch64-linux-gnu-gcc -g -o program hello.c
 
# Запускаем под qemu в режиме отладки
# Обратите внимание на опцию -g ПОРТ
> qemu-aarch64 -L /opt/aarch64-sysroot -g 1234 ./program
 
# В другом терминале можно подключиться к программе из gdb
> /opt/aarch64-gcc/bin/aarch64-linux-gnu-gdb ./program
(gdb) target remote localhost 1234

Программирование на языке ассемблера armv8

Программы на языка ассемблера для компилятора GNU сохраняются в файле, имя которого оканчивается на .s или .S. Во втором случае (с заглавной буквой) подразумевается, что текст программы может быть обработан препроцессором, то есть можно использовать конструкции #define или #include.

Для компиляции используется одна из команд: aarch64-linux-gnu-as или aarch64-linux-gnu-gcc. В первом случае текст только компилируется в объектный файл, во втором - в выполняемую программу, скомпонованную со стандартной библиотекой Си, из которой можно использовать функции ввода-вывода.

Полная документация по архитектуре команд armv8 приведена в официальном источнике. Описание основных инструкций приведено в разделе C3: A64 Instruction Set Overview.

Общий синтаксис программ на ассемблере

Разберем синтаксис ассемблера armv8 на примере реализации функции f, которая доступна извне, и вычисляет выражение A*x*x + B*x + C, где A, B, C и x являются агументами функции f(A,B,C,x).

// Это комментарий, как в Си/С++
   .text      // начало секции .text с кодом программы
   .global f  // указание о том, что метка f будет доступна извне
              // (аналог extern в языке Си)
              
f:						// метка - имя функции или строки для перехода
    
    // последовательность команд для вычисления
    mul       x0, x0, x3
    mul       x0, x0, x3
    mul       x1, x1, x3
    add       x0, x0, x1
    add       x0, x0, x2
    // возвращаемся из функции f
    ret

Целочисленные регистры

Процессор может выполнять операции только над регистрами - ячейками памяти в ядре процессора. У armv8 есть 31 регистр общего назначения, доступных программно: x0, x1, ... , x30. Размер каждого регистра - 64 бит. Обращение по именам w0, w1, ..., w30 к этим регистрам означает использование младших 32-битных частей соответствующих регистров с префиксом x.

Регистры x30 и sp следует изменять с осторожностью, поскольку у них есть специальное назначения для всех платформ armv8, независимо от используемой операционной системы: x30 хранит адрес возврата из функции, sp - указатель на вершину стека. Кроме того, в системе Linux предусмотрены следующие соглашения об использовании регистров:

Регистры Назначение
x0 ... x7 аргументы функции и возвращаемое значение (x0)
x8 ... x18 временные регистры, для которые не гарантируется сохранение результата, если вызывать какую-либо функцию
x19 ... x28 регистры, для которых гарантируется, что вызываемая функция их не будет портить
x29 указатель на границу фрейма функции, обычно используется отладчиком
x30 адрес возврата из функции
sp указатель на вершину стека

Помимо регистров общего назначения предусмотрены еще два специальных регистра: xzr - всегда хранит значение 0, и pc - Program Counter, который хранит адрес следующей инструкции, которая должна быть выполнена.

Флаги

Выполнение команд может приводить к появлению некоторой дополнительной информации, которая хранится в регистре флагов. Флаги относятся к последней выполненной команде. Основные флаги, это:

  • C: Carry - возникло беззнаковое переполнение
  • V: oVerflow - возникло знаковое переполнение
  • N: Negative - отрицательный результат
  • Z: Zero - обнуление результата.

Команды процессора

Команды процессора выполняются определенные действия над регистрами. Некоторые команды, которые называются командами переходов, позволяют менять значение регистра pc.

Для арифметических команд необходимо указывать в качестве первого аргумента регистр, в который нужно записать результат, а остальные аргументы - это аргументы операции. Если используются регистры с префиксом w, то подразумевается 32-битная арифметика, если регистры с префиксом x - 64-битная арифметика. Для того, чтобы арифметические команды изменяли флаги, необходимо указать им суффикс s, - в противном случае значения флагов не изменятся.

Арифметические и поразрядные операции

Базовые арифметические команды:

  • add X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 + X𝕓
  • sum X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 - X𝕓
  • mul X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 ∙ X𝕓
  • madd X𝕕, X𝕒, X𝕓, X𝕔 // X𝕕 ← X𝕒 ∙ X𝕓 + X𝕔
  • umaddl X𝕕, W𝕒, W𝕓, X𝕔 // X𝕕 ← W𝕒 ∙ W𝕓 + X𝕔, где W𝕒 и W𝕓 - беззнаковые значения
  • smaddl X𝕕, W𝕒, W𝕓, X𝕔 // X𝕕 ← W𝕒 ∙ W𝕓 + X𝕔, где W𝕒 и W𝕓 - знаковые значения
  • udiv X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 / X𝕓, где X𝕒 и X𝕓 - беззнаковые значения
  • sdiv X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 / X𝕓, где X𝕒 и X𝕓 - знаковые значения

Побитовые операции:

  • and X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 & X𝕓
  • orr X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 | X𝕓
  • eor X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 ^ X𝕓
  • asr X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 >> X𝕓 (арифметический сдвиг, деление на степени 2)
  • lsr X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 >> X𝕓 (логический сдвиг)
  • lsl X𝕕, X𝕒, X𝕓 // X𝕕 ← X𝕒 << X𝕓 (логический сдвиг)

Копирование и приведения типов:

  • mov X𝕕, X𝕒 // X𝕕 ← X𝕒
  • uxtb X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это uint8_t
  • uxth X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это uint16_t
  • uxtw X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это uint32_t
  • sxtb X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это int8_t
  • sxth X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это int16_t
  • sxtw X𝕕, X𝕒 // X𝕕 ← X𝕒, где X𝕒 - это int32_t

Команды управления ходом программы

Внутри программы отдельные инструкции могут иметь метки, - именованные относительные адреса в программе, которые ассемблер при сборке программы заменяет на численные смещения относительно текущей инструкции. Управление ходом программы осуществляется инструкциями перехода на метки, которые могут быть как безусловными, так и выполняться при выполнении определенного условия.

Безусловный переход на другую инструкцию

Команды безусловного перехода:

  • b LABEL - безусловный переход на метку LABEL, которая закодирована в команду как смещение относительно текущей выполняемой инструкции, эта инструкция обычно используется для переходов внутри функции;
  • bl LABEL - безусловный переход на метку LABEL, как в случае инструкции b, но при этом в регистр x30 сохраняется адрес следующей за bl инструкции, чтобы к нему можно было вернуться инструкцией ret; обычно эта инструкция используется для вызова функций, метки которых синтаксически ничем не отличаются от меток внутри функций.

Метки должны быть отдалены от текущей инструкции не более чем на ±32Mb. Этого обычно более чем достаточно для вызова функции из того же самого исполняемого файла, где используется вызов функции, но возможны и вызовы функций, которые в памяти располагаются значительно дальше, например функции библиотек. Для таких случаев предусмотрена возможность перехода на инструкцию по 64-битному адресу:

  • br X - безусловный переход на адрес, который хранится в регистре X;
  • brl X - безусловный переход на адрес, который хранится в регистре X, с сохранением адреса возврата в регистре x30 , который может быть использован инструкцией ret.

Условные переходы

Арифметические операции, у которых в названии указан суффикс s, а также команда сравнения регистров cmp X𝕒, X𝕓 изменяют набор флагов процессора. Эти флаги могут использоваться в качестиве условий для операций условного перехода, которые кодируются в виде суффиксов инструкции b:

EQ        equal  (Z)
NE        not equal  (!Z)
CS or HS  carry set / unsigned higher or same  (C)
CC or LO  carry clear / unsigned lower  (!C)
MI        minus / negative  (N)
PL        plus / positive or zero  (!N)
VS        overflow set  (V)
VC        overflow clear  (!V)
HI        unsigned higher  (C && !Z)
LS        unsigned lower or same  (!C || Z)
GE        signed greater than or equal  (N == V)
LT        signed less than  (N != V)
GT        signed greater than  (!Z && (N == V))
LE        signed less than or equal  (Z || (N != V))

Пример. Реализация цикла, который в Си нотации эквивалентен for (x0=0; x0<x1; x0++) {}:

  mov    x0, 0      // x0 = 0
  mov    x1, 10     // x1 = какое-то значение, например 10
LoopBegin:          // метка начала цикла
  cmp    x0, x1     // сравниваем  
  bge    LoopEnd    // переходим в конец цикла, если x0 >= x1
  // какие-то инструкции внутри цикла
  add    x0, x0, 1  // инкремент переменной цикла
  b      LoopBegin  // переходим к началу цикла, где будет проверка
LoopEnd:
  // цикл закончился

Взаимодействие с памятью

В традиционной для RISC-архитектур модели адресации, процессоры умеют выполнять действия только над регистрами. Любое взаимодействие с памятью осуществляется отдельными командами загрузки и сохранения.

Для базового обращения к памяти используются инструкции:

  • ldr R𝕕, [X𝕒] - прочитать из памяти содержимое по адресу, указанному в регистре X𝕒 и сохранить результат в R𝕕
  • str R𝕒, [X𝕕] - сохранить содержимое регистра R𝕒 в памяти по адресу, указанному в регистре X𝕕

Эти инструкции оперируют 32 или 64-битными данными (размерность определяется названием регистра). Для чтения/записи операндов меньшего размера эти инструкции имеют дополнительные суффиксы:

  • ldrb/strb - для uint8_t
  • ldrsb/strsb - для int8_t
  • ldrh/strh - для uint16_t
  • ldrsh/strsh - для int16_t
  • ldrsw/strsw - для int32_t

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

Для  64-битной архитектуры ARMv8 (но не для 32-битных архитектур ARM) возможно указание смещения относительно базового адреса. Этот синтаксис может быть использован для обращения к полям структур, если известен адрес начала структуры в памяти, либо для обращения к элементам массивов по индексу.

// загрузка значения по адресу из x1 со смещением 8 байт
  ldr  x0, [x1, 8]       // x0 = *(x1 + 8)

// загрузка значения по адресу из x1 с индексом элемента x2,
// в предположении, что размер одного элемента равен 8 байтам
  ldr  x0, [x1, x2, lsl 3]  // x0 = *(x1 + x2 * (1 << 3))