Перейти к содержимому

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

  • автор:

Теория

Чтобы понять, в чем заключается роль разработчика ОС, представим, что происходит после нажатия на кнопку включения ПК.

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

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

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

Инструментарий

Теоретически, разработку можно вести на любой ОС, но большинство инструментов рассчитаны на UNIX-подобные системы, и хотя бы собрать их на Windows уже будет страданием. Более того, поскольку WSL не поддерживает модули ядра, смонтировать образ диска не получится, и придется настраивать коммуникацию между WSL и Windows. На этом этапе уже становится проще поставить виртуальную машину с Linux. В статье будут предоставлены инструкции для Linux и macOS.

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

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

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

Со сборкой же всё не так просто: понадобится кросс-компилятор под x86. Зачем кросс-компилятор, если собирать под ту же архитектуру? Дело в том, что стандартный компилятор генерирует код, опирающийся на ту же ОС, на которой он запущен, или т.н. hosted-код. Hosted-код использует системные вызовы, взаимодействует с другими процессами, но привязан к операционной системе. Freestanding-код существует сам по себе и для запуска требует только само оборудование. Ядро ОС относится к freestanding, а программы, им запускаемые — к hosted. Кросс-компилятору достаточно соответствующего флага, и будет сгенерирован freestanding-код.

Подготовка

Сборка инструментов

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

$TARGET — система, под которую будет собирать полученный компилятор. Обычно она называется наподобие i686-linux-gnu , но здесь результат запускается без ОС, поэтому указывается просто формат исполняемого файла. Почему i686, а не i386? Просто архитектуре 80386 уже, кхм, много лет, и с тех пор многое изменилось; в частности, появились кэши, многоядерные и многопроцессорные системы, встроенные FPU, “большие” атомарные инструкции вроде CMPXCHG , так что, собирая под i386, можно сильно потерять в быстродействии и немного приобрести в поддержке старых компьютеров.

$PREFIX — то, куда будут установлены инструменты. Обычно используются пути вроде /usr/i686-elf , /usr/local/i686-elf и подобные, но можно установить и в произвольную папку. Этот каталог также называется sysroot, поскольку он будет представлять собой корневой каталог для кросс-компилятора и утилит. Говоря точнее, это не полноправный путь, а именно префикс к пути; таким образом, для установки в корень $PREFIX будет представлять из себя пустую строку, а не / . На время сборки GCC потребуется добавить в PATH путь $PREFIX/bin .

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

Binutils

Загружаем и распаковываем последнюю версию с официального FTP. Осторожно: minor-версии давно перешагнули за 10, вследствие чего сортировка по алфавиту сломалась, для поиска актуальной версии можно использовать сортировку по дате последнего изменения. На момент написания статьи актуальной версией Binutils является 2.29.

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

Подробнее о параметрах:

—with-sysroot — использовать sysroot;
—disable-nls — выключить поддержку родного языка. OSDev-сообщество не так велико, чтобы на какую-нибудь непонятную ошибку сборки обязательно нашёлся человек, говорящий на языке того, у кого она возникла;
—disable-werror — компилятор при сборке Binutils выдает предупреждения, а с -Werror это приводит к остановке сборки.

Так же загружаем, распаковываем и создаем каталог для сборки. Процесс сборки немного отличается. Понадобятся библиотеки GMP, MPFR и MPC. Их можно установить из стандартных репозиториев многих пакетных менеджеров, а можно запустить из каталога с исходным кодом скрипт contrib/download_prerequisites , который их скачает и использует при сборке. Конфигурацию выполняем так:

—disable-nls — то же самое, что и для Binutils;
—without-headers — не предполагать, что на целевой системе будет стандартная библиотека (этим, собственно, и отличается необходимый нам компилятор от стандартного);
—enable-languages=c,c++ — собрать компиляторы только для выбранных языков. Опционально, но существенно ускоряет сборку.

В условиях отсутствия целевой ОС обычный make && make install не подойдет, поскольку некоторые компоненты GCC ориентируются на готовую операционную систему, поэтому собираем и устанавливаем только необходимое:

libgcc — библиотека, в которой содержатся внутренние функции компилятора. Компилятор вправе вызывать их для некоторых вычислений, например, для 64-битного деления на 32-битной платформе.

На большинстве дистрибутивов Linux эту секцию можно пропустить, поскольку на них уже установлены подходящие утилиты для работы с GRUB. Для других же ОС его потребуется загрузить и собрать. Также понадобится маленькая утилита objconv:

На время сборки GRUB потребуется добавить в PATH только что собранный objconv
и кросс-инструменты (i686-elf-*).

GDB (для macOS)

Стандартная версия GDB не знает об ELF-файлах, поэтому при использовании GDB его потребуется пересобрать с их поддержкой. Загрузка, распаковка, сборка:

Образ диска

Процесс создания такового в разных ОС происходит по-своему, поэтому здесь я приведу отдельные инструкции.

Создаем пустой файл:

Создаем таблицу разделов:

Создаём файловую систему:

В дальнейшем можно будет монтировать посредством

Устанавливаем загрузчик (здесь GRUB):

Создаем пустой файл:

Разделяем таблицу разделов и единственный раздел:

Подключаем раздел как диск:

Создаем ФС, здесь FAT32:

“Склеиваем” MBR и ФС обратно:

Подключаем и запоминаем точку монтирования (обычно “/Volumes/NO NAME”):

Образ диска после этого спокойно подключается встроенными средствами системы. Можно на собственное усмотрение создать иерархию директорий и настроить загрузчик. Например, для GRUB можно создать такой grub.cfg в /boot/grub:

Настройка сборочной системы

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

Ассемблерные файлы собираем в объектные формата ELF (32 бита):

C-файлы собираем при помощи кросс-компилятора с флагом -ffreestanding:

Для компоновки используем всё тот же кросс-компилятор, но указываем чуть больше информации:

-ffreestanding — генерировать freestanding-код;
-nostdlib — не включать стандартную библиотеку, поскольку ее реализация является hosted-кодом и будет совершенно бесполезна;
-lgcc — подключаем описанную выше libgcc. Ее подключение всегда идет после остальных объектных файлов, иначе компоновщик будет жаловаться на неразрешенные ссылки;
-T — поскольку нужно где-то разместить заголовок Multiboot, обычная раскладка ELF-файла не подойдёт. Ее можно изменить при помощи скрипта компоновщика, который и задает этот флаг. Вот готовый его вариант:

Минимальное ядро

Получаем управление

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

Proof of Work

Чтобы хоть как-то увидеть, что код действительно выполняется, можно вывести что-то на экран. Полноценный драйвер терминала — тема большая, но, вкратце, по адресу 0xB8000 располагается буфер на 2000 записей, каждая из которых состоит из атрибутов и символа. Белому тексту на черном фоне соответствует байт атрибутов 0x0F. Попробуем что-либо вывести при помощи заранее подготовленной строки:

Запуск

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

Отладка

Для отладки в QEMU можно задать флаги -s -S . QEMU будет дожидаться отладчика и включит сетевую отладку. Также стоит заметить, что отладка не будет работать при использовании ускорителя, так что флаг —enable-kvm придется убрать, если он используется.

Bochs понадобится собрать с —enable-gdb-stub , а в конфиг включить строку наподобие gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0 .
В GDB можно подключиться и запустить машину таким образом (kernel.elf — файл ядра):

Все остальное работает так же, как и всегда — точки останова, чтение памяти и пр. Также можно включить отладчик в само ядро, что позволит производить отладку на реальной машине. Можно написать его самостоятельно, но отладка ошибки в отладчике принесет много-много радости. GNU распространяет почти готовые отладчики, требующие от ядра только несколько функций. Например, для i386. Впрочем, пока это делать рано, поскольку еще нет необходимых функций, таких как установка обработчика прерывания или получения/отправки данных через последовательный порт.

Заключение

На текущий момент до минимальной рабочей операционной системы остается настроить следующее:

# Голое железо

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

# Настройка кросс-компилятора

Первым делом вам необходимо настроить GCC для i686-elf. Вы еще не модифицировали свой компилятор, чтобы дать ему знать о существовании вашей операционной системы, поэтому мы будем использовать стандартную сборку под i686-elf, который предоставляет инструменты для System V ABI.

Вы не сможете правильную сборку вашей ОС без кросс-компилятора.

Вы не сможете корректно завершить этот туториал с x86_64-elf, потому что GRUB способен загружать только 32х-битные мультизагрузочные

(opens new window) ядра. Если это ваша первая ОС, то следует начать с 32х-битного ядра.

# Обзор

Теперь вам понадобится три входных файлов:

  • boot.s — точка входа ядра, которая настроит среду процессора,
  • kernel.c — код вашего ядра,
  • linker.ld — информация компоновки файлов выше.

# Загружаем ядро ОС

