Напишите свой собственный загрузчик uefi

Обновлено: 21.11.2024

Программирование UEFI — первые шаги

В этой статье я опишу шаги, необходимые для начала разработки реальных приложений UEFI на ПК с архитектурой x86, и поделюсь некоторым практическим опытом решения проблем, связанных с этим. Я сосредоточусь на 64-битной версии UEFI, потому что 32-битная версия в этой области мало используется (скорее всего, из-за решения Microsoft не поддерживать UEFI в 32-битной Vista). Итак, чтобы выполнить некоторые из моих шагов, вам понадобится 64-битный процессор (но не 64-битная ОС, вы также можете использовать любую 32-битную ОС). Мы закончим эту статью приложением EFI Hello World.

Эта статья является продолжением моей предыдущей статьи Введение в UEFI. Прежде чем читать дальше, убедитесь, что поняли все, что там описано.

Конечно, все, что вы пытаетесь сделать в соответствии с этой статьей, вы делаете на свой страх и риск.

Получение оборудования

Чтобы начать разработку UEFI, прежде всего вам необходимо получить материнскую плату, BIOS которой поддерживает UEFI. (точнее, наверное, следует сказать «чья прошивка имеет поддержку UEFI», но я буду использовать эту форму). Выяснить, поддерживает ли тот или иной BIOS UEFI, часто оказывается довольно сложной задачей. Производители материнских плат лицензируют BIOS у других компаний, обычно у AMI (Aptio, AMIBIOS), Phoenix (SecureCore, TrustedCore, AwardCore) или Insyde (InsydeH20). Забудьте об определении поддержки UEFI только по статистике конечных пользователей, которую вы видите в большинстве магазинов. Поскольку поддержка UEFI все еще находится в экспериментальном состоянии, во многих случаях она даже не указана в технической спецификации материнской платы. В таком случае вам остается гуглить и спрашивать на форумах, где вы часто получаете только внутреннее название бренда, которое часто трудно сопоставить с обозначением продукта для конечного пользователя.

Одна хитрость, которая, как я обнаружил, работает для плат Intel (но она вполне может работать и для других плат), заключается в том, чтобы посмотреть примечания к выпуску обновления BIOS, например. документ, в котором перечислены изменения и исправления BIOS. Если плата поддерживает UEFI, вы, вероятно, найдете упоминание UEFI там (и только там в случае Intel).

Короче говоря, определить поддержку UEFI гораздо сложнее, чем может показаться. Некоторые машины с этой технологией перечислены здесь. Я использую плату Intel DG33BU (по какой-то причине она продавалась как Intel DG33BUC).

Вам также понадобится место для загрузки. Теоретически должно быть достаточно только USB-пера, но на практике ни один из 4 брендов, которые я пробовал, не работал с реализацией UEFI моей платы. Поэтому вам, возможно, придется использовать жесткий диск. Я настоятельно рекомендую диск IDE, потому что для дисков SATA может потребоваться некоторая настройка параметров BIOS или они могут вообще не работать. Как и USB-ручки, USB-клавиатура тоже может быть проблемой. Я бы не стал этого сильно бояться, но если вы можете использовать клавиатуру PS/2, сделайте это.

Получение программного обеспечения

Чтобы продолжить разработку UEFI, вам потребуются два пакета разработки: (EDK ) и .

Первый пакет, EFI Development Kit, содержит исходный код TianoCore (общедоступная часть эталонной реализации UEFI от Intel), а также множество примеров и двоичных файлов EFI Shell (мы поговорим об этом позже), все готово к сборке. с хорошей системой изготовления. Его даже можно встроить в эмулятор Win32 UEFI для более удобной разработки и тестирования, но я не буду здесь это описывать. В этой статье я также не буду демонстрировать использование системы сборки EDK. Хотя это может быть хорошей идеей для реального проекта, я хочу дать вам немного больше информации о том, что здесь происходит «под капотом».

Второй пакет, EFI Toolkit, представляет собой набор дополнительных приложений UEFI, таких как FTP-клиент, порт Python, текстовый редактор и т. д. Строго говоря, нам это на самом деле не нужно, но в нем есть набор заголовков C, который несколько упрощает использовать, чем у EDK (если вы не используете систему сборки EDK для своего проекта). Однако обратите внимание, что он еще не содержит всех заголовков — вы быстро столкнетесь с этой проблемой, если попробуете настоящую разработку с ним.

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

И последнее, но не менее важное: вам понадобится 64-битный компилятор C. Я предлагаю Microsoft Visual C++, чья 64-битная версия находится в свободном доступе в Windows DDK. Вы можете получить его здесь.

Загрузка в оболочке EFI

Возможно, до этого момента у вас не было визуального представления об UEFI. В конце концов, это был всего лишь программный интерфейс. Целью этой главы было бы преодолеть это, загрузив так называемую оболочку EFI.Оболочка EFI во многом похожа на любую другую известную вам оболочку: у вас есть командная строка, через которую вы можете вводить команды для просмотра диска, запуска приложений, настройки системы и т. д. Единственное отличие состоит в том, что оболочка EFI построена только с использованием служб UEFI, и как так что для запуска не требуется никакой операционной системы. Работа в оболочке EFI очень похожа на работу в какой-то очень простой операционной системе. Из этой оболочки вы также будете запускать свои приложения (далее в этой статье).

Фактические действия по загрузке оболочки EFI могут сильно различаться в зависимости от марки BIOS. Некоторые BIOS (в основном на MAC, где EFI является основным стандартом) имеют дополнительные параметры для указания файла для загрузки или даже могут иметь встроенную оболочку EFI внутри ПЗУ. Однако у меня есть опыт работы только с реализацией Intel UEFI, отличной от MAC, которая, похоже, поддерживает только самый минимум функций, необходимых для установки EFI-версий Windows. Требования Microsoft к реализации UEFI, среди прочего, указывают, что загрузчик UEFI должен использовать фиксированный путь к файлу для загрузки, и если он по какой-либо причине не может загрузить UEFI, он должен автоматически переключиться на загрузку по умолчанию. Это вызывает много головной боли, когда ваш UEFI не загружается должным образом, и вы должны выяснить, в чем проблема, без какой-либо информации от загрузчика.

Для 64-разрядных реализаций UEFI путь к загружаемому файлу — \EFI\BOOT\BOOTX64.EFI . Загрузчик UEFI ищет этот файл во всех файловых системах, к которым он может получить доступ, и когда он его находит, он выполняется. Как я уже сказал, если файл не найден, загрузка продолжается с прежним BIOS. Загрузчик UEFI может читать таблицы разделов MBR или GPT и может читать только разделы FAT32. Сюда входят USB-накопители, поэтому можно загрузить оболочку EFI с USB-накопителя в формате FAT32. К сожалению, в моих тестах 3 из 4 USB-ручек не работали, а 4-я тоже перестала работать после обновления BIOS. У меня была аналогичная проблема с одним из двух дисков SATA, не работающих с UEFI. Поэтому я настоятельно рекомендую использовать IDE-диск, если у вас есть какие-либо проблемы. Если у вас уже есть хотя бы один раздел FAT32 на вашем диске, вы можете использовать его, в противном случае вам нужно создать новый новый.

