Runnable примеры
Технически, программа, которая работает без ОС, является ОС. Итак, давайте посмотрим, как создать и запустить несколько крошечных операционных систем hello world.
Код всех примеров ниже представлен на этом репозитории GitHub .
Загрузочный сектор
На x86 самое простое и низкоуровневое средство , которое вы можете сделать, - это создать главный загрузочный сектор (MBR) , который является типом загрузочного сектора , и затем установить его на диск.
Здесь мы создаем один с одним printf
вызовом:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Результат:
Проверено на Ubuntu 18.04, QEMU 2.11.1.
main.img
содержит следующее:
\364
в восьмеричном == 0xf4
в шестнадцатеричном виде: кодировка для hlt
инструкции, которая сообщает ЦП прекратить работу.
Поэтому наша программа не будет ничего делать: только запускать и останавливать.
Мы используем восьмеричное, потому что \x
шестнадцатеричные числа не указаны в POSIX.
Мы могли бы легко получить эту кодировку с помощью:
echo hlt > a.asm
nasm -f bin a.asm
hd a
но 0xf4
кодировка также задокументирована в руководстве Intel, конечно.
%509s
произвести 509 мест. Необходимо заполнить файл до байта 510.
\125\252
в восьмеричном == с 0x55
последующим 0xaa
: магические байты, необходимые для оборудования. Это должны быть байты 511 и 512.
Если нет, оборудование не будет воспринимать это как загрузочный диск.
Обратите внимание, что даже не делая ничего, несколько символов уже напечатаны на экране. Они печатаются прошивкой и служат для идентификации системы.
Работать на реальном оборудовании
Эмуляторы забавны, но аппаратная часть - реальная сделка.
Обратите внимание, что это опасно, и вы можете по ошибке стереть диск: делайте это только на старых машинах, которые не содержат критически важных данных! Или, что еще лучше, такие доски, как Raspberry Pi, см. Пример ARM ниже.
Для типичного ноутбука вы должны сделать что-то вроде:
Запишите образ на USB-накопитель (уничтожит ваши данные!):
sudo dd if=main.img of=/dev/sdX
подключите USB к компьютеру
включи
скажи ему загрузиться с USB.
Это означает, что прошивка выбирает USB перед жестким диском.
Если это не стандартное поведение вашей машины, продолжайте нажимать Enter, F12, ESC или другие подобные странные клавиши после включения питания, пока не появится меню загрузки, где вы можете выбрать загрузку с USB.
Часто можно настроить порядок поиска в этих меню.
Например, на моем старом Lenovo Thinkpad T430, UEFI BIOS 1.16, я вижу:
Привет, мир
Теперь, когда мы создали минимальную программу, давайте перейдем к привету.
Очевидный вопрос: как сделать IO? Несколько вариантов:
- попросите прошивку, например, BIOS или UEFI, чтобы сделать, если для нас
- VGA: специальная область памяти, которая выводится на экран при записи в. Может использоваться в защищенном режиме.
- написать драйвер и поговорить напрямую с оборудованием дисплея. Это «правильный» способ сделать это: более мощный, но более сложный.
последовательный порт . Это очень простой стандартизированный протокол, который отправляет и получает символы с хост-терминала.
Источник .
К сожалению, он не представлен на большинстве современных ноутбуков, но является наиболее распространенным способом разработки плат разработки, см. Примеры ARM ниже.
Это действительно позор, так как такие интерфейсы действительно полезны для отладки ядра Linux, например .
использовать функции отладки чипов. ARM называет их полухостингом, например. На реальном оборудовании это требует дополнительной аппаратной и программной поддержки, но на эмуляторах это может быть бесплатный удобный вариант. Пример .
Здесь мы сделаем пример BIOS, поскольку он проще на x86. Но учтите, что это не самый надежный метод.
main.S
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Собрать и связать с:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Результат:
Проверено на: Lenovo Thinkpad T430, UEFI BIOS 1.16. Диск создан на хосте Ubuntu 18.04.
Помимо стандартных инструкций по сборке пользовательского пространства, у нас есть:
.code16
: говорит ГАЗ выводить 16-битный код
cli
: отключить программные прерывания. Это может заставить процессор начать работать снова послеhlt
int $0x10
: вызывает ли BIOS. Это то, что печатает символы один за другим.
Важные флаги ссылок:
--oformat binary
: выводить сырой двоичный код сборки, не деформируйте его внутри ELF-файла, как в случае с обычными исполняемыми файлами пользовательского пространства.
Используйте C вместо сборки
Поскольку C компилируется в сборку, использование C без стандартной библиотеки довольно просто, вам просто нужно:
- скрипт компоновщика для размещения вещей в памяти в нужном месте
- флаги, которые говорят GCC не использовать стандартную библиотеку
- маленькая точка входа в сборку, которая устанавливает требуемое состояние C для
main
, в частности:
TODO: ссылка на пример с x86 на GitHub. Вот ARM, который я создал .
Однако, если вы захотите использовать стандартную библиотеку, все станет интереснее, поскольку у нас нет ядра Linux, которое реализует большую часть функциональности стандартной библиотеки C через POSIX .
Несколько возможностей, без перехода на полноценную ОС, такую как Linux, включают:
Newlib
Подробный пример по адресу: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
В Newlib вы должны сами реализовывать системные вызовы, но вы получаете очень минимальную систему, и их очень легко реализовать.
Например, вы можете перенаправить printf
на системы UART или ARM или внедрить exit()
с помощью полухостинга .
встроенные операционные системы, такие как FreeRTOS и Zephyr .
Такие операционные системы обычно позволяют отключить упреждающее планирование, что дает вам полный контроль над временем выполнения программы.
Их можно рассматривать как своего рода предварительно реализованный Newlib.
РУКА
В ARM общие идеи совпадают. Я загрузил:
Для Raspberry Pi https://github.com/dwelch67/raspberrypi выглядит как самый популярный учебник, доступный сегодня.
Некоторые отличия от x86:
IO делается путем написания магических адресов напрямую, там нет in
и out
инструкций.
Это называется IO с отображением памяти .
для некоторого реального оборудования, такого как Raspberry Pi, вы можете самостоятельно добавить прошивку (BIOS) в образ диска.
Это хорошо, поскольку делает обновление прошивки более прозрачным.
Прошивка
По правде говоря, ваш загрузочный сектор не является первым программным обеспечением, которое работает на процессоре системы.
На самом деле сначала запускается так называемая прошивка , то есть программное обеспечение:
- сделано изготовителями оборудования
- как правило, закрытый источник, но, вероятно, на основе C
- хранится в постоянной памяти, и поэтому его трудно / невозможно изменить без согласия поставщика.
Хорошо известные прошивки включают в себя:
- BIOS : старая прошивка для x86. SeaBIOS - это стандартная реализация с открытым исходным кодом, используемая QEMU.
- UEFI : преемник BIOS, лучше стандартизированный, но более способный и невероятно раздутый.
- Coreboot : благородная попытка с открытым исходным кодом
Прошивка делает такие вещи, как:
Зацикливайтесь на каждом жестком диске, USB, сети и т. д., пока не найдете что-нибудь загрузочное.
Когда мы запускаем QEMU, -hda
говорит, что main.img
это жесткий диск, подключенный к оборудованию, и
hda
является первым, который будет испытан, и он используется.
загрузите первые 512 байт в адрес памяти RAM 0x7c00
, поместите туда RIP процессора и дайте ему поработать
показывать на дисплее такие вещи, как меню загрузки или вызовы печати BIOS
Прошивка предлагает функциональность, подобную ОС, от которой зависит большинство ОС. Например, подмножество Python было портировано для работы в BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Можно утверждать, что прошивки неотличимы от ОС, и что прошивка - это единственное «истинное» программирование на «голое железо», какое только можно сделать.
Как говорит этот разработчик CoreOS :
Сложная часть
Когда вы включаете компьютер, микросхемы, составляющие чипсет (северный мост, южный мост и SuperIO), еще не инициализированы должным образом. Даже несмотря на то, что ПЗУ BIOS удалено от ЦП настолько, насколько это возможно, оно доступно ЦП, поскольку должно быть, иначе ЦП не будет иметь никаких инструкций для выполнения. Это не означает, что ПЗУ BIOS полностью сопоставлено, обычно нет. Но достаточно для отображения процесса загрузки. Любые другие устройства, просто забудьте об этом.
Когда вы запускаете Coreboot в QEMU, вы можете экспериментировать с более высокими уровнями Coreboot и с полезными нагрузками, но QEMU предлагает небольшую возможность экспериментировать с кодом запуска низкого уровня. Во-первых, RAM просто работает с самого начала.
Начальное состояние после BIOS
Как и многие вещи в оборудовании, стандартизация слаба, и одна из вещей, на которые вы не должны полагаться, это начальное состояние регистров, когда ваш код начинает работать после BIOS.
Поэтому сделайте себе одолжение и используйте некоторый код инициализации, например, следующий: https://stackoverflow.com/a/32509555/895245
Регистры любят %ds
и %es
имеют важные побочные эффекты, поэтому вы должны обнулять их, даже если вы не используете их явно.
Обратите внимание, что некоторые эмуляторы лучше, чем реальное оборудование, и дают вам хорошее начальное состояние. Затем, когда вы работаете на реальном оборудовании, все ломается.
GNU GRUB Multiboot
Загрузочные сектора просты, но не очень удобны:
- Вы можете иметь только одну ОС на диск
- код загрузки должен быть очень маленьким и вмещаться в 512 байт. Это можно решить с помощью вызова BIOS int 0x13 .
- Вы должны сделать много запуска самостоятельно, например перейти в защищенный режим
Именно по этим причинам GNU GRUB создал более удобный формат файлов, называемый multiboot.
Минимальный рабочий пример: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Я также использую его в репозитории GitHub examples, чтобы иметь возможность легко запускать все примеры на реальном оборудовании, не перегружая USB миллион раз. На QEMU это выглядит так:
Если вы подготовите свою ОС как мультизагрузочный файл, GRUB сможет найти его в обычной файловой системе.
Это то, что делает большинство дистрибутивов, помещая образы ОС под /boot
.
Мультизагрузочные файлы - это в основном файл ELF со специальным заголовком. Они указаны GRUB по адресу: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html.
Вы можете превратить мультизагрузочный файл в загрузочный диск с помощью grub-mkrescue
.
Эль Торито
Формат, который можно записать на компакт-диски: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Также возможно создать гибридное изображение, которое работает на ISO или USB. Это может быть сделано с grub-mkrescue
( например ), а также выполняется ядром Linux на make isoimage
использование isohybrid
.
Ресурсы