Raspbian: сборка образа. Часть 2

Автор: Евгений Голышев

Статья была опубликована в 236-м выпуске журнала Linux Format. По истечении полугода с момента публикации права на статью переходят ее автору, поэтому материал публикуется в нашем блоге.

Тема сборки минимально функциональной версии Raspbian продолжается. Эту часть я хочу начать с обсуждения несправедливо забытого термина JeOS (читается как juice). Этот термин, который, кстати, расшифровывается как «just enough operating system», был очень популярен лет десять назад, когда производители коммерческих дистрибутивов GNU/Linux были увлечены разработкой инструментов для индивидуальной настройки [customization] операционных систем. JeOS – это подход, согласно которому для решения какой-то конкретной задачи достаточно взять минимально возможную версию операционной системы и установить поверх нее зависимости, необходимые для запуска одного конкретного приложения, решающего эту задачу. Таким образом, решение получается самодостаточным и максимально эффективным с точки зрения производительности. В зависимости от инструмента, помогающего кастомизировать операционные системы, конечный результат был пригоден для запуска как на реальной, так и в виртуальной машине. Самым ярким из этих инструментов был SUSE Studio, который неоднократно освещался на страницах Linux Format (к примеру, в LXF125 и LXF138). Он одним из первых начал широко использовать термин «виртуальное устройство [virtual appliance]», который означает операционную систему узкого назначения вкупе с целевым приложением, пригодную для запуска в виртуальной машине. Но времена меняются, и всё чаще для решении какой-то конкретной задачи выделяется целый Raspberry Pi или любой другой одноплатный компьютер. Терминология здесь еще недостаточно проработана, и необходимо это исправить. Назовем, к примеру, подготовленный в первой части этого руководства образ минимально функциональной версии Raspbian термином JeOSI (предлагаю произносить его как juicy). По аналогии с JeOS новый термин расшифровывается как «just enough operating system image». С переводом будет немного сложнее. Дело в том, что словосочетание «just enough» в данном случае является наречием, а не существительным, что делает эти термины больше похожими на слоганы, чем на что-то другое. После нескольких часов размышлений я не придумал ничего лучше, чем «достаточно просто операционной системы» [можно также перевести как «ОС в обрез», – прим. ред.] для JeOS и «достаточно просто образа операционной системы» для JeOSI (как рекламный слоган популярного в 1990-х растворимого напитка Invite – «просто добавь воды»). На самом деле куда важнее, что JeOSI может сбить с толку тем, что перекликается с сетевой моделью OSI или некоммерче­ской организацией Open Source Initiative, название которой также часто сокращается до OSI; но более удачного термина я пока предложить не могу.

Итак, пора определиться с целями второй части учебника.

Показанный в первой части подход позволяет собирать минимально функциональные образы для Raspberry Pi на основе Debian-подобных дистрибутивов. Прежде чем написать статью, я успел опробовать этот подход на Raspbian, Devuan и Ubuntu. Однако именно Raspbian использовался в качестве примера для демонстрации сборки образа на протяжении всей первой части руководства, т.к. он является «родным» для RPi. Таким образом, выбор в пользу этого дистрибутива был исключительно символическим, и нет никаких препятствий в использовании любого другого основанного на Debian дистрибутива. Экспериментируйте.

Особенность второй части учебника заключается в том, что она в основном состоит из набора не зависящих друг от друга рецептов, которые (каждый по-своему) предлагают сделать полученный в предыдущей части образ более полезным с практической точки зрения. Поэтому эту часть учебника можно читать как от начала до конца, так и выборочно, переходя только к тем рецептам, которые представляют наибольший интерес, без риска для понимания идущего за ними (или перед ними) материала. Вторая часть учебника предполагает наличие образа, который у вас должен был появиться после работы с первой частью. Создайте где-нибудь две директории – boot и rootfs – и смонтируйте в них загрузочный и корневой раздел соответственно.

$ sudo mkdir /mnt/boot /mnt/rootfs
$ LOOP_DEV=$(sudo losetup --partscan --show --find raspbian-stretch.img)
$ sudo mount ${LOOP_DEV}p1 /mnt/boot
$ sudo mount ${LOOP_DEV}p2 /mnt/rootfs

А теперь – вперед.

Поддержка сети

Чтобы убедиться в нормальном функционировании на RPi собранного в первой части учебника образа, необходимо было подключить к устройству монитор и клавиатуру, что по очевидным причинам является не очень удобным, а в некоторых случая – даже не всегда возможным. Поэтому в качестве первого улучшения я предлагаю добавить поддержку сети для того, чтобы можно было установить в систему SSH-сервер и подключиться к машине удаленно с любого устройства, на котором возможно запустить SSH-клиент.

SUSE Studio был пионером и отличным примером решения в области кастомизации операционных систем.
SUSE Studio был пионером и отличным примером решения в области кастомизации операционных систем.

Raspbian использует системный менеджер systemd, который, помимо всего прочего, пытается решить проблему с именованием сетевых интерфейсов. Таким образом, в рамках systemd 197, который вышел 7 января 2013 г., в udev была добавлена поддержка т. н. «предсказуемых имен сетевых интерфейсов [Predictable Network Interface Names]». И первым делом я предлагаю отключить эту поддержку. Во-первых, по иронии судьбы предсказуемые имена не всегда предсказуемы. Во-вторых, в случае RPi и его единственного проводного сетевого интерфейса нет никакой проблемы с именованием. В-третьих, я хочу, чтобы этот учебник был применим ко всем производным от Debian дистрибутивам, которые могут по тем или иным причинам не использовать systemd.

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

net.ifnames=0 biosdevname=0

Для этого добавьте в самое начало файла cmdline.txt, который находится на загрузочном разделе, эти два параметра, отредактировав файл вручную или воспользовавшись следующей командой:

$ sudo sh -c "echo 'net.ifnames=0 biosdevname=0 $(cat /mnt/cmdline.txt)' > /mnt/boot/cmdline.txt"

Если на данном этапе вы запишете образ на SD-карту, загрузите с нее устройство и выполните

$ ip link show

