В данном цикле из трёх частей займёмся хардкором, и с нуля разработаем свою операционную систему «CodebyOS». Это отличная школа, которую должен пройти любой системный программист. Холодное дыхание компьютерного железа передаёт незабываемые ощущения, а работа с портами ввода-вывода контролёров оставит свой отпечаток на всю оставшуюся жизнь. Только исследуя нёдра системы на нижнем уровне можно осознать, какие гениальные умы придумали это всё. Материал рассчитан на пользователя среднего уровня, который знаком с таким понятием как «регистр процессора». Краткое описание частей приводится ниже:
Часть #1.
1. Планирование архитектуры ОС
• Интерфейс взаимодействия с пользователем
• Текстовый режим на входе
• Переключение в графический режим
2. Базовые сведения о загрузки компьютерной системы
• Начальная инициализация при включении
• Карта распределения памяти в реальном режиме
3. Загрузчик операционной системы
• Поиск носителей
• Практическая часть
• Отладка и тестирование
Часть #2.
1. Ядро операционной системы
• Сбор информации об устройствах
• Конфигурационное пространство PCI
2. Память
• Сегментная модель – таблица GDT (Global Descriptor Table)
• Страничная модель – таблица PDT (Page Directory Table)
• Обработка прерываний – таблица IDT (Interrupt Dispatch Table)
• Режимы Flat-x32, и Long-x64
Часть #3.
1. Драйверы устройств
• Диск, клавиатура, сеть
• Работа с портами ввода-вывода
2. Файловая система
• FAT – File Allocation Table NTFS
• NTFS – NT File System
1. Планирование архитектуры ОС
Как и в любом начинании – нам нужен план (не путать с флорой). Здесь нужно учитывать, что написать полноценную ОС в одиночку практически нереально (если не хотим потратить на это всю оставшуюся жизнь). Поскольку для больших проектов требуется коллектив из тысячи профессионалов, наша ОС будет носить ярко выраженный учебный характер. Цель не создать продукт для рынка, а понять основные принципы функционирования отдельных узлов ЦП, и как собрать их в единый работающий механизм. Исходя из этого реализуем только следующее:
1. При загрузке ставим текстовый режим 80х25 16-цветов, а всю информацию будем выводить напрямую в память видео-адаптера по сегментному адресу реального режима
B800:0000
. Во-первых это проще реализовать, во-вторых получим более высокую скорость по сравнению с сервисом видео int-10h
, ну и в третьих позволит использовать 16 цветов основной палитры, которые передадут дух ушедшей от нас эпохи. В этом режиме для каждого символа выделяются по 2-байта: первый собственно ANSI-код символа, а второй для его цвета и фона. Поддержка фона с символом «Space» (пробел) позволит отображать на экране разнообразные блоки, например кнопки, или выделить шапку консольного окна как на рис.ниже. Задающий цвет байт-атрибута кодируется следующим образом – в старшей тетраде фон, а в младшей цвет символа. Например чтобы вывести белый текст на красном фоне, нужно будет указать атрибут 64+8=72=48h=0100`1111
, и т.д:2. Интерфейс взаимодействия с юзером скоммуниздим у такого софта как Far/Hiew/Total, где в качестве меню используются клавиши от F1 до F12. Пародию на буттоны можно расположить хоть сверху, хоть снизу окна – здесь уже по желанию. Тогда получим фейс, как на следующем скрине. Учитывая текст.режим 80 столбцов и 25 строк, в рабочем пространстве остаётся всего 23 строки для вывода текста и данных, т.к. две отправятся в штрафбат под шапку и кнопки меню. Этого вполне достаточно для большинства типичных задач.
3. Один из пунктов меню оставим для перехода в защищённый режим, в котором раскрывается вся сила и мощь современных процессоров. Здесь полностью настроим виртуальную память, страничный режим «Пейджинг», напишем драйвер диска и клавиатуры, и переведём ЦП в 64-битный режим «Long-Mode». Именно на эту часть операционной системы сделаем основную ставку – она будет занимать львиную долю в нашем ядре.
2. Начальный этап инициализации
Коротко рассмотрим начальный этап загрузки компьютерной системы.
По логике вещей, после включения кнопки питания Power, какой-то участок кода должен получить управление, чтобы вдохнуть жизнь в огромный механизм. Однако в этот момент «живых» существ в системе ещё нет, и в наличии имеем лишь электрические цепи. Тогда кто-же первым раскручивает маховик? Ответ можно найти в библии Интела том(3) на стр.314, где представлена табличка ниже.
Оказывается инженеры процессоров х86 ещё на заводе программируют свои железяки так, что при включении машины в регистровую пару
CS:EIP
аппаратно прописываются значения F000:FFF0
, в следствии чего управление получает расположенный именно по этому адресу код. Обратите внимание, что значения селекторов остальных сегментных регистров SS\DS\ES\FS\GS
равно нулю, и только сегменту кода выпала такая честь CS=F000h
:Ладно, с указателем разобрались.. а как-же код, который получит бразды правления?
Здесь тоже имеем аппаратную фишку, только на этот раз «проецирования микросхемы ROM-BIOS в физ.память ОЗУ», за что отвечает уже регистр чипсета
PAM-0
(Programmable Attribute Map #0). Как результат, содержимое флэшки биоса отображается/мапится в самые верхние адреса реального режима 1МБ, куда процессор и передаёт управление. При наличии 32-битной WinXP, этот участок кода можно просмотреть в достопочтенном отладчике debug.exe – как видим здесь расположен прыжок JMP F000:E05B
, с датой выпуска текущего биос «23 июня 1999-года»:С этого момента в дело вступает процедура инициализации системы POST (Power On Self Test), далее идёт вся остальная кухня типа выделения ресурсов аппаратным устройствам, и наконец код биоса ищет в конце первого сектора указанного в настройках диска сигнатуру загрузочного девайса
55AAh
, и если таковая обнаружится, то копирует этот сектор в память по адресу 0000:7С00
, после чего передаёт на него управление. Такому алгоритму следуют все чипсеты на процессорах х86. От сюда следует, что если мы напишем код загрузчика ОС и расположим его в первом секторе диска, то биос сам подтянет наш бутлоадер в память, а мы получим управление.2.1. Распределение памяти в реальном режиме
В процессе инициализации, код биоса настраивает и карту физической памяти ОЗУ, сохраняя некоторые полезные блоки данных по определённым адресам – эти данные известны как BDA, или «Bios Data Area». В частности, в нижний 256-байтный блок по адресу
0000:0400
сбрасывается всевозможного рода инфа, а в верхнем 128-Кбайтном блоке 0008:0000
лежит уже код обработчиков 32-х прерываний биос с номерами от int-00
до int-19h
(прерывания ms-dos начинаются с int-20h и нас не интересуют). Позже мы можем использовать эти инты в своих целях, например для работы с видео int-10h
, жёстким диском int-13h
, или клавиатурой int-16h
.Таким образом, среди всей доступной нам физ.памяти ОЗУ имеются и занятые регионы, трогать которые можно, но не рекомендуется (получим выстрел в ногу). В таблице ниже, доступные разработчику области я выделил серым. Как видим, внутри первого мегабайта в нашем распоряжении аж 510 КБ памяти, что для реального режима работы ЦП представляет собой целый материк.
Ну и немного про адресацию физической памяти в реальном режиме..
При включении компьютера, пока мы самостоятельно не откроем т.н. шлюз А20, шина-адреса ЦП имеет разрядность всего 20-бит. Это ограничивает доступную память ОЗУ значением
2^20 = 1 Мбайт = 0FFFFFh
. Поскольку на данном этапе разрядность регистров итого меньше 16-бит, то приходится прыгать по памяти блоками по 2^16=64 КБ = FFFFh
, которые и назвали сегментами. Итого для 1 МБ ОЗУ фактически имеем 16 сегментов по 64К в каждом, что можно представить в виде адреса 000F:FFFF
(segment : offset). Любое другое представление сегментной/левой части адреса превращает адрес в логический, например F23A:0015
, поскольку значение сегмента не может превышать отметки 000F:xxxx
.Процессор оперирует только линейным/физическим адресом, поэтому перед тем-как выставить его на шину, специальный транслятор в блоке MMU (Memory Management Unit) переводит сегментный адрес в физический по формуле:
Physical = (Segment*16) + Offset
. Учитывая, что умножение на 16 равносильно сдвигу на 4-бита влево, можно легко переводить сегм.адреса в физические например так: F000:FFF0 = 0x000FFFF0
.И вообще с адресами в архитектуре х86 полный кошмар – в штате имеется аж 5 типов адресации:
1. DRAM-адрес используется внутри чипов ОЗУ, и адресует прямоугольную матрицу ячеек памяти как «банк/строка/столбец».
2. Физический адрес, который в большинстве случаях совпадает с линейным от нуля и до объёма установленной ОЗУ.
3. Сегментный адрес введён для адресации 20-бит, при помощи двух 16-битных регистров, например CS:IP, DS:SI, SS:SP.
4. Логический адрес, как форма сегментного адреса.
5. Виртуальный адрес защищённого режима, когда поверх сегментной накладывается страничная адресация Paging.
3. Пишем первичный загрузчик
Таким образом, код биоса во-флэшке ROM аппаратно получает управление по адресу
F000:FFF0
, и после инициализации оборудования процедурой POST, эстафетой передаёт его загрузчику ОС по жёстко назначенному адресу 0000:7С00
. Получив руль и педали управления от биоса, загрузчик должен проделать теперь следующие операции:1. Определить разметку диска – устаревшая MBR (Master Boot Record), или более современная GPT (Guid Partition Table);
2. По сигнатуре
80h
в записях Entry таблицы разделов, найти загрузочный раздел Volume на указанном диске;3. Передать управление непосредственно загрузчику операционной системы.
От сюда следует, что во всех современных ОС имеется 2 типа загрузчиков – первичный для поиска Boot-раздела на диске (как правило это С:\), и вторичный для загрузки ОС с найденного раздела. Например раньше на системах WinXP вторичным загрузчиком являлся файл NTLDR, а начиная с Vista его заменили на BOOTMGR (если диск имеет разметку GPT см.зарезервированный том размером 100 МБ).
Когда системный биос передаёт управление первичному загрузчику в точку
0000:7С00
, в регистре DL
мы получаем сразу и номер диска, который указан в BIOS-Setup как загрузочный. В нумерации дисков, старший бит определяет контролёр на материнской плате: 0000`0000=Floppy#A
(старший бит сброшен), 1000`0000=80h=HDD#0
(старший бит взведён), 1000`0001=81h=HDD#1
и т.д. Номер активного диска потребуется для чтения его секторов, поэтому имеет смысл на входе в загрузчик сразу сохранить его прозапас в переменной.Двоичный редактор HxD может работать с дисками. Если открыть в нём физический хард, то упрёмся как-раз в загрузчик, а в конце первого сектора
200h=512
байт, будет лежать сигнатура загрузочного девайса 55AAh
. Помимо загрузчика, в хвосте сектора располагается и таблица разделов диска «Partition Table», которая начинается с со-смещения 01BEh
и описывает 4 основных тома текущего накопителя – на скрине ниже я выделил эту таблицу в блок.Чтобы было наглядней, можно скопировать таблицу разделов в новый файл. Каждая запись Entry в ней размером 16-байт (одна строка в дампе), и описывает один раздел. Здесь видно, что на моём харде всего 2 раздела C:\ и D:\ , при этом по флагу
80h
в первом байте можно сделать вывод, что загрузочным является именно C:\. Два последних дворда в каждой записи позволяют вычислить геометрию тома в логических блоках LBA «Logical Block Address». Один блок – это сектор размером 512-байт. В первом дворде лежит номер сектора с которого начинается том, а во-втором общее кол-во блоков в нём. В данном случае, раздел C:\ начинается у меня с сектора 3Fh=63
, и содержит всего 03AE49C5=61.753.797
секторов. Соответственно размер его 61.753.797*512
примерно 30 ГБайт.На современных дисках разметку MBR заменили уже на GPT, однако для совместимости устаревшая таблица разделов присутствует и на них. Такие диски можно опознать по флагу файловой системы «BootID» – в байте по смещению(4) у них будет прописано значение
EEh
. В данном случае, в первой записи видим флаг 07h
= основной раздел NTFS, а во-второй записи 0Fh
= расширенный раздел. Эта тема уже обсуждалась в статье Диски MBR и GPT, поэтому детально рассматривать её здесь не будем.Таким образом, получив управление от биоса, наш первичный лоадер должен по флагу
80h
найти загрузочный раздел на диске, далее прочитав дворд по-смещению(8) взять LBA его начала, и наконец загрузив первый сектор загрузочного раздела (не путать с первым сектором диска), передать на него управление. Как упоминалось выше, в этом секторе должен лежать вторичный загрузчик ОС.Здесь нужно отметить, что если в качестве Boot-девайса выступает гибкий диск Floppy (указывается в Bios-Setup), то никакие флаги проверять не нужно, поскольку их попросту нет, ..как нет и самой таблицы разделов. В этом случае просто загружаем в память ОС из заранее известного сектора флопа, и передаём ей управление.
3.1. Практическая часть
Вот пример загрузчика, который реализует всё вышеизложенное. На данном этапе ОС у нас ещё нет, а потому акцент делается на интерфейс пользователя, а так-же проверку типа загрузочного устройства HDD или FDD. Если это FDD, то тест пропускается и загрузчик просто виснет с выводом соответствующего сообщения. Для печати текстовых строк, обязательно нужны переменные с их длиной, т.к. вывод осуществляется по счётчику в регистре
CX
. В свою очередь для работы с диском (чтение/запись секторов), я использую не стандартный, а расширенный сервис биоса, который требует специального пакета с адресом. Остальное всё в комментах:
C-подобный:
;// FASM code: Первичный загрузчик ОС
;//-----------------------------------------------
org 0x7c00 ;// адрес загрузки в память
jmp @start ;// уходим на точку-входа
disk db 0 ;// переменная под номер диска
header db '*** [ CodebyOS ] *** : ver0.1x Build 04.2025'
hLen = $ - header
bootDev db ' Boot device:'
bootDevLen = $ - bootDev
floppy db ' Floppy(A) 1.44 Mb'
floppyLen = $ - floppy
hdd db ' Hard #'
hddLen = $ - hdd
error db ' Error! Boot partition not found'
errorLen = $ - error
loadOs db ' Load OS into memory? y/n...'
loadLen = $ - loadOs
;// Пакет адреса для расширенного сервиса INT-13h (см.Гук стр.417)
;//---------------------------------------------------------------
DiskPacket:
SizeOf dw 16 ;// размер пакета
Counter dw 0 ;// секторов для R/W
BuffOffs dw 0 ;// адрес буфера приёма-передачи,
BuffSeg dw 0 ;// ...и его сегмент.
StartLBA dd 0,0 ;// 64-бит LBA для чтения-записи
align 4
@start: mov [disk],dl ;//<------------- Точка входа! Биос передаёт нам № диска - запомнить его
xor ax,ax ;// настраиваем сегментные регистры
mov ds,ax ;// все по нулям!
mov es,ax
mov ss,ax
mov sp,0x7c00 ;// настраиваем стек SS:SP
sub sp,4 ;//
;// Ставим видео-режим 80х25 и прячем курсор
mov ax,3
int 10h
mov ah,1
mov cx,0x2000
int 10h
;// Выводим красную шапку сверху окна
push es 0 0xb800 ;// 0xb800 = сегмент видео/буфера
pop es di ;// ES:DI = позиция в окне для вывода
mov cx,80 ;// счётчик символов для вывода
mov al,' ' ;// сам символ
mov ah,01001111b ;// атрибуты: белый текст на красном фоне
rep stosw ;// запись AX в видео/буфер!
;// Напечатаем текст в шапку
mov si,header ;// источник
mov cx,hLen ;// длина строки
mov di,36 ;// позиция в окне
@@: lodsb ;// читаем из источника DS:SI по 1-байту,
stosw ;// и выводим в в/буфер ES:DI по 2-байта (с атрибутом)
loop @b ;// повторить СХ-раз..
;// Нарисуем ниже шапки пока пустое меню
mov ah,00110000b ;// атрибут: чёрный на пурпурном
mov di,164 ;// позиция в окне: (80*2)+4 = строка(2)
mov al,' ' ;// печатать пробелы
mov bp,7 ;// счётчик пунктов меню
@@: mov cx,10 ;// ширина одного пункта
rep stosw ;// первый пошёл!
add di,2 ;// пропустить одну позицию
dec bp ;// счётчик -1
jnz @b ;// промотать, пока счётчик не станет нуль
;// Сообщение о типе загрузочного устройства
mov ah,00000111b ;// серый на чёрном
mov di,(160*4) ;// позиция в окне = строка(4)
mov si,bootDev ;// адрес строки для вывода
mov cx,bootDevLen ;// её длина
@@: lodsb ;// печать в цикле!
stosw
loop @b
mov si,floppy ;// в дефолте выводить "Floppy"
mov cx,floppyLen ;//
cmp [disk],0 ;// это флоп?
je @f ;// да - печать
mov si,hdd ;// иначе: меняем адрес строки на "HDD"
mov cx,hddLen ;//
@@: lodsb ;// печать!
stosw
loop @b
pop es ;// восстановить ES (нужен для int-13h)
cmp [disk],0 ;// это флоп?
je @fdd ;// да - пропустить
;// иначе: загрузочный девайс = HDD
;//-------------------------------------------------
;// Найти и скопировать вторичный загрузчик в память
mov si,0x01BE ;// смещение к таблице разделов HDD
mov cx,4 ;// всего записей в ней
@find: cmp byte[si],0x80 ;// проверить флаг загрузочного раздела!
je @ok ;// если нашли..
add si,0x10 ;// иначе: переход к сл.записи в таблице
loop @find ;// промотать СХ-раз..
push es 0xb800 ;// Ошибка! Boot раздел не найден!
pop es
mov si,error
mov cx,errorLen
mov di,(160*5)
@@: lodsb
stosw
loop @b
pop es
jmp $
;// Нашли загрузочный раздел - копируем его сектор в память
;// Сначала заполним пакет-адреса для расширенного сервиса INT-13h
@ok: mov eax,dword[si+8] ;// взять стартовый сектор раздела "LBA-Start"
mov [StartLBA],eax ;// указать его в пакете-адреса
mov [Counter],1 ;// секторов для чтения (зависит от размера загрузчика)
mov ax,ds ;//
mov [BuffSeg],ax ;// сегмент приёмного буфера,
mov [BuffOffs],0x600 ;// ...и его адрес = 0000:0600
mov ah,0x42 ;// функция 42h = чтение секторов HDD в память
mov dl,[disk] ;// номер диска
mov si,DiskPacket ;// указатель на пакет-адреса
int 13h ;// зовём сервис диска биоса!
;// Запрос юзеру на загрузку ОС
@fdd: push es 0xb800 ;// сегмент видео-буфера
pop es
mov si,loadOs ;// адрес строки
mov cx,loadLen ;// её длина
mov di,(160*5) ;// позиция в окне = строка(5)
@@: lodsb ;// печать!
stosw
loop @b
pop es
;// Пока ОС`ки у нас нету, поэтому зависнем
jmp $
;//-------------------------------------------------------------------
;// Забить нулями оставшееся пространство в 512-байтном секторе
;//-------------------------------------------------------------------
times 510-($-$$) db 0
dw 0xAA55 ;//<--- вставим в хвост Boot-сигнатуру 55AAh
3.2. Отладка и тестирование загрузчика
Скомпилировав исходник выше, получим бинарный файл размером в сектор 512-байт. На следующем этапе нужен образ загрузочного диска, в первый сектор которого и поместим свежеиспечённый Boot.bin. Самый простой вариант – это создать в двоичном редакторе HxD новый файл, перейти в меню «Правка», и выбрав пункт «Вставить байты» указать размер в DEC ровно
1.474.560
байт. Если сохранить теперь этот файл с расширением *.img, то получим образ пустого/чистого диска «Floppy 1.44 Mb». На заключительном этапе, чтобы сделать из него загрузочный, тупо выделив удаляем из образа первые 200h=512
байт, и вставляем на их место Boot.bin. Обратите внимание, что в редакторе HxD обязательно нужно сперва удалить блок нужного размера, а потом вставить бинарные данные, иначе размер образа превысит лимит в 1.474.560
байт, и виртуальная машина откажется принимать на борт такой флоп, посчитав его невалидным.Для тестов нашего загрузчика можно использовать любой эмулятор или вирт.машину – лично у меня это VirtualBox. В настройках ставим первым девайсом флоп, указываем путь до образа дискеты, и жмякнув Go получаем картину ниже. Как видим, меню пока девственно чистое (его заполнит позже код ядра самой ОС), но главное загрузчик получил управление от биоса, и это уже гуд.
Иногда в коде лоадера могут возникнуть ошибки, обнаружить которые не так просто, как может показаться на первый взгляд. Проблема в том, что отладчики работают под управлением какой-либо ОС, а загрузчик получает управление намного раньше. Здесь на помощь приходит эмулятор «Bochs» (в народе борщь), в составе которого имеется весьма простенький, но исправно функционирующий дебагер с графическим интерфейсом. Чтобы его активировать, нужно при установке взвести галку «DLX Linux Demo», после чего в папке с установленной программой появиться дир
C:\ProgramFiles(x86)\Bochs-2.xx\dlxlinux
.В дефолте эмуль запускает окно вирт.машины, и чтобы переключить его в режим отладки, нужно внести небольшие правки в файл Run.bat, и конфигуратор BochsRc.bxrc. Оба уже пропатченных этих файла я положил в скрепку, и вам остаётся лишь указать полный путь до образа дискеты
*.img
в строке «floppya» последнего – у меня она выглядит так:floppya: type=1_44, 1_44="F:\Install\DEBUG\ASM\CODE\CodebyOS.img", status=inserted, write_protected=0
При удачных обстоятельствах, после запуска Run.bat получим окно ниже где видно, что отладчик останавливается на точке-входа в биос по сегментному адресу
F000:FFF0
(линейный 0xFFFF0) с процедурой POST, а не на входе в загрузчик. Соответственно чтобы прыгнуть сразу на полезную нагрузку, придётся командой b 0x7c00
поставить брэйкпоинт, и запустить процесс на исполнение Continue [c]
. Одной из полезных фишек этого отладчика является то, что он может дампить все основные структуры в памяти типа GDT/IDT
, а так-же в строке статуса отображает текущий режим работы процессора Real или Protected-Mode:4. Заключение
В следующей части перейдём к более сложным механизмам ядра самой операционной системы, и попробуем из конфигурационного пространства PCI собрать информацию об установленном оборудовании, для последующей работы с ним. В скрепку положил конфиг-файлы Bochs, а так-же скомпилированный загрузчик Boot.bin (если захотите в HxD собрать имидж самостоятельно), и готовый образ дискеты CodebyOs.img. Всем удачи, пока!
Хек-редактор HxD:
Ссылка скрыта от гостей
Эмулятор Bochs:
Ссылка скрыта от гостей