Портирование ОС на Aarch64

91sk1l1eqxkwlaa5ndnvwv5rcwk.png 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-битные приложения.


pauwhdk-xp3y_ixyfuu2yawmeno.png

Переходы между 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 бит:


lahgy5x8kbmbnrh1xyuzirvoujo.png

Стало 32 регистра по 128 бит:


xylsdp-1iwyse4ezlhp9prfsy14.png

Подробнее про 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.


gr1j-sdvpb7cc4y0kkmkbkqwbu8.jpeg

В отличие от 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, можно из нашего репозитория, если есть какие-то вопросы — пишите в комментах, или в рассылку, или в чат в Телеграме (есть ещё канал).


Полезные ссылки


P.S.

24–25 августа мы будем выступать на TechTrain, слушайте наши выступления раз два три, приходите к стенду — ответим на ваши вопросы :)

© Habrahabr.ru