то увидите в списке всем хорошо знакомый eth0.

Следующим шагом необходимо разобраться с «именем хоста [hostname]». Дело в том, что debootstrap (см. первую часть учебника) назвал chroot-окружение именем системы, в которой оно собиралось, поэтому две ваши машины – RPi и та, на которой происходила сборка окружения – сейчас имеют одинаковые имена. Это необходимо исправить. Для этого придумайте новое имя для RPi и добавьте его в /etc/hostname и /etc/hosts, к примеру, следующим образом:

$ sudo sh -c "echo raspbian-jeosi > /mnt/rootfs/etc/hostname"
$ sudo sed -i '2i 127.0.0.1\traspbian-jeosi' /mnt/rootfs/etc/hosts

В данном случае в качес тве имени использовалось raspbian-jeosi. Назовите машину с учетом ваших личных вкусов и предпочтений, но помните, что согласно RFC 952 имена хостов не должны превышать 24-х символов; в качестве символов, из которых разрешается составлять имена – буквы латинского алфавита в верхнем и нижнем регистре, цифры, знак минуса (-) и точка (.).

В заключение необходимо установить пакеты, которые содержат DHCP-клиент, ряд вспомогательных утилит типа ping, всё необходимое для взаимодействия устройства с другими устройствами в сети (на базе стека протокола TCP/IP, разумеется) и SSH-сервер, ради которого всё это затевалось

$ sudo chroot /mnt/rootfs apt-get update
$ sudo chroot /mnt/rootfs apt-get install netbase net-tools iscdhcp-client inetutils-ping openssh-server

а также отредактировать конфигурационный файл /etc/network/interfaces, добавив в него следующее содержимое:

auto lo
iface lo inet loopback
auto eth0
allow-hotplug eth0
iface eth0 inet dhcp

И хотя после всех этих манипуляций образ наконец имеет всё необходимое, чтобы работать в Сети, остается одна небольшая про­блема с подключением к SSH-серверу. Дело в том, что OpenSSH, который достался дистрибутиву от Debian, сконфигурирован таким образом, чтобы не разрешать вход от имени root с парольной аутентификацией. Решить эту проблему можно одним из двух способов: либо измените в /etc/ssh/sshd_config текущее значение PermitRootLogin на yes, включив тем самым возможность входа как root по паролю (что настоятельно не рекомендуется), либо перейдите к следующему разделу и заведите еще одного пользователя в системе, на которого не будет накладываться это ограничение.

Пользователи

На данный момент в системе есть только один пользователь – root. Этого было достаточно, чтобы, не отвлекаясь на лишние детали, убедиться, что новоиспеченная система находится в рабочем состоянии. Теперь настало время обратить внимание на опущенные ранее детали.

В настоящее время разработчики дистрибутивов делятся на два лагеря, когда речь заходит о пользователе root (напомню, что по отношению к нему часто используется термин «суперполь­зователь»). Более консервативные следуют практике наличия по крайней мере двух учетных записей в системе: root’а для административных задач и непривилегированного пользователя для всех остальных. Этот канонический подход дошел до нас в почти неизменном виде с самых первых версий UNIX; но он не лишен недостатков. На мой взгляд, наиболее ярким из них является необходимость постоянного контроля за тем, чтобы сессия root’а была вовремя закрыта. Таким образом, всегда есть риск забыться и продолжить решение своих повседневных задач от лица суперпользователя, что может привести к серьезным негативным последствиям – ведь безграничные возможности, скрытые в учетной записи root’а, обладают поистине разрушительной силой, которую не каждый способен держать под контролем.

Debian является примером одного из консервативных с этой точки зрения дистрибутивов. Ubuntu, в свою очередь, это один из первых дистрибутивов, который взял на вооружение подход, при котором все административные задачи решаются посредством sudo. Суть этого подхода заключается в том, чтобы

  • не устанавливать пароль для суперпользователя, оставляя его учетную запись заблокированной, или заблокировать учетную запись явно, указав вместо пароля (или перед паролем) восклицательный знак (!);
  • установить программу sudo;
  • добавить первого пользователя (т. е. того, который был создан во время установки системы) в группу sudo, дав ему тем самым неограниченные права при выполнении любых операций в системе.
Получив дополнительные права через sudo, не забывайте, что с великой силой приходит великая ответственность.
Получив дополнительные права через sudo, не забывайте, что с великой силой приходит великая ответственность.

С одной стороны, такая практика противоречит философии программы – дать каждому пользователю как можно меньше привилегий, но так, чтобы их было достаточно для решения поставленных перед этими пользователями задач; а с другой – помогает решить проблему с закрытием сессии root.

В официальном образе Raspbian используется именно этот подход. Какой подход будет использоваться в вашем образе – решать вам, но для начала необходимо завести в системе еще одного пользователя, от имени которого будет осуществляться вход, в том числе и через SSH.

Итак, чтобы создать пользователя с именем pi, выполните

$ sudo chroot /mnt/rootfs useradd -m -s /bin/bash pi

Заметьте, что useradd и другие смежные с ней программы, в том виде, в котором они используются в этом разделе, специфичны для всех систем, в которых используется shadow для управления пользователями и группами. В отличие от coreutils, интерфейс программ (и даже название самих программ) для управления пользователями и группами сильно варьируется от реализации к реализации, поэтому всё описанное здесь может отличаться от того, что принято в других Unix-подобных операционных системах и даже других дистрибутивах GNU/Linux.

Опция -m позволяет указать, что в /home необходимо создать домашнюю директорию пользователя с его именем, а опция -s говорит, что оболочкой по умолчанию должна стать Bash. Можно было бы посредством опции -p указать пароль, но это небезопасно. Во-первых, вышеприведенная команда останется в истории, к которой может получить доступ администратор или злоумышленник, если пользователь, от имени которого она выполнялась, будет скомпрометирован. Во-вторых, процесс создания пользователя будет фигурировать в списке процессов, из которого администратор может получить все переданные useradd параметры. В-третьих, пароль на экране может быть просто подсмотрен из-за плеча. Даже если эта и другие приведенные в данной статье команды выполняются на персональной машине в пустом и запертом помещении, я всё равно предлагаю пойти правильным путем и установить пароль пользователя отдельно. В конце концов, чтобы стать настоящим хакером, нужно быть немного шизофреником.