Теперь вам нужно скопировать бинарный файл оболочки EFI в этот раздел. Вы можете найти 64-битный бинарный файл оболочки EFI в EFI Development Kit: \Other\Maintained\Application\UefiShell\bin\x64\Shell_full.efi . Скопируйте его в раздел FAT32 как \EFI\BOOT\BOOTX64.EFI .

Теперь перезагрузитесь и войдите в настройки BIOS. На вкладке «Загрузка» вы должны увидеть параметр «Загрузка UEFI», включите его. Если все в порядке (вероятно, нет), теперь после перезагрузки вы должны оказаться в EFI Shell. Если да, поздравляю. Если вы этого не сделали и вместо этого ваша обычная ОС загрузилась как обычно, это, скорее всего, означает, что диспетчеру загрузки UEFI не удалось получить доступ к диску.

Сначала попробуйте войти в меню загрузки во время экрана BIOS (на моей машине F10). Если оболочка EFI в разделе FAT32 была обнаружена, но не загрузилась, вы увидите ее как один из вариантов (в моем случае [Внутренняя оболочка EFI — Жесткий диск]). Если увидишь, просто запусти. Это может произойти, если у вас уже установлена ​​какая-либо операционная система EFI, и она записала себя как загрузочную запись EFI по умолчанию в NVRAM.

Если вы не видите оболочку EFI в меню загрузки, это означает, что загрузчик UEFI не смог найти ни одного диска FAT32 с \EFI\BOOT\BOOTX64.EFI . Если вы пытаетесь загрузить оболочку EFI с USB-накопителя, попробуйте настроить параметры эмуляции USB в BIOS. То же самое относится к дискам SATA и настройкам SATA/IDE. Если ни одна из настроек не работает или ваша машина не загрузилась с IDE-диска, я ничем не могу вам помочь, кроме как перепроверить все, что я написал до сих пор (особенно опечатки в пути к оболочке EFI).

Если у вас есть опыт загрузки оболочки EFI, не описанный в этой главе, напишите мне, чтобы я мог обновить эту статью.

Создание приложения UEFI

Итак, мы находимся в оболочке EFI. Это означает, что мы, наконец, можем протестировать любое написанное нами (64-разрядное) приложение EFI. Время начать писать один. Разумеется, мы будем писать и компилировать приложения в обычной операционной системе, а не в оболочке EFI.

Приложение или драйвер EFI — это обычный DLL-файл Windows PE, только с другим значением подсистемы в заголовке. Есть 3 новых значения: приложение EFI = 10, драйвер службы загрузки EFI = 11, драйвер среды выполнения efi = 12 (числа десятичные). На вопрос, как настроить подсистему, ответим позже, а пока сосредоточимся на файле DLL.

Приложение EFI PE не имеет никаких особенностей, которые есть в Windows PE, таких как таблицы символов, экспорт, статические данные обработки исключений и т. д. В нем даже нет импорта — все, что вам когда-либо понадобится в приложении EFI, передается в качестве аргумента. к функции точки входа. Единственное, что нужно, кроме данных и кода, — это релокации. Итак, это простейшее приложение EFI:

Скомпилируйте его с помощью MS Visual C (укажите свой собственный путь к набору инструментов EFI):

Здесь мы устанавливаем путь к общим заголовкам EFI и к заголовкам EFI для конкретной платформы. Переключатель /c отключает связывание (мы будем связывать отдельно для лучшей читабельности), а /Zl отключает зависимость от библиотек libc по умолчанию.

Теперь у нас есть Windows DLL, нам просто нужно изменить значение подсистемы PE на EFI. Для этого в EFI Toolkit есть простая (глючная) утилита, меняющая подсистему на одно из 3-х значений EFI. Мы находим эту утилиту в EFI_Toolkit\build\tools\bin\fwimage.exe. Чтобы настроить подсистему на приложение EFI, мы будем использовать его следующим образом:

Созданный файл hello.efi теперь должен быть функциональным пустым приложением EFI. Нам просто нужно скопировать его в наш раздел FAT32, перезагрузиться в оболочку EFI и протестировать:

Если мы не получаем сообщения об ошибке, приложение работает.

Программирование UEFI

Теперь мы, наконец, можем углубиться в программирование UEFI. Вашим основным источником этой информации должна быть спецификация UEFI, я лишь подытожу основные моменты.

Прежде всего, мы должны знать кое-что об окружении приложений UEFI. Здесь я опишу только среду 64-битного x86 UEFI (остальное можно найти в спецификации UEFI).

UEFI работает в однопроцессорном плоском 64-разрядном режиме. Обычно UEFI работает с отключенным отображением памяти (физический адрес ОЗУ = адресу виртуального ОЗУ), но поскольку для 64-разрядного режима x86 требуется, чтобы отображение было включено, UEFI отображает всю память так, чтобы виртуальный адрес совпадал с физическим (т. прозрачный). Соглашение о вызовах — обычный 64-битный fastcall (первые 4 аргумента в RCX, RDX, R8, R9 с пространством, зарезервированным в стеке; остальные аргументы передаются стеком; RAX, R10, R11 и XMM0 — XMM5 не сохраняются вызываемой функцией), поэтому вам не нужно беспокоиться о специальных настройках компилятора. Примечательной особенностью EFI является то, что для каждой поддерживаемой архитектуры он определяет точный двоичный интерфейс (ABI).

Теперь давайте посмотрим, как наше приложение взаимодействует со службами UEFI. Во-первых, UEFI предоставляет набор сервисов, называемых . Они доступны для драйверов EFI, приложений и загрузчика ОС во время загрузки. В какой-то момент во время загрузки ОС загрузчик ОС может решить их удалить, и после этого эти службы становятся недоступными. Существует также небольшое количество служб, которые всегда остаются доступными, называемых «Службы времени выполнения». Помимо этих двух наборов услуг, все, что предлагает UEFI, доступно через так называемые . Протокол очень похож на класс в объектно-ориентированном программировании. UEFI сам определяет набор протоколов (например, протоколы, обрабатывающие USB, файловую систему, сжатие, сеть и т. д.), а приложение может определять свои собственные протоколы (отсюда и «Расширяемый» в «Унифицированном расширяемом интерфейсе встроенного ПО»). Протоколы идентифицируются по GUID (погуглите, если не знаете, что это такое). Только очень немногие протоколы являются обязательными в спецификации UEFI, а все остальные могут быть реализованы или не реализованы в конкретной прошивке. Если протокол не реализован в прошивке, вы можете загрузить драйвер, который его реализует, или даже написать такой драйвер самостоятельно.

Теперь давайте посмотрим, как получить доступ к этим службам. Как я уже объяснил, все, что вам нужно, передается в качестве аргумента функции точки входа. Прототип функции точки входа выглядит так:

Первый аргумент — это дескриптор нашего процесса, о нем нечего сказать. Во-вторых, это указатель на EFI_SYSTEM_TABLE , структуру EFI верхнего уровня, которая хранит ссылки на все, что существует: службы загрузки/выполнения, драйверы, реализации протоколов, карты памяти и т. д. Рекомендуется всегда сохранять оба этих аргумента в глобальной переменной. , так что вы можете получить к ним доступ из любого места в исходном коде. Вы можете найти подробное описание системной таблицы EFI в главе 4 спецификации UEFI — Системная таблица EFI. Его определение C выглядит следующим образом:

Здесь мы видим ссылки на службы загрузки и выполнения, три стандартных дескриптора ввода-вывода (как реализации протоколов SIMPLE_TEXT_OUTPUT и SIMPLE_INPUT) и указатель на . Таблица конфигурации содержит ссылки на все другие реализации протокола, активные в настоящее время в системе.

Сначала мы покажем пример использования Boot Service. EFI_BOOT_SERVICES — это просто структура, содержащая указатели на функции, описанные в главе 6 спецификации UEFI: Службы — службы загрузки. Сейчас мы будем использовать только простую функцию Exit(), которая немедленно завершает текущее приложение EFI.

Теперь мы покажем простое приложение Hello World, использующее реализацию ConOut протокола SIMPLE_TEXT_OUTPUT. Этот протокол описан в главе 11.4 спецификации UEFI — Простой протокол вывода текста. Его заголовок C выглядит следующим образом:

Конечно, нас интересует функция OutputString(), прототипом которой является:

Обратите внимание, что UEFI использует только строки Unicode, поэтому CHAR16 *String . Значение этого указателя точно такое же, как и в любом объектно-ориентированном программировании. С этой информацией мы сможем легко написать приложение Hello World:

Также обратите внимание, что UEFI использует терминаторы строк CRLF ( \r\n ) вместо просто LF ( \n ), и когда мы используем собственные функции EFI, нет слоя, который переинтерпретирует LF в CRLF. Обычно приложения используют дополнительную библиотеку, которая выполняет преобразование LF->CRLF.

Программирование UEFI с помощью FASM

В качестве дополнения я также покажу тот же пример Hello World на ассемблере (используя FASM, который в настоящее время имеет экспериментальную поддержку UEFI, начиная с версии 1.67.28):

Что делать, если вы хотите сделать очень экономичную машину и обойтись без операционной системы? Или, может быть, вы хотите попробовать написать свою собственную ОС, хотя бы просто для развлечения? Может быть, вы читали о крутой архитектуре ОС и подумали про себя: «Я могу это написать!». Что ж, прежде чем углубляться в свой код, вам сначала нужно написать что-то, называемое загрузчиком.

Загрузчик – это код, который запускается на ранних этапах загрузки ПК, Mac, Raspberry Pi или микроконтроллера, до того, как запустится операционная система. Часто его задача состоит в том, чтобы настроить минимальное аппаратное обеспечение, такое как ОЗУ, а затем загрузить ОС или встроенный код.

[Alex Parker] написал серию четких сообщений в блоге, состоящую из трех частей, которые упрощают написание части загрузчика, по крайней мере, для машин x86. И приятно то, что для начала вам не нужен x86. Он делает это на Mac с помощью эмулятора процессора QEMU, хотя он также рассказывает о том, как сделать это под Windows и Linux.

В первой части серии загрузчик оставляет вас в реальном режиме x86 с 16-битными инструкциями и доступом к одному мегабайту памяти — вспомните дни до 80 286 или 1982 год для тех из нас, кто вычислял обратно. тогда. Чтобы доказать, что это работает, он использует вызовы BIOS для отображения «Hello world!». Это также показывает, что через BIOS у вас есть набор периферийных устройств, с которыми вы можете работать.

Во второй части он показывает, как настроить 32-битный защищенный режим и глобальную таблицу дескрипторов, упрощая доступ к большому объему памяти.

В первых двух частях код написан на ассемблере, поэтому в третьей части он завершает серию, показывая, как загружать код C++ в память и выполнять его. Этот код C++, конечно же, будет вашим приложением, которое мы оставим на ваше воображение.

Достаточно редко пишут код загрузчика для настольных компьютеров, тем более для микроконтроллеров. Например, [Дмитрий Гринберг] написал свой собственный загрузчик, чтобы он мог зашифровывать образы ПЗУ для своего AVR на USB. И мы говорили о руководстве [Леди Ады] по записи загрузчиков Arduino. Но если вы хотите перейти к «голому железу» на своем x86, загрузчик — это то, с чего стоит начать. И это не так уж плохо.

40 мыслей о «Напишите свой собственный загрузчик X86»

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

Вытащил Кобаяши Мару, не так ли?

Технически BIOS уже является загрузчиком. Он обеспечивает основной ввод и вывод в систему. Он заботится о настройке памяти и других вещей, таких как дисковод, видео и жесткий диск, после чего загружает загрузчик второй ступени, подобный этому.

Без BIOS загрузчик будет намного сложнее, чем этот..

Спросите ребят, работающих над Cromwell BIOS для оригинальной XBOX, или людей, разрабатывающих Libreboot, насколько сложно создать настоящий загрузчик для современной системы x86.

Выполнение этого для более старого ПК (скажем, оригинального IBM PC или 286 или 386), вероятно, было бы намного проще, если бы вам не требовалась особая совместимость с существующим программным обеспечением для ПК.

Ну, это было бы намного проще, если бы не огромное количество различных процессоров и конфигураций, не говоря уже о трех различных способах определения объема и расположения оперативной памяти…

Прекратите использовать реальный режим. Пожалуйста.

Не могли бы вы уточнить это для тех из нас, кому интересно, почему.

Попробую... Возможно, причина в том, что некоторые думают так:

Не так хорошо, как это:

Хотя можно утверждать, что проще получить доступ к манифесту оборудования через BIOS в реальном режиме. Смотрите это:

Это похоже на то, как перестать держаться за неправильный конец горячего паяльника. Реальный режим (какое вымышленное имя, что в нем «настоящего») — это старый не-32-битный режим, похожий на режим древнего процессора Intel, в котором запускаются процессоры x86. Одна только мысль об этом заставляет меня радоваться, что я работаю с ARM. дней. Теперь, когда я подсел на ARM, я не думаю, что когда-нибудь оглянусь назад. Подумайте о стоматологической работе без анестезии. Я мог бы продолжить, но надеюсь, вы поняли.

Причина, по которой существует «настоящий» режим, заключается в обратной совместимости. Правда, в наши дни он используется редко. но он все еще использовался в те дни, как когда 386, 486 и ранние машины с пентиумом имели «турбо» кнопки.На самом деле это была не кнопка турбо, а кнопка замедления. Тактовая частота этих новых чипов снизилась бы до скоростей 8088/8086 для более старого программного обеспечения. Это часто было проблемой с некоторыми играми, где, если чип не замедлялся, игра играла бы в сверхбыстром движении из-за таких вещей, как циклы синхронизации, рассчитанные на работу на медленных скоростях 1-го 8088/8086. Все это по-прежнему существует сегодня из-за конструктивных решений x86, учитывающих обратную совместимость. Конечно, вы можете отказаться от всего этого, но тогда это действительно будет уже не x86.

Если ваш процессор x86 не поддерживает реальный режим, вы не сможете загрузить загрузочный USB-накопитель с DOS. DOS нужен реальный режим.

