Пишем игрушечную ОС (Часть I)
 
Автор: (C) Krishnakumar R. <[email protected]>
Перевод: (C) Александр Куприн

Эта статья -- практическое пособие по написанию кода для создания загрузочного сектора диска (boot sector). В первой ее части рассматриваются теоретические основы того, что происходит после включения питания компьютера. Заодно излагается план наших действий. Во второй части речь идет о том, что нам понадобится для того, чтобы двигаться дальше, а в третьей мы уже будем иметь дело с самой программой. Программа не будет загружать Linux, она просто покажет кое-что на экране.


1. Предыстория

1.1 Маскарадный костюм

Процессор -- это сердце компьютера. При включении питания, каждый микропроцессор представляет из себя всего лишь еще один 8086-й. (В этой статье речь идет об x86-совместимых процессорах. Прим.перев.). Даже если у вас самый последний Pentium, в этот момент он обладает лишь возможностями своего далекого предка. И только программно переключив процессор в малоизвестный защищенный режим, мы получим доступ ко всей его мощи.

1.2 Наша роль

Первоначально, контроль находится в руках BIOS (Basic Input/Outpuit System; Базовая Система Ввода-Вывода). Это набор программ, которые хранятся в ROM (Read Only Memory; ПЗУ -- программируемое запоминающее устройство). После включения питания BIOS выполняет POST (Power On Self Test). Это программа проверки целостности компьютера (проверка корректности работы памяти, клавиатуры и других подключенных к компьютеру периферийных устройств). Все это происходит в тот момент, когда вы слышите звуковой сигнал. (Если в процессе проверки обнаруживается ошибка, то компьютер опять подает звуковые сигналы, но уже другой длительностью и в иной последовательности -- в описаниях к материнским платам иногда встречаются звуковые последовательности, соответствующие той или иной неисправности. Если такого такого нет в вашем описании, то вы можете его найти на сайте производителя прошивки для BIOS. Например, сайт компании Phoenix Technologies Ltd [http://www.phoenix.com/products/specs.html] (ищите на нем pdf-файл userman.pdf). Только учтите, у них сейчас идет реконструкция, поэтому карта сайта может измениться. Прим.перев.) Если все в порядке, то BIOS выбирает загрузочное устройство. Он копирует первый сектор (boot sector) с устройства в ОЗУ по адресу 0x7C00. (Следует уточнить, что если речь идет о жестких дисках, то первый сектор называется не boot sector, а master boot record -- главная загрузочная запись или MBR. Прим.перев.) Затем управление передается по этому адресу. Загрузочным устройством может служить флоппи-диск, CD-ROM, жесткий диск или любое другое устройство по вашему выбору. В качестве такового мы воспользуемся флоппи-диском. Если записать исполняемый код в загрузочный сектор гибкого диска, то он будет выполнен при попытке загрузки с дискеты. Наша задача проста: написать небольшую программу и разместить ее в загрузочном секторе.

1.3 План

Для начала напишем программу на ассемблере процессора 8086 (не паникуйте; я объясню как) и скопируем ее в загрузочный сектор флоппи-диска. Для копирования мы напишем программу на C. Загрузим компьютер с дискеты и будем наслаждаться. 8-) (немножко протащимся? -- прим. редактора:)

2. Что еще нам нужно

as86
Это ассемблер. С его помощью исходная программа на ассемблере преобразовывается в объектный файл.
ld86
Это компоновщик. Он конвертирует сгенерированный as86 объектный код в "актуальный" (т.е. пригодный к загрузке на выполнение -- прим. редактора ) машинный код. Это будет машинный язык, понятный процессору 8086.
gcc
Компилятор C. Нужен для компиляции программы, которая "доставит" наш код в загрузочный сектор.
Свободная дискета
Флоппи-диск, который будет использоваться для хранения нашей операционной системы (Перевод дословный. Судя по всему, автор об этом расскажет в следующих статьях. В этой же идет речь только о маленькой программке, показывающей на экране символ "A". Прим.перев.). К тому же будет выполнять функции загрузочного устройства.
Старый добрый Linux
Уверен, вы знаете, о чем я.

as86 и ld86 присутствуют в большинстве стандартных дистрибутивов. Если же их у вас нет, то вы можете взять их на сайте http://www.cix.co.uk/~mayday/ . Они оба включены в один пакет -- bin86. Кое-что из документации вы можете найти здесь www.linuxdoc.org/HOWTO/Assembly-HOWTO/as86.html.

3. 3, 2, 1, Старт!

3.1 Загрузочный сектор

Хватайте ваш любимый редактор и наберите следующее:


    entry start
    start:
        mov ax,#0xb800
        mov es,ax
        seg es
        mov [0],#0x41
     seg es
        mov [1],#0x1f
    loop1: jmp loop1

Это понятный as86 диалект ассемблера. Первая инструкция описывает точку входа в программу. Мы объявляем, что управление первоначально должно быть передано на метку start. Вторая строка описывает расположение этой метки (не забудьте поставить ":" после "start"). Первая инструкция, которая должна быть выполнена в этой программе, расположена сразу после нее.

0xb800 -- это адрес сегмента видеопамяти в текстовом режиме. Символ # указывает на непосредственное значение. После выполнения команды

mov ax,#0xb800

регистр ax будет содержать значение 0xb800, это адрес расположения видеопамяти. Теперь мы копируем это значение в сегментный регистр es. Помните, что 8086 имеет сегментную архитектуру. У него четыре сегментных регистра, указывающих на: сегмент кода ( CS ), сегмент данных (DS), сегмент стека ( SS ) и дополнительный сегмент (ES ). Фактически, мы сделали видеопамять нашим дополнительным сегментом, так что все записанное в него, будет записано в видеопамять.

Для отображения любого символа на экране, вам нужно записать в видеопамять два байта. Первый -- это значение ascii-кода символа, который вы хотите вывести на экран. Второй -- атрибут символа. Атрибут содержит следующую информацию: цвет символа, цвет фона, мерцание. Инструкция seg es фактически является префиксом, указывающим относительно какого сегмента должна выполняться следующая операция. Итак, мы заносим в первый байт видеопамяти (адрес 0xb800:0) значение 0x41, которое соответствует ascii-коду символу "A" (большая английская A). Затем мы должны прописать значение атрибута символа в следующем байте видеопамяти. Сюда мы записываем значение 0x1f, соответствующее белому символу на синем фоне. (Чтобы понять, где что спрятано в байте атрибута, лучше преобразовать 0x1f в двоичное представление: 00011111. Здесь первые четыре бита [справа налево] -- цвет символа, следующие три -- цвет фона и последний 7-й бит [отсчет ведется с нуля] -- это признак мерцания. Из вышесказанного можно сделать следующий вывод: в текстовом режиме символ может иметь 16 цветов, фон -- 8. Прим.перев.) Теперь, если мы выполним эту программу, то получим белую "A" на синем фоне. В конце стоит программная "петля" (команда jmp указывающая на саму себя). Нам нужно, либо остановить выполнение кода после того, как отобразим символ, либо "намертво" замкнуть цикл. Сохраните файл как boot.s .

Идея с манипуляцией видеопамятью может быть не совсем понятна, поэтому позвольте мне объяснить на будущее. Предположим мы используем экран размером 80 на 25 (80 символов в строке и 25 строк). Для каждой строки нам нужно по 160 байт: 80 байт, содержащие коды символов и столько же, содержащие их атрибуты. Если нам нужно вывести 3-й символ в 1-й строке, то мы должны пропустить байты 0 и 1, как относящиеся к первому символу; байты 2-й и 3-й, как относящиеся ко второму символу и только после этого записать в 4-м байте ascii-код выводимого символа и в 5-м -- его атрибут.

3.2 Запись загрузочного сектора на флоппи-диск

Теперь мы должны написать программу на C, которая скопирует наш код (код нашей ОС) в первый сектор дискеты. Вот она:


    #include <sys/types.h> /* unistd.h needs this */
    #include <unistd.h>    /* contains read/write */
    #include <fcntl.h>

 int main()
    {
        char boot_buf[512];
        int floppy_desc, file_desc;

       file_desc = open("./boot", O_RDONLY);
       read(file_desc, boot_buf, 510);
       close(file_desc);

       boot_buf[510] = 0x55;
       boot_buf[511] = 0xaa;

       floppy_desc = open("/dev/fd0", O_RDWR);
       lseek(floppy_desc, 0, SEEK_CUR);
       write(floppy_desc, boot_buf, 512);
       close(floppy_desc);
    }

Сперва мы открываем файл boot в режиме только для чтения и копируем файловый дескриптор в переменную file_desc. Затем читаем первые 510 байт, либо, если файл размером меньше 510 байт, читаем весь файл. Наш код невелик, поэтому последний случай наш. Будьте паинькой -- не забудьте закрыть файл. 8-)