Для запуска ОС необходимо иметь ПО, которое сможет загрузить её. Такое ПО называют загрузчиками, мы будем использовать GRUB. Ядро получает от загрузчика минимальную среду, в которой ещё не настроен стек, виртуальная память, подключенные устройства и так далее.

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

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

Теперь вы можете собрать boot.s , используя:

# Реализация ядра

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

# Отдельные и размещённые среды

Если вы уже использовали C/C++ для пользовательских прогрмамм, то вы использовали так называемую резмещённую (англ. hosted) среду, что означает наличие стандартной библиотеки языка и различных удобств, доступных во время выполнения (англ. runtime). Также сущесвуют отдельные (англ. freestanding) среды, которые мы и будем использовать. Это означает, что у нас ничего не будет из стандартного набора, и мы должы будет реализовать их самостоятельно. Тем не менее, у нас будут некоторые заголовочные файлы, которые являются частью компилятора, а не стандарта C.

  • stdbool.h — тип для булевых (логических) значений;
  • stddef.h — size_t и NULL ;
  • stdint.h — intx_t и uintx_t для точных размеров переменных (если вы использовали short вместо uint16_t , то при изменении размера типа всё может неожиданно сломаться;
  • float.h , iso646.h , limits.h , и stdarg.h .

# Пищем ядро на C

Для начала мы напишем простое ядро, которое будет использовать текстовый режим VGA (адрес 0xB8000 ). Оно будет запоминать положение следущего символа в буффере и выводить простые симфолы, но буде поддержки переноса строк. Добавление поддержки этой функции может стать вашей первой самостоятельной задачей. Потратьте немного времени, чтобы понять собственный код.

Текстовый режим (также как и BIOS) является устаревшим на новых устройствах, т. к. UEFI поддерживает только буффер пикселей. Для будущей совместимости вы можете начать с этого. Просим загрузчик настроить графический буффер соответствующим флагом мультизагрузки, либо самостоятельно используем вызовы VESA VBE. В отличии от текстового режима VGA, буффер имеет пиксели, что озачает ручную отрисовку каждого символа, поддержку пролистывания (скролла), перемещения курсока и т. д. Также значит, что вам потребуется другой terminal_putchar и шрифт (изображения для каждого символа). Все дистрибутивы Linux поставляют экранные шрифты (англ. PC Screen Fonts), которые вы можете использовать.

Обратите внимание, что в коде мы хотели использовать обычную функцию C strlen , которая является частью стандартной библиотеки C, которой у вас нет. Вместо этого мы положились на отдельный заголовок stddef.h , и собственную реализацию. И так вам придётся делать с каждой функцией из стандартной библиотеки, которую захотите использовать (заголовки для отдельной среды предоставляют только типы данным и макросы).

Пример выше можно собрать, выполнив:

Заметим, что код примера использует некоторый функционал из сборщика GNU для C99.

# Пишем ядро на C++

Написать ядро на C++ легко, но стоит помнить, что не весь функционал языка доступен. К примеру, выбрасывание исключений требует особой поддержи в среде выполнения, а также выделений памяти. В нашем случае просто добавляется extern "C" для главной функции. Важно, чтобы функция kernel_main была объявлена как видимостью для C, иначе компилятор добавит информацию в название сборки (англ. assembly). Это делает невозможным вызов этой функции из ассеблера.

Сохраните всё в файл kernel.cpp (или с вашим любимым расширением файла с кодом на C++). Вы можете скомпилировать этот файл, используя:

# Компоновка ядра

Выше упомянутые boot.s и kernel.c содержат части нашего ядра, и для создания полноценного ядра остаётся только скомпоновать их собранные версии в программу ядра, которую загрузчик сможет использовать. При сборке пользовательских программ мы используем стандартные скрипты компоновки инструментов разработки. В нашем случае нужно написать собственный скрипт. Ниже пример linker.ld :

Мы используем компилятор в качестве компоновщика из-за большего контроля процесса. Не забудьте, что если вы использовали C++ при написании ядра, то нужно будет использовать компилятор для C++.

Вы можете скомпоновать ядро командой ниже:

Некоторые туториалы предлагаю использовать i686-elf-ld вместо компилятор Note: Some tutorials suggest linking with i686-elf-ld rather than the compiler, но это не позволяет выполнять компилятору некоторые задачи во время компоновки.

Теперь файл my_kernel.bin — ваше ядро (другие файлы более не нужны)! The file myos.bin is now your kernel (all other files are no longer needed). Note that we are linking against libgcc, which implements various runtime routines that your cross-compiler depends on. Leaving it out will give you problems in the future. If you did not build and install libgcc as part of your cross-compiler, you should go back now and build a cross-compiler with libgcc. The compiler depends on this library and will use it regardless of whether you provide it or not.

# Проверяем ядро

# Проверка мультизагрузочности

Проверить присутствие заголовка мультизагрузочности v1 можно при помощи GRUB:

Важно, чтобы заголовок находился в первых 8 КиБ с выравнивание в 4 байта, иначе GRUB выйдет с кодом 1. Поверка v2 заголовка проводится с аргументом —is-x86-multiboot2 . Чтобы не проверять возвращаемый код, можно использовать sh-скрипт:

# Сборка загрузочного CD-ROM образа

Для начала нужно создать файл boot/grub/grub.cfg в папке isodir , который будет хранить загрузочную конфигурацию:

Далее переместим скомпилированное ядро myos.bin в isodir/boot и соберём сам образ:

# Тестовый запуск

После загрузки вы, скорее всего, увидите "Hello, kernel World!".

Дополнительно, QEMU поддерживает прямую загрузку ядра без загрузочного образа:

# Что дальше?

Только что на свет появилась ещё одна ОС, поздравляем! Но это только начало. Вот что вы можете сделать:

# Поддержка переносов строки в драйвер терминала

Текстовый режим VGA на месте \n хранит другой символ, поэтому следует добавить проверку в terminal_putchar , которая будет увелимивать terminal_row и сбрасывать terminal_column .

# Пролистывание терминала

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

# Цветные ASCII-арты

Текущий терминальный драйвер поддерживает только 16 цветов (из них 8 фоновых), поэтому вам понадобится нормальный VGA драйвер.

# Глобальные конструкторы

This tutorial showed a small example of how to create a minimal environment for C and C++ kernels. Unfortunately, you don’t have everything set up yet. For instance, C++ with global objects will not have their constructors called because you never do it. The compiler uses a special mechanism for performing tasks at program initialization time through the crt*.o objects, which may be valuable even for C programmers. If you combine the crt*.o files correctly, you will create an _init function that invokes all the program initialization tasks. Your boot.o object file can then invoke _init before calling kernel_main .

# Организация ОС

Этот туториал покажет как нетерпеливому новичку получить приветствие от своей ОС. В нём не будет лучших практик организации ОС, однако вы узнаете как сделать ОС с ядром, стандартной библиотекой и подготовленным пространством для пользователя.

# Дальнейшее изучение

# Частые вопросы

# Почему используется мультизагрузочный заголовок? Разве GRUB не умеет работать с ELF?

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

# GRUB очищает секцию BSS перед загрузкой ядра?

Да. Для ELF секция .bss автоматически определяется и очищается (хотя в стандарте мультизагрузочности нет чётких указаний). Для других форматов вы можете использовать флаг №16 мультизагрузочного заголовка и не нулевое значение для поля bss_end_addr .

# [GRUB] Error 13: Invalid or unsupported executable format

Chances are the Multiboot header is missing from the final executable, or it is not at the right location. If you are using some other format than ELF (such as PE), you should specify the AOUT kludge in the Multiboot header. The grub-file program describe aboveand "objdump -h" should give you more hints about what is going on. It may also happen if you use an ELF object file instead of an executable (e.g. you have an ELF file with unresolved symbols or unfixable relocations). Try to link your ELF file to a binary executable to get more accurate error messages. A common problem when your kernel size increases, is that the Multiboot header does no longer appear at the start of the output binary. The common solutions is to put the Multiboot header in a separate section and make sure that section is first in the output binary, or to include the Multiboot header itself in the linker script.

# [QEMU] Could not read from CD-ROM (code 0009)

If your development system is booted from EFI it may be that you don’t have the PC-BIOS version of the grub binaries installed anywhere. If you install them then grub-mkrescue will by default produce a hybrid ISO that will work in QEMU. On Ubuntu this can be achieved with: apt-get install grub-pc-bin.

Что нужно знать для разработки операционной системы

Создание операционной системы — одна из сложнейших задач в программировании, поскольку требует обширных и комплексных знаний о работе компьютера, — пишет tproger.ru в своем переводе статьи «How To Program Your Very Own Operating Systems (OS)».

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

Какие именно знания нужны? Разбираемся ниже.

Что такое ОС

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

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

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

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

Кратко об истории операционных систем

Первая операционная система была создана в 1956 году компанией General Motors для единственного компьютера IBM. Остальные владельцы этих компьютеров последовали примеру и начали создавать собственные ОС.

Как можно догадаться, первые ОС сильно отличались друг от друга. Почти на каждом компьютере стояла своя система. Хоть они и облегчали написание программ, использовать такие программы можно было только на одном компьютере.

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

В эти же годы компании Control Data Corporation, Computer Sciences Corporation, Burroughs Corporation, GE, Digital Equipment Corporation и Xerox тоже начали реализовывать свои ОС.

В конце 60-х была разработана первая версия ОС Unix. Написанная на Си, свободно доступная первые годы, Unix привлекала всё больше внимания и аудитории. Она легко портировалась на новые системы и начала набирать обороты.

Многие современные ОС, включая Apple OS X и все разновидности Linux-систем, являются дальними потомками Unix.

Microsoft Windows, в свою очередь, была написана для ряда персональных компьютеров IBM.

Первая ОС от Microsoft называлась не Windows, а MS-DOS. Эта система была создана в 1981 году, когда систему 86-DOS купили у компании Seattle Computer Products и модифицировали её под требования IBM.

Всем привычное название Windows появилось в 1985, когда MS-DOS обзавелась графическим интерфейсом.

Apple OS X, Microsoft Windows и ряд Linux-систем (включая Android) сейчас контролируют большую часть рынка операционных систем.

Что нужно для создания операционной системы

Составляющие операционной системы

ОС состоит из двух основных частей:

  • ядро системы;
  • системные программы.
Ядро (англ. kernel)

Сердце операционной системы. Именно оно запускается первым при включении компьютера (не считая BIOS и загрузчик). Ядро контролирует ресурсы компьютера и обрабатывает запросы от системных программ.

Системные программы

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

Пользовательские программы

Не являются частью операционной системы. Именно эти программы уже имеют конкретное назначение. Текстовые редакторы, браузеры, медиа-плееры — всё это пользовательские программы. Они контролируются ядром и используют системные программы для доступа к периферии.

Что необходимо знать

Список вещей, которые необходимо знать для создания собственной ОС, очень длинный. Но в нём есть три основных пункта, на которые нужно обратить внимание в первую очередь:

  • базовое понимание компьютерной науки (информатики);
  • теория и опыт в программировании;
  • знание высоко- и низкоуровневых языков программирования.

Что нужно знать для создания своей операционной системы

Изучение информатики

Разработка ОС — это не тоже самое, что, скажем, веб-разработка. Тут нельзя будет просто и быстро разобраться в базовых вещах. Для начала нужно получить крепкий базис информатики и только потом переходить к другим темам.

Coursera: Computer Science 101 — это курс, который подойдёт для тех, кто только начал осваиваться в информатике и программировании. Если у вас уже имеются элементарные знания в в этой области, то пропустите этот курс и переходите к следующему.

Udacity: Intro to Computer Science использует веб-разработку в качестве подхода к обучению компьютерной науке. Хоть курс и не направлен на непосредственную разработку ОС, он даёт мощный базис в области программирования.

edX: Introduction to Computer Science — этот курс является самым исчерпывающим и углублённым в этом списке. Несмотря на это, он полностью бесплатен. Курс был разработан в Гарвардском Университете. В нём вас научат алгоритмам, структурам данных, управлению ресурсами, разработке ПО, а так же познакомят с такими языками программирования, как C, PHP и JavaScript.

Подборка книг для самостоятельного изучения Computer Science.

Изучение программирования

С крепкими знаниями в области информатики и хотя бы базовым пониманием программирования вам нужно набраться опыта в разработке проектов.

Udacity: Software Development Process — отличный курс для тех, кто ещё не принимал участие в крупных проектах. Тут вас ознакомят с подробностями рабочего процесса и работой с инструментарием (например Git), а также расскажут о настройке интегрированной среды разработки.

Изучение языков программирования

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

  • низкоуровневый язык ассемблера;
  • высокоуровневый язык программирования.

Первый используется для работы напрямую с процессором. Процессор «понимает» только машинный код, и для каждого типа процессора есть только один соответствующий язык. Одной из самых популярных архитектур процессора является x86. Она была разработана компанией Intel и на текущий момент широко используется многими производителями компьютерного железа, включая AMD. По этой причине в этой статье акцент будет именно на архитектуру x86.

Высокоуровневые языки программирования, в свою очередь, работают сразу на нескольких архитектурах. Самый популярный из таких языков — Cи. Чаще всего именно на нём и пишутся операционные системы. Но это не означает, что этот язык единственный. Для написания ОС можно использовать и более высокоуровневые языки, например C++ или Python.

Прим. перев. Есть пример написания «игрушечных» операционных систем на C#. Cosmos — некий конструктор ОС. В этой статье на практическом примере показывают написание системы всего в нескольких десятках строк.

Что нужно для создания операционной системы

Язык ассемблера для x86

«x86 Assembly Guide» — неплохо подойдёт для начального изучения ассемблера. Несмотря на то, что эта статья коротка для полного курса, в ней содержится всё необходимое для дальнейшего углубления.

Книга «Programming from the Ground Up» Джонатана Бартлетта является одной из основных по изучению ассемблера. В ней основами языка программирования подкрепляются базисы информатики.

Для изучения ассемблера есть ещё одна показательная книга — «The Art of Assembly Language», написанная Рэнди Гайдом. Изначально книга писалась специально для курсов, которые вёл автор в Калифорнийском университете Cal Poly и UC Riverside. В середине 90-х годов книга была признана определяющей в этой сфере.

Если вдруг вышеописанные материалы вам не подошли, вот ещё пара отличных книг:

  • «Assembly Language Step-by-Step: Programming with Linux» Джефа Дантеманна;
  • «Modern X86 Assembly Language Programming» Дэниэля Кассварма.
Язык Cи

Как уже упоминалось выше, для написания ОС есть несколько высокоуровневых языков программирования. Однако самый популярный из них — Си.

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

«Learn C the Hard Way» — название ещё одной книги. Кроме привычной теории в ней собрано много практических решений. Этот учебник расскажет обо всех аспектах языка.

Либо же можете выбрать одну из этих книг:

  • «The C Programming Language» Кернигхана и Ритчи;
  • «C Programming Absolute Beginner’s Guide» Пэрри и Миллера.

Разработка операционной системы

Разработка ОС

После освоения всего необходимого, что касается информатики, языка ассемблера и Cи, вам стоит прочесть хотя бы одну или две книги про непосредственную разработку ОС. Вот несколько ресурсов для этого:

«Linux From Scratch». Здесь рассматривается процесс сборки операционной системы Linux (учебник переведён на много языков, в том числе и на русский). Тут, как и в остальных учебниках, вам предоставят все необходимые базовые знания. Полагаясь на них можно попробовать себя в создании ОС. Чтобы сделать программную часть ОС более профессиональной, присутствуют дополнения к учебнику: «Beyond Linux from Scratch», «Automated Linux from Scratch», «Cross Linux from Scratch» или «Hardened Linux from Scratch».

«The little book about OS development» Эрика Хэйлина и Адама Ренберга. Этот учебник разработан в рамках продвинутого курса информатики и на момент его написания авторы являлись студентами Королевского Института Технологий в Стокгольме. В этой книге рассматривается разработка ОС для архитектуры x86 (причём с самого начала — с установки среды разработки). В этом курсе рассказывается о многих интересных темах, включая многозадачность, управление памятью и разработку файловой системы.

«Operation System Development Series» Broken Thorn Entertainment — серия из 25 уроков, посвящённых разработке ОС с нуля.

Примечание Эти уроки рассчитаны на то, что вы уже знакомы с IDE и владеете как языком ассемблера, так и Cи.

Вот ещё три книги, которые помогут вам ознакомиться с дисциплиной разработки ОС:

  • «Modern Operating Systems» Таненбаума и Боза;
  • «The Design of the UNIX Operating System» Маурайса Баха;
  • «Operating System Concepts» Сибершатза, Гальвина и Ганге.

Сообщества, посвященные разработке ОС

Заниматься разработкой ОС в одиночку смело, но сложно. Гораздо проще найти единомышленников, которые, как и вы, решили попытать удачу в этом нелёгком деле. Существует пара подходящих мест:

    — это Вики с исчерпывающей информацией о разработке ОС и отличным форумом, где вы можете попросить о помощи или же наоборот сами кого-нибудь выручить. на Реддите. Канал, также посвящённый разработке ОС. , Programmers, StackOverflow от StackExchange — площадки, где вы можете задавать различные технические вопросы.

Заключение

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

UNIX-подобная операционная система и загрузка x86 машины

UNIX-подобная операционная система

Что такое UNIX-подобная операционка? Это ОС, созданная под влиянием UNIX. Но прежде чем заняться написанием ядра для нее, давайте посмотрим, как машина загружается и передает управление ядру.

Большинство регистров x86 процессора имеют четко определенные значения после включения питания. Регистр указателя инструкций (EIP) содержит адрес памяти для команды, выполняемой процессором. EIP жестко закодирован на значение 0xFFFFFFF0. Таким образом, у процессора есть четкие инструкции по физическому адресу 0xFFFFFFF0, что, по сути, – последние 16 байт 32-разрядного адресного пространства. Этот адрес называется вектором сброса.

Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 сопоставляется с определенной частью BIOS, а не с ОЗУ. Между тем, BIOS копирует себя в ОЗУ для более быстрого доступа. Это называется затенением (shadowing). Адрес 0xFFFFFFF0 будет содержать только инструкцию перехода к адресу в памяти, где BIOS скопировал себя.

Таким образом, код BIOS начинает свое выполнение. Сначала BIOS ищет загрузочное устройство в соответствии с настроенным порядком загрузочных устройств. Он ищет определенное магическое число, чтобы определить, является устройство загрузочным или нет (байты 511 и 512 первого сектора равны 0xAA55).

После того, как BIOS обнаружил загрузочное устройство, он копирует содержимое первого сектора устройства в оперативную память, начиная с физического адреса 0x7c00; затем переходит по адресу и выполняет только что загруженный код. Этот код называется системным загрузчиком (bootloader).

Затем bootloader загружает ядро ​​по физическому адресу 0x100000. Адрес 0x100000 используется как стартовый адрес для всех больших ядер на x86 машинах.

Все x86 процессоры стартуют в упрощенном 16-битном режиме, называемом режимом реальных адресов. Загрузчик GRUB переключается в 32-битный защищенный режим, устанавливая младший бит регистра CR0 равным 1. Таким образом, ядро ​​загружается в 32-разрядный защищенный режим.

Обратите внимание, что в случае обнаружения ядра Linux, GRUB получит протокол загрузки и загрузит ​​Linux-ядро в реальном режиме. А ядро Linux сделает переключение в защищенный режим.

Что нам понадобится?

  • x86 компьютер (разумеется)
  • Ассемблер NASM
  • GCC
  • ld (GNU Linker)
  • GRUB
  • Исходный код

Ну и неплохо было бы иметь представление о том, как работает UNIX-подобная ОС. Исходный код можно найти в репозитории на Github.

Точка входа и запуск ядра

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

Как убедиться, что этот код послужит отправной точкой для ядра?

Мы будем использовать скрипт компоновщика, который связывает объектные файлы с целью создания окончательного исполняемого файла ядра. В этом скрипте явно укажем, что бинарный файл должен быть загружен по адресу 0x100000. Этот адрес, является тем местом, где должно быть ядро.

Первая инструкция bits 32 не является инструкцией сборки x86. Это директива для ассемблера NASM, которая указывает, что он должен генерировать код для работы на процессоре, работающем в 32-битном режиме. Это не обязательно требуется в нашем примере, однако это хорошая практика – указывать такие вещи явно.

Вторая строка начинается с текстового раздела. Здесь мы разместим весь наш код.

global — еще одна директива NASM, служит для установки символов исходного кода как глобальных.

kmain — это собственная функция, которая будет определена в нашем файле kernel.c. extern объявляет, что функция определена ​​в другом месте.

Функция start вызывает функцию kmain и останавливает CPU с помощью команды hlt. Прерывания могут пробудить CPU из выполнения инструкции hlt. Поэтому мы предварительно отключаем прерывания, используя инструкцию cli.

В идеале необходимо выделить некоторый объем памяти для стека и указать на нее с помощью указателя стека (esp). Однако, GRUB делает это за нас, и указатель стека уже установлен. Тем не менее, для верности, мы выделим некоторое пространство в разделе BSS и поместим указатель стека в начало выделенной памяти. Для этого используем команду resb, которая резервирует память в байтах. После этого остается метка, которая указывает на край зарезервированного фрагмента памяти. Перед вызовом kmain указатель стека (esp) используется для указания этого пространства с помощью команды mov.

Ядро на C

В kernel.asm мы сделали вызов функции kmain(). Таким образом, код на C начнет выполнятся в kmain():

Наше ядро ​​будет очищать экран и выводить на него строку «my first kernel».

Для начала мы создаем указатель vidptr, который указывает на адрес 0xb8000. Этот адрес является началом видеопамяти в защищенном режиме. Текстовая память экрана – это просто кусок памяти в нашем адресном пространстве. Ввод/вывод для экрана на карте памяти начинается с 0xb8000 и поддерживает 25 строк по 80 ascii символов каждая.

Каждый элемент символа в этой текстовой памяти представлен 16 битами (2 байта), а не 8 битами (1 байт), к которым мы привыкли. Первый байт должен иметь представление символа, как в ASCII. Второй байт является атрибутным байтом. Он описывает форматирование символа, включая разные атрибуты, например цвет.

Чтобы напечатать символ с зеленым цветом на черном фоне, мы сохраним символ s в первом байте адреса видеопамяти и значение 0x02 во втором байте.

0 — черный фон, а 2 — зеленый.

Ниже приведена таблица кодов для разных цветов:

В нашем ядре мы будем использовать светло-серые символы на черном фоне. Поэтому наш байт атрибутов должен иметь значение 0x07.

В первом цикле while программа записывает пустой символ с атрибутом 0x07 по всем 80 столбцам из 25 строк. Таким образом, экран очищается.

Во втором цикле while символы строки «my first kernel» записываются в кусок видеопамяти. Для каждого символа атрибутный байт содержит значение 0x07.

Таким образом, строка отобразится на экране.

Связующая часть

Мы собираем kernel.asm и NASM в объектный файл, а затем с помощью GCC компилируем kernel.c в другой объектный файл. Теперь наша задача – связать эти объекты с исполняемым загрузочным ядром.

Для этого мы используем явный скрипт компоновщика, который можно передать как аргумент ld (наш компоновщик).

Во-первых, мы устанавливаем выходной формат исполняемого файла как 32-битный исполняемый (ELF). ELF – стандартный формат двоичного файла для Unix-подобных систем на архитектуре x86.

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

SECTIONS – самая важная часть, где мы определяем разметку исполняемого файла. Здесь указывается, как должны быть объединены различные разделы и в каком месте они будут размещаться.

В фигурных скобках, следующих за инструкцией SECTIONS, символ периода (.) – представляет собой счетчик местоположения.

Счетчик местоположения всегда инициализируется до 0x0 в начале блока SECTIONS. Его можно изменить, присвоив ему новое значение.

Как уже говорилось, код ядра должен начинаться с адреса 0x100000. Таким образом, мы установили счетчик местоположения в 0x100000.

Посмотрите на следующую строку .text:

Звездочка (*) является спецсимволом, который будет соответствовать любому имени файла. То есть, выражение *(.text) означает все секции ввода .text из всех входных файлов.

Таким образом, компоновщик объединяет все текстовые разделы объектных файлов в текстовый раздел исполняемого файла по адресу, хранящемуся в счетчике местоположения. Раздел кода исполняемого файла начинается с 0x100000.

После того, как компоновщик разместит секцию вывода текста, значение счетчика местоположения установится в 0x1000000 + размер раздела вывода текста.

Аналогично, разделы данных и bss объединяются и помещаются на значения счетчика местоположения.

Grub и Multiboot

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

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

GRUB загрузит ядро только в том случае, если оно соответствует Multiboot-спецификации.

Согласно ей, ядро должно содержать заголовок в пределах его первых 8 килобайт.

Кроме того, этот заголовок должен содержать дополнительно 3 поля:

  • поле магического числа: содержит магическое число 0x1BADB002, для идентификации заголовка.
  • поле флагов: сейчас оно не нужно, просто установим его значение в ноль.
  • поле контрольной суммы: когда задано, должно возвращать ноль для суммы с первыми двумя полями.

Итак, kernel.asm будет выглядеть таким образом:

Сборка ядра

Теперь создадим объектные файлы из kernel.asm и kernel.c, а затем свяжем их с помощью скрипта компоновщика.

запустит ассемблер для создания объектного файла kasm.o в формате 32-битного ELF.

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

запустит компоновщик с нашим скриптом и сгенерирует исполняемое именованное ядро.

Настройка GRUB и запуск ядра

UNIX-подобная ОС с ее ядром почти поддалась. GRUB требует, чтобы ядро имело имя вида kernel-<version>. Переименуйте ядро, к примеру, в kernel-701.

Теперь поместите его в каталог /boot. Для этого вам потребуются права суперпользователя.

В конфигурационном файле GRUB grub.cfg вы должны добавить запись такого вида:

Не забудьте удалить директиву hiddenmenu, если она существует.

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

Ядро в работе

Это ваше ядро! Оказывается, UNIX-подобная операционная система и ее составляющие не так уж сложны, верно?

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

Если вы хотите запустить ядро на эмуляторе qemu вместо загрузки с помощью GRUB, вы можете сделать так:

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *