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’овскому ядру, в ней отсутствует поддержка сети и т.д. Этим и другим темам будет посвящена вторая часть данного учебника.

Leave a Reply

Your email address will not be published. Required fields are marked *