Кроме того, в системах x86 почти всем загрузчикам нужен реальный режим. Системы не переключаются в защищенный режим, пока ядро ​​операционной системы не начнет загружаться и ядро ​​не внесет это изменение.

Поздоровайтесь с UEFI!

@Jim:
См. мой комментарий ниже…
Загрузчик, который загружает загрузчик режима UEFI… Он запускается в 16-битном реальном режиме для распаковки подпрограмм UEFI, модулей, кода, больших двоичных объектов и т. д.
Тогда UEFI имеет встроенный драйвер файловой системы для загрузки файла из указанной файловой системы, а указанный файл является еще одним загрузчиком.
Итак, современная цепочка загрузки X86_64 выглядит следующим образом:

Платформа запускает загрузчик ОС - загружается: ОС платформы ожидает замыкания выключателя питания на GPIO (intel ME или безопасность платформы) - на Power sw GPIO закрывает загрузку: загрузчик UEFI - загружает: службу UEFI - загружает: приложение UEFI (загрузчик ОС ) — загружается: ОС.

Я попытаюсь TL;DR, что:

Bootchain:
PCH — Intel ME — загрузчик ПЗУ — служба UEFI — приложение UEFI — операционная система.

0-бит – 0-бит – 16-бит – 32/64-бит – 32/64-бит – 32/64-бит
Где 0-бит означает, что основной процессор выключен!

@Mr Mannering
Я бы посоветовал прочитать спецификацию инициализации платформы (PI).
В двух словах, вот процесс загрузки, исходящий из вектора сброса процессора x86 (есть множество вещей, которые PCH должен делать в режиме дескриптора, но это зависит от набора микросхем).

Вектор сброса — это фаза SEC спецификации PI, а ЦП находится в реальном режиме. Здесь делается все необходимое для установления корня доверия, делается какая-то особая магия, чтобы ЦП мог использовать часть своего кеша в качестве ОЗУ для стека C, ЦП переводится в 32-битный защищенный режим, стек настраивается (для C code), и код искал основную точку входа Pre EFI initalization (PEI). основная память еще не обнаружена и не инициализирована, поэтому код выполняется из флэш-памяти, а куча крайне ограничена. Это приводит к некоторым интересным предостережениям, например, все глобальные переменные доступны только для чтения, и есть предостережения о том, как работать с переменными. Основной целью системы сейчас является запуск кода, необходимого для программирования контроллера памяти и межсоединений ЦП (что немного сложнее в многосокетном сервере).
После того, как этот код будет завершен, PEI теперь может поддерживать кучу, теневой код в ОЗУ, а не выполнять его непосредственно из флэш-памяти, и может обрабатывать сжатые модули. Здесь может потребоваться оставшаяся часть инициализации набора микросхем, но цель системы на данном этапе — найти ядро ​​DXE и передать ему управление.

После того как управление будет передано точке входа ядра Driver Execution Environment (DXE), система будет переведена в 64-битный режим, если ЦП его поддерживает. Формат и структуры данных, указанные в спецификации PI, становятся гораздо более важными. DXE просматривает указанные тома прошивки (FV) и анализирует их записи файловой системы прошивки (FFS). Эти записи FFS будут содержать разделы, которые могут быть сжаты или несжаты, могут быть исполняемым кодом, могут быть данными или чем-то определенным поставщиком.

Поведение DXE во многом зависит от OEM-производителя. Модули, которые выполняет ядро ​​DXE, могут быть определены как минимальный набор, необходимый для доступа к определенному загрузочному устройству, или могут быть полной инициализацией системы. Затем система входит в режим выбора загрузочного устройства (BDS), где пытается выяснить, где находится ОС, которую пользователь намеревается загрузить, и загрузить ее загрузчик.

Также важно отметить, что записи FV и FFS используются в PEI и DXE. Обычно все PEI находятся в одном FV. Ядро DXE и необходимые архитектурные компоненты будут находиться в отдельном FV. Встроенные модули могут находиться в том же FV, что и ядро ​​DXE, или в отдельном FV. Как правило, это зависит от OEM-производителя, хотя эталонные проекты обычно представляют собой один FV для PEI и один для DXE и BDS.

Обратная совместимость в системе UEFI обеспечивается так называемым модулем поддержки сопоставимости (CSM). Те, что я видел, в основном урезаны из устаревших проектов BIOS. Все такие вещи, как векторы прерываний и таблицы вызовов памяти для 16-битных интерфейсов, находятся в этом коде.Могут быть сложные системы, в которых вектор прерывания (например, int 13 или int 10) запускает переход к 32- или 64-битному коду UEFI, который выполняет работу, система переключается обратно в 16-битный режим, и все данные передаются как устаревший вызов указывает. Это также можно сделать с помощью так называемого прерывания управления системой (SMI), которое имеет возможность перехватывать доступ к определенным портам ввода-вывода, чтобы можно было заставить устаревшие доступы с клавиатуры и мыши работать с современными USB-клавиатурами и мышами.

Верно и обратное: можно обернуть устаревшую службу драйвером UEFI, чтобы можно было использовать блочный драйвер ввода-вывода UEFI для карты расширения, которая поддерживает только устаревший интерфейс int 13.
Оба этих метода действительно хакерские и несут ответственность за такие вещи, как странность int 10, когда происходил первоначальный переход ОС на UEFI.

Да, я ознакомился как с указанными спецификациями PI, так и с техническими описаниями конкретных платформ, изучил набор микросхем Q67, набор микросхем GM45 и ознакомился с техническими описаниями некоторых платформ для архитектуры 5-го поколения.

Итак, по большей части вы только что подтвердили то, что я только что сказал, подумайте об этом:
Вы уже говорили о специфичных для платформы вещах с «режимом дескриптора», то есть PCH+ME и других предварительных вещи при включении питания. Хотя есть вещи режима дескриптора, которые происходят после подачи основного питания:

0,8 В в режиме ожидания для PCH и ME (AMT) уступили место 5 В в режиме ожидания (AT/ATX) или 3,2–19 В (портативный/работающий от батареи)
(все остальные VRM юниты выключены)

Включение питания:
PCH @ 0,8 В-AMT + 0,8 В до 1,5 (или предоставляется пользователем/MFG) для периферийных устройств и межсоединений,
0,7 В до 1,5 В ЦП (по запросу от VID линии)
2,5 В-DDR 1,8 В-DDR2 или для DDR3: LDDR3 1,2 В, DDR3 1,5, HDDR3 1,8 В (высокая производительность)
и любые другие системные напряжения.

Если то, что вы сказали для современного ядра серии i, так же верно, как и для таблицы данных, то почему для coreboot, Libreboot и Purisim так сложно просто отключить Intel ME или любое другое программное обеспечение для больших двоичных объектов?
а почему нельзя просто собрать свою прошивку ME и просто запустить?
Кроме того, почему Libreboot и его друзья продолжают говорить о том, как мало документации по этим вещам, и почему они дают хотя бы какие-то результаты, если они всегда ошибаются? (ведь это один из моих многочисленных источников + опыт)

