Как запустить программу без операционной системы?

Как вы запускаете программу самостоятельно без операционной системы? Можете ли вы создавать программы сборки, которые компьютер может загружать и запускать при запуске, например, загружать компьютер с флэш-накопителя и запускать программу, которая находится на процессоре?

2 ответа

Решение

Как вы запускаете программу самостоятельно без операционной системы?

Вы помещаете свой двоичный код в место, где процессор ищет после перезагрузки (например, адрес 0 на ARM).

Можно ли создавать программы сборки, которые компьютер может загружать и запускать при запуске (например, загружать компьютер с флэш-накопителя, и он запускает программу, находящуюся на диске)?

Общий ответ на вопрос: это можно сделать. Это часто упоминается как "программирование голого металла". Чтобы читать с флешки, вы хотите знать, что такое USB, и вам нужен драйвер для работы с этим USB. Программа на этом диске также должна быть в каком-то определенном формате. На какой-то конкретной файловой системе... Это то, что обычно делают загрузчики. Многие платы ARM позволяют вам делать некоторые из этих вещей. Некоторые из них имеют загрузчик, чтобы помочь вам с базовой настройкой.

Здесь вы можете найти отличный учебник о том, как сделать базовую операционную систему на Raspberry PI.

Редактировать: эта статья и весь wiki.osdev.org ответят на большинство ваших вопросов http://wiki.osdev.org/Introduction

Кроме того, если вы не хотите экспериментировать непосредственно на оборудовании, вы можете запустить его как виртуальную машину, используя гипервизоры, такие как qemu. Посмотрите, как запустить "hello world" непосредственно на виртуализированном оборудовании ARM здесь.

Runnable примеры

Давайте создадим и запустим несколько крошечных программ "Привет, мир!", Которые работают без ОС на:

Мы также максимально опробуем их на эмуляторе QEMU, так как это безопаснее и удобнее для разработки. Тесты QEMU были проведены на хосте Ubuntu 18.04 с предварительно упакованным QEMU 2.11.1.

Код всех примеров x86, приведенных ниже, и многое другое представлено в этом репозитории GitHub.

Как запустить примеры на реальном оборудовании x86

Чтобы иметь возможность запускать примеры на ноутбуке x86, вам необходимо немного ознакомиться с меню загрузки.

Помните, что запуск примеров на реальном оборудовании может быть опасным, например, вы можете по ошибке стереть диск или сделать аппаратное устройство из кирпича: делайте это только на старых машинах, которые не содержат критических данных! Или, что еще лучше, используйте дешевые полуразборные девборды, такие как Raspberry Pi, см. Пример ARM ниже.

Для типичного ноутбука x86 вы должны сделать что-то вроде:

  1. Запишите образ на USB-накопитель (уничтожит ваши данные!):

    sudo dd if=main.img of=/dev/sdX
    
  2. подключите USB к компьютеру

  3. включи

  4. скажи ему загрузиться с USB.

    Это означает, что прошивка выбирает USB перед жестким диском.

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

    Часто можно настроить порядок поиска в этих меню.

Например, на моем T430 я вижу следующее.

После включения, это когда мне нужно нажать Enter, чтобы войти в меню загрузки:

введите описание изображения здесь

Затем здесь я должен нажать F12, чтобы выбрать USB в качестве загрузочного устройства:

введите описание изображения здесь

Оттуда я могу выбрать USB в качестве загрузочного устройства следующим образом:

введите описание изображения здесь

В качестве альтернативы, чтобы изменить порядок загрузки и выбрать более высокий приоритет для USB, я бы нажал F1 на экране "Startup Interrupt Menu", а затем перешел к:

введите описание изображения здесь

Загрузочный сектор

На x86 самое простое и низкоуровневое средство, которое вы можете сделать, - это создать главный загрузочный сектор (MBR), который является типом загрузочного сектора, и затем установить его на диск.

Здесь мы создаем один с одним printf вызов:

printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img

Результат:

введите описание изображения здесь

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

А на T430 у нас просто пустой экран с мигающим курсором:

введите описание изображения здесь

