Пишем игрушечную ОС (часть II)

Автор: (C) Krishnakumar R.
Перевод: (C) Александр Куприн


Часть I (http://gazette.linux.ru.net/lg77/articles/rus-krishnakumar.html) опубликована в апрельском номере журнала.

Следующее, что необходимо разобрать после изучения процесса создания загрузочного сектора и до того, как мы перейдём к описанию приёмов переключения в защищённый режим, это использование прерываний BIOS[1]. Прерывания BIOS представляют собой подпрограммы низкого уровня, облегчающие работу создателю операционной системы. В этой части статьи мы будем иметь дело с ними.

1. Теория

1.1 Почему BIOS ?

BIOS копирует загрузочный сектор в оперативную память и передаёт на него управление. Кроме этого, существует ещё несколько функций, которые могут выполняться BIOS'ом. В момент загрузки операционная система ещё не располагает драйверами для поддержки оборудования. Размещение такого драйвера в загрузочном секторе -- задача практически невыполнимая. Должен существовать другой способ решить эту проблему и в этом нам поможет BIOS. Он содержит множество подпрограмм, служащих для различных целей и которыми мы можем воспользоваться. Например, существуют подпрограммы проверки установленного оборудования, подпрограммы управления выводом на печать, определения размера оперативной памяти и пр. Эти подпрограммы называются подпрограммами или обработчиками прерываний BIOS.

1.2 Как же вызывать прерывания BIOS ?

В "обычных" языках программирования вызов подпрограммы реализуется через обращение к её имени. К примеру, если в программе на C у нас есть подпрограмма display, получающая в качестве параметров атрибуты (attr) и количество отображаемых символов на экране (noofchar), то обратиться к ней мы можем просто указав её имя и необходимые параметры. Однако, здесь мы будем использовать прерывания, вызов которых осуществляется посредством инструкции ассемблера int.

Например, для вывода символов на экран в C мы используем функцию похожую на:

display(noofchar, attr);

Эквивалентно этому, оперируя средствами ассемблера и BIOS, мы напишем:

int 0x10

1.3 Ладно, а как мы передаём параметры ?

Перед тем как вызвать прерывание BIOS, нам нужно загрузить данные в заранее определённом формате в регистры процессора. Предположим, мы используем прерывание 0x13, предназначенное для чтения/записи с дискеты. Прежде чем вызвать его, мы должны определить адрес в оперативной памяти, куда будут загружены данные. Также мы должны передать информацию о номере устройства (fd0 - 0x00, fd1 - 0x01, hda - 0x80, hdb - 0x81 и т.д.), цилиндре, секторе и количестве копируемых секторов. Эти данные должны быть загружены в определённые регистры. Всё это вам станет понятно после того, как вы прочтёте описание работы кода загрузочного сектора, который мы разработаем чуть позже.

Есть одна очень важная деталь, о которой вы должны знать -- одно и тоже прерывание может использоваться для различных целей. Всё это зависит от номера функции, который указывается в регистре ah (иногда ax). К примеру, прерывание 0x10 может быть использовано как для вывода на экран строки, так и для получения координат курсора. Если мы запишем в регистр ah значение 0x03, то тем самым при вызове прерывания 0x10 мы выберем функцию, используемую для получения координат курсора. Для вывода строки на экран мы записываем в регистр ah значение 0x13, которое является номером функции вывода строки на экран.

2. Что мы будем делать теперь ?

На этот раз наш исходный код состоит из двух программ на ассемблере и одной на C. Первый файл (на ассемблере) -- это код загрузочного сектора. В нём хранятся инструкции, копирующие второй сектор флоппи-диска в сегмент памяти 0x500 (абсолютный адрес 0x5000)[2]. Операция выполняется при помощи прерывания BIOS 0x13. После этого код загрузчика передаёт управление по адресу 0x500:0 (сегмент -- 0x500, смещение -- 0). Код второго файла на ассемблере выводит на экран сообщение, используя прерывание BIOS 0x10. Программа на C предназначена для записи исполняемого кода программ на ассемблере в 1-й и во 2-й сектора дискеты.

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

Используя прерывание 0x13, код загрузочного сектора читает и копирует второй сектор флоппи-диска в память по адресу 0x5000 (сегментный адрес 0x500). Пример этого показан ниже. Сохраните его в файле bsect.s.

LOC1=0x500

entry start
start:
        mov ax,#LOC1
        mov es,ax
        mov bx,#0

        mov dl,#0
        mov dh,#0
        mov ch,#0
        mov cl,#2
        mov al,#1

        mov ah,#2

        int 0x13

        jmpi 0,#LOC1

Первая строка -- это макро-определение. Следующие две инструкции уже знакомы вам по предыдущей статье. Записать данные непосредственно в регистры сегментов нельзя, поэтому следующие две строки используются для загрузки в регистр es значения 0x500. Регистр bx содержит значение смещения в сегменте, по которому будут загружены данные.

Далее мы записываем...

Итак, мы загружаем второй сектор нулевой дорожки устройства номер 0 (что соответствует приводу флоппи-дисков на 1.44Мб) по адресу 0x500:0.

Значение 2 в регистре ah указывает на номер функции прерывания. Мы выбираем функцию номер 2, которая используется для чтения данных с диска (жёсткого или гибкого).

Теперь мы вызываем прерывание 0x13 и последней командой передаём управление коду, загруженному по адресу 0x500:0.

4. Второй сектор

Второй сектор будет содержать следующий код :

entry start
start:
        mov     ah,#0x03
        xor     bh,bh
        int     0x10

        mov     cx,#26
        mov     bx,#0x0007
        mov     bp,#mymsg
        mov     ax,#0x1301
        int     0x10

loop1:  jmp     loop1

mymsg:
        .byte  13,10
        .ascii "Handling BIOS interrupts"

Этот код загружается и выполняется в сегменте 0x500. Он использует прерывание 0x10 для получения текущей позиции курсора и вывода на экран сообщения.

Первые три строки кода (отсчёт начинается с третьей строки, пропуская инструкции определения точки входа) используются для получения текущей позиции курсора. Для этого используется функция 0x03 прерывания 0x10. Перед её вызовом мы обнуляем значение регистра bh[3]. После выполнения прерывания, интересующий нас результат будет хранится в регистрах dh и dl (номер строки и колонки соответственно). Переходим ко второй части программы. В регистр...

Начало сообщения содержит два байта со значениями 13 и 10, что соответствует нажатию клавиши enter. Эти два кода идут вместе, 13 -- код возврата каретки (Carriage Return, CR), 10 -- код перевода строки (Line Feed, LF). Сама строка содержит 24 символа. Символы CR и LF трактуются функцией 0x13 прерывания 0x10 как управляющие и поэтому не высвечиваются. Теперь вызываем прерывание. И последней инструкцией "вешаем" компьютер.

5. Программа на C

Код программы на C, являющийся "ракетоносителем" (точнее "программоносителем" 8-), приведен ниже. Сохраните его в файле write.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("./bsect", 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_SET);
                write(floppy_desc, boot_buf, 512);

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

                lseek(floppy_desc, 512, SEEK_SET);
                write(floppy_desc, boot_buf, 512);

                close(floppy_desc);
}

