Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Как создать свою операционную систему.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
33 Кб
Скачать
☆

Int 0x10 ; вызываем функцию bios

     jmp  puts_loop

puts_loop_exit:

     jmp  $ ; вечный цикл

 

message:

     db   'Hello World!',0

finish:

     times 0x1FE-finish+start db 0

     db   0x55, 0xAA ; сигнатура загрузочного сектора

Эта короткая программа требует ряда важных пояснений. Строка org 0x7C00 нужна для того, чтобы ассемблер (имеется в виду программа, а не язык) правильно рассчитал адреса для меток и переменных (puts_loop, puts_loop_exit, message). Вот мы ему и сообщаем, что программа будет загружена в память по адресу 0x7C00.

В строках

    mov         ax, cs

    mov         ds, ax

происходит установка сегмента данных (ds) равным сегменту кода (cs), поскольку в нашей программе и данные, и код хранятся в одном сегменте.

Далее в цикле посимвольно выводится сообщение «Hello World!». Для этого используется функция 0x0E прерывания 0x10. Она имеет следующие параметры:

AH = 0x0E (номер функции)

BH = номер видеостраницы (пока не заморачиваемся, указываем 0)

AL = ASCII-код символа

В строке «jmp $» программа зависает. И правильно, незачем ей выполнять лишний код. Однако чтобы компьютер опять заработал, придется перезагрузиться.

В строке «times 0x1FE-finish+start db 0» производится заполнение остатка кода программы (за исключением последних двух байт) нулями. Делается это для того, чтобы после компиляции в последних двух байтах программы оказалась сигнатура загрузочного сектора.

С кодом программы вроде разобрались, давайте теперь попробуем скомпилировать это счастье. Для компиляции нам понадобится, собственно говоря, ассемблер — выше упомянутый yasm. Он есть в большинстве репозиториев Linux. Программу можно откомпилировать следующим образом:

$ yasm -f bin -o hello.bin hello.asm

Полученный файл hello.bin нужно записать в зарузочный сектор дискеты. Делается это примерно так (разумеется, вместо fd нужно подставить имя своего дисковода).

$ dd if=hello.bin of=/dev/fd

Поскольку далеко не у всех остались дисководы и дискеты, можно воспользоваться виртуальной машиной, например, qemu или VirtualBox. Для этого придётся сделать образ дискеты с нашим загрузчиком и вставить его в «виртуальный дисковод».

Создаём образ диска и заполняем его нулями:

$ dd if=/dev/zero of=disk.img bs=1024 count=1440

Записываем в самое начало образа нашу программу:

$ dd if=hello.bin of=disk.img conv=notrunc

Запускаем полученный образ в qemu:

$ qemu -fda disk.img -boot a

После запуска вы должны увидеть окошко qemu с радостной строчкой «Hello World!»

Давайте на минуту вспомним нашу программу из предыдущего выпуска. Она запускалась вместо операционной системы и выводила сообщение «Hello world». Программа была написана на 16-битном ассемблере и работала в так называемом реальном режиме. Как вы наверняка знаете, при адресации в реальном режиме физический адрес формируется с помощью сегмента и смещения и имеет размерность 20 бит. Несложная математика подсказывает нам, что таким образом можно обращаться всего к мегабайту оперативной памяти.

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

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

Итак, адресация в защищённом режиме может происходить в одном из трёх режимов (извините за тавтологию) — в сегментном, страничном и сегментно-страничном. В этом выпуске мы рассмотрим только сегментный режим.

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

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

Итак, как мы уже говорили, в дескрипторе хранится база (base address), размер (segment limit) и всякие дополнительные атрибуты сегмента. Давайте взглянем на схему дескриптора.

Обратите внимание, что под базу выделено 32 бита, а под лимит всего 20. Как же так, — спросите вы — разве нельзя создать сегмент, размер которого будет больше мегабайта? Можно. Для этого используется несложный трюк. Если бит G установлен в единицу, то лимит считается не в байтах, а в блоках по 4 Кбайт. Надо заметить, что лимит содержит размер сегмента минус один в единицах гранулярности, т.е. если лимит равен 0, то сегмент имеет размер 1 байт или 4 Кбайт. Кроме того, как вы видите, дескриптор включает ещё несколько полей. Их подробное описание можно найти, например, здесь.

Как упоминалось, дескрипторы хранятся в специальных таблицах. Таблиц может быть несколько, но в любом случае в памяти обязана присутствовать 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

or al, 2

out 0x92, al

происходит разблокирование адресной линии A20. Что это значит? Вспомним, что в реальном режиме можно адресовать 1 МБ памяти в формате сегмент:смещение (20 бит на адрес). Однако, обратившись, например, по адресу FFFF:FFFF, можно “прыгнуть” немного выше этой планки, и полученный адрес будет иметь длину 21 бит. В процессорах до 80286 старший (двадцатый, если считать от нуля) отбрасывался, и поэтому для совместимости со старыми программами ввели блокировку адресной линии A20. В настоящее время все ОС работают в защищенном режиме, и поэтому для нужд адресации необходимо как можно раньше разблокировать этот бит. Такие дела.

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

Включение защищенного режима осуществляется путем установки младшего бита регистра CR0. Сразу после перехода в защищенный режим возникает некая неопределенность: в CS:EIP надо установить точку входа в защищенный режим в формате селектор:смещение, а у нас там все еще пережитки реального режима. Поэтому выполняем следующую инструкцию:

O32 jmp 00001000b:pm_entry

Здесь используется префикс преобразования разрядности операнда O32, который позволяет делать дальний безусловный переход с 32-битным смещением. Ура, мы наконец-то можем воспользоваться прелестями защищенного режима!

На написание этого поста меня подтолкнуло то, что посты iley прекратились, а хабралюди всё же хотели посты по осьдеву. Оговорюсь сразу, я сам изучаю и пишу небольшую ось, но изучаю, так что продвигаться будем вместе. Я собираюсь рассмотреть написание систем под защищённый режим для IA-32, рассказать о:

1) Проектировании;

2) Отладке;

3) Работе с графикой;

4) Работе с дисками;

5) Memory management;

6) Task management;

Возможно о чём-нибудь ещё, если это будет востребовано.

I) Проектирование.

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

1) Итак, начнём с идеи написания своей оси — зачем же это? Наверное, вы уже знаете ответ — изучение архитектуры, понимание работы процессоров и периферийных устройств. Сие правильно.

2) Следующее — какой тип ядра использовать?

Давайте перечислю виды и особенности ядер:

1) Монолитные.

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

2) Модульные.

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

3) Микроядро или µ-ядро.

Здесь всё примерно как и в модульном ядре, только сервисы (модули) исполняются в разных адресных пространствах.

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

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

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

Теперь о загрузчике: некоторые используют известные GRUB или LILO, но по-моему лучше писать самому. Примером для разбора лично для меня послужил загрузчик от KOLIBRI, благо проект opensource; берём лучший дебаггер для начинающих кодеров – блокнот и карандаш, и разбираем бутлоадер по полочкам.

Файловая система: вообще лучше начинать с FAT’a, потом переходить уже к другим. Некоторые решают написать свою собственную, и это хорошо – очень хорошо проясняется архитектура и работа с дисковыми накопителями.

Теперь поговорим про сервисы (раз уж принялись за модульное ядро).

Неплохо реализовать следующие:

1) Video service: печать, стандартные функции работы с экраном.

2) Disk service: на него возлагаем все операции с дисками – от перевода THS в LBA до операций с флешками.

3) Memory management service: тут по названию понятно. Хотя это – самая сложная в реализации и теории часть.

4)Task management service: это тоже нелёгкая задача, но мы к ней ещё подойдём.

Введение в защищённый режим.

Итак, PM значительно отличается от всем привычного ещё со времён DOS’a реального режима (RM). Теперь придётся привыкать: здесь нет статичных, 64 килобайтных сегментов, таблицы прерываний в 1’ом килобайте, адресов баз сегментов в сегментных регистрах, в общем совершенно новый мир.

Теперь сегменты описываются в Global Descriptor Table (GDT). Сия таблица может быть только в одном экземпляре. Она структура в памяти. Не сегмент! Может располагаться в памяти где угодно, но её адрес и лимит записываются в регистр GDTR. Вот его структура:

Сама таблица состоит из записей следующей структуры (кстати нулевая запись пустая. Это важно. При обращении к памяти, ‘описываемой’ нулевым дескриптором, получите #GP – General Protection Fault):

Давайте рассмотрим эту структуру повнимательней.