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

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


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



    P.S.


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

    • +34
    • 4,3k
    • 2
    Embox
    135,95
    Открытая и свободная ОС для встроенных систем
    Поделиться публикацией

    Похожие публикации

    Комментарии 2

      0
      Embox запускается после ATF?
        0
        Насколько я понимаю, на железке u-boot запускается уже после Arm Trusted Firmware, так что и Embox — тоже.

        В QEMU, как я понял, можно подсунуть ATF, но мы этого не делаем :)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое