Книга «Операционная система с 0 до 1» опубликована на GitHub и имеет более 2 000 звездочек и 100 форков. Как понятно из названия, прочитав её, вы сможете создать свою собственную операционную систему - и, пожалуй, мало что в мире программистов может быть круче.
Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро
Давайте напишем простое ядро, которое можно загрузить при помощи бутлоадера GRUB x86-системы. Это ядро будет отображать сообщение на экране и ждать.
Прежде чем мы начнём писать ядро, давайте разберёмся, как система загружается и передаёт управление ядру.
В большей части регистров процессора при запуске уже находятся определённые значения. Регистр, указывающий на адрес инструкций (Instruction Pointer, EIP), хранит в себе адрес памяти, по которому лежит исполняемая процессором инструкция. EIP по умолчанию равен 0xFFFFFFF0 . Таким образом, x86-процессоры на аппаратном уровне начинают работу с адреса 0xFFFFFFF0. На самом деле это - последние 16 байт 32-битного адресного пространства. Этот адрес называется вектором перезагрузки (reset vector).
Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 принадлежит определённой части BIOS, не RAM. В это время BIOS копирует себя в RAM для более быстрого доступа. Адрес 0xFFFFFFF0 будет содержать лишь инструкцию перехода на адрес в памяти, где хранится копия BIOS.
Так начинается исполнение кода BIOS. Сперва BIOS ищет устройство, с которого можно загрузиться, в предустановленном порядке. Ищется магическое число, определяющее, является ли устройство загрузочным (511-ый и 512-ый байты первого сектора должны равняться 0xAA55 ).
Когда BIOS находит загрузочное устройство, она копирует содержимое первого сектора устройства в RAM, начиная с физического адреса 0x7c00 ; затем переходит на адрес и исполняет загруженный код. Этот код называется бутлоадером .
Бутлоадер загружает ядро по физическому адресу 0x100000 . Этот адрес используется как стартовый во всех больших ядрах на x86-системах.
Все x86-процессоры начинают работу в простом 16-битном режиме, называющимся реальным режимом . Бутлоадер GRUB переключает режим в 32-битный защищённый режим , устанавливая нижний бит регистра CR0 в 1 . Таким образом, ядро загружается в 32-битном защищённом режиме.
Заметьте, что в случае с ядром Linux GRUB видит протоколы загрузки Linux и загружает ядро в реальном режиме. Ядро самостоятельно переключается в защищённый режим.
Как бы не хотелось ограничиться одним Си, что-то придётся писать на ассемблере. Мы напишем на нём небольшой файл, который будет служить исходной точкой для нашего ядра. Всё, что он будет делать - вызывать внешнюю функцию, написанную на Си, и останавливать поток программы.
Как же нам сделать так, чтобы этот код обязательно был именно исходной точкой?
Мы будем использовать скрипт-линковщик, который соединяет объектные файлы для создания конечного исполняемого файла. В этом скрипте мы явно укажем, что хотим загрузить данные по адресу 0x100000.
Вот код на ассемблере:
;;kernel.asm bits 32 ;nasm directive - 32 bit section .text global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space:
Первая инструкция, bits 32 , не является x86-ассемблерной инструкцией. Это директива ассемблеру NASM, задающая генерацию кода для процессора, работающего в 32-битном режиме. В нашем случае это не обязательно, но вообще полезно.
Со второй строки начинается секция с кодом.
global - это ещё одна директива NASM, делающая символы исходного кода глобальными. Таким образом, линковщик знает, где находится символ start - наша точка входа.
kmain - это функция, которая будет определена в файле kernel.c . extern значит, что функция объявлена где-то в другом месте.
Затем идёт функция start , вызывающая функцию kmain и останавливающая процессор инструкцией hlt . Именно поэтому мы заранее отключаем прерывания инструкцией cli .
В идеале нам нужно выделить немного памяти и указать на неё указателем стека (esp). Однако, похоже, что GRUB уже сделал это за нас. Тем не менее, вы всё равно выделим немного места в секции BSS и переместим на её начало указатель стека. Мы используем инструкцию resb , которая резервирует указанное число байт. Сразу перед вызовом kmain указатель стека (esp) устанавливается на нужное место инструкцией mov .
В kernel.asm мы совершили вызов функции kmain() . Таким образом, наш “сишный” код должен начать исполнение с kmain() :
/* * kernel.c */ void kmain(void) { const char *str = "my first kernel"; char *vidptr = (char*)0xb8000; //video mem begins here. unsigned int i = 0; unsigned int j = 0; /* this loops clears the screen * there are 25 lines each of 80 columns; each element takes 2 bytes */ while(j < 80 * 25 * 2) { /* blank character */ vidptr[j] = " "; /* attribute-byte - light grey on black screen */ vidptr = 0x07; j = j + 2; } j = 0; /* this loop writes the string to video memory */ while(str[j] != "\0") { /* the character"s ascii */ vidptr[i] = str[j]; /* attribute-byte: give character black bg and light grey fg */ vidptr = 0x07; ++j; i = i + 2; } return; }
Всё, что сделает наше ядро - очистит экран и выведет строку “my first kernel”.
Сперва мы создаём указатель vidptr , который указывает на адрес 0xb8000 . С этого адреса в защищённом режиме начинается “видеопамять”. Для вывода текста на экран мы резервируем 25 строк по 80 ASCII-символов, начиная с 0xb8000.
Каждый символ отображается не привычными 8 битами, а 16. В первом байте хранится сам символ, а во втором - attribute-byte . Он описывает форматирование символа, например, его цвет.
Для вывода символа s зелёного цвета на чёрном фоне мы запишем этот символ в первый байт и значение 0x02 во второй. 0 означает чёрный фон, 2 - зелёный цвет текста.
Вот таблица цветов:
0 - Black, 1 - Blue, 2 - Green, 3 - Cyan, 4 - Red, 5 - Magenta, 6 - Brown, 7 - Light Grey, 8 - Dark Grey, 9 - Light Blue, 10/a - Light Green, 11/b - Light Cyan, 12/c - Light Red, 13/d - Light Magenta, 14/e - Light Brown, 15/f – White.
В нашем ядре мы будем использовать светло-серый текст на чёрном фоне, поэтому наш байт-атрибут будет иметь значение 0x07.
В первом цикле программа выводит пустой символ по всей зоне 80×25. Это очистит экран. В следующем цикле в “видеопамять” записываются символы из нуль-терминированной строки “my first kernel” с байтом-атрибутом, равным 0x07. Это выведет строку на экран.
Мы должны собрать kernel.asm в объектный файл, используя NASM; затем при помощи GCC скомпилировать kernel.c в ещё один объектный файл. Затем их нужно присоединить к исполняемому загрузочному ядру.
Для этого мы будем использовать связывающий скрипт, который передаётся ld в качестве аргумента.
/* * link.ld */ OUTPUT_FORMAT(elf32-i386) ENTRY(start) SECTIONS { . = 0x100000; .text: { *(.text) } .data: { *(.data) } .bss: { *(.bss) } }
Сперва мы зададим формат вывода как 32-битный Executable and Linkable Format (ELF). ELF - это стандарный формат бинарных файлов Unix-систем архитектуры x86. ENTRY принимает один аргумент, определяющий имя символа, являющегося точкой входа. SECTIONS - это самая важная часть. В ней определяется разметка нашего исполняемого файла. Мы определяем, как должны соединяться разные секции и где их разместить.
В скобках после SECTIONS точка (.) отображает счётчик положения, по умолчанию равный 0x0. Его можно изменить, что мы и делаем.
Смотрим на следующую строку: .text: { *(.text) } . Звёздочка (*) - это специальный символ, совпадающий с любым именем файла. Выражение *(.text) означает все секции.text из всех входных файлов.
Таким образом, линковщик соединяет все секции кода объектных файлов в одну секцию исполняемого файла по адресу в счётчике положения (0x100000). После этого значение счётчика станет равным 0x100000 + размер полученной секции.
Аналогично всё происходит и с другим секциями.
Теперь все файлы готовы к созданию ядра. Но остался ещё один шаг.
Существует стандарт загрузки x86-ядер с использованием бутлоадера, называющийся Multiboot specification . GRUB загрузит наше ядро, только если оно удовлетворяет этим спецификациям .
Следуя им, ядро должно содержать заголовок в своих первых 8 килобайтах. Кроме того, этот заголовок должен содержать 3 поля, являющихся 4 байтами:
Наш kernel.asm станет таким:
;;kernel.asm ;nasm directive - 32 bit bits 32 section .text ;multiboot spec align 4 dd 0x1BADB002 ;magic dd 0x00 ;flags dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space:
Теперь мы создадим объектные файлы из kernel.asm и kernel.c и свяжем их, используя наш скрипт.
Nasm -f elf32 kernel.asm -o kasm.o
Эта строка запустит ассемблер для создания объектного файла kasm.o в формате ELF-32.
Gcc -m32 -c kernel.c -o kc.o
Опция “-c” гарантирует, что после компиляции не произойдёт скрытого линкования.
Ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o
Это запустит линковщик с нашим скриптом и создаст исполняемый файл, называющийся kernel .
GRUB требует, чтобы имя ядра удовлетворяло шаблону kernel-
Теперь поместите его в директорию /boot . Для этого понадобятся права суперпользователя.
В конфигурационном файле GRUB grub.cfg добавьте следующее:
Title myKernel root (hd0,0) kernel /boot/kernel-701 ro
Не забудьте убрать директиву hiddenmenu , если она есть.
Перезагрузите компьютер, и вы увидите список ядер с вашим в том числе. Выберите его, и вы увидите:
Это ваше ядро! В добавим систему ввода / вывода.
Читая Хабр в течении последних двух лет, я видел только несколько попыток разработки ОС (если конкретно: от пользователей и (отложено на неопределённый срок) и (не заброшено, но пока больше походит на описание работы защищённого режима x86-совместимых процессоров, что бесспорно тоже необходимо знать для написания ОС под x86); и описание готовой системы от (правда не с нуля, хотя в этом нет ничего плохого, может даже наоборот)). Мне почему-то думается, что почти все системные (да и часть прикладных) программисты хотя бы раз, но задумывались о написании собственной операционной системы. В связи с чем, 3 ОС от многочисленного сообщества данного ресурса кажется смешным числом. Видимо, большинство задумывающихся о собственной ОС так никуда дальше идеи и не идёт, малая часть останавливается после написания загрузчика, немногие пишут куски ядра, и только безнадёжно упёртые создают что-то отдалённо напоминающее ОС (если сравнивать с чем-то вроде Windows/Linux). Причин для этого можно найти много, но главной на мой взгляд является то, что люди бросают разработку (некоторые даже не успев начать) из-за небольшого количества описаний самого процесса написания и отладки ОС, который довольно сильно отличается от того, что происходит при разработке прикладного ПО.
Этой небольшой заметкой хотелось бы показать, что, если правильно начать, то в разработке собственной ОС нету ничего особо сложного. Под катом находится краткое и довольно общее руководство к действию по написанию ОС с нуля.
Не надо писать загрузчик. Умные люди придумали Multiboot Specification , реализовали и подробно описали, что это такое и как его использовать. Не хочу повторяться, просто скажу, что это работает, облегчает жизнь, и его надо применять. Спецификацию, кстати, лучше прочесть полностью, она небольшая и даже содержит примеры.
Не надо писать ОС полностью на ассемблере. Это не так чтобы плохо, скорее наоборот - быстрые и маленькие программы всегда будут в почёте. Просто так как этот язык требует значительно больших усилий на разработку, то использование ассемблера приведёт только к уменьшению энтузиазма и, как следствие, к забрасыванию исходников ОС в долгий ящик.
Не надо загружать кастомный шрифт в видео память и выводить что-либо на русском. Толку от этого никакого. Гораздо проще и универсальнее использовать английский, а изменение шрифта оставить на потом, загружая его с жёсткого диска через драйвер файловой системы (заодно будет дополнительный стимул сделать больше, чем просто начать).
После начального ликбеза необходимо определиться с главными вопросами:
На данном шаге необходимо проверить как можно больше возможностей средств разработки, которые планируется использовать в будущем. Например, загрузку модулей в GRUB или использование в виртуальной машине физического диска/раздела/флешки вместо образа.
После того как этот этап прошёл успешно, начинается настоящая разработка.
Естественно, тут же следует упомянуть и о стандартной библиотеке. Полная реализация не является необходимой, но основное подмножество функций реализовать стоит. Тогда написание кода будет значительно привычнее и быстрее.
Можно посоветовать следующее:
Необходимости в документации, описывающей процесс написания того или другого типа программы, нет. Но сделать заготовку из типовых элементов стоит. Это не только упростит добавление программ (что можно делать и копированием существующих программ с их последующим изменением, но это потребует больше времени), но также позволит легче их обновлять при изменениях в интерфейсах, форматах или чем-то ещё. Понятно, что таких изменений в идеале быть не должно, но так как разработка ОС - вещь нетипичная, то есть достаточно много мест для потенциально неверных решений. А вот понимание ошибочности принятых решений как всегда придёт через некоторое время после их внедрения.
Аббревиатура "NT" маркетингом расшифровывается как "New Technologies", но в проектной документации, она означала совсем другое. Дело в том, что Windows NT разрабатывалась для нового, еще не выпущенного в 1988-м году, процессора Intel i860. Его кодовое название было "N10" (N T
en).
Первая версия - Windows NT 3.1, вышла через 5 лет, в 1993 году. На этот момент в команде было уже 250 разработчиков.
20-30 лет назад использовалась только одна методология программирования "Водопад". Она представляет собой последовательность:
Спецификации → Дизайн → Реализация → Тестирование → Поставка.
Но такая методология работает только для небольших проектов. Для такого продукта, как Windows сегодня, нужны другие методологии:
У всех этих методологий есть и преимущества и недостатки. В зависимости от размера команды и этапа развития компонента разные группы разработчиков Windows применяют разные методологии разработки.
Для Windows, как продукта в целом, используется Product Cycle Model:
Самая главная проблема в разработке продукта такого масштаба состоит в том, что разработка требует времени. На начальном этапе решаются те проблемы, которые существуют в текущем времени и существующими средствами. Но единственная вещь, которая постоянна, это то, что все изменится. За годы разработки:
Несмотря на то, что разные команды ведут разработку по-разному, существуют "универсальные" правила:
От себя отмечу, что за месяц работы с Windows 7 build 6801 в качестве основной ОС на домашнем компьютере, у меня сформировалось положительное впечатление об этой сборки.
Весь процесс разработки Windows построен вокруг ежедневной сборки:
Когда-то раньше была только одна ветка исходного кода, и все разработчики вносили изменения прямо в неё. Сейчас команда разработчиков настолько большая, что это не работает. Поддерживается множество веток, среди которых есть основная - WinMain. У каждой лаборатории есть своя локальная ветка разработки, в которую интегрируются изменения. Проверенные изменения со временем интегрируются в WinMain.
Ежедневный цикл разработки:
Все участники проекта, включая самых высокопоставленных руководителей, используют промежуточные версии на своих рабочих (а обычно и домашних) компьютерах.
На чем пишется Windows?
Многие внутренние инструменты, такие как build, можно скачать с microsoft.com/whdc/devtools.
Ядро Windows 7 претерпело следующие изменения:
Раньше обновления зачастую были кумулятивными(накапливаемыми). Это означало, что если ошибочный код содержался в раннем обновлении компонента, то и поздние версии будут содержать этот код. Но не всем пользователям нужны все обновления, у них разная конфигурация.
Теперь после выпуска (RTM) в Windows существует 2 версии исходного кода:
Работа по созданию обновления безопасности начинается с обнаружения уязвимости. Есть масса разных способов обнаружения - внутренние команды безопасности, партнеры безопасности, разработчики. Когда уязвимость обнаружена, начинается 2 параллельных процесса:
После разработки исправления, начинаются проверки его кода. Когда они завершатся, исправление интегрируется в сборку, и сборка отправляется на тестирование:
Только исправления, удовлетворяющие всем критериям качества, допускаются к выпуску на Windows Update и Download Center.
Если подходить по существу...
ОС - это такая штука, которая реализует многозадачность (обычно) и заведует распределением ресурсов между этими задачами и вообще. Нужно следить, чтобы задачи друг другу не могли вредить и работали в разных областях памяти и с устройствами работали по очереди, это хотя бы. А ещё надо предоставить возможность передавать сообщения от одной задачи к другой.
Ещё ОС, ежели имеется долговременная память, должна предоставлять доступ к ней: то есть предоставлять все функции для работы с файловой системой. Это минимум.
Почти везде самый первый загрузочный код должен писаться на ассемблере - там бывает куча правил, где оно должно быть, как должно выглядеть, что должно делать, и какой размер не превышать.
Для РС надо на асме писать бутлоадер, который будет вызываться BIOS и кой должен, не превышая четырёх с копейками сотен байт, что-то сделать и запустить основную ОС - передать управление основному коду, который в ближайшей же перспективе можно писать уже и на С.
Для ARM надо на асме делать таблицу прерываний (сброс, ошибки разные, прерывания IRQ, FIQ и пр.) и передачу управления в основной код. Хотя, во многих средах разработки такой код для почти любого контроллера имеется.
То есть, необходимо для этого:
Далее. Допустим, вы что-то написали. Надо это дело тестировать. Либо надо устройство физическое, на коем будут идти эксперименты (отладочная плата, второй компьютер), либо эмулятор его. Второе обычно использовать и проще, и быстрее. Для PC, например, VMWare.
Статей по этой теме в интернете тоже достаточно, если хорошо поискать. А также есть множество примеров готовых ОС с исходниками.
Даже можно при большом желании посмотреть исходники старого ядра NT-систем (Windows), как отдельно (кое микрософтом выложено, с комментариями и разного рода справочными материалами), так и в совокупности со старыми же ОС (утекло).