Записки эмулятора. Часть 1
автор: Артур Кемурджиан
Записки эмулятора
Часть 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
. Надеюсь, было не очень скучно, а, может, вы даже узнали что-нибудь новое. Если так, то я очень рад и продолжу стараться. Впереди еще очень много работы.