Голый Линукс — запуск ядра-одиночки
- суббота, 9 ноября 2024 г. в 00:00:08
Итак, Linux - не операционная система, а только ядро для неё. Всё остальное приходит от проекта GNU (и других). И вот интересно - на что годится ядро само по себе?
Эта статья - очень "начального" уровня. Устроим маленький эксперимент - создадим чистую виртуальную машину и попробуем запустить ядро Linux "без всего". Или почти "без", т.к. нам понадобится загрузчик ОС - и какая-нибудь "пользовательская программа" (её мы сотворим сами). Конечно, продвинутые пользователи Linux такой "эксперимент" могут провести просто отредактировав параметры запуска при включении - но наш рассказ всё же для тех кто почти (или совсем) не в теме :)
Бонусом чуть-чуть коснёмся системных вызовов и пару слов скажем о других ядрах.
На данном этапе нам достаточно представлять что ядро - некая программулина в виде здорового файла. Говоря по-программистски - фреймворк для запуска наших программ - и в то же время большая библиотека системных вызовов и коллекция драйверов.
Иными словами ядро воплощает работу со всеми основными сущностями ОС (в частности, процессами - в которых будут запускаться другие программы) - а также содержит драйвера для работы со всевозможным оборудованием компьютера. Ну, не все возможные на свете драйвера - но для наиболее актуальных и популярных систем. Для дисков, экрана, сетевых интерфейсов.
Поэтому наша цель - запустить ядро и передать управление небольшой демонстрационно программке которая сможет успешно "дёргать" эти системные вызовы. Попутно мы рассмотрим некоторые дополнительные вещи - хотя не актуальные для нашего эксперимента - но о которых полезно иметь представление.
Вообще можно и на физической машине потренироватся, но мы рекомендуем начать с виртуалки. Скачайте VirtualBox (или другой эмулятор, если у вас есть какой-то любимый), создайте новую виртуалку (можно задать ей гигабайт оперативки, а можно и меньше - и диск автоматического размера - нам потребуется совсем немного).
Здесь и далее мы хотя говорим подробно о шагах которые следует выполнить, но всё же избегаем излишне детальных указаний "нажмите такую-то кнопку". Пусть подобные мелочи останутся именно в качестве "упражнения" :) В частности интерфейс VirtualBox довольно интуитивный, да и нагуглить вопросы по ней легко.
Итак, машина создана - если вы попытаетесь её запустить, появится чёрный экранчик с сообщением что не найдено устройство с которого можно загрузиться. Оно и понятно - ведь диск пока девственно чист. Нужно записать на него загрузчик. Очевидно для любых операций нужно временно запуститься с какого-нибудь LiveCD. Опять же можете выбрать по своему усмотрению, но я рекомендую (для данной цели) скачать относительно небольшой образ SystemRescueCD. Скачайте образ и подключите в настройках созданной виртуалки в качестве компакт-диска. Перезапустите машину - после загрузки должно появиться загрузочное меню системы на LiveCD:
Сейчас мы запустим дефолтный пункт меню (просто нажмите Enter) - но на будущее обратите внимание на пункт "Boot existing OS" - он отменяет загрузку с LiveCD и грузит то что у вас установлено на основном диске - мы будем этим пользоваться чтобы удобнее проверять что получилось.
Итак нажмите "Boot SystemRescueCD using default options". Начнётся какая-то активность которая рано или поздно закончится надписью "automatic login" и ниже приглашением командной строки в духе [root@systemresccd ~]#
- в принципе можно запустить графическую оболочку (startx) но нам это сейчас не нужно.
Наш жёсткий диск виден среди девайсов, попробуйте ввести ls /dev/sd*
и вы должны обнаружить вероятно диск /dev/sda
- он ещё не разбит на разделы (вроде /dev/sda1
) - и этим мы сейчас займёмся.
Запустите утилиту fdisk
указав ей в качестве параметра обнаруженный диск, то есть fdisk /dev/sda
- у неё простой интерфейс, команды однобуквенные - и сразу напоминает что для списка команд можно нажать m
(почему-то).
Из всего этого многообразия нам нужно немного:
создать новую таблицу разделов (сделайте DOS partition table) нажав "o"
создать новый раздел (первичный) нажмите "p" и выделите под него весь диск
сделайте этот раздел загружаемым (toggle bootable flag) нажав "a"
можете проверить получившуюся таблицу нажав "p"
и наконец запишите все изменения (и выйдите) нажав "w"
Теперь команда ls /dev/sd*
будет сообщать что у вас появился ещё и раздел /dev/sda1
- им мы будем активно пользоваться в дальнейшем.
В частности нужно создать на нём файловую систему - это простая командаmkfs.ext4 /dev/sda1
Теперь всё готово к записи загрузчика. Ну или почти всё (но об этом чуть позже).
Можно было создать таблицу разделов GPT (не имеющую отношения к модному сейчас ИИ) - но для наших целей это не важно, а более архаичная MBR немного упростит эксперимент.
Популярным в 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 и давайте исправим этот недочёт.
Вообще-то это опционально. Можно обойти проблему стартуя с 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..."
Прекрасно, ядро грузится но зачем-то хочет какую-то "VFS"?
Дело обстоит так что современные *nix-овые ядра предполагают такой порядок загрузки, что сразу после запуска оно ищет небольшой образ с файловой системой которую можно развернуть прямо в оперативке. А уж остальные файловые системы (на диске и т.п.) подключить потом, проведя разные дополнительные инициализации.
Образ с этой файловой системой передаётся параметром ядра initrd=...
(от слов "init root directory" что ли)
Мы подготовим ему такой образ, состоящий из всего одного файла - нашей пользовательской программы! Здесь мы пойдём на ещё один трюк - первая программа которую ядро пытается запустить - это init
- некий самый главный процесс ОС. Вот мы и назовём исполнимый файл нашей приложеньки именно так - и разместим в корне.
Далее мы попробуем написать и скомпилировать пару незамысловатых программ - если у вас под рукой нет на чем их скомпилировать - не беда - вы сможете скачать готовые образа initrd в том же репозитории где лежат ядра.
Напишем незамысловатую программу на С - она просто вводит строчки от пользователя и печатает их длину:
#include <stdio.h>
#include <string.h>
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)
, как-то так:
Если вы увидите 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".
Можно добавить рядом с файлами загрузчика файл конфигурации, чтобы по умолчанию загружалось некоторое выбранное ядро с некоторым выбранным 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-битном линуксе их внезапно перетасовали, во-вторых Линукс делает вызовы передавая параметры в регистрах (а-ля ДОС) - в то время как остальные пушают их "по-сишному" через стек. В общем, тоже нужна отдельная статья!
Пожалуй часть этих вопросов я сам постараюсь осветить в ближайшее время, они достаточно занятны!