Маленькая ОС с нуля на C++ и ассемблере / Habr
Сразу говорю, не закрывайте статью с мыслями «Блин, еще один Попов». У него всего-то слизанная Ubuntu, а у меня все с нуля, включая ядро и приложения. Итак, продолжение под катом.Группа ОС: вот.
Сначала я кину вам один скриншот.
Больше их нет, а теперь поподробнее о том, зачем я ее пишу.
Был теплый апрельский вечер, четверг. Я еще с детства мечтал написать ОС, как вдруг подумал: «Я же теперь знаю плюсы и асм, чего бы не воплотить мою мечту?». Загуглил сайты по этой тематике и нашел статью с Хабра: «Как начать и не бросить писать ОС». Спасибо ее автору за ссылку на OSDev Wiki внизу. Я зашел туда и начал работу. Там были в одной статье все данные о минимальной ОС. Я начал собирать кросс-gcc и binutils, а потом все переписал оттуда. Видели вы бы мою радость, когда я увидел надпись «Hello, kernel World!» Я прямо со стула подпрыгнул и понял — я не сдамся. Я написал «консоль» (в кавычках, у меня не было доступа к клавиатуре), но потом решил написать оконную систему. В итоге она заработала, но доступа к клавиатуре у меня не было. А потом я решил придумать название, опираясь на X Window System. Загуглил Y Window System — она есть. В итоге назвал Z Window System 0.1, входящая в OS365 pre-alpha 0.1. И да, ее не видел никто, кроме меня самого. Потом я понял, как реализовать поддержку клавиатуры. Скрин самой первой версии, когда еще не было ничего, даже оконной системы:
В ней даже не двигался курсор текста, как вы видите. Затем я написал парочку простых приложений на основе Z. И вот релиз 1.0.0 alpha. Там были много вещей, даже меню системы. А файловый менеджер и калькулятор просто не работали.
Далее 1.0.1 с текстовым редактором, 1.0.2 с граф. режимом 640×480, 1.0.3 с рабочим столом, 1.0.4 с русским языком, 1.0.5 с фиксами и чем-то еще, 1.0.6 с часами, а вот на 1.1 остановимся поподробнее.
Меня прямо терроризировал друг, которому важны одни красивости (Митрофан, сорри). Говорил «Запили VBE-режим 1024*768*32, запили, запили! Ну запили!». Ну я уже устал его выслушивать и все-таки запилил его. О реализации ниже.
Я сделал все моим загрузчиком, а именно GRUB’ом. С его помощью можно задать графический режим без осложнений путем добавления нескольких магических строчек в заголовок Multiboot.
.set ALIGN, 1<<0
.set MEMINFO, 1<<1
.set GRAPH, 1<<2
.set FLAGS, ALIGN | MEMINFO | GRAPH
.set MAGIC, 0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS)
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
.long 0, 0, 0, 0, 0
.long 0 # 0 = set graphics mode
.long 1024, 768, 32 # Width, height, depth
А потом из структуры информации Multiboot я беру адрес фреймбуфера и разрешение экрана и пишу туда пиксели. VESA все сделали очень замороченно — цвета RGB надо вводить в обратном порядке (не R G B, а B G R). Я несколько дней не понимал — почему пиксели не выводятся!? В итоге я понял, что забыл поменять значения 16 цветовых констант с 0…15 на их RGB-эквиваленты. В итоге релизнул, заодно запилил градиентный фон. Потом я сделал консоль, 2 приложения и релизнул 1.2. Ах да, чуть не забыл — скачать ОС можно на сайте (хабраэффект, он лежит).
Продолжение следует…
Пишем свою ОС: Выпуск 2 / Habr
Здравствуйте. Это снова мы, iley и pehat, с долгожданной второй статьёй из цикла «Пишем свою ОС» (первая статья здесь). Извиняемся за большую паузу после первой статьи, нам понадобилось некоторое время, чтобы определить дальнейшее направление нашей работы. В этом выпуске мы кратко рассмотрим защищённый режим 32-битных процессоров Intel. Ещё раз подчеркнём, что мы не ставим себе целью дать исчерпывающие теоретические данные.Давайте на минуту вспомним нашу программу из предыдущего выпуска. Она запускалась вместо операционной системы и выводила сообщение «Hello world». Программа была написана на 16-битном ассемблере и работала в так называемом реальном режиме. Как вы наверняка знаете, при адресации в реальном режиме физический адрес формируется с помощью сегмента и смещения и имеет размерность 20 бит. Несложная математика подсказывает нам, что таким образом можно обращаться всего к мегабайту оперативной памяти.
Небольшой объём доступной памяти — это не единственная проблема реального режима. Представьте себе ситуацию, когда в памяти одновременно находится операционная система и несколько прикладных программ. В реальном режиме ничто не мешает любой из прикладных программ обратиться по адресу, принадлежащему другой программе или даже операционной системе. Таким образом, ошибка в одной программе может привести к краху всей системы.
Чтобы решить эти две проблемы, Intel в своё время разработали новый, значительно более сложный способ адресации оперативной памяти. Точнее, Intel разработали даже несколько способов адресации, и все они известны под собирательным названием защищённый режим.
Итак, адресация в защищённом режиме может происходить в одном из трёх режимов (извините за тавтологию) — в сегментном, страничном и сегментно-страничном. В этом выпуске мы рассмотрим только сегментный режим.
В сегментном режиме в памяти выделяются, как ни странно, сегменты. Эти сегменты значительно отличаются от простых и знакомых нам сегментов реального режима. В данном случае сегмент — это непрерывная область памяти, которая характеризуется базой, лимитом (т.е. грубо говоря, размером) и некими дополнительными атрибутами. База — это, грубо говоря, физический адрес, с которого начинается сегмент (на самом деле, это не физический адрес, а так называемый линейный, но об этом позже). Важно заметить, что, в отличие от сегментов реального режима, наши сегменты могут иметь произвольный размер. База, размер и атрибуты каждого сегмента хранятся в дескрипторе. Дескрипторы хранятся в специальных таблицах, а для выбора конкретного дескриптора из таблицы используются селекторы. Не пугайтесь такой кучи новых страшных слов, мы сейчас всё разложим по полочкам.
Итак, при каждом обращении к памяти процессор берёт из регистра (например, CS), селектор, находит по нему нужный дескриптор в таблице, извлекает из дескриптора адрес начала сегмента, прибавляет к адресу начала смещение и получает линейный адрес (обратите внимание, что линейный адрес и физический адрес — это разные вещи, но на данном этапе можно полагать, что это одно и то же). Кроме того, процессор проверяет, не вышел ли полученный адрес за границу сегмента и допускают ли атрибуты сегмента доступ к нему в данный момент. Схема вычисления линейного адреса выглядит примерно так:
Итак, как мы уже говорили, в дескрипторе хранится база (base address), размер (segment limit) и всякие дополнительные атрибуты сегмента. Давайте взглянем на схему дескриптора.
Обратите внимание, что под базу выделено 32 бита, а под лимит всего 20. Как же так, — спросите вы — разве нельзя создать сегмент, размер которого будет больше мегабайта? Можно. Для этого используется несложный трюк. Если бит
Как упоминалось, дескрипторы хранятся в специальных таблицах. Таблиц может быть несколько, но в любом случае в памяти обязана присутствовать GDT — глобальная таблица дескрипторов. Она одна на всю операционную систему. В свою очередь, у каждой задачи, то есть процесса может быть ноль, одна или несколько LDT — локальных таблиц дескрипторов. Адрес и размер GDT хранится в регистре GDTR, текущей LDT — в регистре LDTR. Пока нам LDT не понадобится. Кроме того, бывают еще IDT — таблицы дескрипторов прерываний, но их рассмотрение отложим до следующего выпуска.
Для выбора дескриптора из таблицы используется структура данных, называемая селектором. Вот как он выглядит:
Селектор содержит в себе бит, указывающий, в какой таблице искать дескриптор, локальной или глобальной (L/G) и собственно номер дескриптора (descriptor number). Кроме того, у селектора есть поле RPL, но оно нас пока не интересует.
Итак, давайте перейдём к делу!
;16-битная адресация, пока мы находимся в реальном режиме
use16
org 0x7c00
start:
jmp 0x0000:entry ;теперь CS=0, IP=0x7c00
entry:
mov ax, cs
mov ds, ax
;очистить экран
mov ax, 0x0003
int 0x10
;открыть A20
in al, 0x92
or al, 2
out 0x92, al
;Загрузить адрес и размер GDT в GDTR
lgdt [gdtr]
;Запретить прерывания
cli
;Запретить немаскируемые прерывания
in al, 0x70
or al, 0x80
out 0x70, al
;Переключиться в защищенный режим
mov eax, cr0
or al, 1
mov cr0, eax
;Загрузить в CS:EIP точку входа в защищенный режим
O32 jmp 00001000b:pm_entry
;32-битная адресация
use32
;Точка входа в защищенный режим
pm_entry:
;Загрузить сегментные регистры (кроме SS)
mov ax, cs
mov ds, ax
mov es, ax
mov edi, 0xB8000 ;начало видеопамяти в видеорежиме 0x3
mov esi, msg ;выводимое сообщение
cld
.loop ;цикл вывода сообщения
lodsb ;считываем очередной символ строки
test al, al ;если встретили 0
jz .exit ;прекращаем вывод
stosb ;иначе выводим очередной символ
mov al, 7 ;и его атрибут в видеопамять
stosb
jmp .loop
.exit
jmp $ ;зависаем
msg:
db 'Hello World!', 0
;Глобальная таблица дескрипторов.
;Нулевой дескриптор использовать нельзя!
gdt:
db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
db 0xFF, 0xFF, 0x00, 0x00, 0x00, 10011010b, 11001111b, 0x00
gdt_size equ $ - gdt
;данные, загружаемые в регистр GDTR
gdtr:
dw gdt_size - 1
dd gdt
finish:
times 0x1FE-finish+start db 0
db 0x55, 0xAA ; сигнатура загрузочного сектора
Теперь немного поясним код.
В строках:
in al, 0x92<br/>
or al, 2<br/>
out 0x92, al
происходит разблокирование адресной линии A20. Что это значит? Вспомним, что в реальном режиме можно адресовать 1 МБ памяти в формате
сегмент:смещение
(20 бит на адрес). Однако, обратившись, например, по адресу FFFF:FFFF
, можно “прыгнуть” немного выше этой планки, и полученный адрес будет иметь длину 21 бит. В процессорах до 80286 старший (двадцатый, если считать от нуля) отбрасывался, и поэтому для совместимости со старыми программами ввели блокировку адресной линии A20. В настоящее время все ОС работают в защищенном режиме, и поэтому для нужд адресации необходимо как можно раньше разблокировать этот бит. Такие дела.После загрузки адреса и лимита глобальной таблицы дескрипторов в регистр GDTR идет поголовный запрет на прерывания. Объясняется это тем, что все обработчики прерываний работают в реальном режиме, и в защищенном режиме начнутся проблемы с адресацией. Так что пока строго-настрого запрещаем прерывания.
Включение защищенного режима осуществляется путем установки младшего бита регистра CR0
. Сразу после перехода в защищенный режим возникает некая неопределенность: в CS:EIP
надо установить точку входа в защищенный режим в формате селектор:смещение
, а у нас там все еще пережитки реального режима. Поэтому выполняем следующую инструкцию:
O32 jmp 00001000b:pm_entry
Здесь используется префикс преобразования разрядности операнда
O32
, который позволяет делать дальний безусловный переход с 32-битным смещением. Ура, мы наконец-то можем воспользоваться прелестями защищенного режима!Для запуска программы нужно проделать манипуляции, полностью совпадающие с описанными в первой статье. Там же вы найдете список литературы на случай, если захотите узнать о защищенном режиме подробнее.
Нам не хватило фантазии на что-нибудь, демонстрирующее реальные возможности защищенного режима, поэтому мы снова выводим “Hello, World!”, но уже пользуясь прямым доступом к видеопамяти. Чтобы сделать что-нибудь красивое, было бы удобно воспользоваться прерываниями. Тому, как использовать их в защищенном режиме, будет посвящена следующая статья нашего цикла. А вторая статья на этом заканчивается. Будем рады увидеть ваши отзывы и пожелания.
пишем ядро на языке C
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 компьютер (разумеется)
- Linux
- Ассемблер NASM
- GCC
- ld (GNU Linker)
- GRUB
- Исходный код
Ну и неплохо было бы иметь представление о том, как работает UNIX-подобная ОС. Исходный код можно найти в репозитории на Github.
Для начала напишем небольшой файл в x86 ассемблере, который будет отправной точкой для запуска ядра. Этот файл будет вызывать внешнюю функцию на C, а затем остановит поток программы.
Как убедиться, что этот код послужит отправной точкой для ядра?
Мы будем использовать скрипт компоновщика, который связывает объектные файлы с целью создания окончательного исполняемого файла ядра. В этом скрипте явно укажем, что бинарный файл должен быть загружен по адресу 0x100000. Этот адрес, является тем местом, где должно быть ядро.
Вот код сборки:
;;kernel.asm bits 32 ;директива nasm - 32 bit section .text global start extern kmain ;kmain определена в C-файле start: cli ;блокировка прерываний mov esp, stack_space ;установка указателя стека call kmain hlt ;остановка процессора section .bss resb 8192 ;8KB на стек stack_space:
Первая инструкция 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.
В kernel.asm мы сделали вызов функции kmain(). Таким образом, код на C начнет выполнятся в kmain():
/* * kernel.c */ void kmain(void) { const char *str = "my first kernel"; char *vidptr = (char*)0xb8000; //видео пямять начинается здесь unsigned int i = 0; unsigned int j = 0; /* этот цикл очищает экран*/ while(j < 80 * 25 * 2) { /* пустой символ */ vidptr[j] = ' '; /* байт атрибутов */ vidptr[j+1] = 0x07; j = j + 2; } j = 0; /* в этом цикле строка записывается в видео память */ while(str[j] != '\0') { /* ascii отображение */ vidptr[i] = str[j]; vidptr[i+1] = 0x07; ++j; i = i + 2; } return; }
Наше ядро будет очищать экран и выводить на него строку «my first kernel».
Для начала мы создаем указатель vidptr, который указывает на адрес 0xb8000. Этот адрес является началом видеопамяти в защищенном режиме. Текстовая память экрана – это просто кусок памяти в нашем адресном пространстве. Ввод/вывод для экрана на карте памяти начинается с 0xb8000 и поддерживает 25 строк по 80 ascii символов каждая.
Каждый элемент символа в этой текстовой памяти представлен 16 битами (2 байта), а не 8 битами (1 байт), к которым мы привыкли. Первый байт должен иметь представление символа, как в ASCII. Второй байт является атрибутным байтом. Он описывает форматирование символа, включая разные атрибуты, например цвет.
Чтобы напечатать символ с зеленым цветом на черном фоне, мы сохраним символ 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.
В первом цикле while программа записывает пустой символ с атрибутом 0x07 по всем 80 столбцам из 25 строк. Таким образом, экран очищается.
Во втором цикле while символы строки «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-битный исполняемый (ELF). ELF – стандартный формат двоичного файла для Unix-подобных систем на архитектуре x86.
ENTRY принимает один аргумент. Он указывает имя символа, которое должно быть точкой входа нашего исполняемого файла.
SECTIONS – самая важная часть, где мы определяем разметку исполняемого файла. Здесь указывается, как должны быть объединены различные разделы и в каком месте они будут размещаться.
В фигурных скобках, следующих за инструкцией SECTIONS, символ периода (.) – представляет собой счетчик местоположения.
Счетчик местоположения всегда инициализируется до 0x0 в начале блока SECTIONS. Его можно изменить, присвоив ему новое значение.
Как уже говорилось, код ядра должен начинаться с адреса 0x100000. Таким образом, мы установили счетчик местоположения в 0x100000.
Посмотрите на следующую строку .text: {*(.text)}
Звездочка (*) является спецсимволом, который будет соответствовать любому имени файла. То есть, выражение *(.text) означает все секции ввода .text из всех входных файлов.
Таким образом, компоновщик объединяет все текстовые разделы объектных файлов в текстовый раздел исполняемого файла по адресу, хранящемуся в счетчике местоположения. Раздел кода исполняемого файла начинается с 0x100000.
После того, как компоновщик разместит секцию вывода текста, значение счетчика местоположения установится в 0x1000000 + размер раздела вывода текста.
Аналогично, разделы данных и bss объединяются и помещаются на значения счетчика местоположения.
Теперь все файлы, необходимые для сборки ядра, готовы. Но, поскольку мы намеренны загружать ядро с помощью GRUB, нужно еще кое-что.
Существует стандарт для загрузки различных x86 ядер с использованием загрузчика, называемый спецификацией Multiboot.
GRUB загрузит ядро только в том случае, если оно соответствует Multiboot-спецификации.
Согласно ей, ядро должно содержать заголовок в пределах его первых 8 килобайт.
Кроме того, этот заголовок должен содержать дополнительно 3 поля:
- поле магического числа: содержит магическое число 0x1BADB002, для идентификации заголовка.
- поле флагов: сейчас оно не нужно, просто установим его значение в ноль.
- поле контрольной суммы: когда задано, должно возвращать ноль для суммы с первыми двумя полями.
Итак, kernel.asm будет выглядеть таким образом:
;;kernel.asm ;nasm directive - 32 bit bits 32 section .text ;multiboot spec align 4 dd 0x1BADB002 ;магические числа dd 0x00 ;флаги dd - (0x1BADB002 + 0x00) ;контрольная сумма. мч+ф+кс должно равняться нулю global start extern kmain ;kmain определена во внешнем файле start: cli ;блокировка прерываний mov esp, stack_space ;указатель стека call kmain hlt ;остановка процессора section .bss resb 8192 ;8KB на стек stack_space:
Теперь создадим объектные файлы из kernel.asm и kernel.c, а затем свяжем их с помощью скрипта компоновщика.
nasm -f elf32 kernel.asm -o kasm.o
запустит ассемблер для создания объектного файла kasm.o в формате 32-битного ELF.
gcc -m32 -c kernel.c -o kc.o
Параметр «-c» гарантирует, что после компиляции связывание не произойдет неявным образом.
ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o
запустит компоновщик с нашим скриптом и сгенерирует исполняемое именованное ядро.
UNIX-подобная ОС с ее ядром почти поддалась. GRUB требует, чтобы ядро имело имя вида kernel-<version>. Переименуйте ядро, к примеру, в kernel-701.
Теперь поместите его в каталог /boot. Для этого вам потребуются права суперпользователя.
В конфигурационном файле GRUB grub.cfg вы должны добавить запись такого вида:
title myKernel root (hd0,0) kernel /boot/kernel-701 ro
Не забудьте удалить директиву hiddenmenu, если она существует.
Перезагрузите компьютер, и вы сможете наблюдать список с именем вашего ядра. Выберите его, и вы увидите:
Это ваше ядро! Оказывается, UNIX-подобная операционная система и ее составляющие не так уж сложны, верно?
PS:
- Всегда желательно использовать виртуальную машину для всех видов взлома ядра.
- Чтобы запустить это ядро на grub2, который является загрузчиком по умолчанию для более новых дистрибутивов, ваша конфигурация должна выглядеть так:
menuentry 'kernel 701' { set root='hd0,msdos1' multiboot /boot/kernel-701 ro }
Если вы хотите запустить ядро на эмуляторе qemu вместо загрузки с помощью GRUB, вы можете сделать так:
qemu-system-i386 -kernel kernel
Теперь вы имеете представление о том, как устроены UNIX-подобная ОС и ее ядро, а также сможете без труда написать последнее.
Создание операционной системы на базе ядра linux. С нуля / Habr
Рано или поздно каждый пользователь Линукса задумывается над созданием собственного дистрибутива. Некоторые аргументируют это тем, что можно «все настроить под себя». Другие сетуют на то, что среди уже представленных дистрибутивов в Ветке нет идеального. А у них, якобы, есть суперконцептуальные идеи для собственной системы. Зачем я всю эту психологию затеял? Для того, чтобы сразу перекрыть кислород играющимся с Линуксом новичкам, которым делать нечего. Если уж задумались над созданием ОС, думайте до конца. Итак,Я хочу создать ОС на базе Linux.
Сразу предупреждаю: был бы XVIII век, всех тех, кто для основы своей будущей системы выбирает другой развитый дистрибутив (и, не дай Бог, популярный…) ждала бы виселица. Пост именно про создание системы с нуля, а значит, всякие Slax и Linux Mint мы трогать не будем.
Шаг 1. Выбор носителя
Вариантов немного: либо ваша ОС запускается с LiveCD, либо с жесткого диска, либо с флеш-устройства. Сразу оговорюсь: не скажу в посте ни слова про жесткий диск, потому что гораздо удобнее создавать гибкий дистрибутив из серии «все свое ношу с собой», либо залоченный дистрибутив на оптическом диске. Если вы научитесь создавать LiveCD или LiveUSB систему, с установкой на жесткий диск проблем не будет.
На всякий случай, приготовьте чистую флешку, CD-диск, и установите, наконец, Virtualbox.
Шаг 2. Компиляция ядра
По поводу выхода третьего ядра Linux, этот шаг воодушевляет на дальнейшие разработки… Итак, нам нужны исходники ядра. Каждый пользователь знает, что их можно достать на сайте kernel.org. Ни в коем случае, слышите?, никогда не прикручивайте к своей системе постороннее ядро, скомпилированное не вами!
Поскольку лень моя зашкаливала, я создал папку /linuxkernel и распаковал туда архив с исходниками. Залогинившись под рутом, я сделал следующее:
cd /linuxkernel
make menuconfig
В принципе, ядро можно конфигурировать тремя способами: make config (диалоговая конфигурация), make menuconfig (псевдографическая конфигурация через ncurses), а также make xconfig (графическая конфигурация). Суть в том, что make config испортит вам настроение надолго, т.к. он задаст все возможные вопросы по всем аспектам всех тем. Проблема с make xconfig встречается не у всех, но вот у меня встречалась и встречается. Если приспичило сделать через X, разбирайтесь сами. Оптимальный вариант — make menuconfig. Эта штука откроет вам псевдографический интерфейс, через который вы сможете настроить ядро на свой лад. Штука требует библиотеки ncurses, которая легко устанавливается.
В принципе, если ваш мозг хоть сколько понимает Линукс, вы разберетесь с конфигурированием. Процесс это интересный, вариантов действительно много, а справка, хоть и на английском языке, но все же радует своей доступностью и простотой.
Однако, направить вас все же придется. Перейдите по адресу File Systems —> и поставьте необходимые звездочки. Буква M означает, что поддержка того или иного драйвера осуществляется с помощью подключения к ядру внешнего модуля (ненавижу их!). Нам понадобится также поддержка isofs, для чтения дисков. File Systems —> CD-ROM/DVD Filesystems —> ISO 9660 CDROM file system support. Можно еще поддержать древнедосовские системы.
Чмошные разработчики Mandriva забыли разрешить File systems —> DOS/FAT/NT Filesystems —> NTFS write support, и на одном из их дистрибутивов я мучился с доступом к древневиндовскому разделу.
Посмотрите Processor type and features —> Processor family, мне порекомендовали выбрать Pentium-MMX.
Еще поройтесь в Device Drivers, полезно. Можете шутки ради понавыбирать там все и скомпилировать ядро весом > 50 Мб.
Далее. Ядро после загрузки себя должно загружать, собственно, систему. Либо из скомпилированных в себе файлов (используются во встраиваемых системах), либо из CPIO архива, сжатого чем-нибудь, либо из Initrd. Здесь вам не DOS, здесь не получится сразу сослаться на какой-нибудь init’овый файл в корневом каталоге диска или флешки. На самом деле получится, не слушайте дядю Анникса! Неправильно это, хоть в Интернете по этому поводу уже нехилая полемика ведется. В своей системе мы будем использовать initrd, т.к. это удобно, и не вызовет нецензурных выражений от сторонних разработчиков, в отличие от CPIO архива.
Ах, да, скомпилируйте ядро командой
make bzImage
Если у вас x86, найдете его по адресу /linuxkernel/arch/x86/boot/bzImage.
Для суровых челябинских программистов можно использовать Кросс-компайлинг…
Создание Ramdisk.
Теперь нам нужен initrd с установленной там простейшей оболочкой. Мы будем использовать busybox, потому что эта няша может все. Способ мы украдем у Роберто де Лео, создателя Movix (я бы даже уважать его начал, если бы не запредельная любовь к Perl):
dd if=/dev/zero of=/dev/ram0 bs=1k count=5000 - Создаем Ramdisk в оперативной памяти нашего компьютера.
mke2fs -m0 /dev/ram0 5000 - Форматируем Ramdisk в системе Ext2
mkdir /distro - Создаем папку
mount /dev/ram0 /distro - Монтируем в папку /distro
Все, теперь у нас есть Ramdisk, емкостью в 5 Мб. Можно и больше, только не нужно. В отличие от Томаса Матеджисека, я не собираюсь пичкать initrd модулями в Squashfs, сжатыми LZMA. Все, что необходимо, будет скомпилировано вместе с ядром. Да, это не очень логично и правильно, но мороки в сто раз меньше. А специально для тех, кто осуждает такой подход, можно разрешить опцию модульности в ядре: Enable loadable module support.
В нашем Ramdisk’е, смонтированном в /distro, есть такая папка, lost+found. Это потому, что мы отформатировали его в ext2. Ни в коем случае нельзя ее удалять, хоть она здесь вряд ли поможет, образ-то фиксированный. Нам бы busybox сначала поставить…
Установка Busybox
Вот почему у таких классных проектов такие отстойные сайты? Хотя… это уже не суть важно, если исходники скачаны и успешно распакованы в папку /busybox.
Сконфигурировать busybox можно так же:
cd /busybox
make menuconfig
Если вы еще не поняли, что это, объясню. Busybox заменяет тонны UNIX приложений, хранящихся в папках /bin, /sbin, /usr/bin, /usr/sbin. Вместо этого, создается только одно приложение: /bin/busybox, а на него создается куча ссылок в указанных выше папках. Установим busybox следующей командой:
make CONFIG_PREFIX=/distro install
Еще Busybox создаст файлы /sbin/init и зачем-то /linuxrc, чтобы ваша система корректно запустилась. Но не все необходимые папки были созданы. Так что завершаем все руками и создаем:
/distro/etc
/distro/lib
/distro/dev
/distro/mnt
distro/proc
/distro/root
/distro/tmp
/distro/root
Если что забыл — вспомните, т.к. директории эти забыть сложно.
Все бы хорошо, вот только busybox для работы требует библиотеки, которые нужно скопировать в наш дистрибутив. Очень легко узнать, какие:
ldd /distro/bin/busybox
Программа покажет нам библиотеки, требуемые для нашей оболочки. Сразу говорю: linux gate создается ядром и скопирован быть не может.
При копировании библиотек можно отсекать отладочную информацию (так Роберто советует):
objcopy --strip-debug откуда куда
Делаем из Линукса Линукс
Надо создать несколько системных текстовых файлов:
Нам нужен /etc/inittab. Удивлю вас: в начале жизни система даже не знает, что такое Root. У нас даже пользователь безымянный, но вот файл общесистемных низкоуровневых фич (ОНФ) должен присутствовать. Пилотное содержание файла следующее:
::sysinit:/etc/rc.d/rc.S
# Запустить оболочку в консоли.
::respawn:-/bin/sh
# Перезагрузка по нажатии на Ctrl+Alt+Del.
::ctrlaltdel:/sbin/reboot
# Команды, выполняемые перед выключением и перезагрузкой.
::shutdown:/sbin/swapoff -a >/dev/null 2>&1
::shutdown:/bin/umount -a -r >/dev/null 2>&1
Следующий файл — /etc/fstab. Это таблица, в которой описано, что и куда монтировать при загрузке. Вещь бесполезная! Нам нужно обязательно смонтировать proc, иначе вообще ничего работать не будет, так что в файле пишем:
none /proc proc defaults 0 0
Для mount нужен также файл /etc/mtab. Создайте его и оставьте пустым.
Но mount сделает все необходимое только тогда, когда мы явно его об этом попросим. А просить мы будем в том самом первозагрузочном файле /etc/rc.d/rc.S (rc.d — папка). Вежливо попросим:
#!/bin/ash
/bin/mount -av -t nonfs
Еще нам необходим файл профиля (b)(a)sh, тут вообще раздолье для фантазии. Создаем файл /etc/profile и заполняем следующим:
PATH="$PATH:/bin:/sbin:/usr/bin:/usr/sbin:"
LESS=-MM
TERM=linux
HOME=/root
PS1='> '
PS2='> '
ignoreeof=10
export PATH DISPLAY LESS TERM PS1 PS2 HOME ignoreeof
Понадобится также файл /etc/shell, в котором указано, что есть оболочка:
/bin/sh
/bin/ash
/bin/bash
Вот собственно и все. Можно записывать наш Ramdisk в файл.
mkdir /os - папка для "готового".
umount /dev/ram0 - размонтируем кусочек оперативной памяти.
dd if=/dev/ram0 of=/os/initrd bs=1k count=5000 - создаем файл.
gzip /os/initrd - сжимаем файл initrd
Создание загрузочной флешки
«Финишная прямая» нашей маленькой разработки. Берем флешку, вставляем, форматируем в vfat (можно и в ext, но не забывайте, что еще не все пользователи Windows застрелились).
На флешке создаем папку boot, в ней папки initrd и kernel.
Из папки /os копируем сжатый Ramdisk в папку boot/initrd на флешке, называем «main.gz». Из папки с исходниками ядра копируем bzImage в папку boot/kernel на флешке, называем «main.lk». Достаем файлы загрузчика Syslinux (в Интернете, либо из другого дистрибутива: тут не принципиально), а именно syslinux.bin, syslinux.boot, syslinux.cfg. Копируем их в корневой каталог нашей флешки. В файле syslinux.cfg пишем что-то подобное:
default mm
prompt 1
timeout 100
label mm
kernel /boot/kernel/main.lk
append initrd=/boot/initrd/main.gz load_ramdisk=1 ramdisk_size=5000 rw root=/dev/ram0
label mc
kernel /boot/kernel/main.lk
append initrd=/boot/initrd/custom.gz load_ramdisk=1 ramdisk_size=5000 rw root=/dev/ram0
label cm
kernel /boot/kernel/custom.lk
append initrd=/boot/initrd/main.gz load_ramdisk=1 ramdisk_size=5000 rw root=/dev/ram0
label cc
kernel /boot/kernel/custom.lk
append initrd=/boot/initrd/custom.gz load_ramdisk=1 ramdisk_size=5000 rw root=/dev/ram0
label hd
localboot 0x80
Тем самым мы поддержали кастомные initrd и ядро, которые, эксперимента ради, можно подключить к нашему дистрибутиву.
Узнаем, каким девайсом в системе является наша флешка (можно запустить mount без параметров и посмотреть). Это либо /dev/sdb1, либо /dev/sdc1, либо /dev/sdd1. Стоит отмонтировать флешку перед началом установки.
Устанавливаем syslinux (если пакета в системе нет, apt-get install syslinux):
syslinux -d путь_к_устройству
В корневом каталоге флешки должен появиться файл ldlinux.sys. Если он есть, значит syslinux.bin, syslinux.boot больше не нужны.
Как настроить BIOS на загрузку из флешки, я вам рассказывать не буду — это легко. Скажу только, что очень удобно создать папку /boot/initrd/init, в которую можно будет смонтировать /boot/initrd/main, для последующей работы с ним. Только не забудьте разжимать и сжимать его gzip’ом.
Ну вот и все.
Как-бы я только что рассказал вам, как создать с нуля систему на Linux. Легко, не правда ли? Далее вы можете редактировать скрипт /sbin/init, ведь у вас еще много работы! Вы должны будете написать скрипт для монтирования флешки, который делает chroot в корневой каталог. В противном случае, вы вынуждены будете работать с ReadOnly разделом, величиной в 5 Мб. Но это уже совсем другая история.
Unnx Davis T, B.
Для непросвещенных:
Томас Матеджисек — создатель Slax и Linux Live Scripts.
Роберто де Лео — создатель Movix.
Пишем операционную системы своими руками
Пишем ОС
Введение
Не давно мне стало интересно на сколько тяжело написать операционную систему, как она вообще устроенна внутри, что должна делать, ну или хотя бы какой минимальный набор функциональности иметь. Так же было интересно низкоуровневое программирование, там где нет помощников и готовых функций, только ты, железо и BIOS, без него было бы совсем туго :). И я решил написать простенькую ОС а попутно подучить уже совсем забытый мной ассемблер, и архитектуру x86.
Первое что я решил заделать это поискать статьи, типа «Пишем простую ОС». Таковых оказалось довольно много но лично я смог уловить по ним только основной принцип написания ОС, довольно поверхностно. Возможно просто мой уровень IQ маловат, хотя признаться я его не когда не измерял, боюсь результата :). Ну так вот открыв кучу вкладок в своём браузере, а также различных исходников ОС в notepad++ я принялся за дело. Ниже я опишу всё что я выяснил о создании собственной ОС.
Загрузка ОС с дискеты
Нашу ОС мы разместим на дискете, по этому давайте рассмотрим процесс загрузки операционной системы с дискеты. Он состоит из нескольких этапов, сначала загружается BIOS (англ. basic input/output system — «базовая система ввода-вывода»), затем BIOS определяет различные устройства в том числе и блочные устройства ввода-вывода, К блочным относятся такие устройства, которые хранят информацию в блоках фиксированной длины, у каждого из которых есть свой собственный адрес. Обычно размеры блоков варьируются от 512 до 32 768 байт. Вся передача данных ведется пакетами из одного или нескольких целых (последовательных) блоков. Важным свойством блочного устройства является то, что оно способно читать или записывать каждый блок независимо от всех других блоков. Среди наиболее распространенных блочных устройств жесткие диски, приводы гибких дисков а так же, приводы компакт-дисков и флэш-накопители USB. Нас же интересуют приводы гибких дисков а именно привод для дискет диаметром 3.5 дюйма и объёмом 1.4МБ(1 474 560 байт)
Немного об устройстве привода. Привод состоит из электронной части и механической, в механической части присутствуют два двигателя, один из них шаговый, шаговым его называют по тому что он вращается не непрерывно, как обычные двигатели, а маленькими точными шагами, шаг двигателя 3.5 дюймового дисковода равен 1,8°,что позволяет с его помощью довольно точно позиционировать головку записи-чтения над определённой областью магнитного диска дискеты. Также к механической части относятся головки чтения-записи они то и считывают-записывают данные на магнитный диск. Их в дисководе две, одна над магнитным диском другая под ним. Ниже показано размещение головок над диском.
Д
иск
имеет магнитное покрытие и разбит на
дорожки и сектора, на одной стороне
диска содержится 80 дорожек, всего сторон
две, нумерация дорожек начинается с 0
по 79, в одной дорожке содержится 18
секторов, емкость одного сектора
составляет 512байт. Нумерация секторов
начинается с первого. Первый сектор
является загрузочным
После определения всех устройств BIOS начинает загрузку с накопителя который выбран как загрузочный. На этом накопителе должен присутствовать загрузочный сектор в котором находится программа загрузки ядра ОС. После чего BIOS прочтёт первый сектор диска, который как правило является загрузочным и если найдёт на нём сигнатуру загрузочного сектора, это два последних байта сектора, имеющие вид в шестнадцатеричной системе AAh,55h, то загрузит этот сектор в оперативную память по адресу 07C00h, и передаст управление загруженному коду. Далее этот код должен загрузить ядро ОС в память и передать ему управление.
Инструментарий
У меня нет ни дискеты ни дисковода, да и писать любую программу куда приятнее если есть возможность запускать её по сто раз за час, дабы убедится в работе каждой написанной строчки кода :). По этому воспользуемся виртуальной машиной, я выбрал Virtual Box. Скачать Virtual Box можно тут https://www.virtualbox.org/ . Моя версия 5.0.0 на Windows 7 64 бит, сразу работать не захотела, порыскав на форумах, оказалось проблема заключается в том, что у многих для установки расширенных тем оформления пропатчен фал Windows\system32\uxtheme.dll. Обычно оригинальный файл находится в том же каталоге и имеет имя uxtheme.dll.backup. Просто поменяйте между собой их названия.
Далее нам понадобится компилятор FASM( flat assembler ) — это свободно распространяемый многопроходной ассемблер, написанный Томашем Грыштаром (польск. Tomasz Grysztar). Скачать его можно с официального сайта http://flatassembler.net/. Писать код мы будем в Notepad++ его скачать можно тут https://notepad-plus-plus.org/. Установите FASM в корневой каталог диска C, затем установите Notepad++.
Также нам понадобится программа способная создать виртуальный образ дискеты, для этих целей я написал на C# простенькую программку, которую назвал imgMaster. Принцып её действия довольно прост, после запуска программы, нажимаем кнопку Add File, и выбираем первый файл, его размер должен составлять 512байт не больше, не меньше, не больше так как он может не поместится в загрузочный сектор размер которого, как мы уже знаем 512байт. А не меньше по тому что 511 и 512 байты, это сигнатура загрузочного сектора, без них BIOS решит что программа загрузчик отсутствует, и не станет не чего загружать из сектора. Далее при необходимости добавляем остальные файлы, и нажимаем Create IMG, сохраняем образ. Программа запишет файлы один за другим, в той же очерёдности в которой они расположены в списке, затем оставшееся место заполнит нулями, чтобы размер образа был равен ёмкости дискеты 1.4МБ(1 474 560 байт).
Пишем Hello World
Можно приступать к написанию первой тестовой программы, которая без операционной системы, будет способна вывести на экран традиционную надпись «Hello World!».
И так первое что нам необходимо сделать это узнать где мы будем находится в ОЗУ. Ниже представлена модель адресного пространства ОЗУ, как я её понял.
Адресное пространство:
Размер | Назначение | Физ.Адрес |
1KB | Векторы прерываний | 00000h |
256B | Область данных BIOS | 00400h |
638KB | Первая свободная область (Суда BIOS помещает данные из загрузочного сектора) | 00500h 07С00h |
64KB | Графический видео буфер | A0000h |
32KB | Вторая свободная область | B0000h |
32KB | Текстовый видео буфер | B8000h |
64KB | ПЗУ — Расширения BIOS | C0000h |
128KB | Третья свободная область | D0000h |
64KB | ПЗУ — BIOS | F0000h |
64KB | HMA | 100000h |
До 4GB | XMS Четвёртая свободная область | 10FFF0h |
Первые 640 Кбайт (до графического видео буфера) называются стандартной (conventional) памятью. Начинается стандартная память с килобайта, который содержит векторы прерываний, их 256 на каждый отводится по 4 байта.
Затем идет область данных BIOS. Где находятся данные необходимые для корректной работы функций BIOS. Но также можно модифицировать эту область, тем самым мы влияем на ход выполнения системных функций, по сути дела меняя что либо в этой области мы передаем параметры BIOS и его функциям, которые становятся более гибкими. С адреса 500h начинается свободная область до 640КБ. За 640 килобайтами начинается старшая память или верхняя (upper) память, она располагается до 1 мегабайта (до HMA), т.е. она составляет 384 Кбайт. Тут располагаются ПЗУ (постоянно запоминающее устройство ) : текстовый видео буфер (его микросхема рассчитана на диапазон B8000h…BFFFFh) и графический видео буфер (A0000h…AFFFFh). Если требуется вывести текст то его ASCII коды требуется прописать в текстовый видео буфер и вы немедленно увидите нужные символы. F0000h…FFFFFh, а вот и сам BIOS. Так же есть еще одна ПЗУ – ПЗУ расширений BIOS (C0000h…CFFFFh), её задача обслуживание графических адаптеров и дисков. За первым мегабайтом, с адреса 100000h, располагается память именуемая как расширенная память, конец которой до 4 гигабайт. Расширенная память состоит из 2х подуровней: HMA и XMS. Высокая память (High MemoryArea, HMA) доступна в реальном режиме, а это еще плюс 64 Кбайт (точнее 64 Кбайт – 16 байт), но для этого надо разрешить линию A20 (открыв вентиль GateA20). Функционирование расширенной памяти подчиняется спецификации расширенной памяти (Expanded Memory Specification, XMS), поэтому саму память назвали XMS- памятью, но она доступна только в защищенном режиме.
Красным отмечен адрес начала нашей программы, теперь мы можем смело указать компилятору с какого адреса начинать размещать данные в оперативной памяти. Для этого служит директива org. Давайте приступим к написанию программы. Запустите Notepad++, затем создайте новый файл и сохраните его где вам удобно, назвав boot.asm. После чего Notepad++ начнёт подсвечивать ассемблерный синтаксис. Теперь набираем данный код, сообщая компилятору адрес начала нашей программы.
org 07C00h ;BIOS помещает нашу программу по адресу 07C00h, поэтому указываем что все ;данные программы мы будем размещать начиная с этого адреса
Далее необходимо провести инициализацию регистров данных и стека.
;Инициализация
;————-
cli ;Запретить прерывания, чтобы не чего не отвлекало 🙂
xor ax,ax ;Обнуляем регистр ax
mov ds,ax ;Сегмент данных. Наши данные размещены от начала программы
mov ss,ax ;Сегмент стека.
mov sp,07C00h ;Стек начинается в начале программы и растёт в ;противоположную сторону, в начало памяти
sti ;Разрешить прерывания
Давайте узнаем какой размер стека у нас получится при такой инициализации. Для этого выполним следующие действия: 7C00h – 0500h = 7700h. Где 7C00h – адрес начала нашей программы, 0500h – адрес начала свободного пространства. Полученное шестнадцатеричное значение 7700h, это размер стека в байта, переведём его в десятичное получим 30464байт, разделим на 1024 и получим 29КБ. Для наших целей этого более чем достаточно.
Теперь для удобства работы давайте создадим несколько макросов, в дальнейшем мы можем вынести их в отдельный файл.
macro displayClean ;Очистка дисплея
{
mov ax, 02h ;очищаем экран — функция 02h прерывания 10h
int 10h
}
Макрос displayClean служит для очистки экрана от всего лишнего.
Далее идёт макрос setCursor служащий для установки курсора в нужную нам позицию, имеющий два аргумента, координату по оси X, и координату по оси Y. От местоположения курсора зависит, место куда будет выведен следующий символ выводимой нами строки.
macro setCursor x, y ;Установить курсор
{
mov dx,x
mov dh,y
;установка курсора : функция 02h прерывания 10h
mov ah,02h
xor bh,bh
int 10h
}
И последний необходимый нам макрос Print будет выводить нашу строку, у него есть три аргумента, string – адрес первого символа строки, length — количество символов строки, и color — цвет строки.
macro print string, lenght, color ;Вывод строки
{
mov bp,string ;Адрес строки
mov cx,lenght ;Длина строки
mov bl,color ;в регистре bl- атрибут цвета
mov ah,13h ;функция 13h прерывания 10h
mov al,01h ;строка только с символами + смещение курсора
;xor dx,dx ;dh-номер строки на экране, dl — номер столбца на экране
int 10h
}
После определения макросов можно разместить нашу строку в памяти
msg0 db «Hello World!» ;Текст сообщения
msg0_len = $ — msg0 ;Длина строки, $ текущий адрес минус адрес начала строки msg0
Далее следует этот код:
displayClean ;Очистим экран с помощью подготовленного нами макроса
setCursor 0, 0 ;Утановим курсор в верхний левый угол
println msg0, msg0_len, 03h ;Выведим приветствие
На этом наша программа закончена осталось забить оставшееся место ( минус два байта сигнатуры ) нулями, чтобы файл занимал равно 512байт.
times (512-2)-($-$$) db 0 ;Цикл выполняется (512-2)-($-$$) раз. Где
;512 — размер загрузочного сектора
;2 — два последних байта
;$ — текущий адрес в памяти
;$$ — Начальный адрес в памяти
И записать два байта сигнатуры, указывающие BIOS-у что это кот загрузчика
;Конец загрузочного сектора
;—————
db 0AAh,55h ;сигнатура, символизирующая о завершении загрузочного сектора
;последние два байта — признак конца таблицы MBR — код 0xAA55.
;──────────────────────────────END──────────────────────────
Давайте теперь скомпилируем наш код. Для этого нажмите F5, в появившемся окне введите, C:\fasm\FASM.EXE(FULL_CURRENT_PATH), нажмите сохранить, и выберите комбинацию клавиш для быстрого вызова. Теперь после нажатия выбранной комбинации, компилятор FASM скомпилирует нашу программу, и рядом с исходным кодом появится бинарный файл, этот файл и будет нашим загрузчиком, который мы должны будем записать на первый сектор дискеты.
Теперь давайте проверим работу нашей программы. Запустите imgMaster и добавте в список скомпилированный файл, он должен называется boot.bin, затем нажмите Create IMG и сохраните образ дискеты. Далее запустите Virtual Box и нажмите Создать, затем заполните поля как на рисунке ниже:
Д
алее
жмём создать и выставляем:
И
снова жмём создать
После чего в списке появится наша новенькая виртуальная машина. Выбираем её и жмём Настроить, выбираем вкладку Носители, мы видим что к контроллеру IDE у нас подключен один жёсткий диск My_OS.vdi и один CD-ROM, поскольку CD-ROM мы использовать не будем мы можем его удалить, что бы не мешал. После того как мы удалили CD-ROM, жмём зелёную кнопку Добавить новый контроллер и выбираем Добавить Floppy контроллер. Затем на появившемся в списке контроллере жмём добавить привод гибких дисков, откроется окно с предложением выбрать образ, вбираем сохранённый ранее образ и жмём ОК. Теперь запускаем нашу виртуальную машину. После запуска на экране у нас должно отобразится приветствие Hello World!.
Ну вот наша первая программа которая работает без ОС готова, в дальнейшем она научится сама читать файлы с диска в ОЗУ и превратится в полноценный загрузчик ядра ОС.
os — Что нужно для написания операционной системы?
Если подходить по существу…
ОС — это такая штука, которая реализует многозадачность (обычно) и заведует распределением ресурсов между этими задачами и вообще. Нужно следить, чтобы задачи друг другу не могли вредить и работали в разных областях памяти и с устройствами работали по очереди, это хотя бы. А ещё надо предоставить возможность передавать сообщения от одной задачи к другой.
Ещё ОС, ежели имеется долговременная память, должна предоставлять доступ к ней: то есть предоставлять все функции для работы с файловой системой. Это минимум.
Далее. Платформа. От неё зависят инструменты, кои понадобятся для разработки ОС. Некоторые платформы: x86, x86-64, ARM, ну и куча других.
Почти везде самый первый загрузочный код должен писаться на ассемблере — там бывает куча правил, где оно должно быть, как должно выглядеть, что должно делать, и какой размер не превышать.
Для РС надо на асме писать бутлоадер, который будет вызываться BIOS и кой должен, не превышая четырёх с копейками сотен байт, что-то сделать и запустить основную ОС — передать управление основному коду, который в ближайшей же перспективе можно писать уже и на С.
Для ARM надо на асме делать таблицу прерываний (сброс, ошибки разные, прерывания IRQ, FIQ и пр.) и передачу управления в основной код. Хотя, во многих средах разработки такой код для почти любого контроллера имеется.
То есть, необходимо для этого:
- Знать ассемблер целевой платформы.
- Знать архитектуру процессора и всякие служебные команды и регистры, чтобы настроить его для работы в нужном режиме. В РС это переход в защищённый режим, например, или в 64битный режим… В ARM — настройка тактирования ядра и периферии.
- Знать, как именно будет запускаться ОС, куда и как нужно пихать свой код.
- Знать язык С — большой код на асме написать затруднительно без опыта, поддерживать его будет ещё труднее. Посему надо ядро писать на С.
- Знать принципы работы ОС. Ну, книжек на русском языке по этой теме много всяких, правда, не знаю, все ли они хорошие.
- Иметь много-много терпения и усидчивости. Ошибки будут и их надо будет искать и исправлять. А ещё надо будет очень много читать.
- Иметь много-много времени.
Далее. Допустим, вы что-то написали. Надо это дело тестировать. Либо надо устройство физическое, на коем будут идти эксперименты (отладочная плата, второй компьютер), либо эмулятор его. Второе обычно использовать и проще, и быстрее. Для PC, например, VMWare.
Статей по этой теме в интернете тоже достаточно, если хорошо поискать. А также есть множество примеров готовых ОС с исходниками.
Даже можно при большом желании посмотреть исходники старого ядра NT-систем (Windows), как отдельно (кое микрософтом выложено, с комментариями и разного рода справочными материалами), так и в совокупности со старыми же ОС (утекло).