Голый Линукс — запуск ядра-одиночки

Итак, Linux — не операционная система, а только ядро для неё. Всё остальное приходит от проекта GNU (и других). И вот интересно — на что годится ядро само по себе?

Эта статья — очень «начального» уровня. Устроим маленький эксперимент — создадим чистую виртуальную машину и попробуем запустить ядро Linux «без всего». Или почти «без», т.к. нам понадобится загрузчик ОС — и какая-нибудь «пользовательская программа» (её мы сотворим сами). Конечно, продвинутые пользователи Linux такой «эксперимент» могут провести просто отредактировав параметры запуска при включении -, но наш рассказ всё же для тех кто почти (или совсем) не в теме:)

Бонусом чуть-чуть коснёмся системных вызовов и пару слов скажем о других ядрах.

Что такое ядро — и как им воспользоваться

На данном этапе нам достаточно представлять что ядро — некая программулина в виде здорового файла. Говоря по-программистски — фреймворк для запуска наших программ — и в то же время большая библиотека системных вызовов и коллекция драйверов.

Иными словами ядро воплощает работу со всеми основными сущностями ОС (в частности, процессами — в которых будут запускаться другие программы) -, а также содержит драйвера для работы со всевозможным оборудованием компьютера. Ну, не все возможные на свете драйвера -, но для наиболее актуальных и популярных систем. Для дисков, экрана, сетевых интерфейсов.

Поэтому наша цель — запустить ядро и передать управление небольшой демонстрационно программке которая сможет успешно «дёргать» эти системные вызовы. Попутно мы рассмотрим некоторые дополнительные вещи — хотя не актуальные для нашего эксперимента -, но о которых полезно иметь представление.

Создайте виртуальную машину

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

Здесь и далее мы хотя говорим подробно о шагах которые следует выполнить, но всё же избегаем излишне детальных указаний «нажмите такую-то кнопку». Пусть подобные мелочи останутся именно в качестве «упражнения» :) В частности интерфейс VirtualBox довольно интуитивный, да и нагуглить вопросы по ней легко.

пустая виртуалка - кнопка настроек (шестеренка) и запуска (стрелка) сверху

пустая виртуалка — кнопка настроек (шестеренка) и запуска (стрелка) сверху

Итак, машина создана — если вы попытаетесь её запустить, появится чёрный экранчик с сообщением что не найдено устройство с которого можно загрузиться. Оно и понятно — ведь диск пока девственно чист. Нужно записать на него загрузчик. Очевидно для любых операций нужно временно запуститься с какого-нибудь LiveCD. Опять же можете выбрать по своему усмотрению, но я рекомендую (для данной цели) скачать относительно небольшой образ SystemRescueCD. Скачайте образ и подключите в настройках созданной виртуалки в качестве компакт-диска. Перезапустите машину — после загрузки должно появиться загрузочное меню системы на LiveCD:

вообще эта штука может и в хозяйстве пригодиться

вообще эта штука может и в хозяйстве пригодиться

Сейчас мы запустим дефолтный пункт меню (просто нажмите Enter) -, но на будущее обратите внимание на пункт «Boot existing OS» — он отменяет загрузку с LiveCD и грузит то что у вас установлено на основном диске — мы будем этим пользоваться чтобы удобнее проверять что получилось.

Подготовка жёсткого диска

Итак нажмите «Boot SystemRescueCD using default options». Начнётся какая-то активность которая рано или поздно закончится надписью «automatic login» и ниже приглашением командной строки в духе [root@systemresccd ~]# — в принципе можно запустить графическую оболочку (startx), но нам это сейчас не нужно.

SystemRescueCD грузится в консольный режим но предлагает запустить оконный интерфейс

SystemRescueCD грузится в консольный режим, но предлагает запустить оконный интерфейс

Наш жёсткий диск виден среди девайсов, попробуйте ввести ls /dev/sd* и вы должны обнаружить вероятно диск /dev/sda — он ещё не разбит на разделы (вроде /dev/sda1) — и этим мы сейчас займёмся.

Запустите утилиту fdisk указав ей в качестве параметра обнаруженный диск, то есть fdisk /dev/sda — у неё простой интерфейс, команды однобуквенные — и сразу напоминает что для списка команд можно нажать m (почему-то).

список команд появляющийся по команде

список команд появляющийся по команде «m»

Из всего этого многообразия нам нужно немного:

  • создать новую таблицу разделов (сделайте DOS partition table) нажав «o»

  • создать новый раздел (первичный) нажмите «p» и выделите под него весь диск

  • сделайте этот раздел загружаемым (toggle bootable flag) нажав «a»

  • можете проверить получившуюся таблицу нажав «p»

  • и наконец запишите все изменения (и выйдите) нажав «w»

Теперь команда ls /dev/sd* будет сообщать что у вас появился ещё и раздел /dev/sda1 — им мы будем активно пользоваться в дальнейшем.

В частности нужно создать на нём файловую систему — это простая команда
mkfs.ext4 /dev/sda1

Теперь всё готово к записи загрузчика. Ну или почти всё (но об этом чуть позже).

Можно было создать таблицу разделов GPT (не имеющую отношения к модному сейчас ИИ) -, но для наших целей это не важно, а более архаичная MBR немного упростит эксперимент.

Загрузчик «extlinux»

Популярным в Linux загрузчиком является grub2. Однако он достаточно большой и сложный в смысле конфигурации, поэтому в рамках эксперимента полезно посмотреть на альтернативы:

  • также популярный systemd-boot -, но он по-моему требует EFI что создаёт дополнительные ненужные шаги в нашем эксперименте

  • syslinux / extlinux -, а вот их мы и возьмём в дело, тем более что они использованы для самого SystemRescueCD

Мы используем extlinux — это версия syslinux для ext4fs, линуксовой файловой системы. Если вы попробуете запустить его из командной строки он выдаст подсказку, которая сообщает среди прочего что диск для установки нужно сперва примонтировать.

У нас есть пустая директория /mnt — давайте туда его и подключим:

mount /dev/sda1 /mnt

Это необязательно, но чтобы не нарушать популярной схемы размещения файлов, давайте создадим папку /boot в корне:

mkdir /mnt/boot

Теперь всё готово к записи загрузчика, используйте команду которую он и сам подсказывает:

extlinux --install /mnt/boot

Он поместит пару файлов в указанный каталог, а кроме того (благодаря ключу --install) запишет загрузочный код в начало раздела. К сожалению этого ещё недостаточно для запуска, сейчас мы убедимся.

Можете для любопытства посмотреть что теперь в корневой папке нашего диска и в папке /boot — с помощью команд ls /mnt и ls /mnt/boot сооответственно — когда налюбуетесь, давайте отмонтируем диск (чтобы быть уверенными что всё записалось) командой umount /mnt после чего выполним следующее:

  • в настройках виртуалки (меню Devices) извлеките виртуальный CD

  • в меню Machine нажмите Reset

Машина перезагрузится и снова пожалуется что у вас нет загрузочного девайса!

Это потому что отсутствует загрузочный код в самом первом секторе таблицы разделов (MBR — master boot record). Вставьте виртуальный диск обратно, перезагрузите машину снова в SystemRescue и давайте исправим этот недочёт.

Запись MBR — мелкий штрих

Вообще-то это опционально. Можно обойти проблему стартуя с SystemRescueCD и выбирая пункт «Boot existing OS» — в этом случае «эстафетная палочка» загрузки переходит сразу к нужному разделу диска, минуя MBR. Но всё же потратим пару минут чтобы сделать сразу хорошо.

Где-то в недрах файловой системы лежит файл mbr.bin — в нём как раз код который нужно записать. Найдите его командой

find / -name mbr.bin

у меня он оказался в /usr/lib/syslinux/bios/mbr.bin — запишите его на диск командой cat:

cat /usr/lib/syslinux/bios/mbr.bin > /dev/sda

(просто sda, а не sda1 — т.к. это MBR). Теперь если вы повторите эксперимент с перезагрузкой, вы должны увидеть что extlinux запустился — он скажет что не нашёл конфигурационного файла и покажет приглашение boot: — в принципе тут можно вручную указать ядро и параметры загрузки. Но ядра у нас пока нет.

Добавим ядро, а лучше два

Итак, вновь перезагрузите машину в SystemRescue — в дальнейшем не «извлекайте» виртуальный CD, а когда требуется попробовать загрузку с жёсткого диска просто используйте пункт «Boot Existing OS» из загрузочного меню.

Примонтируйте жёсткий диск как и раньше и перейдите в директорию /mnt/boot — давайте затащим сюда ядро!