Современный x86 представляет собой луковицу слоев и, по сути, является эмулятором x86 на паре чипов с большим количеством эмуляторов x86 (ARC в более старых ME), разбросанных вокруг и выполняющих различные функции «безопасности».

P.S. Модули SoC, такие как Baytrail и CheryTrail, хуже: безопасная загрузка необязательно записывается в таблицу предохранителей на самом ЦП / SoC, и, таким образом, ПЗУ проверяется до того, как ПЗУ может проверить себя (кажется разделяемым, поскольку разрешены разделы эмуляции NVRAM). изменения)

TL;DR:
Мой пост, против которого вы написали, был просто версией TL;DR того, что вы написали: черт возьми, я даже пропустил режим Cache-as-ram, поскольку он был о цепочке загрузки, а не о подробное пошаговое описание.

Дополнительные отступы для комментариев x86 и этой погони за кошками и мышами с использованием различных дескрипторов, которые требуют проверки из-за определенных целей для напоминания определенным людям об определенных вещах… Мэтт, этот заголовок можно игнорировать, так как содержание находится ниже… надеюсь, с меньшей задержкой.< /p>

Да, я ознакомился как с указанными спецификациями PI, так и с техническими описаниями конкретных платформ, изучил набор микросхем Q67, набор микросхем GM45 и ознакомился с техническими описаниями некоторых платформ для архитектуры 5-го поколения.

Итак, по большей части вы только что подтвердили то, что я только что сказал, подумайте об этом:
Вы уже говорили о специфичных для платформы вещах с «режимом дескриптора», то есть PCH+ME и других предварительных вещи при включении питания. Хотя есть вещи режима дескриптора, которые происходят после подачи основного питания:

0,8 В в режиме ожидания для PCH и ME (AMT) уступили место 5 В в режиме ожидания (AT/ATX) или 3,2–19 В (портативный/работающий от батареи)
(все остальные VRM юниты выключены)

Включение питания:
PCH @ 0,8 В-AMT + 0,8 В до 1,5 (или предоставляется пользователем/MFG) для периферийных устройств и межсоединений,
0,7 В до 1,5 В ЦП (по запросу от VID линии)
2,5 В-DDR 1,8 В-DDR2 или для DDR3: LDDR3 1,2 В, DDR3 1,5, HDDR3 1,8 В (высокая производительность)
и любые другие системные напряжения.

Если то, что вы сказали для современного ядра серии i, так же верно, как и для таблицы данных, то почему для coreboot, Libreboot и Purisim так сложно просто отключить Intel ME или любое другое программное обеспечение для больших двоичных объектов?
а почему нельзя просто собрать свою прошивку ME и просто запустить?
Кроме того, почему Libreboot и его друзья продолжают говорить о том, как мало документации по этим вещам, и почему они дают хотя бы какие-то результаты, если они всегда ошибаются? (ведь это один из моих многочисленных источников + опыт)

Современный x86 представляет собой луковицу слоев и, по сути, является эмулятором x86 на паре чипов с большим количеством эмуляторов x86 (ARC в более старых ME), разбросанных вокруг и выполняющих различные функции «безопасности».

P.S. Модули SoC, такие как Baytrail и CheryTrail, хуже: безопасная загрузка необязательно записывается в таблицу предохранителей на самом ЦП / SoC, и, таким образом, ПЗУ проверяется до того, как ПЗУ может проверить себя (кажется разделяемым, поскольку разрешены разделы эмуляции NVRAM). изменения)

TL;DR:
Мой пост, против которого вы написали, был просто версией TL;DR того, что вы написали: черт возьми, я даже пропустил режим Cache-as-ram, поскольку он был о цепочке загрузки, а не о подробное пошаговое описание.

Реальный режим — это подмножество 16-битных инструкций с незащищенным доступом к сегментированной памяти. Что в этом плохого? Если вы используете язык более высокого уровня, единственная проблема заключается в выборе правильной модели памяти, если вы пишете код на ассемблере, то зачем это делать, если вам это не нравится?

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

Поэтому я поставил перед собой цель написать x86, 32-битную операционную систему. Чтобы убедиться, что я действительно понял все детали, я решил вести блог о своем прогрессе. Итак, вот и первая запись в блоге.

Мы напишем с нуля простой загрузчик, используя язык ассемблера x86, и загрузим минимальное ядро ​​операционной системы, написанное на C. Для простоты мы будем использовать BIOS, а не возиться с UEFI.

Пост структурирован следующим образом. Прежде чем мы перейдем к деталям, возможно, имеет смысл кое-что посмотреть, чтобы иметь возможность следовать моим кратким объяснениям. Следовательно, следующие разделы содержат некоторые ключевые слова, которые вы можете прочитать. После этого мы напишем наш загрузчик шаг за шагом. Затем мы реализуем наше минималистичное ядро, написанное на C. В последнем разделе мы соединим все вместе и загрузим нашу собственную операционную систему.

Исходный код можно найти на GitHub.

Чтобы сделать этот пост коротким, я сосредоточусь на том, что наиболее важно для достижения нашей цели. Это означает, что некоторые вещи останутся необъяснимыми. Тем не менее, если вы потратите некоторое время на то, чтобы прочитать о них более подробно в ходе этой публикации, у вас должно получиться очень хорошо.

Вот список тем, которые полезно знать/прочитать, чтобы понять содержание этого поста.

  • Основное понимание архитектуры компьютера (ЦП, память, диск, BIOS)
  • Основное понимание того, как взаимодействовать с процессорами x86 (архитектура x86, ассемблер x86)
  • Основное понимание того, как компилировать программу на C (make, gcc, ld)

Что касается инструментов, нам понадобится эмулятор (QEMU) для запуска нашей операционной системы, ассемблер x86 (NASM) для написания кода загрузчика, а также компилятор C (gcc) и компоновщик (ld) для для создания исполняемого ядра операционной системы. Мы соединим все вместе с помощью GNU Make.

На машине x86 BIOS выбирает загрузочное устройство, а затем копирует первый сектор с устройства в физическую память по адресу памяти 0x7C00. В нашем случае этот так называемый загрузочный сектор будет содержать 512 байт. Эти 512 байт содержат код загрузчика, таблицу разделов, подпись диска, а также «магическое число», которое проверяется BIOS во избежание случайной загрузки того, что не должно быть загрузочным сектором. Затем BIOS указывает ЦП перейти к началу кода загрузчика, фактически передавая управление загрузчику.

В этом руководстве нас будет интересовать только код загрузчика, который запускает ядро ​​операционной системы. Это необходимо, потому что мы не сможем уместить всю операционную систему в 512 байт. Чтобы запустить наше ядро, загрузчик должен выполнить следующие задачи:

  1. Загрузка ядра с диска в память.
  2. Настройка глобальной таблицы дескрипторов (GDT).
  3. Переключение с 16-битного реального режима на 32-битный защищенный режим и передача управления ядру.

