Архитектура i386 содержит в себе массу функций, призваных обеспечить совместимость со старыми машинами, вплоть до серии 8086. С технической точки зрения, в этом нет необходимости, так как любой современный компьютер, основанный на этой архитектуре использует полностью 32-разрядную операционную систему, которая могла работать и без кода, обеспечивающего обратную совместимость. К сожалению, требования к совместимости остаются и тормозят развитие нового программного обеспечения.
Одной из деталей, которая не изменялась в течение многих лет является процесс загрузки i386. Спроектирован он был в дни, когда компьютеры имели накопители на гибких магнитных дисках и небольшое firmware. С тех пор процедура не перенесла никакого изменения, что делает некоторые задачи очень сложными. Одна из них — загрузка нескольких операционных систем на одной машине.
Новое firmware для Intel-based машин, Extensible Firmware Interface (EFI) решает эту проблему, обеспечивая более универсальный процесс загрузки. Другие архитектуры уже предоставляют улучшенные firmware с более хорошими механизмами загрузки, включая, например, способность загрузки и выполнения ELF обряза прямо из кода инициализации машины. Так обстоит дело с OpenFirmware в Macintosh PowerPC.
Даже не смотря на наличие альтернативных firmware, архитектура i386, известная сейчас, будет с нами еще достаточно долгое время. Поэтому, было бы хорошо решить некоторые из ограничений с помощью программного обеспечения. Это — то, что пытается сделать в загрузочной области Multiboot Specification: обеспечить способность загрузить любую операционную систему с помощью единственного загрузчика, моделируя таким образом улучшеное firmware.
Я недавно изменил ядро NetBSD таким образом, чтобы обеспечить поддержку Multiboot. В коде есть много справочной информации, но основная идея этого эссе заключается в том, чтобы дать представление о Multiboot и показать Вам, что реальная операционная система может легко быть преобразована для поддержки этой спецификации. Пожалуйста отметьте, что все комментарии в коде указывают на netbsd-4, чтобы гарантировать, что код остается совместимым с объяснениями, данными здесь.
Процесс загрузки i386
Традиционная i386 архитектура использует очень простое встроенное программное обеспечение, известное как Basic Input/Output System или BIOS. BIOS отвечает за инициализацию аппаратных средств после включения машины и обеспечивает интерфейс низкого уровня, чтобы обратиться к нему из загрузчика и ОС. К сожалению, было унаследовало много ограничений: этот сервис доступен только из реального режима и поддерживаются абстракции высокого уровня для используемого оборудования.
Как следствие, BIOS не может получить доступ к любой файловой системе на диске (в том числе и FAT), поэтому не может непосредственно загрузить исполняемый модуль — например ядро ОС. Вместо этого весь BIOS загружает первый сектор загрузочного диска в специфическую область памяти (07C0h:0000h) и передает контроль выполнения ей. Ко всем неприятностям, каждое ядро традиционно предоставляет свой загрузочный код, сообразно своим потребностям. Например, MS-DOS загружается с раздела FAT и выполняется в реальном режиме. Другие системы могут грузиться с различных ФС (FAT, NTFS, Ext2 и т.д) и работают в защищенном режиме с самого начала, так как их ядра слишком большие, чтобы вписаться в первый мегабайт памяти (вся память, адресуемая из реального режима).
Факт, что каждая ОС нуждается в собственном загрузчике, вызывает много проблем при установке несколько различных систем на машине и задает много вопросов, на которые пользователь наиболее вероятно будет неспособен ответить. Что Вы устанавливаете в MBR? Куда Вы помещаете каждый загрузчик? Почему Вы должны конфигурировать каждый из них независимо? Почему Вы нуждаетесь в больше чем одном загрузчике?
Могло бы быть очень удобно, если бы имелся один общий интерфейс, который разделял загрузку и начальное выполнение ядра ОС от загрузчика. Пойдя этим путем, разработчик ОС мог бы сосредоточиться исключительно на прикладных задачах и забыть о написании загрузчика. Точно так же разработчики загрузчика могли объединить силы, чтобы написать более полную утилиту или, на пример, написать минимально требуемый код, способный загрузить любую ОС, поддерживающую данный интерфейс (все ОС, в идеальном случае). Хорошо то, что разработчики GRUB уже озаботились этой идеей в прошлом и разработали такой интерфейс: Multiboot.
Спецификация Multiboot
Multiboot Specification (MS) определяет протокол между загрузчиками и ядрами ОС, который позволяет любому Multiboot-совместимому загрузчику загружать и выполнять любое Multiboot-совместимое ядро ОС. Это разрешает конечному пользователю устанавливать единственный загрузчик на его машину и использовать его для загрузки любой системы непосредственно, без необходимости в цепной загрузке различных утилит.
Чтобы достигнуть такой абстракции, MS определяет два элемента:
— Multiboot Header (MH) Это — 4-байтовая упорядоченная структура данных, расположенная в пределах первых 8 Кбайт ядра ОС. Это обеспечивает магическое число, по которому идентифицируется файл, как являющийся Multiboot-совместимым, ряд флагов, указывающих определенные потребности ядра и дополнительные области, описывающие структуру бинарного файла. Дополнительные области используются только если ядро находится в формате a.out (с некоторыми исключениями). Использование формата ELF делает Это всё намного более простым и более универсальным.
— Multiboot Information Structure (MIS)Это — структура данных, созданная загрузчиком и переданная ядру ОС как часть процесса загрузки. Она включает в себя такую информацию, как загрузочный диск, карта памяти, параметры ядра, размещение в памяти дополнительных модулей ядра (если они загружаются) и т.д.
Эти две структуры взаимодействуют между собой, например MH может запросить загрузчик установить некоторые поля в MIS для успешной начальной загрузки. Если загрузчик не может выполнить потребности ядра, загрузка будет корректно прервана.
Если Вы используете эти две структуры, будет простым делом написать простой бинарный файл, который действует как ядро. То есть бинарный файл может выполняться обособленно, без необходимости в любой другой ОС. Смотрите boot.S, kernel.c и связанные файлы, которые формируют образцово-показательное ядро в каталоге docs.
Также интересно отметить, что Multiboot-совместимый загрузчик перейдет в защищенный режим и установит предварительную глобальную таблицу дескрипторов (GDT) для модели непрерывной памяти, таким образом ядро не должно делать этого самостоятельно. Конечно, ядро должно будет перезагрузить глобальную таблицу дескрипторов собственными значениями позже в процессе инициализации, но того, что было установлено загрузчиком достаточно для старта. Становится возможным написать ядро ОС таким образом, как будто реальный режим не существовал вообще, как это происходит на некоторых (более крутых) платформах.
Процесс загрузки NetBSD/i386
NetBSD/i386 использует двухступенчатый загрузчик. Первая часть устанавливается по известному физическому месторасположению, обычно это первый сектор жесткого диска или раздела, в котором установлена система и имеется некоторое зарезервированное свободное пространство в файловой системе. Эта небольшая программа, которая имеет ограниченный размер, способна прочитать вторую часть загрузчика может передать ему контроль выполнением. Он устанавливается внутри корневой файловой системы как /boot и его физическое местоположение может изменяться после перезагрузок: файловая система не связывает файл с определенными дисковыми блоками.
Как только вторая часть загрузчика получает управление, она переводит машину в плоский защищенный режим (нет страниц, сегменты занимают все пространство памяти), загружает ядро с диска и запускает его. Оно также принимает команды пользователя для выбора загружаемого ядра и его опций. Этот загрузчик также передает загрузочную информацию к ядру посредством структуры bootinfo. Проще говоря, это — таблица, которая содержит информацию, о машине и переменных среды, включая:
— Объем доступной памяти
— Загрузочное устройство
— Имя ядра
— Какая консоль подсоединена (видеокарта, COM)
Файл src/sys/arch/x86/include/bootinfo.h содержит полный список возможных значений элемента bootinfo. bootinfo подобен а MIS, хотя информация, которую он включает, немного отлична и, в некоторых определенных случаях, более полна. Фактически, это было самой большой головной болью, при адаптации ядра NetBSD для поддержки Multiboot.
После того как ядро получит управление происходит следущее:
— Сохранение загрузочной информации (bootinfo или MIS) в безопасном месте. Смотрите функцию native_loader src/sys/arch/i386/i386/machdep.c.
— Определение используемой модели CPU
— Установка предварительного каталога страниц и соответствующих таблиц страниц, чтобы повторно отобразить виртуальные адресации ядра выше 0xC01000000.
— Установка страниц и переход в верхнюю область памяти
— Продолжение процесса загрузки и обработка загрузочной информации в течение ее инициализации.
Формат ELF решает эту проблему: каждая секция в образе (текст, данные, bss и так далее) определяет, какой адрес является его стартовой виртуальной адресациейе и указывает его физический адрес. Скрипт компоновщика ядра NetBSD использует это обстьоятельство в своих интересах для генерации образа ELF формата, отображаемого по 0xC0100000, но помещенного по физическому адресу 0x00100000. Обратите внимание, что адрес не нечинается с 0x00000000, чтобы гарантировать то, что ядро не перезапишет BIOS и/или данные, сохраненные ниже первого мегабайта (единственное адресное пространство, доступное из реального режима) при загрузке.
Прежде, чем будет разрешено использование страниц, очень критичным становится код ядра, потому что необходимо делать все возможное чтобы не использовать прямые адреса, сгенерированные компоновщиком (поскольку они указывают на недоступные области памяти). Макрос RELOC решает эту проблему, преобразовывая данную виртуальную адресацию в соответствующее физическое местоположение. К счастью, как только появляются страницы, эта проблема отпадет.
Делаем NetBSD Multiboot-совместимой
Из-за некоторых ограничений в родном загрузчике NetBSD, я должен был загружать NetBSD с помощью GRUB на резервной машине, которую я использовал для экспериментов над ядром. Делая так, я нашел, что поддержка загрузки немодифицированной NetBSD реализовано в GRUB очень рудиментарно, вплоть до ошибок в некоторых случаях. Например, не правильно устанавливается ksyms, таким образом вывод ddb(4) очень трудно понять. Кроме того, код не поддерживает загрузочные опции для ядра, но к счастью pkgsrc включает некоторые патчи для решения этой проблемы.
Было два различных решения данной проблемы: исправление GRUB, чтобы включить полную поддержку загрузки NetBSD или сделать ядро NetBSD Multiboot-совместимым. Я выбрал второе потому что мне нравится идея Multiboot: она дает возможность создания абстрактного интерфейса между двумя различными системными компонентами. Что еще более важно, хотя изменение ядра NetBSD само по себе достижение, что в код GRUB не будет вноситься изменений: разработчики GRUB — являются ядром разработчиков Multiboot Specification и, поскольку в GRUB не будет никаких патчей, специфичных для NetBSD, нет необходимости в наличии данной ОС для того, чтобы гарантировать ее поддержку.
Первый шаг должен был определить некоторые структуры данных высокого уровня для представления и управления MH и MIS изнутри ядра — многое становится легче благодаря наличию хорошой документации. Результаты находятся в src/sys/arch/i386/include/multiboot.h.
Тогда, очевидным действием было бы добавить MH к ядру так, чтобы GRUB мог его опознать. Чтобы гарантировать, что это все хозяйство будет располагаться в пределах первых 8 КБ образа, я добавил его в src/sys/arch/i386/i386/locore.S рядом с точкой входа ядра. Это не было легко: скрипт компоновщика ядра имел ошибку, которая заставляла физические адреса секций указать на виртуальные адреса. Это вынудило меня использовать область адресов в MH для индикации, где загружается файл, но GRUB не проверяет их для ELF. Я собирался патчить GRUB, пока такой же разработчик, Павел Кахина, не устранил проблему в корне: он переписал скрипт компоновщика так, чтобы генерировались соответствующие физические адреса. В настоящее время дополнительные области в MH не используются, и GRUB (имеющийся практически в любом дистрибутиве GNU/Linux) может загрузить ядро NetBSD.
С ядром, опознаваемым как Multiboot, мне необходимо было добавить код для анализа MIS в период начальной загрузки и преобразовать его в родной формат bootinfo, чтобы минимизировать изменения во всем ядре. Имейте в виду, что я только добавил другую точку входа в ядре, не удаляя старую, таким образом обе будут сосуществовать. Такая хитрость была необходима из-за изменения виртуального адресного пространства в период начальной загрузки, как объяснено ранее: обработка MIS должна произойти прежде, чем ядро разрешало страницы и разрушало важную информацию (в основном ksyms). Код C, обрабатывающий это, довольно деликатен и поэтому я сохранял его как можно короче. Как только ядро разрешило страницы, уже произошел реальный синтаксический анализ MIS и ядро продолжает загрузку. Вы можете увидеть все это в исходном файле src/sys/arch/i386/i386/multiboot.c.
Наконец, я хотел бы прокомментировать еще одну проблему. Я не понял это предложение he native boot loader loads the NetBSD kernel and stores it in memory following a specific memory layout: the kernel is first, followed by an integer that registers how many symbols the kernel has, followed by a minimal ELF image that contains the kernel’s symbol and string tables. Я не понял это предложение. Используя GRUB, эти таблицы загружаются в различных и непредсказуемых местах. Первым путем в решении этой проблемы было бы зарезервировать некоторое пространство в ядре, чтобы скопировать таблицу символов в соответствующее место на лету, но это было бы совсем некрасиво. Сейчас я перемещаю таблицу символов сразу после ядра (заботясь, чтобы не записать ее поверх ядра или другой важной информации при перемещении). Ядро также имеет определенную функцию инициализации глобальной таблицы ksyms, основанную на области памяти (не обязательно полный образ ELF). Вы можете увидеть это в функции ksyms_init_explicit, определенной в src/sys/kern/kern_ksyms.c.
Заключение
Если бы все операционные системы поддерживали Multiboot Specification, то пользователи были бы более счастливы, чем теперь: продвинутый загрузчик, поддерживающий все операционные системы пользовался успехом и его инсталляция могла быть значительно упрощена. Плюс ко всему, эти пользователи могли бы не заботиться об инсталляции ОС плохо сочетающихся друг с другом.
Лично я нашел процесс модификации ядра NetBSD для поддержки Multiboot очень интересной и поучительной задачей. Кроме того, поскольку почти все дистрибутивы Linux в настоящее время устанавливают по умолчанию GRUB, стало намного проще установить машину с двумя ОС — и Linux и NetBSD. В будущем необходимо разделение ядра на две различные части в пределах образа ELF, назначая каждой из них разные виртуальные адреса. Это могло упростить решение некоторых проблем, которые возникают в коде, выполняемом прежде, чем разрешаются страницы.