автор: Артур Кемурджиан

Записки эмулятора

Часть 1 - Предыстория

Одним замечательным днем мне удалось получить на работе крайне интересное задание - добавить в наш эмулятор для одной из архитектур поддержку ОС GNU/Linux. В качестве архитектуры было решено взять ARM, в качестве ядра - arm1176 (почему бы и нет?), а в качестве ядра ОС - Linux 2.6.31.12 (серьезно, почему бы и нет?). Код эмулятора для ARM на момент начала работы был вполне работоспособен и проверен временем. В итоге мне пришлось восполнять многие недостающие компоненты, вроде MMU, исправлять баги категории “как оно вообще работало до сих пор?” и решать загадки тулчейна, связанные с недостающими символами во время линковки.

Данная статья будет первой в цикле и посвящена она, главным образом, процессу портирования системного софта, такого как U-boot и Linux, на новую платформу (не архитектуру!), и тем ошибкам, которые мне предстояло решить. Также в ней много справочной информации, призванной еще больше погрузить читателя в концепцию работы этого софта. Приятного чтения!

Часть 2 - Немного болтовни

В качестве дистрибутива было решено взять Buildroot, поскольку он идеально подходит для решения поставленной задачи:

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

Признаюсь, сперва я рассчитывал, что удастся выбрать некоторую конфигурацию оборудования “по-умолчанию”, которая бы отвечала всем необходимым требованиям. Но позже выяснилось, что это попросту невозможно, главным образом из-за того, что в реальной жизни “абстрактных” устройств, очевидно, не существует. В реальной ситуации выбор устройств, микросхем памяти, их расположения на адресной шине и т.д. всегда будет нестандартным. Некоторую часть необходимо задать в виде кода на Си. Другая часть задается через конфигурационные параметры. Третья - иными способами, как, например, деревья устройств через отдельный формат файлов или MTD-партиции через аргументы ядра.

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

Часть 3 - Дилемма компилятора

До этого проекта я был уверен в том, что чем софт новее, тем лучше: меньше багов, больше опций и т.д. Как оказалось, это работает, только когда нет зависимости от другого софта. С появлением такой зависимости возникает и необходимость в совместимости с конкретными версиями софта. А ввиду того, что у большинства разработчиков подобное мнение о новизне продукта, судя по всему, распространено повсеместно, получается, что чем версия одной программы новее, с тем меньшим количеством версий других программ она совместима. Думаю, многие уже догадались, к чему я веду. Новая версия Buildroot попросту не поддерживала сборку ядер 2.6.x. А если точнее, она не поддерживала необходимый мне кросс-компилятор GCC 4, который является последним поддерживаемым компилятором для ядер Linux 2.6.x.

В чем же выражается эта самая неспособность собираться на новых GCC? Чтобы понять это, необходимо либо попробовать скомпилировать его тем, что есть, либо прочитать эту статью, где автор скажет заглянуть в директорию include/linux и посмотреть, какие файлы вида compiler-gcc*.h там находятся. Для 2.6.х это число не превышает четырех, что не позволит определить, какие макросы поддерживает компилятор, и каким образом они задаются, что критично для ядра операционной системы. Таким образом, если правильно добавить сюда необходимую, верно написанную версию comiler-gcc, то в теории это может заставить ядро собираться и новыми версиями компилятора. И более того, так даже иногда делают, в частности, я видел, как в ядро 3.4.0 добавляли поддержку GCC 6. Но стоит заметить, что содержимое этих файлов также меняется от версии к версии, отчего просто привить уже существующий файл от новых ядер к 2.6.x без изменений попросту не получится.

Еще стоит добавить, что с течением времени помимо GCC стали добавляться прочие компиляторы, как, например, clang, а версионные файлы GCC все же слились в один.

Почему бы не использовать готовый компилятор? Тут все не так просто. Если вам приходилось когда-нибудь заниматься кросс-компиляцией, то вы, вероятно, знаете, что у всех утилит обычно есть длинный префикс, вроде arm-none-eabi-. Все эти слова дают дополнительную информацию о том, для чего компилятор годится. Флаги none и eabi означают, что сборка осуществляется под встраиваемые системы (eabi) без операционной системы, а потому без стандартной библиотеки Си (none). Разумеется, такой компилятор не подойдет для сборки ядра и дистрибутива, вот только все предварительно собранные компиляторы являются именно none-eabi. К тому же могут возникнуть проблемы, даже если минорные версии заголовков ядра (linux-headers), с которыми тулчейн собирался, будут отличаться у него и у самого ядра. Поэтому самым логичным решением будет сборка тулчейна с нуля, благо Buildroot позволяет это сделать относительно просто.

Таким образом, я просто решил скачать такую версию Buildroot, которая еще поддерживала GCC 4. Ей оказалась 2019.02. Далее все шло на удивление хорошо… Пока очередь не дошла до сборки загрузчика. Когда я предпринял попытку собрать U-boot, то с удивлением обнаружил ситуацию, аналогичную предыдущей:

*** Your GCC is older than 6.0 and is not supported
arch/arm/config.mk:66: recipe for target 'checkgcc6' failed

Выходит, невозможно собрать последний U-boot, используя GCC 4. Что ж, методом градиентного спуска я нашел подходящую версию U-boot - 2017.11. После всех настроек компилятор дошел до конца, и можно было переходить от работы в терминале к работе в текстовом редакторе.