$ sudo chroot /mnt/rootfs passwd pi

На данном этапе образ имеет всё необходимое, чтобы считаться приверженцем консервативного подхода. Чтобы заставить его следовать подходу, при котором все административные задачи решаются через sudo, сначала установим саму программу, а затем добавим пользователя в соответствующую группу.

$ sudo chroot /mnt/rootfs apt-get update
$ sudo chroot /mnt/rootfs apt-get install sudo
$ sudo chroot /mnt/rootfs usermod -aG sudo pi

Теперь пользователь pi может временно заимствовать права суперпользователя для тех вещей, которые выполняются через sudo.

Финальный штрих – блокировка учетной записи root. Для этого выполните

$ sudo chroot /mnt/rootfs passwd -l root

Данная команда поставит восклицательный знак (!) перед хэшем пароля, и это обозначит, что учетная запись заблокирована.

Загляните в «теневой файл паролей [shadowed password file]» /etc/shadow, чтобы в этом убедиться.

На этом тему можно было бы считать закрытой, но остается еще одна вещь, которая не дает мне покоя. Дело в том, что за полтора десятка лет существования Ubuntu успело подрасти целое поколение линуксоидов, которые убеждены, что sudo – это не более чем просто способ временного получения прав суперпользователя, и если оставить всё как есть, то наш учебник только поддержит это распространенное заблуждение. Я хочу внести свой посильный вклад в то, чтобы исправить сложившуюся ситуацию. Для этого я предлагаю решить одну простую, но очень актуальную для RPi задачу, суть которой заключается в том, чтобы дать пользователю возможность выключать или перезагружать свое устройство через Android-приложение нажатием одной кнопки. Таким образом, потребуется посредством sudo дать пользователю право выполнять команды systemctl poweroff и systemctl reboot. Более того, необходимо не только дать пользователю право выполнения этих команд, но и при этом не спрашивать его пароль. Дело в том, что Android-приложение SSH Button, которое я предлагаю использовать для создания кнопок выключения и перезагрузки устройства, как и любое подобное приложение, очень примитивно. Оно подключается к SSH-серверу, выполняет указанную программу и анализирует код завершения [exit status]. Если в результате своей работы программа переходит в режим, в котором она ожидает каких-то действий от пользователя (к примеру, ввода пароля), то SSH Button считает выполнение программы неудачным. Всё это создает идеальные условия для того, чтобы копнуть чуть глубже возможности, предлагаемые sudo, чем я и предлагаю заняться прямо сейчас.

Правила, согласно которым sudo принимает решение, кто и что в праве делать, находятся в /etc/sudoers. Однако, вместо того чтобы редактировать этот файл напрямую, я предлагаю добавить новое правило в виде отдельного файла через подключаемую директорию /etc/sudoers.d. Создайте файл, к примеру, с именем shutdown со следующим содержимым.

pi raspbian-jeosi =NOPASSWD: /usr/bin/systemctl halt,/usr/bin/systemctl reboot

Все файлы в этой директории должны иметь права 0440 (-r–r—–).

$ sudo chmod 0440 /mnt/rootfs/etc/sudoers.d/shutdown

Теперь установите SSH Button на любое устройство под управлением Android. Затем создайте в нем кнопку и укажите в ее настройках адрес вашего RPi, логин и пароль пользователя pi и одну из разрешенных этому пользователю команд — systemctl poweroff или systemctl reboot.

Удобный способ выполнить любую команду на RPi с мобильного устройства.
Удобный способ выполнить любую команду на RPi с мобильного устройства.

Этот подход особенно удобен, когда вам нужно подключиться к RPi только для того, чтобы корректно завершить все процессы на нем перед его выключением. Читайте следующий раздел, в котором пойдет речь о том, как сделать корневую файловую систему доступной только на чтение и, таким образом, избавиться от необходимости т.н. корректного завершения работы.

Доступный только на чтение корень

Продвинутые владельцы одноплатников, и тем более те, кто создает на их основе продукты, предпочитают (очевидно, если это не противоречит поставленной задаче) делать корневую файловую систему доступной только на чтение. В таком случае можно не переживать за целостность данных из-за внезапного отключения питания.

Доступный только на чтение корень – это умышленное ограничение функциональности системы для повышения ее живучести. Но основная проблема здесь не в том, как этого добиться, а в том, как минимизировать последствия от этого ограничения: ведь будет очень обидно, если демон, предназначенный для решения той самой единственной задачи, потеряет способность вести журнал… Тому, как сделать корень доступным только на чтение и не терять логи в этих условиях, посвящена большая часть этого раздела. (В силу того, что материал руководства выходит далеко за пределы журналирования, разговор на эту тему не будет исчерпывающим, поэтому я настоятельно рекомендую обратиться к статье «Записки демонов» из LXF91 за более детальными подробностями по этому вопросу.)

Традиционно в Unix-подобных операционных системах журналированием занимается демон syslogd. В Debian, к примеру, используется одна из его реализаций под названием rsyslog. Когда сообщение доходит до syslogd, возможен один из пяти вариантов развития событий:

  • сообщение может быть добавлено в файл;
  • сообщение может быть выдано на терминал любого указанного пользователя;
  • сообщение может быть записано в FIFO (именованный канал);
  • сообщение может быть перенаправлено syslogd, находящемуся на другой машине;
  • сообщение может быть проигнорировано.

Самый распространенный первый вариант не подходит, т.к. корневая файловая система доступна только на чтение. Можно, конечно, передавать логи на другую машину, но в некоторых случаях наличие еще одной машины может оказаться избыточным. К счастью, другая реализация syslogd, разработанная в рамках проекта Busybox, поддерживает использование так называемого «кольцевого буфера [circular buffer]» для хранения логов. Таким образом, создается иллюзия того, что логи пишутся в обычном режиме так, как если бы корневая файловая система не была доступна только на чтение.

