В современном мире нас окружает огромное количество электронных устройств различной степени сложности. Если устройство более или менее сложное, например, телевизор, маршрутизатор, смартфон, то с большой долей вероятности оно работает под управлением операционной системы Linuх, и эта мысль не даёт мне покоя.
Ещё больше не даёт покоя мне тот факт, что все ядра операционной системы Linux, которые работают на различных устройствах и серверах, собраны из исходного кода, находящегося в репозитории на сайте
kernel.org.
Такие разные устройства, а операционная система, работающая на них, собрана из одного и того же исходного кода! Это утверждение, конечно, верно лишь отчасти, так как фактически ядро обычно расширено и модифицировано разработчиками конкретных дистрибутивов Linux, а также разработчиками конкретных устройств, но общего исходного кода достаточно много.
Мне всегда хотелось собрать операционную систему Linux самому из исходного кода, но процесс этот всегда казался сложным и запутанным, да и многого я не понимал. Но всё-таки в определённый момент времени я накопил достаточное количество знаний, чтобы осуществить свою мечту. В этой статье я хочу рассказать вам, как собрать минимальную Linux из исходного кода и запустить её у себя на компьютере.
Она не позволит использовать все возможности вашего компьютера, но будет иметь главное – интерфейс командной строки. Поверьте мне, получив работающий интерфейс командной строки Linux на вашем реальном компьютере, вы испытаете неповторимые ощущения.
Вы удивитесь, но минимальный набор, необходимый для получения командной строки Linux содержит всего два файла: файл ядра Linux и файл начального образа корневой файловой системы. Естественно, необходим загрузчик, который загрузит эти два файла и инициирует выполнение ядра, передав ему образ начальной корневой файловой системы и другие параметры, если они необходимы.
▍ Минимальная операционная система Linux
Чтобы вы как-то смогли работать с операционной системой, вам нужны четыре составляющие: загрузчик, ядро, начальная корневая файловая система и набор утилит, являющийся интерфейсом к ядру операционной системы.
Загрузчик — это специальная программа, которая позволяет процессору начать выполнение машинных инструкций, находящихся в файле ядра операционной системы.
Ядро — это программный код, который содержит:
aбстракции для различных физических устройств ввода-вывода, с которыми может работать процессор (драйвера устройств),
aбстракции структур данных для хранения (файловые системы),
aбстракции для разделения во времени выполнения программных инструкций (процессы, потоки),
другие абстракции.
Именно благодаря ядру разработчику прикладных программ часто вообще нет разницы, какая видеокарта, клавиатура или жёсткий диск установлены на компьютере. Он просто пишет код, который работает с устройствами ввода-вывода, процессами, файлами, сокетами и др.
Начальная корневая файловая система нужна для того, чтобы ядро выполнило начальную загрузку файлов, необходимую для дальнейшей загрузки операционной системы. Самыми важными являются модули ядра Linux, не вошедшие в состав файла ядра, но нужные для дальнейшей загрузки, и файл, на основании которого будет создан самый первый процесс при загрузке операционной системы (init).
Набор утилит позволяет вам работать с абстракциями, находящимися в ядре операционной системы. В зависимости от сложности и назначения устройства, на котором будет работать операционная система, этот набор может различаться. Утилиты определяют функциональность и интерфейс взаимодействия с пользователем. Например, для роутера они будут одни, для телефона другие, а для персонального компьютера третьи.
▍ Загрузка операционной системы Linux
Загрузка операционной системы Linux может отличаться на различных архитектурах и компьютерах, но для архитектуры x86 загрузка выглядит в большинстве случаев так:
Происходит включение компьютера.
BIOS или UEFI находит на компьютере загрузчик операционной системы и передаёт управление ему.
Загрузчик операционной системы загружает в оперативную память файл ядра Linux и файл образа начальной файловой системы (файл initrd).
Загрузчик операционной системы передаёт управление ядру операционной системы Linux.
Ядро операционной системы проводит начальную инициализацию.
Ядро операционной системы получает доступ к файлам, которые находятся в образе начальной файловой системы (монтирует образ).
Ядро ищет файл init в начальной файловой системе и запускает самый первый процесс пользователя на его основе.
Процесс init монтирует уже постоянную файловую систему, продолжает инициализацию операционной системы и переносит корень файловой системы Linux на смонтированную файловую систему и запускает другие процессы, которые необходимы для инициализации.
▍ Дистрибутивы Linux
Дистрибутив – это ядро Linux, набор библиотек, утилит и программ, который устанавливается на компьютер или устройство.
На данный момент количество различных дистрибутивов огромно. Их перечень вы можете посмотреть на сайте
DistroWatch.
Современные дистрибутивы Linux обычно распространяются в виде образов ISO и позволяют устанавливать обновления и дополнительные программы (пакеты), но мы делаем минимальный дистрибутив, поэтому, естественно, у нас такой возможности не будет.
Одна из самых полных инструкций, как собрать дистрибутив Linux с нуля, находится
здесь. Сборка дистрибутива Linux — процесс интересный и позволит вам узнать много нового, но уж очень он долгий, а значит вам необходима огромная сила воли, чтобы выполнить его от начала до конца.
Когда ты получаешь такой огромный массив информации, не сильно разбираясь в теме, есть вероятность, что ты часть вещей выполнишь, не сильно вникая в суть.
Поэтому я стремился упростить создание дистрибутива до минимума: мы не будем монтировать постоянную файловую систему, а в качестве файла init будем использовать файл скрипта, который выполнит минимальную инициализацию и запустит оболочку sh.
▍ Загрузка операционной системы
За долгие годы своего существования Linux был портирован на множество аппаратных платформ. Загрузка Linux для каждой платформы отличается.
Для x86 загрузка может отличаться следующим:
Будет ли использоваться для загрузки BIOS или UEFI.
На каком носителе (жёсткий диск, флеш-накопитель, оптический диск, компьютерная сеть) BIOS или UEFI будет искать загрузчик.
Как размечен жёсткий диск или флеш-накопитель (MBR или GPT).
На каком носителе и в какой файловой системе (FAT, NTFS, EXT, CDFS и др.) будут располагаться файл ядра и файл с образом начальной корневой файловой системы, называющийся initrd.
▍ Структура начальной корневой файловой системы
Начальная корневая файловая система содержит минимальное количество файлов и директорий, необходимых для дальнейшей работы Linux. В нашем случае это директории bin, dev, proc, sys. В директории bin cодержатся утилиты для работы с ядром Linux.
▍ Наборы утилит
Минимальный Linux — это ядро и набор утилит командной строки. Ядро и утилиты командной строки разрабатываются разными командами программистов.
Из-за того, что BusyBox отличается простотой и занимает немного места на диске, его часто используют на встраиваемых устройствах. Мы же будем его использовать из-за простоты.
▍ Создание среды для сборки Linux
Как бы это парадоксально ни звучало, но Linux обычно собирают в Linux. Для этого вам необходима операционная система Linux, в которой присутствуют программы, позволяющие собрать ядро Linux и набор утилит для сборки.
Например, на Ubuntu 22.10 нам необходимо установить следующие пакеты: make, build-essential, bc, bison, flex, libssl-dev, libelf-dev, wget, cpio, fdisk, extlinux, dosfstools, qemu-system-x86. Для других систем набор пакетов может отличаться.
$ tar -xvf downloads/linux-5.15.79.tar.xz -C sources
$ tar -xjvf downloads/busybox-1.35.0.tar.bz2 -C sources
4. Собираем бинарные файлы BusyBox и для ядра Linux. Этот процесс займёт достаточно много времени, порядка 10 минут и даже больше, поэтому не пугайтесь.
$ cd sources/busybox-1.35.0
$ make defconfig
$ make LDFLAGS=-static
$ cp busybox ../../out/
$ cd ../linux-5.15.79
$ make defconfig
$ make -j8 || exit
$ cp arch/x86_64/boot/bzImage ~/simple-linux/linux/vmlinuz-5.15.79
5. Создаём файл init.
$ mkdir -p ~/simple-linux/build/initrd
$ cd ~/simple-linux/build/initrd
$ vi init
Вместо редактора vim (команда vi) вы можете использовать другой текстовый редактор, например gedit.
Файл init
#! /bin/sh
mount -t sysfs sysfs /sys
mount -t proc proc /proc
mount -t devtmpfs udev /dev
sysctl -w kernel.printk="2 4 1 7"
/bin/sh
poweroff -f
6. Cоздаём структуру директорий и файлов.
$ chmod 777 init
$ mkdir -p bin dev proc sys
$ cd bin
$ cp ~/simple-linux/build/out/busybox ./
$ for prog in $(./busybox --list); do ln -s /bin/busybox $prog; done
7. Помещаем структуру в файл initrd, который у нас является cpio-архивом.
9. Попробуем ввести известные вам команды Linux. Выходим из эмулируемой Linux, набрав команду exit.
▍ Создание загрузочного образа для флеш-накопителя
Если мы хотим запустить наш Linux на реальном железе, то, наверное, самый простой способ — это создать образ для размещения на загрузочном накопителе и записать его. Создание такого образа и загрузка с накопителя, с моей точки зрения, самый сложный процесс и требует более продвинутых знаний от вас.
При создании образа нужно принять несколько решений:
что будет инициировать загрузку (BIOS или UEFI),
какой накопитель вы будете использовать (CDROM, флеш-накопитель, жёсткий диск),
как вы разметите накопитель (MBR, GPT) и будете ли его размечать,
какой загрузчик вы будете использовать,
какая файловая система будет использоваться там, где будут располагаться файлы Linux и загрузчика.
Я использовал флеш-накопитель с MBR и установленным загрузчиком EXTLINUX, одним разделом FAT32, на котором располагаются файлы. Загрузку у меня инициировал BIOS (опция Legacy boot, если у вас на компьютере прошит UEFI BIOS).
Алгоритм создания образа загрузочного флеш-накопителя следующий:
Файл boot-disk.img будет содержать загрузочный образ флеш-накопителя
▍ Использование Docker для сборки Linux
Описанные выше алгоритмы содержат много команд и параметров, в них достаточно просто ошибиться при наборе. Команды можно объединить в bash-скрипты, а чтобы можно было собрать Linux в операционной системе Windows 10 или 11, рационально использовать
Docker Desktop.
Суть Docker в следующем:
В файле Dockerfile вы описываете структуру окружения для вашей программы или скрипта.
При помощи утилиты docker на основании Dockerfile вы создаёте образ этого окружения в определённом формате.
При помощи этой же утилиты вы можете запустить на основе образа экземпляр вашей программы или скрипта, работающий в изолированном окружении и называемый Docker-контейнер в терминологии Docker.
Созданные вами образы можно хранить в репозитории и повторно использовать. Docker-контейнеры, созданные на основании одного и того же образа, будут идентично выполняться на всех компьютерах, способных его выполнить.
Dockerfile удобно читать и изучать, также его удобно распространять.
На GitHub у меня есть
проект, содержащий исходный код среды для сборки Linux на основе технологии Docker-контейнеров.
Ниже приведу содержимое Dockerfile:
FROM ubuntu:22.10
RUN apt update && apt install --yes make build-essential bc bison flex libssl-dev libelf-dev wget
RUN apt install --yes cpio fdisk extlinux dosfstools qemu-system-x86
RUN apt install --yes vim
ARG APP=/app
ARG LINUX_DIR=$APP/linux
ARG FILES_DIR=$APP/files
ARG SCRIPTS_DIR=$APP/scripts
ENV BUILD_DIR=$APP/build
ENV LINUX_DIR=$LINUX_DIR
ENV FILES_DIR=$FILES_DIR
ENV LINUX_VER=5.15.79
ENV BUSYBOX_VER=1.35.0
ENV BASH_ENV="$SCRIPTS_DIR/bash-env/env"
COPY ./scripts $APP/scripts
COPY ./files $APP/files
RUN mkdir -p $LINUX_DIR
RUN ln -s $APP/scripts/start-linux.sh /usr/bin/start &&\
ln -s $APP/scripts/build-linux.sh /usr/bin/build &&\
ln -s $APP/scripts/build-image.sh /usr/bin/image
WORKDIR $APP/scripts
CMD build
Команда FROM является самой важной в нём, она указывает, на основании какого образа файловой системы будет строиться наш образ для сборки Linux. В данном случае это ubuntu:22.10.
Команда RUN запускает команды внутри создаваемого нами образа. Т. е. команды, которые следуют после RUN, будут выполнены так, как было бы, если бы вы работали в Ubuntu 22.10 и выполнили их в командной строке. В результате работы команды образ файловой системы у вас изменится, так как эти команды изменяют файловую систему внутри него.
Команда COPY копирует файлы из файловой системы нашей операционной системы внутрь создаваемого образа. Как и RUN, она изменяет файловую систему внутри образа.
Команды ARG и ENV вызывают путаницу. Не знаю, проясню я вам или нет, но ARG – это создание переменных, которые используются при создании образа, а ENV – это создание переменных, которые используются, когда уже на основании этого образа создан контейнер, и эти переменные будут видны внутри него.
Команда WORKDIR указывает, какая директория будет рабочей при запуске контейнера, созданного на базе нашего образа.
Команда CMD указывает, какая команда будет выполнена по умолчанию внутри контейнера при его запуске.
▍ Запуск и сборка минимальной Linux при помощи Docker
Вы можете поэкспериментировать с моим проектом. В Windows лучше всего запускать Docker в PowerShell.
$ mkdir linux
$ cd linux
$ docker run -v ${pwd}:/app/linux --rm -it simple-linux build
В созданной вами директории linux будет находиться собранный файл ядра Linux и файл образа начальной корневой системы.
3. Создание загрузочного образа для флеш-накопителя.
Обратите внимание, что нужно использовать опцию --privileged в docker, так как скрипт image использует loopback-устройство.
$ docker run -v ${pwd}:/app/linux –-privileged --rm -it simple-linux image
Если вы будете использовать Docker Desktop for Linux, Docker придётся запускать, используя sudo и вместо ${pwd} нужно будет использовать $(pwd).
▍ Запись загрузочного образа для флеш-накопителя на носитель
Созданный файл образа для флеш-накопителя (linux-5.15.79-busybox-1.35.0-disk.img) вы можете записать на флеш-накопитель при помощи утилиты
Win32DiskImager. Следует заметить, что при записи вы потеряеете все данные, хранящиеся на флеш-накопителе, поэтому лучше использовать накопитель, на котором нет никаких файлов.
После записи образа на флеш-накопитель перезагрузите компьютер и выберите загрузку с USB-HDD, т. е. c созданного вами флеш-накопителя. Скорее всего, перед этим вам будет нужно выбрать Legacy Boot и отключить Secure Boot в BIOS.
▍ Выводы
Если вы дочитали эту статью до конца, то у меня для вас есть небольшой -==BONUS==-
Имея установленный Docker Desktop для Windows, посмотреть, как всё работает, и запустить сборку моей минимальной ОС Linux можно одной командой в PowerShell.
docker run -v ${pwd}:/app/linux --rm -it artyomsoft/simple-linux build
У вас появится командная строка моей минимальной Linux, а при выходе из неё вы увидите в текущей директории файл ядра Linux и initrd-файл.
В этой статье я привёл подробную инструкцию, как можно получить работающую систему Linux из исходного кода.
Кому-то эта статья может показаться сильно простой и не заслуживающей внимания. Но я, чтобы не отпугнуть вас подробностями, не углублялся в такие темы, как BIOS, UEFI, файловые системы, загрузчики, библиотека glibc, подробный процесс загрузки операционной системы, различные спецификации, динамическая и статическая линковка, модули ядра Linux… Я только привёл минимальное количество теории, которая позволит понять, что же, собственно, вы делали, и разобраться в теме гораздо быстрее меня, не собирая всю информацию по крупицам.
Полученную операционную систему вы вряд ли будете использовать в дальнейшем, но я надеюсь, что абстрактные знания о Linux у вас превратятся в понимание и умение.