main.img содержит следующее:

  • \364 в восьмеричном == 0xf4 в шестнадцатеричном виде: кодировка для hlt инструкция, которая говорит процессору перестать работать.

    Поэтому наша программа не будет ничего делать: только запускать и останавливать.

    Мы используем восьмеричное, потому что \x шестнадцатеричные числа не определены POSIX.

    Мы могли бы легко получить эту кодировку с помощью:

    echo hlt > a.S
    as -o a.o a.S
    objdump -S a.o
    

    какие выводы:

    a.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 <.text>:
       0:   f4                      hlt
    

    но это также задокументировано в руководстве Intel, конечно.

  • %509s произвести 509 мест. Необходимо заполнить файл до байта 510.

  • \125\252 в восьмеричном == 0x55 с последующим 0xaa: магические байты, необходимые для оборудования. Это должны быть байты 511 и 512.

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

Если вы не printf мастер, вы можете подтвердить содержание main.img с:

hd main.img

который показывает ожидаемое:

00000000  f4 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |.               |
00000010  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
*
000001f0  20 20 20 20 20 20 20 20  20 20 20 20 20 20 55 aa  |              U.|
00000200

где 20 это пробел в ASCII.

Привет мир загрузочный сектор

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

Очевидный вопрос: как сделать 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"

GitHub вверх по течению.

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
qemu-system-x86_64 -hda main.img

Результат:

введите описание изображения здесь

И на Т430:

введите описание изображения здесь

Проверено на: Lenovo Thinkpad T430, UEFI BIOS 1.16. Диск создан на хосте Ubuntu 18.04.

Помимо стандартных инструкций по сборке пользовательского пространства, у нас есть:

  • .code16: говорит ГАЗу выводить 16-битный код

  • cli: отключить программные прерывания. Это может заставить процессор начать работать снова после hlt

  • int $0x10: BIOS вызывает. Это то, что печатает символы один за другим.

Важные флаги ссылок:

  • --oformat binary: выводить сырой двоичный код сборки, не деформируйте его внутри ELF-файла, как в случае с обычными исполняемыми файлами пользовательского пространства.

Кулер x86 голые метал программы

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

Используйте C вместо сборки

Поскольку C компилируется в сборку, использование C без стандартной библиотеки довольно просто, вам просто нужно:

  • скрипт компоновщика для размещения вещей в памяти в нужном месте
  • флаги, которые говорят GCC не использовать стандартную библиотеку
  • маленькая точка входа в сборку, которая устанавливает требуемое состояние C для main в частности:
    • стек
    • обнулить BSS

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.

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:

введите описание изображения здесь

T430:

введите описание изображения здесь

Если вы подготовите свою ОС как мультизагрузочный файл, GRUB сможет найти его в обычной файловой системе.

Это то, что делает большинство дистрибутивов, помещая образы ОС под /boot,

Мультизагрузочные файлы - это в основном файл ELF со специальным заголовком. Они указаны GRUB по адресу: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html

Вы можете превратить мультизагрузочный файл в загрузочный диск с помощью grub-mkrescue,

Прошивка

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

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

  • сделано изготовителями оборудования
  • как правило, закрытый источник, но, вероятно, на основе 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 предлагает мало возможностей для экспериментов с низкоуровневым кодом запуска. Во-первых, оперативная память работает с самого начала.

Начальное состояние после BIOS

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

Поэтому сделайте себе одолжение и используйте некоторый код инициализации, например, следующий: /questions/25233556/kak-sozdat-minimalnyij-zagruzochnyij-sektor-bios-hello-world-s-gcc-kotoryij-rabotaet-s-usb-nakopitelya-na-realnom-oborudovanii/25233574#25233574

Регистрирует как %ds а также %es имеют важные побочные эффекты, поэтому вы должны обнулить их, даже если вы не используете их явно.

Обратите внимание, что некоторые эмуляторы лучше, чем реальное оборудование, и дают вам хорошее начальное состояние. Затем, когда вы работаете на реальном оборудовании, все ломается.

Эль Торито

Формат, который можно записать на компакт-диски: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29

Также возможно создать гибридное изображение, которое работает на ISO или USB. Это можно сделать с помощью grub-mkrescue ( пример), и также делается ядром Linux на make isoimage с помощью isohybrid,

РУКА

В ARM общие идеи совпадают.

Не существует широко доступной полу-стандартизированной предустановленной прошивки, такой как BIOS, для использования для ввода-вывода, поэтому мы можем сделать два наиболее простых типа ввода-вывода:

  • сериал, который широко доступен на девбордах
  • мигать светодиодом

Я загрузил:

Некоторые отличия от x86:

  • IO делается путем записи на магические адреса напрямую, нет in а также out инструкции.

    Это называется IO с отображением памяти.

  • для некоторого реального оборудования, такого как Raspberry Pi, вы можете сами добавить прошивку (BIOS) в образ диска.

    Это хорошо, поскольку делает обновление прошивки более прозрачным.

Ресурсы

  • http://wiki.osdev.org/ является отличным источником по этим вопросам.
  • https://github.com/scanlime/metalkit - это более автоматизированная / общая система компиляции с открытым исходным кодом, которая предоставляет крошечный настраиваемый API

Операционная система как вдохновение

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

Например, эту страницу можно использовать как отправную точку:

Как написать простую операционную систему

Здесь вся операционная система полностью помещается в 512-байтовый загрузочный сектор (MBR)!

Такая или похожая простая ОС может быть использована для создания простой структуры, которая позволит нам:

сделать в Загрузчик нагрузка последующих сектора на диске в ОЗУ, и перейти к этой точке, чтобы продолжить выполнение. Или вы можете прочитать о FAT12, файловой системе, используемой на гибких дисках, и реализовать ее.

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

Загрузчик как вдохновение

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

Как разработать свой собственный загрузчик

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

  1. Правильная загрузка в память по адресу 0000:7C00.
  2. Вызов функции BootMain, разработанной на языке высокого уровня.
  3. Показать сообщение "Hello, world…" с нижнего уровня на дисплее.

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

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

для сборки программы и получения исполняемого файла вам понадобятся компилятор и компоновщик Ассемблера для 16-битного режима. Для C/C++ вам понадобится только компилятор, который может создавать объектные файлы для 16-битного режима.

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

Приложения UEFI как вдохновение

В приведенных выше примерах использован факт загрузки сектора MBR на носитель данных. Тем не менее, мы можем углубиться в глубины, например, используя приложения UEFI:

Помимо загрузки ОС, UEFI может запускать приложения UEFI, которые хранятся в виде файлов в системном разделе EFI. Они могут быть выполнены из командной оболочки UEFI, диспетчером загрузки микропрограммы или другими приложениями UEFI. Приложения UEFI можно разрабатывать и устанавливать независимо от производителя системы.

Тип приложения UEFI - это загрузчик ОС, такой как GRUB, rEFInd, Gummiboot и Windows Boot Manager; который загружает файл ОС в память и выполняет его. Кроме того, загрузчик ОС может предоставить пользовательский интерфейс, позволяющий выбрать другое приложение UEFI для запуска. Такие утилиты, как оболочка UEFI, также являются приложениями UEFI.

Если мы хотим начать создавать такие программы, мы можем, например, начать с этих веб-сайтов:

Программирование для EFI: создание программы "Hello, World" / Программирование UEFI - первые шаги

Изучение проблем безопасности как вдохновение

Хорошо известно, что существует целая группа вредоносных программ (то есть программ) , которые запускаются до запуска операционной системы.

Огромная группа из них работает с сектором MBR или приложениями UEFI, как и все вышеперечисленные решения, но есть и те, которые используют другую точку входа, такую ​​как Volume Boot Record (VBR) или BIOS:

Существует как минимум четыре известных вируса атаки на BIOS, два из которых предназначены для демонстрационных целей.

или, возможно, еще один.

Атаки перед запуском системы

Буткиты прошли путь от разработки Proof-of-Concept до массового распространения и теперь фактически превратились в программное обеспечение с открытым исходным кодом.

Различные способы загрузки

Я также думаю, что в этом контексте также стоит упомянуть, что существуют различные формы загрузки операционной системы (или исполняемой программы, предназначенной для этого). Есть много, но я хотел бы обратить внимание на загрузку коды из сети, используя опцию загрузки по сети (PXE), что позволяет запускать программу на компьютере, независимо от операционной системы и даже независимо от того, любого носителя, который напрямую подключен к компьютеру:

Что такое загрузка по сети (PXE) и как ее использовать?

Я написал программу на C++ на основе Win32 для записи сборки в загрузочный сектор флеш-накопителя. Когда компьютер загружается с флэш-накопителя, он успешно выполняет код - посмотрите здесь Программа C++ для записи в загрузочный сектор USB-накопителя

Эта программа представляет собой несколько строк, которые должны быть скомпилированы на компиляторе с настроенной компиляцией Windows - например, компилятором Visual Studio - любой доступной версии.

Другие вопросы по тегам