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

в 11:27, , рубрики: AArch64, Cortex A53, ассемблер, Блог компании Embox, портирование, Программирование, системное программирование

Портирование ОС на Aarch64 - 1 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-битные приложения.

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

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

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

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

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

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

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

В отличие от 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, слушайте наши выступления раз два три, приходите к стенду — ответим на ваши вопросы :)

Автор: Денис Дерюгин

Источник


* - обязательные к заполнению поля