Последние четыре строки кода открывают устройство флоппи-диска (которым, как правило, является /dev/fd0). Затем переводим указатель в начало файла, используя lseek и записываем 512 байт из буфера на дискету.

Страницы справочного руководства функций read, write, open и lseek (смотрите man 2) дадут вам достаточно информации о том, что обозначают другие параметры функций и как их использовать. Есть две строки, которые выглядят немного таинственно. Вот они:


    boot_buf[510] = 0x55;
    boot_buf[511] = 0xaa;

Это информация для BIOS. Если предполагается, что BIOS должна распознать устройство как загрузочное, то устройство должно содержать значения 0x55 и 0xaa, расположенные по смещениям 510 и 511. (Не берусь утверждать на все 100%, но раньше [по слухам], во времена MS DOS 2.0 и ниже, этой сигнатуры не было вообще. Она появилась позднее, как ответ на загрузочный вирус ( помните такие? -- прим. ред.), который при помощи этой сигнатуры проверял , заражал ли он этот компьютер или нет. Если нет, то вирус делал свое "черное дело" и "приписывал" эти два байта. Прим.перев.). Теперь почти все. Программа читает файл boot в буфер boot_buf. Делает нужные изменения в 510 и 511 байтах и записывает boot_buf на флоппи-диск. Сохраним файл как write.c .