А где его взять? нетрудно догадаться что как минимум одно должно быть где-то в недрах самого SystemRescueCD — попробуем найти его (оно обычно имеет название начинающееся с vmlinuz:

find / -name vmlinuz*

У меня оно нашлось например где-то в недрах /usr/lib — файл размером около 5 мегабайт. Скопируем его (находясь в /mnt/boot):

cp /usr/lib/.../vmlinuz vmlinuz1

Как видите, мы задали ему имя с суффиксом 1 — загрузке это не помешает, а мы сможем различать ядра. Так как мы собираемся попробовать разные.

Дело в том что Linux (и другие *nix системы) позволяют легко подменять ядра, выбирая нужное при загрузке. Второе ядро я взял из основной ОС на моём ноутбуке (Ubuntu 18.04 кажется). У вас под рукой такой возможности может не быть, но наверняка вы можете найти разные ядра в интернете. Свои я загрузил на гитхаб — так что вы можете воспользоваться прямой ссылкой:

wget https://github.com/RodionGork/bare-linux-experiment/raw/refs/heads/main/vmlinuz64

Естественно, сделайте это в той же папке /mnt/boot чтобы ядра лежали рядом с загрузочными файлами (это необязательно, но удобно). Если вы обнаружите что виртуалка не может достучаться в сеть, проверьте настройки сети (в ней) — для запросов в интернет проще всего выбрать NAT.

Внимание: использовать «готовые» ядра затащенные непонятно откуда — плохая идея! По-хорошему нужно взять исходники и вдумчиво скомпилировать ядро нужной версии и с требуемыми настройками. Мы пропускаем этот шаг только для упрощения эксперимента!

Так или иначе, надеюсь запастись ядрами вам удалось и команда ls /mnt/boot показывает наличие файлов vmlinuz1 и vmlinuz64 — попробуем их загрузить!

Не забудьте umount, а теперь перезапускайте машину и выбирайте «Boot existing OS».

В приглашении загрузчика, которое выглядит как boot: пишите /boot/vmlinuz1 например — полный путь до скачанного нами ядра. Жмите Enter.

Через пару секунд активной деятельности на экране появится сообщение c «kernel panic» и «Unable to mount root fs…»

476b1c851975f3574467d52c94fbad8b.png

Прекрасно, ядро грузится, но зачем-то хочет какую-то «VFS»?

Готовим initrd — виртуальную файловую систему

Дело обстоит так что современные *nix-овые ядра предполагают такой порядок загрузки, что сразу после запуска оно ищет небольшой образ с файловой системой которую можно развернуть прямо в оперативке. А уж остальные файловые системы (на диске и т.п.) подключить потом, проведя разные дополнительные инициализации.

Образ с этой файловой системой передаётся параметром ядра initrd=... (от слов «init root directory» что ли)

Мы подготовим ему такой образ, состоящий из всего одного файла — нашей пользовательской программы! Здесь мы пойдём на ещё один трюк — первая программа которую ядро пытается запустить — это init — некий самый главный процесс ОС. Вот мы и назовём исполнимый файл нашей приложеньки именно так — и разместим в корне.

Далее мы попробуем написать и скомпилировать пару незамысловатых программ — если у вас под рукой нет на чем их скомпилировать — не беда — вы сможете скачать готовые образа initrd в том же репозитории где лежат ядра.

Напишем незамысловатую программу на С — она просто вводит строчки от пользователя и печатает их длину:

#include 
#include 

int main() {
  printf("I'm mini-shell, type in your commands:\n");
  while (1) {
    char ur[1024];
    fgets(ur, sizeof(ur), stdin);
    if (ur[0] < ' ') break;
    printf("%ld - not supported\n", strlen(ur));
  }
  return 0;
}

Программа представляется как «mini-shell» хотя на самом деле конечно никакой это не shell — если вы захотите добавить здесь какие-то полезные команды, придётся их заимплементить. Пока что простим себе этот маленький обман и соберем программу, указав ключ статической компиляции (т.к. никаких динамических библиотек у нас под рукой не будет). Эта программа использует только функции стандартной библиотеки C (которые будут добавлены в исполнимый код) и системные вызовы ядра для ввода и вывода — так что все должно быть в порядке. Назовите файл init.c

gcc --static -o init init.c

Теперь нужно закинуть скомпилированный файл init в виртуалку. В ней во-первых перезагрузитесь снова в SystemRescueCD, примонтируйте диск и перейдите в /mnt/boot -, а во-вторых переключите настройки сети на «host network only» (перед этим нужно в настройках самого VirtualBox создать новый host-only адаптер). После эого вы сможете либо приконнектиться из виртуалки к родительской машине по sftp и стянуть файл, либо запустите на родительской машине какой-нибудь веб-сервер (хотя бы python3 -m http.server) и из виртуалки вытяните файл wget-ом. В обоих случаях адрес родительской машины будет что-то в духе 192.168.56.1 (проверьте ifconfig-ом).

Когда вам удалось заполучить скомпилированный файл init в виртуалке, убедитесь что у него присутствует исполнимый флаг (или просто проставьте его для уверенности chmod u+x init) — теперь нам нужна уличная магия собирающая образ с помощью команды cpio:

echo init | cpio -o --format=newc > initrd-c

в результате появится файл initrd-c который мы и собираемся использовать при запуске. Отмонтируем диск, перезагружаемся в «existing OS» и пробуем загрузить ядро указав нужный initrd файл:

boot: /boot/vmlinuz1 initrd=/boot/initrd-c

С большой долей вероятности эта попытка обломится — вы увидите похожий экран с логом загрузки, однако утверждающий что не удалось запустить init. Немного выше по логу возможно будет отыскать конкретную ошибку, например (error -8), как-то так:

e06cdeb7a9d1f4a6d2634a0490c3c800.png

Если вы увидите error -13 — это означает «permission denied» — вы забыли сделать файл исполнимым. А вот error -8 про другое «wrong executable file format» — файл собран под 64-битную систему, а ядро запускает 32-битную.

Естественно это зависит от того на какой системе и как компилировали файл.

Исправить ситуацию можно двумя способами — либо попробуйте указать другое ядро при запуске (то которое vmlinuz64) — либо возьмите другой initrd-файл — например в репозитории выше есть initrd-asm — в нём init собранный маленькой программой на ассемблере (её код там тоже где-то есть для любопытных — своего рода «hello-world»). Если захотите собрать сами, используйте команды

as --32 init.c && ld -m elf_i386 -o init a.out

После чего затащите его в виртуалку и запакуйте как и раньше (для удобства предлагаю файл назвать иначе — например initrd32 или initrd-asm).

Что касается этой ассемблерной программы — она лишь развитие примерчика из прошлой статьи про 5 ассемблеров — если вам любопытно углубиться, я обязательно напишу отдельную статейку с разбором этой программульки после которой вы сами сможете писать подобные — для развлечения или в образовательных целях!

Думаю, с нескольких попыток вам повезет :) Вы либо увидите сообщение что «mini-shell» готов к вашим экспериментам — попробуйте вводить строки, а когда надоест нажмите Ctrl-C — чтобы узнать что случается если init-процесс в линуксе завершается. Либо увидите сообщение про «nedo-bash».

