Перспективы создания ОС 2

Продолжение. Начало в КГ №№ 7, 8

Итак, сегодня мы попытаемся потихоньку перейти к практике. Какова же будет наша цель? Не так давно через печально известный сайт www.osdev.org я вышел на проект, целью которого было написать операционную систему, которая вмещалась бы в 512 байт загрузочного сектора дискеты. Задача не из легких: шутка ли, целая операционная система да занимает всего ничего. Это кажется просто муравьем с высоты таких великанов, как Microsoft Windows, Linux и других мощных ОС. Однако стоит сразу признать, что хоть производительности вышеназванных ОС мы, естественно, добиться не сможем даже при всем желании, но изучить принципы работы системы и вдобавок создать что-то более-менее полезное нам под силу.

Какие средства нам понадобятся для разработки? Это, безусловно, компилятор ассемблера. Я выбрал FASM (www.flatassembler.net). Этот компилятор имеет множество преимуществ, среди которых — поддержка трех платформ (Windows, MS-DOS и Linux), высокая частота обновлений, подержка всех современных технологий процессоров Intel и AMD и маленький размер. Кроме того, код FASM понятен и есть поддержка процедур, структур и т.п. Если вас не устраивает этот компилятор, выберите другой — на свой вкус. Во всяком случае, перенести код FASM на компиляторы NASM или MASM не составит никакого труда. Операционную систему выбирайте также по своему желанию. Я лично не имею ничего против Linux, а тем более Windows. Как я заметил выше, можно использовать MS-DOS. Если вы боитесь за жизнеспособность своего компьютера, рекомендую вам скачать эмулятор PC. Это такая программа, которая эмулирует работу настоящего компьютера, но, в отличие от настоящего ПК, диски представлены не в виде реальных, существующих у вас на винчестере дисков, а в виде одного файла, размер которого характеризует размер виртуального диска. Самый лучший вариант, на мой взгляд, — бесплатный эмулятор Bochs (www.bochs.sourceforge.net). Он очень прост в использовании, имеет много возможностей и, к тому же, обладает небольшим размером. Второй в списке — VMWare. Эта программа уже платная, однако вы можете каждый месяц на сайте разработчика запрашивать пароль для активации на месяц. Только вот размер VMWare уже никак не радует: выкачать из Интернета 30 с лишним мегабайт мне явно не под силу. Также обратите внимание на Virtual PC корпорации Microsoft, однако его использование — удовольствие для богатых, так как он полностью платный. Также нам надо будет каким-то образом записать нашу операционную систему на дискету. Для тех, кто работает в Windows, рекомендую популярную и простую в обращении программу RawWrite. Найти ее вы сможете по адресу www.uranus.it.swin.edu.au/~jn/linux. Для пользователей Linux все решается очень просто. Для записи нашей системы зайдите в консоль и выполните следующую команду: dd if=имя_файла_ОС of=/dev/fd0 bs=512 count=1. Разумеется, прямой доступ к дисководу должен быть открыт администратором Linux, что, впрочем, устанавливается по умолчанию. Кстати, пользователи MS-DOS тоже не останутся без средств. В Интернете есть одноименая RawWrite-программа, но написанная для этой системы. Она прекрасно дружит с командной строкой, так что компиляцию можно выполнять, записав все команды в .bat-файл. Наряду с программными средствами предполагается, что вы уже имеете некоторый багаж знаний по архитектуре процессоров Intel. А иначе лучше вооружиться какой-нибудь хорошей книгой по ассемблеру. Я настоятельно рекомендую всем желающим приобрести учебник В.И. Юрова (не перепутайте с книгой "Практикум" того же автора!).

Что нам надо сделать в первую очередь? Заметим, что наша ОС будет работать в реальном режиме, так что компилятору нужно это "сказать". Делается это директивой 'use16'. Теперь проследим тот путь, который проделывает система, прежде чем начать загрузку пользовательского кода. После включения питания компьютера начинает исполняться код видеокарты, о чем свидетельствует соответствующая надпись на мониторе, которая, как правило, содержит в себе имя и модель видеокарты. Затем компьютер совершает так называемый POST — Power-On Self Test. Этот тест выполняет проверку всех жизненно важных частей компьютера и выдает сигнал, если обнаружилась какая-то неисправность. Если POST проходит успешно, то компьютер устанавливает регистр AX-процессора в ноль или в иное значение, если были обнаружены неполадки. Затем управление получает BIOS — Basic Input/Output System. Кстати, именно функции BIOS мы будем использовать в дальнейшем. В зависимости от своих настроек BIOS загружает некоторый код, содержащийся на носителе, определенном в настройках BIOS. В нашем случае система будет загружать 512 байт из загрузочной записи на дискете. Поэтому, если у вас не выставлены настройки в BIOS, позволяющие загружать систему с дискеты, выставьте их или на крайний случай используйте какой-нибудь мультизагрузочный диск. Следует заметить, что загрузочная запись загружается по адресу в памяти 0000h:7C00h. Поэтому не забудем сразу выставить этот адрес в нашем коде. Делается это путем добавления строки 'org 0x7C00'. Собственно, все готово для того, чтобы начать писать код. Для начала давайте создадим вспомогательные процедуры. В первую очередь это должны быть процедуры "общения" с компьютером посредством клавиатуры и монитора.

Сперва напишем процедуру вывода символа на экран. Делается это элементарно. Сначала мы заносим в регистр AH номер функции вывода символа в режиме TTY (то есть со смещением каретки). Номер требуемой функции равен 0xE. Затем занесем в регистр BH номер страницы. Как показал опыт, большинство программистов не делают этого, и все работает нормально, однако предпочту перестраховаться, тем более, что размер ядра нам пока позволяет выбирать не самые оптимальные варианты. По умолчанию всегда устанавливается страница с номером 0. Так что занесем это значение в регистр BH. Для экономии места для нашего ядра (а нам дано всего лишь 512 байт) мы будем не заносить напрямую значение в регистр, а выполним команду xor bh, bh. Таким образом мы сэкономили целый байт. Вот, все готово для вызова прерывания BIOS, что мы и делаем. Номер прерывания, отвечающий за экран, равен 0x10. Заметим, что входной параметр у функции только один: в регистре AL должен находиться ASCII-код нужного нам символа: так требует прерывание. Итак, у меня получился следующий код:

writechar:
mov ah, 0xE
xor bh, bh
int 0x10
ret

За ввод/вывод в контексте клавиатуры отвечает прерывание 0x16. Процедуру чтения символа с клавиатуры написать очень легко. Для того, чтобы это сделать, определим имя функции чтения клавиатуры с ожиданием. Эта функция имеет номер 0. Все номера функций, как правило, заносятся в регистр AH. Сделаем так же, как делали и с предыдущей процедурой: обнулим значение регистра AH с помощью команды XOR. Затем вызываем прерывание 0x16 BIOS. Компьютер ждет, пока пользователь не нажмет хоть какую-нибудь клавишу, помещает ее ASCII-код в регистр AL и продолжает работу. Затем мы схитрим и передадим управление процедуре вывода символа на экран. Согласитесь, это будет логично. Вот, что получилось у меня:

readkey:
xor ah, ah
int 0x16
call writechar
ret

Ну вот у нас уже что-то есть. Далее мы напишем процедуру, которая будет выводить текстовую строку на экран. Эта процедура будет не менее легкая, чем вышеописанные. Сразу опишем ее параметры. На входе у нас будет два параметра: в регистре SI будет адрес строки, а в регистре BL — ее длина. Для посимвольного чтения мы будем использовать команду LODSB. После ее выполнения в регистре AL окажется символ, на который указывала пара регистров DS:SI. При выполнении команды в данном случае регистр SI увеличивается на единицу. Стоит заметить, что такое происходит при флаге DF, установленном в 0. Поэтому в начале процедуры мы выполним команду сброса этого флага — CLD. Великолепно. Теперь организуем цикл до тех пор, пока регистр BL не обратится в ноль. В теле цикла мы будем выполнять только три команды — чтения символа, вызова процедуры writechar и декремента регистра BL. Как видите, ничего сложного здесь нет. Вот мой вариант:

writestr:
cld
writestr_r1:
lodsb
call writechar
dec bl
cmp bl, 0
ja writestr_r1
ret