3.3 Теперь давайте сделаем ЭТО

Для создания исполняемых файлов вам нужно выполнить следующие команды:


    as86 boot.s -o boot.o

    ld86 -d boot.o -o boot

    cc write.c -o write

Сперва, мы компилируем объектный файл boot.o из boot.s . Затем конвертируем его в двоичный, boot . Ключ -d заставляет компоновщик ld86 удалить все заголовки и создать "голый" двоичный файл. Если у вас возникли сомнения или появились неясности в этом вопросе, прочтите страницы справочного руководства по as86 и ld86. Последним мы компилируем C-программу и получаем исполняемый файл write.

Вставьте пустую дискету в дисковод и наберите команду (Убедитесь, что у вас есть права на запись в /dev/fd0. И вообще, никто вам не мешает использовать для тех же целей команду dd [dd if=boot of=/dev/fd0 ]. или команду копирования cp [cp boot /dev/fd0 ]. Возможно, автор планирует расширить возможности этой программы для дальнейшего использования в следующих статьях. Прим.перев.):

./write

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

Теперь вы можете видеть символ 'A' (белого цвета на синем фоне). Это означает, что программа, которую мы написали и скопировали в загрузочный сектор, была загружена с дискеты и выполнена. Теперь она находится в бесконечном "программном" цикле в конце кода загрузчика. Чтобы вернуться в привычную среду обитания (читай -- Linux 8-) нужно перезагрузить компьютер, предварительно удалив дискету из дисковода.

В дальнейшем, мы сможем вставлять больше кода в нашу программу загрузки, заставляя ее делать более сложные вещи (используя прерывания BIOS, переключение в защищенный режим и прочее.) Следующие части (вторая, третья и пр.) этой статьи станут вашими проводниками на пути усовершенствования кода нашего загрузчика. До встречи!


Krishnakumar R.

Кришнакумар -- студент последнего курса B.Tech в Govt. Engg. College Thrissur, Kerala, Индия. Его путешествие в земли Операционных Систем началось с программирования модулей для Linux. Он создал операционную систему GROS, основная цель которой -- выполнение функции маршрутизатора. (Детали вы можете найти на его домашней странице: www.askus.way.to ) Другие его интересы -- это сетевые драйвера, драйвера устройств, портирование компиляторов и встроенные системы (Compiler Porting and Embedded systems).


Copyright (С) 2002, Krishnakumar R.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 77 of Linux Gazette, April 2002

Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин, Александр Куприн

Со всеми предложениями, идеями и комментариями обращайтесь к Сергею Скороходову ([email protected]). Убедительная просьба: указывайте сразу, не возражаете ли Вы против публикации Ваших отзывов в рассылке.

Сайт рассылки: http://gazette.linux.ru.net
Эту статью можно взять здесь: http://gazette.linux.ru.net/lg77/articles/rus-krishnakumar.html

В предыдущий выпуск вкралась ошибка. Исправляюсь. Вот diff для того, чтобы быть в стиле:)

--- toy-os.html.old Thu May 02 15:13:12 2002
+++ toy-os.html Thu May 02 15:12:15 2002
@@ -291,7 +291,7 @@
  class="sdfootnotesym"></a><sup><u><a href="#sdfootnote1anc">1</a></u></sup>
     На <a href="http://www.linuxgazette.com">Linux Gazette</a> на эту тему
     была лишь небольшая статья Константина Болдышева "Introduction to UNIX
-    Assembly Programming". Issue #52 апрель 2000г.</div>
+    Assembly Programming". Issue #53, май 2000г.</div>
 </div>
 <div id="sdfootnote2">
   <div class="sdfootnote"><a name="sdfootnote2sym" href="#sdfootnote2anc"