Однажды на работе техлид порекомендовал мне проштудировать книгу Understanding the Linux Kernel Бове и Чезати. В ней рассмотрена версия Linux 2.6, сильно не дотягивающая до более современной версии 6.0. Но в ней явно ещё много ценной информации. Книга толстая, поэтому на её изучение мне потребовалось немало времени. Занимаясь по ней, я решил настроить такую среду разработки, в которой я мог бы просматривать и изменять новейшую версию ядра Linux — чтобы было ещё интереснее.
Есть и другие статьи, в которых рассказано, как собрать ядро Linux. Но в этой статье я немного иначе организую и подаю информацию.
❯ Этапы
Работа пойдёт в 2 основных этапа:
Собираем и запускаем Linux на qemu
Собираем и запускаем Linux на qemu с поддержкой пользовательского пространства Busybox
Кроме того, через qemu можно подключить отладчик прямо к действующему ядру Linux. Сначала я планировал изучить этот процесс и именно о нём написать статью, но передумал, увидев, что статья получается слишком длинной. Здесь можно почитать о kgdb. Можете сами убедиться – раньше я с ней не работал.
❯ Установка qemu
Будем работать с эмулятором Qemu, который имитирует железо. Именно на нём будет работать тот Linux, который bs собираем. Для установки выполните:
~ sudo apt install qemu qemu-system
Сначала я пытался собрать qemu, взяв за основу исходный код, но у него очень много зависимостей, и вскоре я стал понимать, что напрасно теряю время.
❯ Клонируем Linux,настраиваем ветку на локальной машине
Сначала клонируем репозиторий с Linux.
~ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/
Отличная задача, на которой можно протестировать скорость загрузки.
~ du --max-depth=1 --block-size=GB | grep linux
6GB ./linux
Затем отметим галочкой ту версию, которая нас интересует. Я остановился на 5.19.
~ cd linux
# alias gco="git checkout"
~ gco v5.19
~ git branch -M 5.19
❯ Собираем Linux
Как вы можете убедиться, весь процесс сборки документирован прямо в дереве исходников Linux, см. readme. Также можно ввести команду make help
, и она выведет доступные опции. Ниже я пошагово опишу работу, которую проделал.
Очистка (на первый раз не нужна)
Избавьтесь от всех устаревших файлов .o
, оставшихся предыдущих попыток. Нам это сейчас не нужно, поскольку мы в первый раз приступаем к сборке, однако хорошие привычки не повредит усваивать заблаговременно.
~ make mrproper
Собираем образ ядра
У ядра множество возможностей — выбирайте те, что вам нравятся. Например, в ядре есть множество драйверов, вам же, вероятно, нужны лишь некоторые из них. Если скомпилировать ядро сразу со всеми драйверами, это даже может привести к отказу некоторых функций. Драйверы – это лишь один пример. Есть подобные возможности, связанные с виртуализацией, файловыми системами и т.д. Много чего конфигурировать! Следовательно, при сборке ядра делается отдельный шаг, на котором вы явно указываете все возможности, которые вам понадобятся, а лишь затем приступаете собственно к сборке.
Если вы выбрали для сборки ядра путь /home/$USER/linux-build
, то укажите флаг O
(каталог вывода) как показано ниже.
~ OUTPUT_DIR=/home/$USER/linux/build
# создать файл конфигурации сборки ядра. При этом максимально возможное количество значений
# устанавливается в no. В сущности, нужно отключить как можно больше фич,
# так у вас получится компактное ядро.
# Если хотите сделать минимальное ядро, воспользуйтесь tinyconfig вместо allnoconfig.
# Не представляю, чем они отличаются.
~ make O=$OUTPUT_DIR allnoconfig
# Здесь можно просматривать конфигурацию ядра через визуально приятный пользовательский интерфейс.
# Тут пока нечего включать.
~ make O=$OUTPUT_DIR menuconfig
# Собираем само ядро
# Заменяем 8 на столько процессов, сколько поддерживает ваш компьютер.
# cat /proc/cpuinfo | grep processor | wc -l.
~ make O=$OUTPUT_DIR -j8
На выходе получаем образ ядра --файл bzImage
—и убеждаемся, что его размер составляет всего 1,5 МБ.
❯ Этап второй: Запускаем Linux на qemu
Теперь давайте попробуем запустить при помощи qemu то ядро, которое у нас получилось.
В man-подобной справке по qemu достаточно хорошо объяснено, как работают разные флаги.
~ OUTPUT_DIR=/home/$USER/linux/build
# -nographic в сущности, означает, что мы обходимся одной лишь консолью для последовательного ввода и не нуждаемся в gui/устройстве с дисплеем.
# -append позволяет qemu передать следующую строку в качестве командной строки ядра.
# Так можно сконфигурировать ядро в процессе загрузки:
# - console=ttyS0 сообщает ядру, что нужно использовать последовательный порт.
# - earlyprintk=serial,ttyS0 сообщает ядру, что нужно отправлять через последовательный порт информацию логов, чтобы мы могли, опираясь на неё,
# отлаживать систему после отказов ещё до того, как инициализируется код консоли. Попробуйте от этого избавиться – и увидите, что получится!
# -kernel указывает, какой образ ядра использовать.
#
~ qemu-system-x86_64 -kernel $OUTPUT_DIR/arch/x86/boot/bzImage -nographic -append "earlyprintk=serial,ttyS0 console=ttyS0"
Если нажать ctrl + a
, а затем x
, то мы покинем экран консоли. Если нажать ctrl + a
, а затем h
, то будет выведено меню справки и другие опции.
В любом случае, если запустить собранное ядро через qemu, в ядре возникнет паника:
Warning: unable to open an initial console.
List of all partitions:
No filesystem could mount root, tried:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
На следующем этапе мы от этой паники избавимся.
❯ Обзаводимся Busybox
Теперь у нас есть ядро Linux, но нет ни пользовательского пространства, ни файловой системы. Воспользуемся файловой системой, в которой обеспечена поддержка памятью (initramfs, можете посмотреть эту ссылку Gentoo в качестве тизера). Что-то должно пойти в файловую систему, поскольку мы не хотим, чтобы наше пользовательское пространство пустовало.
Именно здесь нам пригодится Busybox. В нём предоставляются такие команды как ls
, cd
, cp
, mv
, vim
, tar
, grep
, dhcp
, mdev
(события горячего подключения устройств к Linux), ifplugd
(мониторинг сетевого канала/интерфейса) – всё через маленький двоичный файл. Пожалуй, эти команды не будут такими многофункциональными и разнообразно конфигурируемыми, как их альтернативы, применяемые вне Busybox, но их нам хватит.
Посмотрите файл README к исходному коду busybox после того, как скачаете его по ссылке ниже – там всё подробно написано.
Переходите по ссылке https://busybox.net/ и забирайте новейшую стабильную версию busybox.
❯ Конфигурируем и собираем Busybox
Процесс такой же, как и при работе с ядром Linux.
Посмотрите файл INSTALL file в исходниках к busybox, как только они скачаются.
Сначала выбираем желаемую конфигурацию Busybox, а затем приступаем к сборке.
~ cd busybox-1.33.2
~ mkdir -pv build
~ OUTPUT_DIR=/home/$USER/busybox-1.33.2/build
# создаём файл .config, в котором выставлено множество «yes». Получаем
# массу возможностей Busybox, может быть, даже больше, чем нам требуется.
# Я недостаточно разбираюсь в теме, чтобы начинать с allnoconfig
# поэтому включаю только абсолютный минимум функций – собственно, вот он.
# Возможно, сборка получится и крупнее, чем ядро на 1,5 МБ. Давайте посмотрим
~ make O=$OUTPUT_DIR defconfig
# Открываем конфигурационный UI
~ make O=$OUTPUT_DIR menuconfig
Когда конфигурационный пользовательский интерфейс открыт, выбираем в нём "Settings" (Настройки) (клавишей ввода), а затем "Build Busybox as a static binary" (Собрать Busybox как статический двоичный файл) (клавишей пробела). Дело в том, что в файловой системе пользовательского пространства нашего пустого ядра не будет никаких разделяемых библиотек, поэтому мы можем сразу приступить к работе.
Теперь выходим из конфигурационного меню и сохраняем внесённые изменения.