В Raspbian (и других дистрибутивах, основанных на Debian) реализация syslogd от проекта Busybox находится в пакете busybox-syslogd.

$ sudo chroot /mnt/rootfs apt-get install busybox-syslogd

Если в системе уже был установлен rsyslog, то установка busybox-syslogd приведет к его удалению, т. к. busybox-syslogd возьмет на себя функции rsyslog.

Теперь логи подавляющего большинства демонов в вашей системе будут сохраняться в памяти. busybox-syslogd сконфигурирован в Raspbian таким образом, чтобы использовать для этих целей буфер размером 64 КБ. Однако, если произойдет внезапное отключение питания, то все логи вылетят в трубу. Конечно, самым надежным во всех случаях подходом является хранение логов на отдельной машине, и busybox-syslogd это тоже умеет.

Несмотря на то, что подавляющее большинство демонов используют syslogd для журналирования, есть демоны, которые занимаются этим вопросом самостоятельно. К примеру, Nginx ведет два типа журналов: журнал доступа [access log] и журнал ошибок [error log], и по умолчанию не пользуется услугами syslogd для этих целей. Тем не менее, модуль Nginx ngx_http_log_module, который занимается журналированием, поддерживает использование syslogd. В Debian и его производных Nginx сконфигурирован так, чтобы журнал доступа сохранялся в /var/log/nginx/access. log, а журнал ошибок – в /var/log/nginx/error.log. Очевидно, что в файловой системе, доступной только на чтение, Nginx не сможет писать в access.log и error.log, поэтому необходимо попросить web-сервер регистрировать события через syslogd. Для этого отредактируйте /etc/nginx/nginx.conf так, чтобы директивы access_log и error_log выглядели следующим образом.

access_log syslog:server=unix:/dev/log,nohostname;
error_log syslog:server=unix:/dev/log,nohostname;

И в заключение, необходимо сделать корневой раздел доступным только на чтение. Для этого добавьте параметр ro к командной строке ядра, которая находится в файле cmdline.txt на загрузочном разделе.

Сборка ядра

Сейчас система основана на Raspbian’овском ядре, которое латается и пакетируется самими разработчиками одноплатников, поэтому оно, как ни что другое, отлично поддерживает RPi. Пакет raspberrypi-kernel, установка которого обсуждалась в первой части этого руководства, содержит ядро, предназначенное для решения широкого круга задач, что открывает огромные возможности для оптимизаций. Таким образом, в данном разделе речь пойдет о том, как самостоятельно собрать ядро – для того, чтобы получить возможность подогнать его под решение конкретной задачи.

Теперь осталось определиться, у кого мы возьмем ядро, которое будем здесь собирать: у Линуса, разработчиков RPi или у когото еще. Предлагаю собирать именно «ванильное» ядро, т. к. этот подход является наиболее дистрибутивонезависимым. Базовая поддержка RPi появилась в upstream’е достаточно давно. К примеру, RPi 2 поддерживается Linux’ом, начиная с версии 4.5, которая вышла 13-го марта 2016 г., а RPi 3 – начиная с версии 4.8, которая вышла 2 октября того же года. Безусловно, RPi поддерживается «ванильным», т. е. стандартным, ядром не настолько хорошо, как Raspbian’овским, но для решения некоторых задач этим можно пренебречь.

В качестве примера я возьму Linux 4.14 — последний на момент написания статьи выпуск с длительным сроком поддержки. Так, этот раздел не потеряет своей актуальности ни на грамм, пока не закончится жизненный цикл выпуска 4.14, которое при участии Google будет сопровождаться до 2023 г. Тем не менее, я призываю вас брать самую последнюю на момент чтения этого руководства версию ядра и применять к ней описанные здесь рецепты.

А теперь к делу. Загрузите с kernel.org архив с интересующей вас версией ядра Linux. К примеру, им оказался linux-4.14.52.tar.xz. Распакуйте его.

$ tar xJvf linux-4.14.52.tar.xz

Затем установите необходимые для конфигурации и сборки ядра пакеты (в Debian и производных от него это build-essential, libncurses5-dev и gcc-arm-linux-gnueabihf). После этого выполните следующие команды, чтобы приступить к конфигурации целевого ядра, ради которой мы здесь все собрались.

$ ARCH=arm make bcm2835_defconfig
$ ARCH=arm make menuconfig
menuconfig поможет сконфигурировать ядро для решения конкретной задачи.
menuconfig поможет сконфигурировать ядро для решения конкретной задачи.

На этом этапе, как правило, из ядра выкидывается всё, что не способствует выполнению поставленной задачи, и добавляется всё, чего не хватает. Руководство не предлагает в каче­стве примера какую-то конкретную задачу, поэтому здесь всё зависит от вас. Я настоятельно рекомендую не зацикливаться на этом шаге, двинуться дальше, а потом еще раз вернуться к этому разделу после просмотра доклада «Tuning Linux For Embedded Systems: When Less is More» Даррена Гарта [Darren Hart], где он ставит перед собой цель собрать минимально возможное ядро для встраиваемого устройства.

После того как с конфигурацией будет покончено, можно начинать сборку:

$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- chrt -i 0 make -j4 deb-pkg

В результате получится несколько Deb-пакетов. Сейчас нас интересует linux-image, полное название у которого в моем случае – linux-image-4.14.52_4.14.52-1_armhf.deb. Установите его, предварительно удалив пакет с Raspbian’овским ядром:

$ sudo chroot /mnt/rootfs apt-get purge raspberrypi-kernel
$ sudo cp linux-image-4.14.52_4.14.52-1_armhf.deb /mnt/rootfs
$ sudo chroot /mnt/rootfs dpkg -i linux-image-4.14.52_4.14.52-1_armhf.deb
$ sudo rm /mnt/rootfs/linux-image-4.14.52_4.14.52-1_armhf.deb

Затем удалите с загрузочного раздела всё, что относилось к Raspbian’овскому ядру и загрузите на него новое ядро и dtb-файлы:

$ sudo rm /mnt/boot/{cmdline.txt,bcm283*,kernel7.img}
$ sudo cp /mnt/rootfs/boot/vmlinuz-4.14.52 /mnt/boot/zImage
$ sudo cp /mnt/rootfs/usr/lib/linux-image-4.14.52/bcm283* /mnt/boot

Для загрузки «ванильного» ядра потребуется загрузчик, в качестве которого я предлагаю использовать Das U-Boot. Последней стабильной версией U-Boot на момент написания статьи является 2018.05. Загрузите архив с исходниками U-Boot, укажите файл конфигурации, которая соответствует вашему устройству, и запустите сборку.

Что касается файла конфигурации, то в моем случае это rpi_3_32b_defconfig, т. к. я хочу запустить на Raspberry Pi 3 Model B собранное под ARMv7 ядро Linux, поскольку мы имеем дело с 32-битной пакетной базой Raspbian. Подсмотрите в директории configs все возможные варианты.

$ curl -O ftp://ftp.denx.de/pub/u-boot/u-boot-2018.05.tar.bz2
$ tar xjvf u-boot-2018.05.tar.bz2
$ cd u-boot-2018.05
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- chrt -i 0 make rpi_3_32b_defconfig
$ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- chrt -i 0 make -j4

Теперь подготовьте файл boot.scr со следующим содержимым:

mmc dev 0
setenv fdtfile bcm2837-rpi-3-b.dtb
setenv bootargs earlyprintk console=tty0 console=ttyAMA0
root=/dev/mmcblk0p2 rootwait init=/bin/systemd
fatload mmc 0:1 ${kernel_addr_r} zImage
fatload mmc 0:1 ${fdt_addr_r} ${fdtfile}
bootz ${kernel_addr_r} - ${fdt_addr_r}

Этот файл содержит список команд, которые U-Boot должен выполнить. Такой способ конфигурации U-Boot делает загрузчик очень гибким.

Есть три места, на которые в этом пункте стоит обратить пристальное внимание.

  • 2-я строка содержит имя DTB-файла, соответствующего устройству, на котором планируется загрузка ядра. Указанный в этой строке файл, в числе прочих DTB-файлов, был получен при сборке ядра и уже находится на корневом и загрузочном разделах. Укажите здесь DTB-файл, соответствующий вашему устройству.
  • 3-я строка содержит командную строку ядра.
  • 4-я строка содержит имя двоичного файла ядра (zImage).

Теперь необходимо скомпилировать boot.scr в boot.scr.uimg. Для этого потребуется программа mkimage, которую можно найти в пакете u-boot-tools во всех Debian-подобных дистрибутивах или в uboot-tools в Fedora.

$ mkimage -A arm -O linux -T script -C none -n boot.scr -d boot.scr boot.scr.uimg

Наконец, нужно перекинуть двоичный файл загрузчика, который был получен после сборки U-Boot, и boot.scr.uimg на загрузочный раздел:

$ sudo u-boot.bin /mnt/boot/kernel7.img
$ sudo boot.scr.uimg /mnt/boot

Очистка образа

Неотъемлемой частью подготовки образа для RPi и других одноплатников является его очистка. Если образ решает какую-то распространенную задачу, то он заслуживает того, чтобы им пользовалось как можно большее количество людей. Но крайне непрофессионально распространять образ, в котором остаются следы работы над ним. Это в первую очередь относится к кэшу двоичных пакетов и индексным файлам.

Существует идиома, которая решает эту проблему. Она стала популярной за счет повсеместного использования в Dockerfile’ах, т.к. Docker-образы, как и образы для одноплатников, тоже нуждаются в очистке от «строительного мусора». Выглядит эта идиома следующим образом.