Мы собираемся написать загрузчик на ассемблере x86 с использованием NASM. Ядро будет написано на C. Мы организуем код в несколько файлов, чтобы повысить читабельность и модульность. Для минимальной настройки будут актуальны следующие файлы:

  • mbr.asm — это основной файл, определяющий главную загрузочную запись (загрузочный сектор 512 байт)
  • disk.asm содержит код для чтения с диска с помощью BIOS
  • gdt.asm устанавливает GDT
  • switch-to-32bit.asm содержит код для переключения в 32-битный защищенный режим
  • kernel-entry.asm содержит ассемблерный код для передачи нашей основной функции в kernel.c
  • kernel.c содержит основную функцию ядра
  • Makefile связывает компилятор, компоновщик, ассемблер и эмулятор вместе, чтобы мы могли загрузить нашу операционную систему

В следующем разделе рассматривается запись файлов, связанных с загрузчиком ( mbr.asm , disk.asm , gdt.asm и switch-to-32bit.asm ). После этого мы напишем ядро ​​и входной файл. Наконец, мы собираемся написать все вместе и попытаться загрузиться.

Главный файл загрузочной записи

Главный файл сборки для загрузчика содержит определение основной загрузочной записи, а также операторы include для всех соответствующих вспомогательных модулей. Давайте сначала посмотрим на файл в целом, а затем обсудим каждый раздел в отдельности.

Выйти из полноэкранного режима

Первое, что нужно отметить, это то, что мы собираемся переключаться между 16-битным реальным режимом и 32-битным защищенным режимом, поэтому нам нужно сообщить ассемблеру, какие инструкции он должен генерировать: 16-битные или 32-битные. Это можно сделать с помощью директив [bits 16] и [bits 32] соответственно. Мы начинаем с 16-битных инструкций, так как BIOS переходит к загрузчику, в то время как ЦП все еще находится в 16-битном режиме.

В NASM директива [org 0x7c00] устанавливает счетчик расположения ассемблера. Указываем адрес памяти, куда БИОС помещает загрузчик. Это важно при использовании меток, так как они должны быть преобразованы в адреса памяти, когда мы генерируем машинный код, и эти адреса должны иметь правильное смещение.

Выражение KERNEL_OFFSET equ 0x1000 определяет ассемблерную константу KERNEL_OFFSET со значением 0x1000, которое мы будем использовать позже при загрузке ядра в память и переходе к его точке входа.

Перед вызовом загрузчика BIOS сохраняет выбранный загрузочный диск в регистре dl. Мы храним эту информацию в памяти внутри переменной BOOT_DRIVE, чтобы мы могли использовать регистр dl для чего-то другого без риска перезаписи этой информации.

Прежде чем мы сможем вызвать процедуру загрузки ядра, нам нужно настроить стек, установив регистры указателя стека sp (верхняя часть стека, растет вниз) и bp (нижняя часть стека). Мы поместим нижнюю часть стека в адрес 0x9000, чтобы убедиться, что мы находимся достаточно далеко от другой памяти, связанной с загрузчиком, и избежать коллизий. Стек будет использоваться, например, операторами call и ret для отслеживания адресов памяти при выполнении ассемблерных процедур.

Пришло время поработать! Сначала мы вызовем процедуру load_kernel, чтобы указать BIOS загрузить ядро ​​с диска в память по адресу KERNEL_OFFSET. load_kernel использует нашу процедуру disk_load, которую мы напишем позже. Эта процедура принимает три входных параметра:

  1. Ячейка памяти для размещения прочитанных данных ( bx )
  2. Количество секторов для чтения ( dh )
  3. Диск для чтения ( dl )

Как только мы закончим, мы вернемся к вызову следующей инструкции switch_to_32bit , которая вызывает другую вспомогательную процедуру, которую мы напишем позже. Он подготовит все необходимое для переключения в 32-битный защищенный режим, выполнит переключение и перейдет к метке BEGIN_32BIT, когда это будет сделано, фактически передав управление ядру.

На этом наш основной код загрузчика заканчивается. Чтобы сгенерировать действительную основную загрузочную запись, нам нужно добавить некоторое дополнение, заполнив оставшееся пространство 0 байтами, умноженными на 510 - ($-$$) db 0 и магическим числом dw 0xaa55 .

Далее давайте посмотрим, как определяется процедура disk_load, чтобы мы могли читать наше ядро ​​с диска.

Чтение с диска

Чтение с диска довольно просто при работе в 16-битном режиме, поскольку мы можем использовать функциональные возможности BIOS, отправляя прерывания. Без помощи BIOS нам пришлось бы напрямую взаимодействовать с устройствами ввода-вывода, такими как жесткие диски или дисководы гибких дисков, что значительно усложнило бы наш загрузчик.

Чтобы прочитать данные с диска, нам нужно указать, где начать чтение, сколько читать и где хранить данные в памяти. Затем мы можем отправить сигнал прерывания ( int 0x13 ), и BIOS выполнит свою работу, прочитав следующие параметры из соответствующих регистров:

Регистр Параметр
ah Режим (0x02 = чтение с диска)
al Количество секторов для чтения
ch Цилиндр
cl Сектор
dh Головка
dl Диск
es:bx Адрес памяти для загрузки (указатель адреса буфера)

При наличии ошибок диска BIOS установит бит переноса. В этом случае мы обычно должны показывать пользователю сообщение об ошибке, но, поскольку мы не рассказывали, как печатать строки, и не собираемся делать это в этом посте, мы просто будем зацикливаться на неопределенный срок.

А теперь посмотрим на содержимое disk.asm.

Выйти из полноэкранного режима

Основной частью этого файла является процедура disk_load. Вспомните входные параметры, которые мы установили в mbr.asm:

  1. Ячейка памяти для размещения прочитанных данных ( bx )
  2. Количество секторов для чтения ( dh )
  3. Диск для чтения ( dl )

Первое, что должна сделать каждая процедура, это поместить все регистры общего назначения ( ax , bx , cx , dx ) в стек с помощью pusha, чтобы мы могли извлечь их обратно перед возвратом, чтобы избежать побочных эффектов процедуры.

Кроме того, мы помещаем в стек число секторов для чтения (которое хранится в старшей части регистра dx), потому что нам нужно установить dh равным номеру заголовка перед отправкой сигнала прерывания BIOS, и мы хотим сравните ожидаемое количество прочитанных секторов с фактическим, сообщаемым BIOS, чтобы обнаружить ошибки, когда мы закончим.

Теперь мы можем установить все необходимые входные параметры в соответствующих регистрах и отправить прерывание. Имейте в виду, что вызывающая сторона уже правильно установила bx и dl. Поскольку цель состоит в том, чтобы прочитать следующий сектор на диске, сразу после загрузочного сектора, мы будем читать с загрузочного диска, начиная с сектора 2, цилиндр 0, головка 0.

После выполнения int 0x13 наше ядро ​​должно быть загружено в память. Чтобы убедиться в отсутствии проблем, мы должны проверить две вещи: во-первых, была ли ошибка диска (обозначенная битом переноса) с использованием условного перехода на основе бита переноса jc disk_error . Во-вторых, соответствует ли количество прочитанных секторов (установленное как возвращаемое значение прерывания в al ) количеству секторов, которые мы пытались прочитать (вытолкнули из стека в dh ) с помощью инструкции сравнения cmp al, dh и условного перехода в случае они не равны jne fields_error .

