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

Idtr dw idt_size-1

dd IDT+10000h

Так, а зачем здесь пишем and’ы и shr’ы? – спросите вы. Вот тут трюк: мы же расположим весь код для PM за org’ом (смотрим код в предыдущей статье), в том числе и interrupt handlers… вот здесь и есть уловка: мы составляем 32-битный адрес из 2-х половинок, имея на руках лишь адрес обработчика. Вообще в этом нет ничего магического, просто нужно понимать, что за значение будет в этом двойном слове.

Теперь возникает вопрос, а почему же на местах многих gate’ов нули? А вот почему – совершенно не нужно писать все обработчики, ведь это, мягко говоря тяжело. Проще написать обработчик #GP, ведь, не найдя gate для прерывания, процессор генерирует пресловутое General Protection Fault.

Обработчики аппаратных прерываний вольны делать всё, что им вздумается, но должны сбрасывать флаг наличия прерывания – слать сигнал EOI – End Of Interrupt (мы же так сконфигурировали PIC, верно?).

К примеру вот так:

mov al, 20h

out 20h, al

Давайте напишем простой обработчик для таймера:

timer:

push ax

mov al, 20h

out 20h, al

pop ax

jmp int_EOI

int_EOI: ;вот здесь посылаем и Master'у и Slave'у EOI

push ax

mov al,20h

out 20h,al

out 0a0h,al

pop ax

iretd ;возврат из прерывания

int_EOI удобно использовать для всех обработчиков прерываний.

Ладно, теперь немного про исключения. Когда они происходят стек выглядит так:

Здесь error_code выталкиваем из стека и работаем с ним. Остальным займётся инструкция iretd.

Замечу, что тут содержимое ещё зависит от того, переключали ли мы ring’ или нет.

Структура error_code:

Где:

1) EXT — показывает, что сбой произошёл в обработчике прерывания или исключения.

2) IDT — когда установлен, показывает, что поля индекса относится к IDT.

3) TI — когда бит IDT не установлен, показывает, что икать нужно в GDT или LDT.

4) Segment Selector Index — показывает номер дескриптора (в GDT и LDT) или gate в IDT.

Вообще индекс обработчика исключения полезная штука. Он нам пригодится, когда наша поделка станет побольше.

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

Дам несколько 'подсказок':

1) IDT расположите до org'a (короче вместе с GDT).

2) IDTR загружаем до перехода в PM.

3) Обработчики прерываний распологаем за org'ом.

4) К текстовой видеопамяти можно обращаться как раньше.

5) Не забываем разрешить ВСЕ прерывания после того, как в PM проинициализируем PIC!

6) Paging инициализируем уже в PM.

И ещё, напишите простой обработчик для клавы.

Если будут проблемы — обращайтесь. До следующего поста.

****Исправление****

Функция set_pages сперва была написана неправильно т.к там описывалась всего 1 таблица, вместо 8. Код исправлен.

Немного теории

Адресное пространство в DOS:

Объем Физ. Адрес Сегм. Адрес

1Кбайт Векторы прерываний 00000h 0000h

256байт Область данных BIOS 00400h 0040h

ОС MS-DOS 00500h 0050h

Область для программ

64Кбайт Графический видео буфер A0000h A000h

32Кбайт Свободные адреса B0000h B000h

32Кбайт Текстовый видеобуфер B8000h B800h

64Кбайт ПЗУ-расширения BIOS C0000h C000h

128Кбайт Свободные адреса D0000h D000h

64Кбайт ПЗУ BIOS F0000h F000h

64Кбайт HMA 100000h

До4Гбайт XMS 10FFF0h

Первые 640 Кбайт (до графического видеобуфера) называются стандартной (conventional) памятью. Начинается стандартная память с килобайта, который содержит векторы прерываний, их 256 на каждый отводится по 4 байта.

Затем идет область данных BIOS (при включение компьютера BIOS выполняет POST – диагностику, которая проверяет все оборудование на наличие ошибок, если проверка завершилась удачно то BIOS грузит самый первый сектор (там находится загрузочная программа ОС) с выбранного устройства (дискеты, винчестера) по адресу 0x7C00h куда и передает управление). Где находятся данные необходимые для корректной работы функций BIOS. Но также можно модифицировать эту область, тем самым мы влияем на ход выполнения системных функций, по сути дела меняя что либо в этой области мы передаем параметры BIOS и его функциям, которые становятся более гибкими. В случае установленной DOS с сегментного адреса 500h начинается сама операционная система. После ОС до 640 Кбайт находятся прикладные или системные программы, которые были загружены ОС. 

За 640 килобайтами начинается старшая память или верхняя (upper) память, она располагается до 1 мегабайта (до HMA), т.е. она составляет 384 Кбайт. Тут располагаются ПЗУ (постоянно запоминающее устройство ) : текстовый видеобуфер (его микросхема рассчитана на диапазон B8000h…BFFFFh) и графический видеобуфер (A0000h…AFFFFh). Если требуется вывести текст то его ASCII коды требуется прописать в текстовый видеобуфер и вы немедленно увидите нужные символы. F0000h…FFFFFh, а вот и сам BIOS. Так же есть еще одна ПЗУ – ПЗУ расширений BIOS (C0000h…CFFFFh), её задача обслуживание графических адаптеров и дисков.

За первым мегабайтом, с адреса 100000h, располагается память именуемая как расширенная память, конец которой до 4 гигабайт. Расширенная память состоит из 2х подуровней: HMA и XMS. Высокая память (High Memory Area, HMA) доступна в реальном режиме, а это еще плюс 64 Кбайт (точнее 64 Кбайт – 16 байт), но для этого надо разрешить линию A20 (открыв вентиль GateA20). Функционирование расширенной памяти подчиняется спецификации расширенной памяти (Expanded Memory Specification, XMS), поэтому саму память назвали XMS-памятью, но она доступна только в защищенном режиме.

Давайте вернемся к началу адресного пространства в представлении DOS и рассмотрим его более подробно.

1) Векторы прерываний таковы (это нам понадобится, когда мы будем составлять свою таблицу прерываний):

IRQ INT Причина возникновения

IRQ0 8h Системный таймер

IRQ1 9h Клавиатура

IRQ2 10h Ведомый контроллер

IRQ3 11h Порт COM2, модем

IRQ4 12h Порт COM1, мышь

IRQ5 13h Порт LPT2

IRQ6 14h Дисковод

IRQ7 15h Порт LPT1, принтер

IRQ8 70h Часы реального времени

IRQ9 71h Прерывание обратного хода луча

IRQ10 72h Для дополнительных устройств

IRQ11 73h Для дополнительных устройств

IRQ12 74h PS мышь

IRQ13 75h Ошибка математического сопроцессора

IRQ14 76h Первый IDE-контроллер

IRQ15 77h Второй IDE-контроллер, жесткий диск

2) BIOS. Это в общем-то лирическое отступление, т.к. здесь мы рассмотрим не столько устройство BIOS, сколько научимся добавлять в него свои функции. Использование таковых, конечно, сделает нашу ОС, непереносимой, но для начального этапа они могут оказаться очень полезными. В написании ОС я больше не буду упоминать об этом, это остается пытливому читателю в виде самостоятельного упражнения. =)

Итак, BIOS (я буду говорить об AWARD BIOS, так как это наиболее популярные версии, поэтому возможно незначительные расхождения с другими

BIOS) – это последовательность запакованных файлов, которые заканчиваются файлом bootblock. Структура первого мегабайта памяти, отведенного под BIOS такова:

00000 – xxxxx+1 original.tmp и байт под CRC

xxxxx+1 – yyyyy Запакованный модуль

yyyyy – zzzzz Другие запакованные модули

zzzzz - ~17FFEh Оставшееся свободным пространство

~1C000* – 1FFFFh   Bootblock

До свободного пространства идет основная часть BIOS, а именно:

original.tmp – главная часть, в которой располагается подпрограмма BIOS Setup, а так же части, необходимые для инициализации.

CRC – контрольная сумма BIOS

awardext.rom – подпрограмма вывода конфигурации компьютера

awardepa.bin – изображение

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

Итак, при включении компьютера bootblock инициализирует регистры чипсета, распаковывает заархивированные (с помощью LHA) модули и отправляет их в память.

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

BIOS). Делается это достаточно легко: например используя modbin (стандартная программа от Award) можно распаковать данные файлы

(взятые, например, из Интернета), изменить их по своему усмотрению и записать в

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

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

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

Итак, наша программа будет располагаться в ПЗУ (постоянное запоминающее устройство). BIOS передаст ей управление, но для этого он должен ее найти. Соответственно наша программа должна находиться в области с С800:0 до E000:0 в памяти, так как эта область сканируется BIOS на наличие определенной сигнатуры 0AA55H. В байте за этой подписью количество байт для подсчета их контрольной суммы. Если контрольная сумма равно нулю, то это ПЗУ и управление передается в область памяти, где была найдена данная сигнатура со смещением 3. Для того, чтобы «уровнять» контрольную сумму, необходимо в конце программы дописать байт, в котором будет число, равное разнице 100h и полученной контрольной суммы. 

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

LENGTHROM EQU 2000H ; Размер ПЗУ в байтах = числу после подписи * 200H

CODE SEGMENT BYTE PUBLIC

ASSUME CS:CODE,DS:CODE

ORG 0

START:

DB 55h

DB 0AAh; Размер ПЗУ по модулю 200H

DB LENGTHROM SHR 9; Первая выполняемая команда

JMP BEGIN

BEGIN:

; Заносим в регистры нужные значения

MOV AX,CS

MOV DS,AX

; Код программы

; Вернуть управление БИОС

RETF

; Сюда запишем дополняющий байт

DB (0)

CodeEnd:

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

DB (LENGTHROM-(OFFSET CodeEnd-OFFSET START)) DUP (0FFH)

LastByte:

CODE ENDS

END START

Загрузка Linux и Windows

Это базовая и очень важная тема. Вспомним о БИОС, который загружает самый первый сектор (Master Boot Record) с выбранного в его настройках устройства (дискеты, винчестера, CD-ROM привода и пр.) по адресу 0x7C00h куда и передает управление. Программа, находящаяся в этой памяти называется первичным загрузчиком. У него не очень много возможностей, так как его размер ограничен 512 байтами. Его задачей является подготовка компьютера, а именно: запись в память вторичный загрузчик, предварительно считанный с HDD,

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

Чтобы более точно понять устройство загрузки ОС, перед переходом к исходным тестам рассмотрим принципы загрузки наиболее популярных в наше время ОС: Linux и

Windows.

Linux может загружаться как через специализированный загрузчик (Lilo), так и через boot sector диска. Поскольку загрузчика у нас нет, а есть только желание более полно узнать об устройстве загрузки, рассмотрим второй случай:

1) boot sector записывает свой код в 9000h

2) Загружает с диска Setup, который находится в нескольких последующих секторах (9000h:0200h;)

3) Загружает ядро в 1000h. Ядро так же следует после Setup. Ядро должно быть меньше 508 килобайт

4) Управление передается Setup

5) Setup проверяется на корректность

6) С помощью BIOS определяется оборудование, размер памяти, наличие жестких дисков, наличие шины Micro channel bus, PC/2 mouse, Advanced power management, инициализируются клавиатура и видеосистема

7) Процессор переводится в защищенный режим

8) Управление передается ядру

9) Ядро переписывается по адресу 100000h (если оно было заархивировано, то оно предварительно разархивируется)

10) Управление передается ядру

11) Активируется страничная адресация

12) Происходит инициализация IDT и GDT, при этом в кодовый сегмент и в сегмент данных ядра входит вся виртуальная память

13) Инициализируются драйвера

14) Управление передается процессу init;

15. init запускает все остальные необходимые программы в соответствии с файлами конфигурации(init.X);

Теперь рассмотрим загрузку Windows (NT, так как ранние версии устарели):

1) boot sector загружает NTLDR

2) Процессор переходит в защищенный режим;

3) Делаются таблицы страниц

4) Механизм преобразования страниц;

5) Чтение boot.ini, используя код FS под названием read only. Выводит на экран выбор загрузки ОС (из boot.ini)

6) Из boot.ini считывается адрес директории Windows

7) Управление получает ntdetect.com, определяющий устройства, установленные на компьютере

8) Из %dir%\system32 загружается ntoskrnl.exe, в котором находится ядро.

9) Управление передается hal.dll с информацией об аппаратном обеспечении;

10) Загружаются драйвера и важные файлы

11) Стартует графическая оболочка и пр.

Ближе к практике

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

1) Первичный загрузчик

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

// Принцип работы такой: читать можем только в первые 64к, поэтому сначала считывается цилиндр в 0x50:0 - 0x50:0x2400, а затем копируется туда, куда необходимо. При этом первый цилиндр считываем в конце.

section .text

BITS 16

org 0x7c00

// Ядро отправляем в 0x7c00

%define CTR 10 

%define MRE 5 

// Определение переменных

enter:

cli ;

mov ax, cs

mov ds, ax

mov es, ax

mov ss, ax

mov sp, 0x7c00

sti

// Поскольку мы не знаем значений различных регистров (за исключением CS, значение которого равно 0), то мы должны сами занести данные в данные регистры(а именно “занулить” SS, SP и DS). А так же отключить прерывания, чтобы в это время работу загрузчика ни что не сбивало.

// Далее:

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

// В DS - адрес исходного сегмента

mov ax, 0x07c0

mov ds, ax

// В ES - адрес целевого сегмента

mov ax, 0x9000

mov es, ax

// Копируем с 0

xor si, si

xor di, di

// Копируем 128 двойных слов

mov cx, 128

rep movsd

// Прыжок в новоиспеченный bootsector (0x9000: 0)

jmp 0x9000:start

// следующий код выполняется по адресу 0x9000:0

begin:

// Заполним регистры новыми значениями

mov ax, cs

mov ds, ax

mov ss, ax 

// Сообщим пользователю о загрузке 

mov si, msg_startup

call ps

// Читаем цилиндр начиная с указанного в DI плюс нулевой цилиндр (в самом конце) в AX (адрес, куда будут записаны данные)

mov di, 1

mov ax, 0x290

xor bx, bx

.loop:

mov cx, 0x50

mov es, cx

push di

// Подсчет головки для использования

shr di, 1

setc dh

mov cx, di

xchg cl, ch

pop di

// Считаны ли все цилиндры?

cmp di, CTR

je .quit

call r_cyl

// Цилиндр считали в 0x50:0x0 - 0x50:0x2400 (в линейном варианте - 0x500 - 0x2900)

// Скопируем этот блок в нужный адрес:

pusha

push ds 

mov cx, 0x50

mov ds, cx

mov es, ax

xor di, di

xor si, si

mov cx, 0x2400

rep movsb

pop ds

popa

// Увеличим DI, AX и повторим все сначала

inc di

add ax, 0x240

jmp short .loop

.quit: 

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

mov ax, 0x50

mov es, ax

mov bx, 0

mov ch, 0

mov dh, 0

call r_cyl

// Прыжок на загруженный код

jmp 0x0000:0x0700

r_cyl:

// Читаем заданный цилиндр, ES:BX – буфер, CH – цилиндр, DH - головка

// Сбросим счетчик ошибок

mov [.err], byte 0

pusha

// Сообщение о том, какая головку/цилиндр считывается

mov si, msg_cyl

call ps

mov ah, ch

call pe

mov si, msg_head

call ps

mov ah, dh

call pe

mov si, msg_crlf

call ps

popa

pusha

.start: 

mov ah, 0x02

mov al, 18

mov cl, 1

// Прерывание BIOS

int 0x13

jc .r_err

popa

ret

.err: db 0 

.r_err:

// Об ошибках сообщаем и выводим их код

inc byte [.err]

mov si, msg_err

call ps

call pe

mov si, msg_crlf

call ps

// Что делаем, если ошибок больше нормы:

cmp byte [.err], mre

jl .start

mov si, msg_end

call ps

hlt

jmp short $

table: db "0123456789ABCDEF"

pe:

// ASCII-код преобразуем в его шестнадцатеричного представления и выводим

pusha

xor bx, bx

mov bl, ah

and bl, 11110000b

shr bl, 4

mov al, [table+bx]

call pc

mov bl, ah

and bl, 00001111b

mov al, [table+bx]

call pc

popa

ret

// Из AL выводим символ на экран

pc:

pusha

mov ah, 0x0E

int 0x10

popa

ret

// Строку из SI выводим на экран

ps:

pusha

.loop:

lodsb

test al, al

jz .quit

mov ah, 0x0e

int 0x10

jmp short .loop

.quit:

popa

ret

// Служебные сообщения

msg_startup: db "OS loading...", 0x0A, 0x0D, 0

msg_cyl: db "Cylinder:", 0

msg_head: db ", head:",0

msg_er: db "Error! Code of it:",0

msg_end: db "Errors while reading",0x0A,0x0D, "Reboot the computer, please", 0

msg_crlf: db 0x0A, 0x0D,0 

// Сигнатура бутсектора: 

TIMES 510 - ($-$$) db 0

db 0xAA, 0x55

2) Вторичный загрузчик

А теперь вторичный загрузчик:

[BITS 16]

[ORG 0x700]

// Обнулим регистры, установим стек

cli 

mov ax, 0

mov ds, ax

mov es, ax

mov ss, ax

mov sp, 0x700

sti

// Сообщение о приветствии

mov si, msg_start

call kputs

// Сообщение о переходе в защищенный режим

mov si, msg_entering_pmode

call ps

// Отключение курсора (просто так)

mov ah, 1

mov ch, 0x20

int 0x10

// Установим базовый вектор контроллера прерываний в 0x20

mov al,00010001b 

out 0x20,al 

mov al,0x20 

out 0x21,al 

mov al,00000100b 

out 0x21,al

mov al,00000001b 

out 0x21,al 

// Отключим прерывания

cli

// Загрузка регистра GDTR: 

lgdt [gd_reg]

// Включение A20: 

in al, 0x92

or al, 2

out 0x92, al

// Установка бита PE регистра CR0

mov eax, cr0 

or al, 1 

mov cr0, eax 

// С помощью длинного прыжка мы загружаем селектор нужного сегмента в регистр CS

jmp 0x8: _protect

ps:

pusha

.loop:

lodsb

test al, al

jz .quit

mov ah, 0x0e

int 0x10

jmp short .loop

.quit:

popa

ret

// Следующий код - 32-битный

[BITS 32]

// При переходе в защищенный режим, сюда будет отдано управление

_protect: 

// Загрузим регистры DS и SS селектором сегмента данных

mov ax, 0x10

mov ds, ax

mov es, ax

mov ss, ax

// Наше ядро слинковано по адресу 2мб, переносим его туда. ker_bin - метка, после которой вставлено ядро

mov esi, ker_bin

// Адрес, по которому копируем

mov edi, 0x200000

// Размер ядра в двойных словах (65536 байт)

mov ecx, 0x4000

rep movsd

// Ядро скопировано, передаем управление ему

jmp 0x200000

gdt:

dw 0, 0, 0, 0 

// Нулевой дескриптор

db 0xFF 

// Сегмент кода с DPL=0 Базой=0 и Лимитом=4 Гб 

db 0xFF 

db 0x00

db 0x00

db 0x00

db 10011010b

db 0xCF

db 0x00

db 0xFF 

// Сегмент данных с DPL=0 Базой=0 и Лимитом=4Гб 

db 0xFF 

db 0x00 

db 0x00

db 0x00

db 10010010b

db 0xCF

db 0x00

// Значение, которое мы загрузим в GDTR: 

gd_reg:

dw 8192

dd gdt

msg_start: db "Get fun! New loader is on", 0x0A, 0x0D, 0

msg_epm: db "Protected mode is greeting you", 0x0A, 0x0D, 0

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