В первой части статьи я описал, как создать загрузочную дискету. В данном примере есть небольшие отличия. Сперва мы копируем в загрузочный сектор файл bsect, исполняемый код которого генерируется из bsect.s. Затем наступает очередь sect2 -- мы записываем его во второй сектор флоппи-диска. Вот и всё, изменения, делающие дискету загрузочной, выполнены.[6]

6. Готовые примеры

Вы можете взять исходники примеров здесь

  1. bsect.s
  2. sect2.s
  3. write.c
  4. Makefile

Удалите у файлов расширение txt и введите

make

в ответ на приглашение оболочки или вы можете откомпилировать файлы самостоятельно. В этом случаем введите

as86 bsect.s -o bsect.o

ld86 -d bsect.o -o bsect

и повторите тоже самое для sect2.s. Сборку write.c выполните командой :

cc write.c -o write

и вставив дискету в дисковод выполните программу write.

7. Что дальше ?

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


Krishnakumar R.

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


Примечания переводчика

[1] BIOS -- Basic Input/Output System (Базовая Система Ввода/Вывода), код прошиваемый в ПЗУ и позволяющий работать с оборудованием компьютера.

[2] Чтобы получить абсолютное значение адреса из адреса в формате сегментной адресации, нужно умножить значение сегментного регистра на 0x10 и прибавить величину смещения. В нашем случае это 0x500*0x10=0x5000.

[3] Автор не стал объяснять почему в регистр bh необходимо записать нулевое значение, поэтому это сделаю я 8-). В текстовом режиме существует несколько страниц видеопамяти, которые могут отображаться на экране. Объём каждой из них зависит от режима. Если это режим 80x25, то объём страницы составляет 4000 байт -- (80 * 25 * 2). Два байта, если вы помните из предыдущей статьи, отводятся для хранения кода символа и его атрибутов. Переключение между видео страницами осуществляется при помощи функции 0x05 прерывания 0x10.

[4] На мой взгляд, необходимо немного изменить эту программу -- убрать явное указание количества символов в строке, заменив его вычислением. Т.е. вместо строки

      mov     cx,#26

ставим две других

      mov     cx,#end_mystr
        sub     cx,#mymsg

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

[5] Если быть точным, то на адрес строки указывает пара регистров -- es:bp. Но в нашем случае инициализация регистра es произошла до этого момента (см. код загрузчика).

[6] Для любителей простоты исполнения, код программы на C может быть заменён скриптом на bash'е из двух строк (точнее трёх -- есть ещё заголовок:)

#!/bin/bash
dd if=bsect of=/dev/fd0 conv=notrunc
dd if=sect2 of=/dev/fd0 seek=1 conv=notrunc

Хотя опция conv=notrunc возможно и не нужна в случае с /dev/fd0.


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