Заметим, что значение SI устанавливается через команду lea. Если мы хотим вывести содержимое памяти, на которое указывает метка, допустим, "mytext", то мы должны выполнить команду lea si, [mytext]. И не забудьте установить длину текста. Что же нам еще надо для базового комплекта? А вот что. Давайте условимся, что наша система не будет поддерживать систему FAT12, так как ее реализация заняла бы слишком много места. Так как же общаться с внешним миром, запускать программы и т.д.? Ответ прост: записывать программы по определенным секторам на дискете и, конечно, хранить их адреса в памяти. Обещаю, что этот недостаток мы исправим (используя прикладные программы, но не на уровне ядра), однако сейчас наша задача — реализовать именно запуск программ, зная их положение на носителе. Отсюда вытекает задача — дать возможность пользователю выбрать номер сектора. Этим мы сейчас и займемся. Процедура не представляет никакой сложности и требует минимума — наличия под рукой таблицы ASCII-кодов. Сначала обнулим значение регистра CX. Именно в нем окажется численное значение того номера, который введет пользователь. Организуем цикл, который будет продолжаться до тех пор, пока пользователь не нажмет клавишу Enter или пробел. Смысл проверки наличия проверки на пробел вы поймете позже (хотя не так сложно догадаться и сейчас). Сперва в цикле мы ожидаем нажатия клавиши через процедуру readkey. Затем проверяем значения и, если нажаты Enter или пробел, выходим из цикла. Далее следует смысл данной процедуры. Мы должны сдвинуть содержимое регистра CX на один знак влево. Как это сделать? Если отталкиваться от шестнадцатеричной системы счисления, то логично было бы умножить значение CX на 16 (то есть на 0x10). Но мы сделаем это всего одной процедурой — shl cx, 4. Теперь у нас в первом разряде числа в CX находится 0. Осталось добавить к CX то число, которое написал пользователь в данный момент. Как известно, коды чисел начинаются с 48. Код 48 принадлежит нулю. Посему мы вычтем из AL 48 (напомню, что в AL находится код символа, который пользователь ввел только что). Делается это действием sub al, 48. Но в соответствии с шеснадцатеричной системой счисления пользователь мог ввести символы с A до F. Тогда мы сравниваем значение в AL с 10. Если это значение меньше десяти, то все нормально: была введена цифра. А если нет, то был введен какой-то символ. Тогда мы замечаем, что код символа A равен 65. Также заметим, что код символа цифры "9" равен 48+9=57. А поэтому мы должны убрать разрыв между символом девятки и буквы A. 65-57-1=7. Именно столько мы должны вычесть из AL для нахождения требуемого значения. А дальше все стандартно: добавляем к CX значение AX, предварительно обнулив AH, и возвращаемся в начало цикла. Вот и все. Теперь любое четырехзначное число вплоть до FFFF может быть занесено в регистр CX. Вот мой код:

readnum:
xor cx, cx
readnum_r1:
call readkey
cmp al, ' '
je readnum_r3
cmp al, 0xD
je readnum_r3
shl cx, 4
sub al, 48
cmp al, 10
jl readnum_r2
sub al, 7
readnum_r2:
xor ah, ah
add cx, ax
jmp readnum_r1
readnum_r3:
ret

Вот и базовый набор готов. Сейчас давайте соберем маленькую консоль и оттестируем наше творение. Сперва инициализируем регистры DS, SS и SP. Будьте аккуратны с регистром стека — SP. Если его значение будет находиться слишком близко к месторасположению нашего ядра, то при слишком большом количестве элементов в стеке ядро может затереться. Инициализацию я провел следующим образом:

mov ax, cs
mov ds, ax
mov ss, ax
mov sp, 0x7C00

Хорошо. Сейчас сделаем небольшой штрих, относящийся к экрану. Мы установим на всякий случай стандартный режим 80 на 25 символов. Для этого мы поместим в регистр AH ноль, а в AL — номер режима — в данном случае 0x3. Подробное описание режимов вы можете найти в Интернете. И затем выполняется прерывание 0x10. Хоть эта процедура элементарна, вот как она выполняется:

xor ah, ah
mov al, 0x3
int 0x10

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

main_os_loop:
lea si, [promptline]
mov bl, 2
call writestr
call readkey
cmp al, 'E'
je os_reboot
call readnum
lea si, [caretpos]
mov bl, 2
call writestr
jmp main_os_loop
os_reboot:
jmp 0xFFFF:0x0000

И, конечно же, текстовые ресурсы (заметьте: в кавычках в первой строке находится два знака — стрелка и пробел):

promptline db "> "
caretpos db 0xA, 0xD

И последний штрих. Любой загрузочный сектор должен оканчиваться двумя байтами 0x55 и 0xAA. Поэтому пустое пространство нам надо заполнить нулями. Делается это следующей командой, стоящей после всего кода, но перед этими двумя символами: times 510-($-0x7C00) db 0. А далее мы просто пишем db 0x55, 0xAA и… компилируем! Думаю, собрать всю эту мозаику из процедур для вас не составит труда. А теперь запустите нашу пока еще псевдоОС на эмуляторе, убедитесь, что в CX всегда возникает правильное значение и в целом все работает. А еще лучше — убедитесь в этом на реальном компьютере. Только не забывайте, что при вводе команд требуется верхний регистр символов.
В итоге мы имеем еще более 350 свободных байт. Здесь-то мы и разгуляемся в следующий раз. А пока разбирайтесь с этим кодом, предлагайте свои идеи. Если что-то не получается или у меня где-то есть ошибка, пишите.

Влад Маслаков, Vladislav_1988@mail.ru


Компьютерная газета. Статья была опубликована в номере 09 за 2005 год в рубрике soft :: ос

©1997-2024 Компьютерная газета