Мы готовы приступать к сборке!
# введите make help, чтобы просмотреть доступные опции,
# но, в сущности, можно включить make all или make busybox.
# При первой опции также собирается документация, при второй - только busybox.
~ make O=$OUTPUT_DIR -j8 busybox
И вот,
~ ls -la $OUTPUT_DIR --block-size=KB | grep busybox
-rw-r--r-- 1 yangwenli yangwenli 2kB Aug 23 15:33 .busybox_unstripped.cmd
-rwxr-xr-x 1 yangwenli yangwenli 2694kB Aug 23 15:33 busybox
-rwxr-xr-x 1 yangwenli yangwenli 2987kB Aug 23 15:33 busybox_unstripped
-rw-r--r-- 1 yangwenli yangwenli 2340kB Aug 23 15:33 busybox_unstripped.map
-rw-r--r-- 1 yangwenli yangwenli 105kB Aug 23 15:33 busybox_unstripped.out
Двоичный файл busybox
, который мы хотели получить, действительно оказался размером около 2,7 МБ, больше, чем собранное нами ядро. Вариант busybox_unstripped
нас не интересует. Он немного крупнее и, очевидно, предназначен для изучения при помощи аналитических инструментов, так, как об этом рассказано в Busybox FAQ.
❯ Создаём исходную структуру каталогов
Следующие два раздела сильно вдохновлены вики-справкой по Gentoo, которая приводится в Custom Initramfs здесь.
Теперь нам предстоит собрать исходную структуру файлов для пользовательского пространства нашего Linux.
Нам потребуется убедиться наверняка, что двоичный файл busybox
на своём месте. А также предусмотреть init-процесс/скрипт, чтобы настроить наше пользовательское пространство.
~ mkdir /home/$USER/initramfs && cd initramfs
# создаём ряд базовых каталогов, которые понадобятся нам в нашем пользовательском пространстве Linux
# dev, proc и sys нужны для хранения всякого материала, относящегося к работе ядра – в частности, procfs, sysfs и устройств.
# В etc будем хранить заготовки для конфигурации того материала, которым собираемся заняться в будущем.
# Из root мы будем действовать.
# В bin будут храниться исполняемые файлы.
~ mkdir {bin,dev,etc,proc,root,sys}
# busybox также рассчитывает, что в нём будут эти дополнительные каталоги,
# так что давайте создадим их для него
~ mkdir {usr/bin,usr/sbin,sbin}
# Мы хотим, чтобы busybox был включён в наш initramfs
~ cp /home/$USER/busybox-1.33.2/build/busybox bin/busybox
❯ Создаём init-процесс
Теперь давайте создадим init-процесс. В каталоге initramfs
создаём файл под названием init
~ touch init && chmod +x init
И заполняем его следующим материалом:
#!/bin/busybox sh
# Получаем busybox, чтобы создать нежёсткие ссылки на команды
/bin/busybox --install -s
# Монтируем файловые системы /proc и /sys.
# Можете пропустить этот шаг, если хотите. Просто мне показалось, что хорошо бы их иметь.
mount -t proc none /proc
mount -t sysfs none /sys
# Загружаем командную оболочку, которая теперь должна быть мягко связана с busybox
exec /bin/sh
❯ Создаём initramfs cpio
Cpio – это инструмент-архиватор. В сущности, это означает, что он берёт набор файлов и каталогов и обратимо преобразует их в единственный файл. Примерно как tar. Не понимаю, почему, но initramfs указывается через cpio, поэтому и его мы должны обязательно использовать, чтобы всё упаковать. Для сжатия воспользуемся gzip.
~ find . -print0 | cpio --null --create --verbose --format=newc | gzip --best > ./custom-initramfs.cpio.gz
.
./etc
./root
./sys
./dev
./bin
./bin/busybox
./init
./proc
cpio: File ./custom-initramfs.cpio.gz grew, 1310720 new bytes not copied
./custom-initramfs.cpio.gz
7824 blocks
Вот мы и подготовили initramfs, которым собираемся пользоваться!
❯ Этап: выполняем Linux на qemu при помощи Busybox (с применением initramfs)
Теперь давайте запустим ядро Linux с включённым initramfs!
Возьмём команду qemu, приведённую выше, и добавим флаг command from above and add an initrd, указывая таким образом initramfs.
~ LINUX_BUILD_DIR=/home/$USER/linux/build
~ INITRAMFS_DIR=/home/$USER/initramfs/custom-initramfs.cpio.gz
# В прпинципе, флаг --initrd разрешает Linux использовать тот ram-диск, который мы собрали
~ qemu-system-x86_64 -kernel $LINUX_BUILD_DIR/arch/x86/boot/bzImage -nographic -append "earlyprintk=serial,ttyS0 console=ttyS0" --initrd $INITRAMFS_DIR
Вероятно, вас расстраивает, что паника ядра до сих пор возникает. Дело в том, что мы до сих пор не включили поддержку initramfs в ядре, а также не предусмотрели ещё пару деталей, необходимых для нормальной работы в нашем пользовательском пространстве.
Слегка затронем вопрос о том, как ядро запускает пользовательское пространство, и как оно узнаёт, где найти процесс init
. Чтобы запустить работу пользовательского пространства, ядро ищет /init, а затем /sbin/init, /etc/init, /bin/init и, наконец, finally /bin/sh — именно в таком порядке. Я оставил ссылку на исходник. Кроме того, в командной строке здесь. Я разместил файл init по адресу /bin/init.
Теперь, наладив поддержку initramfs, давайте соберём ещё одно ядро. Повторите шаги, проделанные выше (когда мы его конфигурировали) и соберите ядро:
~ cd /home/$USER/linux
~ make O=$LINUX_BUILD_DIR menuconfig
Перейдите в General Setup
и найдите там файловую систему Initial RAM
, а также диск с оперативной памятью (RAM), затем нажмите «пробел».
В самом верху конфигурационного файла также активируйте 64-битное ядро. Если при работе с двоичным файлом Busybox вы воспользуетесь командой file, то увидите, что он собран для архитектуры x86_64. Вы также убедитесь, что это файл в формате elf, поэтому мы должны будем предусмотреть в ядре поддержку и для этого формата. Поскольку мы используем в нашем init-файле нотацию !#
, нам и для неё нужно будет обеспечить поддержку.


Наконец спуститесь из начала файла к Device Drivers > Character devices > Serial drivers
и 8250/16550
и compatible serial support
и Console on 8250/16550
и compatible serial port
. Эти конфигурационные настройки нужны для того, чтобы использовать последовательный порт в качестве консоли. Подробнее об этом в документации. Если не внести эти изменения, init работать не сможет. Думаю, именно поэтому и нужна последняя строка exec /bin/sh
.

Теперь соберём ядро:
~ make O=$LINUX_BUILD_DIR -j8
А потом снова запустим qemu:
~ qemu-system-x86_64 -kernel $LINUX_BUILD_DIR/arch/x86/boot/bzImage -nographic -append "earlyprintk=serial,ttyS0 console=ttyS0 debug" --initrd $INITRAMFS_DIR
Итак, мы сделали себе рабочий Linux. Если у вас достаточно свободного времени, то можете продолжить этот опыт и выстроить на основе проделанной здесь работы ваш собственный дистрибутив.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

Источники