Aarch64 — это 64-битная архитектура от ARM (иногда её называют arm64). В этой статье я расскажу, чем она отличается от "обычных" (32-битных) ARM и насколько сложно портировать на него свою систему.
Эта статья — не детальный гайд, скорее обзор тех модулей системы, которые придётся переделать, и насколько сильно архитектура в целом отличается от обычных 32-битных ARM-ов; всё это по моему личному опыту портирования Embox на эту архитектуру. Для непосредственного портирования конкретной системы так или иначе придётся разбираться с документацией, в конце статьи я оставил ссылки на некоторые документы, которые могут оказаться полезны.
На самом деле, различий больше, чем сходств, и Aarch64 — это скорее новая архитектура, чем 64-битное расширение привычных ARM. Предшественником Aarch64 во многом является Aarch32 (это расширение обычного 32-битного ARM), но так как у меня не было опыта работы с ним, писать о нём я и не буду :)
Далее в статье, если я пишу о "старом" или "прежнем" ARM, я имею ввиду 32-битный ARM (с набором команд ARM).
Кратко пройдусь по списку изменений по сравнению с 32-битным ARM, а затем разберу их поподробнее.
- Регистры общего назначения стали в 2 раза шире (теперь они по 64 бита), и количество их удвоилось (т.е. теперь их не 16, а 32).
- Отказ от концепции сопроцессорных регистров, теперь к ним можно обращаться просто по имени, например
msr vbar_el1, x0
(против прежнегоmcr p15, 0, %0, c1, c1, 2
) - Новая модель MMU (со старой никак не связана, придётся писать заново).
- Раньше было два уровня привилегий: пользовательский (соответствует режиму процессора USR) и системный (соответствует режимам SYS, IRQ, FIQ, ABT, ...), теперь всё одновременно проще и сложнее — режима теперь 4.
- AdvSIMD пришёл на смену NEON, операции с плавающей точкой делаются через него же.
Теперь подробнее по пунктам.
Регистры и набор команд
Регистры общего назначения — r0-r30, при этом обращаться можно к ним как к 64-битным (x0-x30) или как к 32-битным (w0-w30, доступ к младшим 32 битам).
Набор инструкций для Aarch64 называется A64. Ознакомиться с описанием инструкций можно тут. Базовые арифметические и некоторые другие команды на языке ассемблера остались прежними:
mov w0, w1 /* Записать значение регистра w1 в w0 */
add x0, x1, 13 /* Записать в x0 сумму x1 и числа 13 */
b label /* "Прыгнуть" на метку "label"
bl label /* "Прыгнуть" на метку "label", запомнив адрес возврата в x30 */
ldr x3, [x1, 0] /* Записать в x3 значение, на которое указывает x1 */
str x3, [x0, 0] /* Записать значение x3 по адресу, который лежит в x0 */
Теперь немного о различиях:
- Появился специальный "zero"-регистр
rzr/xzr/wzr
, который равен нулю при чтении (можно применять запись в регистр, но результат вычисления не будет никуда записан).
subs xzr, x1, x2 /* Вычесть x1 и x2 и обновить флаги NZCV, сам результат вычитания никуда не записывается */
- Нельзя складывать в стэк сразу много регистров (
stmfd sp!, {r0-r3}
), придётся делать это парами:
stp x0, x1, [sp, 16]!
stp x2, x3, [sp, 16]!
-
Регистр PC (Program counter, указатель на текущую выполняемую инструкцию) теперь не регистр общего назначения (раньше это был R15), следовательно, к нему нельзя обращаться обычными командами (
mov
,ldr
), только черезret
,bl
и так далее. -
Состояние программы теперь отображает не CPSR (этого регистра попросту нет), а регистры DAIF (содержит маску IRQ, FIQ и т.д., AIF — те самые биты A, I, F из CPSR), NZCV (биты negative, zero, carry, oVerflow — внезапно, те самые NZCV из CPSR) и System Control Register (SCTLR, для включения кэширования, MMU, endianness и так далее).
Вроде бы, этих команд достаточно, чтобы написать простенький загрузчик, который сможет передать управление в платформо-независимый код :)
Режимы исполнения и переключение между ними
Про режимы исполнения хорошо написано в Fundamentals of ARMv8-A, я здесь кратко перескажу суть этого документа.
В Aarch64 есть 4 уровня привилегий (Execution level, дальше сокращённо EL).
- EL3 — Secure Monitor (предполагается, что на этом уровне исполняется прошивка)
- EL2 — Гипервизор
- EL1 — ОС
- EL0 — Приложения
На 64-битной ОС можно выполнять и 32-битные, и 64-битные приложения; на 32-битной ОС можно выполнять только 32-битные приложения.
Переходы между EL совершаются либо при помощи исключений (системные вызовы, прерывания, ошибка доступа к памяти), либо при помощи команды возврата из исключения (eret
).
Каждый EL имеет свои регистры SPSR, ELR, SP (т.е. это "banked registers").
Многие системные регистры также разделены по EL — например, регистр контекста MMU ttbr0
— есть ttbr0_el2
, ttbr0_el1
, и на соответствующем EL нужно осуществлять доступ к своему регистру. Это же относится к регистрам состояния программы — DAIF, NZCV, SCTLR, SPSR, ELR...
MMU
Armv8-A поддерживает MMU ARMv8.2 LPA, подробнее про это можно почитать в главе D5 ARM Architecture Reference Manual для Armv8, Armv8-A.
Если говорить коротко, то этот MMU поддерживает страницы по 4KiB (4 уровня таблиц виртуальной памяти), 16KiB (4 уровня) и 64KiB (3 уровня). На любом из промежуточных уровней можно задать блок памяти, таким образом указывая не на следующий уровень таблицы, а на целый кусок памяти такого размера, какой должна "покрывать" таблица следующего уровня. У меня есть давнишняя статья про виртуальную память, там можно почитать про таблицы, уровни трансляции и вот это всё.
Из небольших изменений — от доменов (domain) отказались, зато добавили флажки вроде dirty bit.
В целом, кроме "блоков" вместо промежуточных табиц трансляции, особых концептуальных изменений не замечено, MMU как MMU.
Advanced SIMD
Есть существенные AdvSIMD отличия у старого NEON, как при работе с плавающей точкой, так и с векторными операциями (SIMD). Например, если раньше D0 состоял из S0 и S1, а Q0 — из D0 и D1, то теперь это не так: Q0 соответствует D0 и S0, для Q1 — D1 и S0 и так далее. При этом поддержка VFP/SIMD обязательна, по соглашению о вызовах теперь нет никакой программной передачи параметров (то, что раньше называлось "soft float ABI", в GCC — флаг -mfloat-abi=softfp
), так что придётся реализовывать аппаратную поддержку плавающей точки.
Было 16 регистров по 128 бит:
Стало 32 регистра по 128 бит:
Подробнее про NEON можно почитать в этой статье, перечень доступных команд для Aarch64 можно найти тут.
Базовые операции с регистрами с плавающей точкой:
fadd s0, s1, s2 /* s0 = s1 + s2 */
fmul d0, d1, d2 /* d0 = d1 * d2 */
Базовые операции SIMD:
/* Для примера, было: NEON, постфикс у команды */
/* q0 = q1 + q2, каждый регистр -- вектор из 4 чисел с плавающей точкой */
vadd.s32 q0, q1, q2
/* Стало: AdvSIMD, постфиксы у регистров */
/* v0 = v1 + v2, каждый регистр -- вектор из 4 чисел с плавающей точкой */
add v0.4s, v1.4s, v2.4s
/* Сложить вектор v1 (в нём 2 64-битных числа) и записать в d1 */
addv d1, v1.ds
/* Записать в каждый из 4 элементов вектора 0 */
movi v1.4s, 0x0
Платформы
QEMU
В QEMU есть поддержка Aarch64. Одна из платформ — virt
, для того, чтобы она запускалась в 64-битном режиме, нужно дополнительно передать флаг -cpu cortex-a53
, примерно так:
qemu-system-aarch64 -M virt -cpu cortex-a53 -kernel ./embox -m 1024 -nographic # ./embox -- ELF-образ ядра
Что приятно, для этой платформы используется куча периферии, драйвера для которой уже были в Embox — например PL011 для консоли, ARM Generic Interrupt Controller и т. д. Само собой, у этих устройств другие базовые адреса регистров и другие номера прерываний, но главное — код драйверов без изменений работает на новой архитектуре. При старте системы управление находится в EL1.
i.MX8
Из-за этой железки и было затеяно портирование на Aarch64 — i.MX8MQ Nitrogen8M.
В отличие от QEMU, u-boot передаёт управление образу в EL2, и, более того, зачем-то включает MMU (вся память мэпируется 1 к 1), что создаёт некоторые дополнительные проблемы при инициализации.
Embox уже поддерживал i.MX6, и, что хорошо, в i.MX8 часть периферии та же самая — например, UART и Ethernet, которые также заработали (пришлось подправить пару мест, где была жёсткая привязка к 32-битным адресам). С другой стороны, контроллер прерываний там другой — ARM GICv3, который достаточно сильно отличается от первой версии.
Заключение
На данный момент поддержка Aarch64 в Embox не полная, но минимальный функционал уже есть — прерывания, MMU, ввод-вывод через UART. Многое ещё предстоит доработать, но первые шаги было сделать проще, чем казалось с самого начала. Документации и статей заметно меньше, чем по ARM, но информации больше, чем достаточно, чтобы со всем разобраться.
В целом, если у вас есть опыт работы с ARM, портирование на Aarch64 — посильная задача. Хотя, как обычно, можно споткнуться на какой-нибудь мелочи :)
Скачать проект, чтобы потыркать его в QEMU, можно из нашего репозитория, если есть какие-то вопросы — пишите в комментах, или в рассылку, или в чат в Телеграме (есть ещё канал).
Полезные ссылки
- Инструкции A64
- Fundamentals of ARMv8-A
- ARM Architecture Reference Manual для Armv8, Armv8-A
- Aarch64 ABI (соглашение о вызовах)
- Migrating code from ARM to ARM64 — небольшая презентация с рекомендациями по написанию портируемого кода
P.S.
24-25 августа мы будем выступать на TechTrain, слушайте наши выступления раз два три, приходите к стенду — ответим на ваши вопросы :)
Автор: Денис Дерюгин