С vmlinuz64 наш

С vmlinuz64 наш «недо-баш» умеет читать с клавиатуры строчки и сообщает их длину

Конфигурация для загрузчика extlinux

Можно добавить рядом с файлами загрузчика файл конфигурации, чтобы по умолчанию загружалось некоторое выбранное ядро с некоторым выбранным initrd (и прочими опциями если нужно). Для этого в SystemRescueCD примонтируйте диск (в очередной раз) и используя vi или nano создайте файл /mnt/boot/extlinux.conf с примерно таким содержимым:

prompt 1
timeout 100
default testlinux

label testlinux
kernel vmlinuz64
append initrd=initrd-c

Первая строчка означает что нужно показать приглашение boot: чтобы пользователь мог ввести альтернативные параметры загрузки, вторая задаёт таймаут (в десятых долях секунды) после которого продолжится загрузка отмеченная названием указанным в третьей строке.

Заключение

Надеюсь вам удалось справиться с этим «упражнением» до конца. Как вы понимаете — оно лишь отправная точка для дальнейших экспериментов. Остались разнообразные интересные вопросы которые можно поисследовать.

Например можно утащить initrd файл с Убунты (он весит около 50 мб) — и попробовать подключить его. Вы получите уже более менее рабочую систему — однако убедитесь что надо создать на диске кое-какие папки (вроде /dev) да и подмонтировать его как рутовую систему.

А можно поинтересоваться, почему наш «nedo-bash» хотя выводит сообщение на экран, но ввод осуществляет только будучи запущенным с одним из двух ядер — как будто у другого ядра не включена клавиатура. Впрочем тут лучше вернуться к совету упомянутому выше — не стоит использовать непонятные-незнакомые ядра. Попробуйте собрать своё.

Отдельным направлением может быть эксперимент с другими ядрами — возьмите ядро от Gnu Hurd — или от FreeBSD. Правда компилируя для них программы нужно иметь в виду что у них немного отличающийся формат системных вызовов в сравнении с Linux (так что просто собрав программу на Linux-машине вы возможно не получите то что нужно).

Вообще это отдельная интересная тема — номера системных функций у *nix-овых систем совпадают, у Линукса в том числе -, но во-первых в 64-битном линуксе их внезапно перетасовали, во-вторых Линукс делает вызовы передавая параметры в регистрах (а-ля ДОС) — в то время как остальные пушают их «по-сишному» через стек. В общем, тоже нужна отдельная статья!

Пожалуй часть этих вопросов я сам постараюсь осветить в ближайшее время, они достаточно занятны!

© Habrahabr.ru