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

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

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

Выполняемые примеры

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

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

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

Как запустить примеры на реальном оборудовании 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, чтобы войти в меню загрузки:

enter image description here

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

enter image description here

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

enter image description here

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

enter image description here

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

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

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

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

Результат:

enter image description here

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

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

enter image description here

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.

    Это 2 обязательных магических байта, которые должны быть байтами 511 и 512.

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

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

Если вы не являетесь мастером 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.

Микропрограмма BIOS считывает эти 512 байт с диска, помещает их в память и устанавливает ПК на первый байт, чтобы начать их выполнение.

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

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

Очевидный вопрос: как сделать IO? Несколько вариантов:

  • спросите прошивку, например BIOS или UEFI, чтобы сделать это для нас
  • VGA: специальная область памяти, которая выводится на экран при записи в. Может использоваться в защищенном режиме.
  • написать драйвер и поговорить напрямую с оборудованием дисплея. Это "правильный" способ сделать это: более мощный, но более сложный.
  • последовательный порт. Это очень простой стандартизированный протокол, который отправляет и получает символы от хост-терминала.

    На десктопах это выглядит так:

    enter image description here

    Источник.

    К сожалению, на большинстве современных ноутбуков его нет, но это наиболее распространенный способ разработки плат разработки, см. примеры 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 upstream.

link.ld

SECTIONS
{
    /* The BIOS loads the code from the disk to this location.
     * We must tell that to the linker so that it can properly
     * calculate the addresses of symbols we might jump to.
     */
    . = 0x7c00;
    .text :
    {
        __start = .;
        *(.text)
        /* Place the magic boot bytes at the end of the first 512 sector. */
        . = 0x1FE;
        SHORT(0xAA55)
    }
}

Собрать и связать с:

as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img

Результат:

enter image description here

И на T430:

enter image description here

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

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

  • .code16: сообщает ГАЗу выводить 16-битный код

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

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

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

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

Чтобы лучше понять часть сценария компоновщика, ознакомьтесь с этапом перемещения ссылок: Что делают компоновщики?

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

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

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

Резюме: используйте GRUB multiboot, который решит множество досадных проблем, о которых вы никогда не задумывались. Смотрите раздел ниже.

Основная сложность на x86 состоит в том, что BIOS загружает только 512 байт с диска в память, и вы, вероятно, взорвете эти 512 байт при использовании C!

Чтобы решить эту проблему, мы можем использовать двухэтапный загрузчик. Это делает дальнейшие вызовы BIOS, которые загружают больше байтов с диска в память. Вот минимальный пример сборки на этапе 2 с нуля с использованием вызовов BIOS int 0x13:

В качестве альтернативы:

  • если он нужен только для работы в QEMU, а не на реальном оборудовании, используйте опцию -kernel, которая загружает весь файл ELF в память. Вот пример ARM, который я создал с помощью этого метода.
  • для Raspberry Pi прошивка по умолчанию заботится о загрузке изображения для нас из файла ELF с именем kernel7.img, так же, как это делает QEMU -kernel.

Только для образовательных целей, вот пример минимального этапа C:

main.c

void main(void) {
    int i;
    char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
    for (i = 0; i < sizeof(s); ++i) {
        __asm__ (
            "int $0x10" : : "a" ((0x0e << 8) | s[i])
        );
    }
    while (1) {
        __asm__ ("hlt");
    };
}

entry.S

.code16
.text
.global mystart
mystart:
    ljmp $0, $.setcs
.setcs:
    xor %ax, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %ss
    mov $__stack_top, %esp
    cld
    call main

linker.ld

ENTRY(mystart)
SECTIONS
{
  . = 0x7c00;
  .text : {
    entry.o(.text)
    *(.text)
    *(.data)
    *(.rodata)
    __bss_start = .;
    /* COMMON vs BSS: https://stackoverflow.com/questions/16835716/bss-vs-common-what-goes-where */
    *(.bss)
    *(COMMON)
    __bss_end = .;
  }
  /* https://stackoverflow.com/questions/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
  .sig : AT(ADDR(.text) + 512 - 2)
  {
      SHORT(0xaa55);
  }
  /DISCARD/ : {
    *(.eh_frame)
  }
  __stack_bottom = .;
  . = . + 0x1000;
  __stack_top = .;
}

бегать

set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw

Стандартная библиотека C

Однако, если вы захотите использовать стандартную библиотеку C, все станет еще интереснее, поскольку у нас нет ядра Linux, которое реализует большую часть функциональности стандартной библиотеки C через POSIX.

Несколько возможностей, без перехода на полноценную ОС, такую как Linux, включают:

  • Напишите свой собственный. Это всего лишь куча заголовков и файлов C в конце концов, верно? Правильно??

  • Newlib

    Подробный пример на: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931

    Newlib реализует для вас все скучные вещи, не связанные с ОС, например, memcmp, memcpy и т.д.

    Затем он предоставляет некоторые заглушки для реализации системных вызовов, которые вам нужны сами.

    Например, мы можем реализовать exit() на ARM с помощью полухостинга с помощью:

    void _exit(int status) {
        __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
    }
    

    как показано на в этом примере.

    Например, вы можете перенаправить printf в системы UART или ARM или реализовать exit() с полуохостингом.

  • встроенные операционные системы, такие как FreeRTOS и Zephyr.

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

    Их можно рассматривать как своего рода предварительно реализованный Newlib.

GNU GRUB Multiboot

Загрузочные секторы просты, но не очень удобны:

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

Именно по этим причинам GNU GRUB создал более удобный формат файла, называемый multiboot.

Минимальный рабочий пример: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world

Я также использую его в своем репозитории GitHub examples, чтобы можно было легко запускать все примеры на реальном оборудовании без миллионной записи USB.

Результат QEMU:

enter image description here

T430:

enter image description here

Если вы подготовите свою ОС как мультизагрузочный файл, 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 байт в адрес памяти ОЗУ 0x7c00, поместите туда процессор RIP и дайте ему запустить

  • показывать такие вещи, как меню загрузки или вызовы печати BIOS на дисплее

Прошивка предлагает функциональность, подобную ОС, от которой зависит большинство ОС. Например. Подмножество Python перенесено для работы в BIOS/UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM

Можно утверждать, что прошивки неотличимы от ОС, и что прошивка - это единственное "настоящее" программирование на голом железе, которое можно сделать.

Как говорит этот разработчик CoreOS:

Трудная часть

Когда вы включаете компьютер, микросхемы, составляющие чипсет (северный мост, южный мост и SuperIO), еще не инициализированы должным образом. Даже несмотря на то, что ПЗУ BIOS удалено от ЦП настолько, насколько это возможно, оно доступно ЦП, поскольку должно быть, иначе ЦП не будет иметь никаких инструкций для выполнения. Это не означает, что ПЗУ BIOS полностью сопоставлено, обычно нет. Но достаточно для отображения процесса загрузки. Любые другие устройства, просто забудьте об этом.

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

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

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

Поэтому сделайте себе одолжение и используйте некоторый код инициализации, например, следующий: fooobar.com/questions/30281/...

Такие регистры, как %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 отсутствуют.

    Это называется ввод-вывод в память.

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

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

Ресурсы

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

Ответ 3

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

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

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

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

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

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

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

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

Boot Loader как вдохновение

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

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

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

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

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

В частности, он показывает, как использовать технику "смешанного кода", благодаря которой можно комбинировать высокоуровневые конструкции (из 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) и как ее использовать?