Если что-то пойдет не так, мы столкнемся с бесконечным циклом. Если все прошло нормально, возвращаемся из процедуры обратно в основную функцию. Следующая задача — подготовить GDT, чтобы мы могли переключиться в 32-битный защищенный режим.

Глобальная таблица дескрипторов (GDT)

Как только мы выходим из 16-битного реального режима, сегментация памяти работает немного по-другому. В защищенном режиме сегменты памяти определяются дескрипторами сегментов, которые являются частью GDT.

Для нашего загрузчика мы настроим простейший из возможных GDT, который напоминает плоскую модель памяти. Код и сегмент данных полностью перекрываются и занимают полные 4 ГБ адресуемой памяти. Наш GDT устроен следующим образом:

  1. Нулевой дескриптор сегмента (восемь нулевых байтов). Это требуется в качестве механизма безопасности для обнаружения ошибок, когда наш код забывает выбрать сегмент памяти, в результате чего недопустимый сегмент становится сегментом по умолчанию.
  2. Дескриптор сегмента кода размером 4 ГБ.
  3. Дескриптор сегмента данных размером 4 ГБ.

Дескриптор сегмента — это структура данных, содержащая следующую информацию:

  • Базовый адрес: 32-битный начальный адрес сегмента в памяти. Это будет 0x0 для обоих наших сегментов.
  • Ограничение сегмента: 20-битная длина сегмента. Это будет 0xffffff для обоих наших сегментов.
  • G (степень детализации): если установлено, предел сегмента считается как страницы размером 4096 байт. Это будет 1 для обоих наших сегментов, что преобразует ограничение в 0xffffff страниц в 0xffffff000 байт = 4 ГБ.
  • D (размер операнда по умолчанию) / B (большой): если установлено, это 32-битный сегмент, иначе 16-битный. 1 для обоих наших сегментов.
  • L (long): если установлено, это 64-битный сегмент (и D должен быть равен 0 ). 0 в нашем случае, так как мы пишем 32-битное ядро.
  • AVL (доступно): можно использовать для чего угодно (например, для отладки), но мы просто установим для него значение 0 .
  • P (присутствует): 0 здесь фактически отключает сегмент, не позволяя никому ссылаться на него. Очевидно, будет 1 для обоих наших сегментов.
  • DPL (уровень привилегий дескриптора): уровень привилегий в кольце защиты, необходимый для доступа к этому дескриптору. В обоих наших сегментах будет 0, так как ядро ​​будет обращаться к ним.
  • Тип: если 1 , это дескриптор сегмента кода. Значение 0 означает, что это сегмент данных. Это единственный флаг, который отличается между нашим кодом и дескрипторами сегментов данных. Для сегментов данных D заменяется на B, C заменяется на E, а R заменяется на W.
  • C (соответствующий): код в этом сегменте может вызываться с менее привилегированных уровней. Мы устанавливаем для этого параметра значение 0, чтобы защитить память нашего ядра.
  • E (расширить вниз): расширяется ли сегмент данных от предела вниз до основания. Релевантно только для сегментов стека и в нашем случае установлено значение 0.
  • R (читаемый): установите, может ли сегмент кода быть прочитан. В противном случае его можно только выполнить. В нашем случае установите значение 1.
  • W (запись): установите, разрешена ли запись в сегмент данных. В противном случае его можно только прочитать. В нашем случае установите значение 1.
  • A (доступ): этот флаг устанавливается аппаратно при доступе к сегменту, что может быть полезно для отладки.

К сожалению, дескриптор сегмента не содержит эти значения в линейной форме, а вместо этого они разбросаны по структуре данных.Это делает определение GDT в ассемблере немного утомительным. Пожалуйста, обратитесь к диаграмме ниже для визуального представления структуры данных.

Помимо самого GDT, нам также необходимо настроить дескриптор GDT. Дескриптор содержит как расположение GDT (адрес памяти), так и его размер.

Хватит теории, давайте посмотрим на код! Ниже вы можете найти наш gdt.asm , содержащий определение дескриптора GDT и двух дескрипторов сегментов, а также две ассемблерные константы, чтобы мы знали, где сегмент кода и сегмент данных расположены внутри GDT.

Выйти из полноэкранного режима

Установив GDT и дескриптор GDT, мы, наконец, можем написать код, выполняющий переключение в 32-битный защищенный режим.

Переход в защищенный режим

Чтобы переключиться в 32-битный защищенный режим, чтобы мы могли передать управление нашему 32-битному ядру, мы должны выполнить следующие шаги:

  1. Отключить прерывания с помощью инструкции cli.
  2. Загрузить дескриптор GDT в регистр GDT с помощью инструкции lgdt.
  3. Включить защищенный режим в управляющем регистре cr0.
  4. Дальний переход в наш сегмент кода с помощью jmp . Это должно быть далеко, так как он очищает конвейер ЦП, избавляясь от любых оставшихся там предварительно выбранных 16-битных инструкций.
  5. Настройте все сегментные регистры ( ds , ss , es , fs , gs ) так, чтобы они указывали на наш единственный сегмент данных размером 4 ГБ.
  6. Настройте новый стек, установив 32-битный нижний указатель ( ebp ) и указатель стека ( esp ).
  7. Вернитесь к mbr.asm и передайте управление ядру, вызвав нашу 32-битную процедуру входа в ядро.

Теперь давайте переведем это на ассемблер, чтобы мы могли написать switch-to-32bit.asm :

Выйти из полноэкранного режима

После переключения режима мы готовы передать управление нашему ядру. Давайте реализуем фиктивное ядро ​​в следующем разделе.

Ядро C

Установив и работая с базовыми функциями загрузчика, нам нужно всего лишь создать небольшую фиктивную функцию ядра на языке C, которую мы сможем вызывать из нашего загрузчика. Хотя выход из 16-битного реального режима означает, что в нашем распоряжении больше не будет BIOS и нам нужно писать собственные драйверы ввода-вывода, теперь у нас есть возможность писать код на языке более высокого порядка, таком как C! Это означает, что нам больше не нужно полагаться на язык ассемблера.

На данный момент задачей ядра будет вывод буквы X в верхнем левом углу экрана. Для этого нам придется модифицировать видеопамять напрямую. Для цветных дисплеев с включенным текстовым режимом VGA память начинается с 0xb8000 .

Каждый символ состоит из 2 байтов: первый байт представляет символ в кодировке ASCII, второй байт содержит информацию о цвете. Ниже показана простая функция main внутри kernel.c, которая печатает X в левом верхнем углу экрана.

Выйти из полноэкранного режима

Запись ядра

Когда вы снова посмотрите на наш mbr.asm , вы заметите, что нам все еще нужно вызвать основную функцию, написанную на C. Для этого мы создадим небольшую программу на ассемблере, которая будет размещена в Расположение KERNEL_OFFSET перед скомпилированным ядром C при создании загрузочного образа.

Давайте посмотрим на содержимое kernel-entry.asm :

Выйти из полноэкранного режима