$ sudo apt-get clean
$ sudo rm -rf /var/lib/apt/lists/*

Первая команда удаляет кэшированные двоичные пакеты, а вторая – индексные файлы. В нашем случае эти команды будут выглядеть так:

$ sudo chroot /mnt/rootfs apt-get clean
$ sudo rm -rf /mnt/rootfs/var/lib/apt/lists/*

Заключение

Несмотря на то, что есть достаточно большое количество скриптов, автоматизирующих весь описанный в этом руководстве процесс, я убежден в большой пользе понимания, что у них под капотом. Во-первых, эти скрипты нуждаются в контрибьюторах – и где как не здесь их выращивать. Во-вторых, спускаясь на уровень сборки своей системы, хоть и на основе двоичных пакетов, есть возможность лучше понять GNU/Linux, а где как не здесь этим заниматься. Я старался выжимать максимум из каждого раздела этой части руководства, выстраивая как можно более длинные цепочки из смежных тем для того, чтобы сделать материал как можно более энциклопедическим. Теперь ваша очередь – сдуйте пыль со своего одноплатника и выжмите из него максимум. Удачного хакинга.

Raspbian: сборка образа. Часть 1

Автор: Евгений Голышев

Статья была опубликована в 235-м выпуске журнала Linux Format. По истечении двух месяцев с момента публикации статьи материал публикуется в нашем блоге.

Вместе с Raspberry Pi (он же – RPi) и другими одноплатными компьютерами развиваются инструменты кастомизации образов для этих устройств. Отличными примерами являются rpi23‐gen-image и pi-gen. Для своего про­екта выходного дня, Pieman, я проделал т.н. Customer Development, чтобы понять, почему люди предпочитают собирать образы самостоятельно вместо использования уже готовых. Оказалось, что многие из тех, с кем мне удалось пообщаться, убеждены, что сборка минимально функциональной операционной системы под конкретную задачу будет работать быстрее, расходовать меньше ресурсов и даже поможет продлить срок жизни SD-карты. С этим сложно поспорить, т.к. чем больше запущено программ, тем

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

Также многие опрошенные указывают на то, что, приблизившись вплотную к сборке образов, вы получаете отличную возможность прокачать себя в Linux. В конце концов, одноплатники воскрешают очарование, испытанное при первом знакомстве с компьютером. Именно тому, что происходит под капотом таких инструментов, как rpi23‐gen-image и pi-gen, я хочу посвятить большую часть нашего урока.

Условимся, что сборка будет происходить на машине x86. Иначе процесс сборки хоть и незначительно, но упростится, оставив несколько интересных тем за бортом.

Всё описанное в этой статье было протестировано в Debian Stretch и Fedora 28.

Чтобы понять, в каком направлении следует двигаться, для начала необходимо воскресить в памяти процесс загрузки RPi.

Немного теории

На SD-карте, с которой будет осуществляться загрузка одноплатника, должно быть как минимум два раздела, где первый используется в качестве загрузочного, а второй – для хранения корневой файловой системы. На первом разделе должна использоваться FAT32, а на втором – любая POSIX-совместимая файловая система, которая удовлетворяет заданным условиям и личным предпочтениям. При включении RPi запускается первая ступень загрузки. На этом этапе загрузочный раздел монтируется загрузчиком, находящимся где-то в недрах SoC [System on a Chip, система на кристалле] BCM2836. Стоит отметить, что этот загрузчик закладывается еще на этапе производства и не может быть ни изменен, ни заменен. Затем за дело берется специальное ядро на графическом процессоре RPi, и загружает файл bootcode.bin с загрузочного раздела в L2‐кэш. Таким образом запускается вторая ступень загрузки. (Может показаться немного странным, что работа RPi начинается с графиче­ского, а не центрального процессора, но так уж устроен SoC BCM2836.) На этом этапе, опуская лишние подробности, загружается прошивка графиче­ского процессора start.elf, которая позволяет запустить kernel7.img и передать управление центральному процессору. kernel7.img может быть как образом ядра Linux, так и программой, специально написанной для RPi для запуска на «голом железе». start.elf использует config.txt для хранения параметров, которые передаются kernel7.img при запуске.

Этот процесс в том или ином виде уже был описан в различных книгах и статьях, которые еще на протяжении долгого времени будут с нами. Дело в том, что сейчас этот процесс незначи­тельно отличается от первоначального, но чтение руководств, которые содержат устаревшие сведения по этапам загрузки RPi, может натолкнуть на мысль, что в данную статью закралась ошибка. Таким образом, стоит отдельно сказать, что до 19‐го октября 2012 г. прошивка RPi включала файл loader.bin, который загружался между bootcode.bin и start.elf и запускал третий этап загрузки. С тех пор этот файл больше не требуется, и загрузка одноплатника ста­ла двухуровневой.

Теперь мы, по крайней мере, выяснили, что требуется образ SD-карты с двумя заранее подготовленными разделами. С этого и предлагаю начать.

Подготовка образа SD-карты

 Cоздадим образ SD-карты размером 8 ГБ:

$ dd if=/dev/zero of=raspbian-stretch.img bs=1024 seek=$(( 1024 * 1024 * 8 )) count=1

В данном примере dd пропускает 8 миллионов блоков размером 1 КБ, а затем заполняет 1 КБ нулями. В результате получится то, что называют разреженным файлом [sparse file]. Этот подход позволяет отводить место только тогда, когда это действительно необходимо. Тогда пустой образ SD-карты размером 8 ГБ по факту будет занимать минимально возможное пространство на диске, т.е. размер блока файловой системы (4 КБ, как правило).

Затем необходимо создать таблицу разделов на будущей SD-карте и разбить ее на два раздела. Linux поддерживает несколько таблиц разделов, но исторически сложилось, что по умолчанию используется MS-DOS. Ее основной характеристикой является поддержка 4‐х первичных разделов, но если по какой-то причине этого количе­ства окажется недостаточно, один из этих 4‐х разделов можно сделать расширенным. Расширенный раздел может содержать до 12 логических. Таким образом, в первом случае мы получаем в свое распоряжение до 4 разделов, а во втором – до 15 (3 + 12, не считая расширенного, т.к. он является всего лишь «контейнером» для логических). Эти разделы в равной степени могут использоваться для хранения как данных, так и области подкачки.

Создадим таблицу разделов и разобьем образ SD-карты на два раздела:

$ sudo parted raspbian-stretch.img mktable msdos
$ sudo parted raspbian-stretch.img mkpart p fat32 4MiB 54MiB
$ sudo parted -s raspbian-stretch.img -- mkpart primary ext2 58MiB -1s

Как уже говорилось выше, эти два раздела являются необходимыми. Остальные разделы (для /home, области подкачки [swap] и пр.) могут быть созданы при желании, как и на любой другой машине под управлением GNU/Linux.

В последнем случае -1s использовалось в качестве индикатора последнего сектора и позволило сказать parted, что необходимо создать раздел, начиная с 58‐го МБ (c 118784‐го сектора) и заканчивая последним сектором на диске. Однако этот индикатор выглядит с точки зрения parted как опция, так что в этой командной строке, в отличие от предыдущей, использовалось --. Иначе программа завершилась бы, выбросив «parted: invalid option – '1'».

В данном примере я создал загрузочный раздел размером 50 МБ, выровняв его по границе 4 МБ. Что касается размера этого раздела, то к нему не предъявляется жестких требований, однако его необходимо сделать таким, чтобы в него поместились образ ядра, DTB-файлы, о которых речь пойдет в разделе «Заполнение загрузочного раздела», и описанные в начале статьи двоичные фрагменты-блобы. Лично мне показалось, что полсотни мегабайт должно хватить с головой, но при желании к этому вопросу можно подойти более педантично. Что касается выравнивания, то все разделы SD-карты рекомендуется выравнивать по границе 4 МБ. Стоит заметить, что пренебрежение этой рекомендацией может привести к падению производительности операций ввода-вывода. За подробностями рекомендую обратиться к статье Арнда Бергманна [Arnd Bergmann] «Optimizing Linux with cheap flash drives».

Заключительным этапом подготовки образа SD-карты станет форматирование разделов. Дело в том, что mkpart только устанавливает идентификатор типа файловой системы, но не форматирует разделы. Эти идентификаторы затем используются другими программами для сообщения пользователю, что собой представляет тот или иной раздел. В качестве идентификатора типа файловой системы для первого раздела использовалась FAT32, а для второго – ext2. Теперь выполним

$ /sbin/fdisk -lu raspbian-stretch.img

чтобы увидеть, что в конце концов получилось. Вывод fdisk будет достаточно информативным. Сначала убедитесь, что каждый раздел находится на своем месте и занимает указанное количество блоков, а затем обратите внимание на то, что первый помечен как W95 FAT32 (LBA), а второй – Linux. Стоит отдельно отметить, что для ext2, ext3, ext4 и большинства других файловых систем, которые считаются для Linux родными, используется один и тот же идентификатор 0x83. Таким образом, часто можно встретить примеры, когда в качестве типа файловой системы указывается ext2, но на деле используется ext4 или что-либо еще.

fdisk предлагает наглядную картину структуры будущего образа.

Чтобы начать форматирование разделов образа, их сначала необходимо подготовить. Для этого предлагаю воспользоваться программой losetup, которая прочитает таблицу разделов образа, ассоциирует одно из устройств обратной связи [loop device] с целым образом и, наконец, создаст виртуальные блочные устройства для каждого раздела. После этого каждый раздел может быть

отформатирован посредством любой программы из семейства mkfs.*. К примеру, команда

$ LOOP_DEV=$(sudo losetup --partscan --show --find raspbian-stretch.img)

создаст два блочных устройства – ${LOOP_DEV}p1 и ${LOOP_DEV}p2, соответствующие загрузочному и корневому разделу соответственно. Теперь их можно отформатировать следующим образом:

$ sudo mkfs.vfat ${LOOP_DEV}p1
$ sudo mkfs.ext4 ${LOOP_DEV}p2

и перейти к начинке для них.

debootstrap устанавливает базовую систему Debian в указанную директорию.

Подготовка chroot-окружения

debootstrap устанавливает базовую систему Debian в указанную директорию и позволяет формировать chroot-окружение на основе указанного выпуска Debian, Ubuntu или любого другого Debian-подобного дистрибутива. Посредством одного из параметров можно указать адрес репозитория дистрибутива, поэтому debootstrap не ограничивается дистрибутивами Debian и Ubuntu, позволяя строить chroot-окружения на основе Devuan, Raspbian и пр. Таким образом, в простейшем случае программе необходимо передать следующие параметры.

  • Кодовое имя дистрибутива (codename) или имя его статуса (status name). В каче­стве кодового имени могут быть использованы, к примеру, jessie, stretch, buster или sid (возможно использование кодовых имен не только Debian, но и Ubuntu), а в качестве имени статуса – соответствующие вышеприведенным кодовым именам oldstable, stable, testing и unstable.
  • Директория, которая будет играть роль корня будущего chroot-окружения.
  • (опционально) Адрес репозитория, который будет использоваться в каче­стве источника двоичных пакетов.

К примеру, $ sudo debootstrap stretch stretch создаст chroot-окружение на базе Debian Stretch (первый параметр), корнем которого будет директория stretch (второй параметр). В данном случае источником двоичных пакетов формально будет https://deb.debian.org/debian, а на деле – одно из ближайших к пользователю зеркал.

Хотя приведенный пример позволяет получить общее представление о debootstrap и процессе сборки chroot-окружений на базе Debian-подобных дистрибутивов, результат работы команды не позволит приблизиться к решению поставленной задачи – подготовить корневую файловую систему на базе текущего стабильного выпуска Raspbian. Чтобы этого добиться, необходимо сделать следующие вещи:

  1. Сообщить debootstrap’у, что формирование chroot-окружения должно производиться на основе Raspbian. Для этого в качестве третьего параметра команды нужно указать адрес репозитория дистрибутива – http://archive.raspberrypi.org/debian, а также целевую архитектуру – armhf (32‐битная архитектура ARM с аппаратной поддержкой операций с плавающей запятой).

  2. Посредством опции --foreign разделить процесс подготовки chroot-окружения на две ступени. Иначе debootstrap упадет на этапе конфигурирования пакетов, т. к. целевая архитектура отличается от архитектуры хоста (т. е. машины, на которой выполняется команда). Дело в том, что этот этап требует вовлечения низкоуровневого пакетного менеджера dpkg и других программ из самого chroot-окружения, которые собраны под архитектуру, отличную от x86. Разделение процесса сборки chroot-окружения на две ступени позволяет сначала довести до конца всё то, что можно сделать средствами хоста, отложив все этапы, которые требуют запуска различных программ и скриптов из самого chroot-окружения. Таким образом, появляется возможность втиснуть между запусками первой и второй ступени добавление двоичного эмулятора, чтобы дать всем программам из chroot-окружения шанс выполниться на процессоре хоста.

  3. Посредством опции --keyring передать debootstrap’у связку ключей для проверки подписей. debootstrap по-взрослому относится к работе с двоичными пакетами, что предполагает проверку их цифровых подписей. Если команда выполняется в любой системе, отличной от Raspbian, то debootstrap упадет на этапе получения списка пакетов, т.к. не сможет найти публичный ключ, закрытым ключом которого был подписан этот список.

  4. Посредством опции --variant=minbase сообщить debootstrap’у, что вам необходимо минимально возможное chroot-окружение. Из всех перечисленных опций эта является наименее критичной, но она позволяет получить минимальную систему, которая гарантированно не будет содержать ничего лишнего. Установка всего необходимого вручную позволит приблизиться к пониманию того, как устроена система.

Первая ступень только загружает и распаковывает пакеты.

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

$ curl http://archive.raspbian.org/raspbian.public.key -O

Файл raspberrypi.gpg.key представляет собой публичный ключ в ASCII-совместимом формате [ASCII-armored format], который можно, например, хранить в Git-репозитории и распространять вместе со скриптом, собирающим образы.

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

$ gpg --no-default-keyring --keyring=$(pwd)/keyring.gpg --import raspbian.public.key

В текущей директории появится файл keyring.gpg. Теперь есть всё необходимое для того, чтобы запустить первую ступень подготовки chroot-окружения на основе стабильного выпуска Raspbian.

Запустите debootstrap в той же директории, в которой выполнялась gpg, следующим образом:

$ sudo debootstrap --arch=armhf --foreign --keyring=$(pwd)/keyring.gpg --variant=minbase stretch stretch http://archive.raspbian.org/raspbian

В обоих случаях значением опции –keyring должен быть полный путь к связке ключей, поэтому использовалась запись $(pwd)/keyring.gpg, которая раскрывается в полный путь.

Перед запуском второй ступени необходимо подготовить двоичные файлы средства эмуляции в режиме пользователя [user mode emulation binaries], чтобы программы, собранные под архитектуру ARM из chroot-окружения, могли выполняться на процессоре хоста x86. Как в производных от Debian дистрибутивах, так и в Fedora эти бинарники содержатся в пакете qemu-user-static. Модуль ядра binfmt_misc, доступный в Linux, начиная с версии 2.1.43 (которая, кстати, вышла в июне далекого теперь 1997 г.), позволяет распознавать различные форматы исполняемых файлов и ассоциировать их с произвольными приложениями. Другими словами, для определенного формата исполняемого файла можно зарегистрировать эмулятор и при каждой попытке запустить исполняемый файл, который имеет этот формат, передавать его эмулятору, а не запускать на текущем процессоре. В производных от Debian дистрибутивах также потребуется установить пакет binfmt-support, в который вынесена функция регистрации эмуляторов.

После установки qemu-user-static в Debian и Fedora и binfmt-support – только в Debian необходимо будет скопировать двоичный файл средства эмуляции в chroot-окружение и запустить вторую ступень.

$ sudo cp /usr/bin/qemu-arm-static stretch/usr/bin
$ sudo chroot stretch /debootstrap/debootstrap --second-stage

Вторая ступень запускает для установки пакетов dpkg и другие программы из chroot-окружения.

Несмотря на то, что в качестве источника двоичных пакетов был указан http://archive.raspbian.org/raspbian, вместо него в /etc/apt/sources.list будет фигурировать http://deb.debian.org/debian.

Это следует исправить:

$ sudo sh -c "echo deb http://archive.raspbian.org/raspbian stretch main > stretch/etc/apt/sources.list"

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

$ sudo chroot stretch passwd

Установка ядра

В предыдущем разделе мы создали chroot-окружение, всё множество пакетов которого называется в терминологии Debian базовой системой (см. подробнее раздел 3.7 руководства Debian Policy). Ядро не входит в это множество, т. к. для функционирования системы, как бы это ни было странно, ядра не требуется. В этом можно убедиться, выполнив, к примеру,

$ sudo chroot stretch bash
# ls

Не имея собственного ядра, chroot-окружение может использовать возможности хостового. (По тому же принципу, но с бóльшим уровнем изоляции, работают Docker-контейнеры и другие средства виртуализации на уровне операционной системы.) Но для того, чтобы это chroot-окружение вышло за рамки своих скромных возможностей и превратилось в полноценную систему, в него необходимо установить ядро.

Пакет с ядром Raspbian находится в репозитории http://archive.raspberrypi.org/debian/. Его адрес необходимо добавить в /etc/apt/sources.list, а его публичный ключ – в список доверенных ключей.

$ sudo sh -c "echo deb http://archive.raspberrypi.org/debian stretch main >> stretch/etc/apt/sources.list"
$ curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key -O
$ sudo cp raspberrypi.gpg.key stretch
$ sudo chroot stretch apt-key add raspberrypi.gpg.key
$ sudo rm stretch/raspberrypi.gpg.key

Теперь обновите индексы и установите пакет с ядром:

$ sudo chroot stretch apt-get update
$ sudo chroot stretch apt-get install raspberrypi-kernel

Заполнение корневого раздела

Несмотря на то, что chroot-окружение еще нуждается в доработке, оно представляет собой минимально функциональную версию системы. Это отличная возможность перейти к компоновке образа Raspbian, который может быть использован на реальном устройстве, и начать подведение итогов нашего урока.

Всё, что сейчас требуется – это смонтировать корневой раздел ${LOOP_DEV}p2 и скопировать на него всё содержимое chroot-окружения.

$ sudo mount ${LOOP_DEV}p2 /mnt
$ sudo rsync -apS stretch/ /mnt
$ sudo umount /mnt

Заполнение загрузочного раздела

Linux-подобные операционные системы на машинах x86, как правило, полагаются на директорию /boot – именно там загрузчик ищет двоичный файл ядра. Но на RPi и других одноплатниках эта директория будет формальностью. Как было сказано выше, бинарник ядра должен быть расположен на загрузочном разделе, который монтируется при старте машины. Таким образом, первым делом необходимо смонтировать загрузочный раздел и скопировать на него ядро, а затем создать файл с командной строкой ядра.

$ sudo mount ${LOOP_DEV}p1 /mnt
$ sudo cp stretch/boot/kernel7.img /mnt
$ sudo sh -c "echo console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 rw rootwait init=/bin/systemd > /mnt/cmdline.txt"

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

$ sudo cp stretch/boot/*.dtb /mnt

Наконец, надо загрузить блобы и поместить их туда же.

$ cd /mnt
$ export BOOT_ADDR=https://github.com/raspberrypi/firmware/raw/master/boot
$ sudo curl $BOOT_ADDR/bootcode.bin -OL
$ sudo curl $BOOT_ADDR/start.elf -OL

Заключение

Для записи полученного образа на SD-карту я настоятельно рекомендую использовать программу Etcher.

На нашем уроке в основном использовался стандартный инструментарий, который должен быть в каждой Linux-подобной операционной системе. Я попытался подробно рассказать о том, как можно с его помощью собрать минимально функциональную систему, которая, тем не менее, будет способна загрузиться на абсолютно любой модели Raspberry Pi. И хотя от такой системы в ее нынешнем виде сейчас мало толку, ее сборка должна была приоткрыть завесу тайны над тем, как работают некоторые инструменты, которые лежат в основе rpi23‐gen-image, pi-gen и даже инсталляторов Debian и Ubuntu. Несмотря на то, что эту систему можно расширить необходимыми для решения конкретной задачи пакетами, она всё еще является привязанной к Raspbian’овскому ядру, в ней отсутствует поддержка сети и т.д. Этим и другим темам будет посвящена вторая часть данного учебника.