Часть 4 - Город, где есть верфь

Выставив первичные настройки конфигурации в меню Buildroot, такие, как архитектура ARM, конкретный CPU и т.д., я выяснил, что для U-boot выбор конфигурации является обязательным. Иными словами, там отсутствует пункт вроде use default defconfig, который встречается в меню настройки ядра. Кстати о ядре: если выбрать дефолтную конфигурацию для Linux, то почему-то это будет ARM Versatile, то есть совсем не бедная на железо платформа. В общем, где-то здесь мне и стало понятно, что придется серьезно вникать в архитектуру загрузчика и ядра. Нужно было написать платформенно-зависимые компоненты.

Как же их написать? Дело в том, что все процессы в Buildroot одинаково для всех пакетов разделены на три стадии - загрузку, патчинг, сборку и установку. В этом конвейере есть только один способ вставить свои пару слов - на стадии патчинга. Для тех, кто не знаком с этим процессом, идея здесь сводится к тому, что необходимые изменения представляются в виде .patch файла, который применяется к ванильной версии изначального продукта. Главное преимущество такого подхода - отсутствие необходимости в миллионе репозиториев, каждый из которых отличается от оригинального 5-10 файлами, что, в свою очередь, экономит дисковое пространство, трафик и т.д. Вместо этого разработчики просто выкладывают сами патчи. Тем не менее, такой подход удобен для продакшн-кода, но совершенно неудобен для разработки.

В поисках решения я сначала придумал, как в Buildroot добавить поддержку локального расположения исходников вместо скачивания их из сети, но потом я понял, что и это необязательно, ведь все, что нужно - это работать в директории output/build/имя_пакета: там и происходит сборка соответствующего софта. Потому нужно просто открыть там Sublime-text, VS Code или любой другой текстовый редактор (желательно с поддержкой дерева проекта и поиска по файлам). Самое главное при такой работе - ни при каких обстоятельствах не вызывать make имя_пакета-dirclean без предварительного бэкапа сделанных изменений.

С этого момента информацию мне пришлось выискивать буквально по крупицам, а в конечном итоге и вовсе опираться на уже существующие реализации платформ за неимением других вариантов. Что же нужно, чтобы разработать свою платформу для U-boot?

По очевидным причинам структура каталогов U-boot старается мимикрировать под Linux. Получается у него, честно говоря, не очень, но от версии к версии различий становится все меньше. Рассмотрим структуру каталогов, с которыми мы будем работать:

  • arch - архитектурно-зависимые компоненты,
    • arm, mips, … - архитектуры процессоров,
      • cpu - определения для конкретных процессоров,
      • dts - Devicetree Source, об этом речь пойдет в другой раз,
      • mach-* - сокращение от machine, далее будет использоваться термин “платформа”,
  • board - код конкретных реализаций, то есть плат,
  • configs - конфигурационные файлы,
  • drivers - драйверы устройств,
  • include - подключаемые заголовочные файлы,
    • configs - конфигурационные заголовочные файлы.

Помимо данных директорий есть множество других, но они нас в рамках данной статьи не интересуют.

Для размещения основных исходных файлов используются директории mach-* и board по принципу “от общего к частному”. Иными словами, отдельная mach-директория существует, главным образом, тогда, когда на базе одной платформы существует несколько различающихся между собой плат (или эмуляторов, как в случае с QEMU). В таких случаях весь общий для них код помещается именно в mach-директорию, а все различающееся отправляется в boards. Если же исходники пишутся под конкретную плату, то будет достаточно их разместить в boards.

Способ задания структуры подкаталогов внутри boards необычен. Для этого используются два конфигурационных параметра:

  • SYS_VENDOR - вендор, то есть поставщик платы,
  • SYS_BOARD - название платы.

Исходя из того, задан ли каждый из параметров или нет, формируется соответствующая иерархия каталогов внутри boards:

  • SYS_BOARD и SYS_VENDOR - board/SYS_VENDOR/SYS_BOARD/
  • Только SYS_BOARD - board/SYS_BOARD/
  • Только SYS_VENDOR - board/SYS_VENDOR/common/
  • Оба не объявлены - файлы внутри board не используются

На самом деле данный путь должен вести к конкретно одному файлу - Makefile, который и используется для сборки исходников, находящихся в одной директории (и поддиректориях) с ним. О том, как конкретно выглядит процесс сборки от и до, будет рассказано в другой раз.

Как можно заметить, для задания конфигурации устройства есть два типа файлов - непосредственно конфигурационные файлы, так называемые defconfig-файлы, и заголовочные .h-файлы. Ситуация складывается таким образом, что некоторые параметры не разрешено объявлять в defconfig-файлах (разрешенные параметры перечислены в scripts/config_whitelist.txt), другие - только через Kconfig. На заголовочные файлы ограничения вроде бы не накладываются, но в этом я не уверен. Иными словами, обязательно перепроверяйте в первую очередь, не затерся ли какой-либо конфигурационный параметр. В моем случае такая проблема повторилась раз десять, так что настоятельно советую обратить внимание на этот момент.

Продолжение следует

В следующий раз мы продолжим разбираться с U-boot. Надеюсь, было не очень скучно, а, может, вы даже узнали что-нибудь новое. Если так, то я очень рад и продолжу стараться. Впереди еще очень много работы.