Как и ожидалось, здесь особо нечего делать. Мы только хотим вызвать нашу основную функцию. Чтобы избежать ошибок в процессе сборки, нам нужно объявить main как внешнюю процедуру, которая не определена в нашем файле сборки. Задача компоновщика состоит в разрешении адреса памяти main таким образом, чтобы мы могли успешно вызвать его.

Важно помнить, что файл kernel-entry.asm не входит в наш mbr.asm, но будет помещен перед двоичным файлом ядра в ходе следующего раздела. Итак, давайте посмотрим, как мы можем объединить все части, которые мы построили.

Чтобы создать образ нашей операционной системы, нам понадобятся некоторые инструменты. Нам нужен nasm для обработки наших файлов сборки. Нам нужен gcc для компиляции нашего кода C. Нам нужно ld, чтобы связать наши скомпилированные объектные файлы ядра и нашу скомпилированную запись ядра в двоичный файл. И мы собираемся использовать cat, чтобы объединить нашу основную загрузочную запись и двоичный файл ядра в один загрузочный двоичный образ.

Но как соединить все эти изящные маленькие инструменты вместе? К счастью, для этого есть еще один инструмент: make . Итак, Makefile:

Выйти из полноэкранного режима

Важно отметить, что вам, возможно, придется перекрестно скомпилировать ld и gcc, чтобы иметь возможность скомпилировать и скомпоновать автономный машинный код x86. По крайней мере, мне пришлось сделать это на моем Mac.

Теперь давайте скомпилируем, соберем, свяжем, загрузим наше изображение в qemu и посмотрим на красивый крестик в левом верхнем углу экрана!

Мы сделали это! Следующим шагом будет написание некоторых драйверов, чтобы мы могли взаимодействовать с нашими устройствами ввода-вывода, но об этом будет рассказано в следующем посте :)

Загрузчик – это небольшая, но чрезвычайно важная часть программного обеспечения, помогающая компьютеру загружать операционную систему (ОС). Его создание — сложная задача даже для опытного разработчика низкого уровня. Именно поэтому специалисты Apriorit по разработке драйверов решили поделиться своим опытом по этому вопросу.

В этой статье мы рассмотрим теорию загрузки системы и покажем, как написать загрузчик. Вы можете проверить решение, которое мы описываем в этом руководстве, в нашем репозитории GitHub.

Эта статья будет полезна разработчикам, работающим с языками высокого уровня, такими как C и C++, которым необходимо научиться разрабатывать загрузчик.

Команда разработчиков драйверов

Содержание:

Этап 1. Подготовка к разработке загрузчика

Начнем с краткого обзора основ разработки загрузчика.

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

Загрузочный сектор обычно является первым сектором диска. Это не обязательно для современной загрузочной системы, но большинство разработчиков размещают загрузчик в первом секторе. Так что пока мы тоже будем придерживаться первого сектора. Но имейте в виду, что загрузочный сектор требуется только для систем на основе BIOS.

Выбор интерфейса встроенного ПО: UEFI или BIOS

Вы можете разработать загрузчик для базовой системы ввода-вывода (BIOS) или унифицированного расширяемого интерфейса встроенного ПО (UEFI). BIOS — это хорошо известное компьютерное оборудование, поддерживаемое всеми устройствами. UEFI — это современная замена BIOS, предоставляемая на современном оборудовании, которая предоставляет разработчикам гораздо больше возможностей для низкоуровневой разработки.

Ключевые преимущества работы с UEFI вместо BIOS:

  1. Возможность работы в режиме 32/64 позволяет получить доступ к большему количеству функций процессора, в то время как BIOS работает только в 16-битном режиме.
  2. Отсутствие ограничений на размер загрузочного диска позволяет использовать любой диск для загрузки системы.
  3. Написание кода непосредственно на C с использованием полных сред разработки, таких как EDK2, избавляет от необходимости изучать ассемблер или смешивать высокоуровневый и низкоуровневый код. проверяет, что устройство загружается с использованием только доверенного программного обеспечения, имеющего электронные подписи.

Но, несмотря на все эти преимущества, в этом руководстве мы будем разрабатывать загрузчик BIOS с нуля. Первая причина этого заключается в том, что BIOS поддерживается более широким спектром оборудования. Кроме того, почти все старые устройства поддерживают только BIOS. Во-вторых, все UEFI-совместимые системы могут вести себя как системы BIOS в устаревшем режиме, поэтому вы сможете запускать наш загрузчик и в системах UEFI.

Понимание процесса загрузки системы

Далее нам нужно получить общее представление о процессе загрузки системы. Вот как компоненты взаимодействуют друг с другом при загрузке системы:

Рисунок 1. Процесс загрузки системы

  1. BIOS считывает первый сектор жесткого диска (HDD).
  2. BIOS передает управление MBR, расположенной по адресу 0000:7c00, что запускает процесс загрузки ОС.

После этого процесс загрузки завершается и запускается ОС.

Выбор языка для разработки загрузчика

Наиболее часто используемые языки используют виртуальную машину для преобразования промежуточного кода в язык, понятный процессору. Вы можете выполнить этот промежуточный код только после его преобразования. Единственными исключениями являются гипотетические случаи, когда вы сами реализуете весь необходимый код среды выполнения, используя некоторый набор родных языков.

Чтобы избежать этой проблемы, мы будем использовать C++ в качестве основного языка низкоуровневого программирования в этом руководстве по разработке загрузчика.

Кроме того, знание основ работы с ассемблером является большим преимуществом при написании загрузчика.На начальных этапах работы компьютера BIOS берет на себя управление аппаратным обеспечением машины с помощью функций, называемых вызовами прерываний, которые написаны на языке ассемблера. При сборке загрузчика не обязательно знать язык ассемблера, так как мы можем смешивать низкоуровневые языковые команды и высокоуровневые конструкции. Но знать это необходимо для отладки написанного загрузчика.

Выбор инструментов

Чтобы иметь возможность смешивать высокоуровневый и низкоуровневый код и писать собственный загрузчик, вам потребуются как минимум три инструмента:

  1. Компилятор для ассемблера
  2. Компилятор C++
  3. Компоновщик

Процессор работает в 16-битном реальном режиме с некоторыми ограничениями и в 32-битном безопасном режиме с полной функциональностью. Когда вы включаете компьютер, его процессор работает в реальном режиме. Вот почему для сборки программы и создания исполняемого файла требуются ассемблерный компилятор и компоновщик, которые могут работать в 16-битном режиме.

Компилятор C++ требуется только для создания файлов *.obj в 16-битном реальном режиме.

Примечание. Последние версии компиляторов не подходят для нашей задачи, так как они предназначены для работы только в 32-разрядном безопасном режиме.

После тестирования нескольких 16-разрядных компиляторов мы выбрали для этого руководства компиляторы и компоновщики Microsoft. Мы использовали их для создания всех примеров кода низкоуровневого языка и другого цитируемого кода. Пакет Microsoft Visual Studio 1.52 уже содержит то, что нам нужно: компилятор и компоновщик для ассемблера и C++.

Вот компоновщики и компиляторы, которые вы можете использовать для разработки собственного загрузчика вместо инструментов, которые мы используем в руководстве:

Читайте также: