Пишем игрушечную ОС (часть II)
Автор: (C) Krishnakumar R.
Перевод: (C)
Александр Куприн
Часть I (http://gazette.linux.ru.net/lg77/articles/rus-krishnakumar.html) опубликована в апрельском номере журнала.
Следующее, что необходимо разобрать после изучения процесса создания загрузочного сектора и до того, как мы перейдём к описанию приёмов переключения в защищённый режим, это использование прерываний BIOS[1]. Прерывания BIOS представляют собой подпрограммы низкого уровня, облегчающие работу создателю операционной системы. В этой части статьи мы будем иметь дело с ними.
BIOS копирует загрузочный сектор в оперативную память и передаёт на него управление. Кроме этого, существует ещё несколько функций, которые могут выполняться BIOS'ом. В момент загрузки операционная система ещё не располагает драйверами для поддержки оборудования. Размещение такого драйвера в загрузочном секторе -- задача практически невыполнимая. Должен существовать другой способ решить эту проблему и в этом нам поможет BIOS. Он содержит множество подпрограмм, служащих для различных целей и которыми мы можем воспользоваться. Например, существуют подпрограммы проверки установленного оборудования, подпрограммы управления выводом на печать, определения размера оперативной памяти и пр. Эти подпрограммы называются подпрограммами или обработчиками прерываний BIOS.
В "обычных" языках программирования вызов подпрограммы реализуется через
обращение к её имени. К примеру, если в программе на C у нас есть подпрограмма
display
, получающая в качестве параметров атрибуты
(attr
) и количество отображаемых символов на экране
(noofchar
), то обратиться к ней мы можем просто указав её имя и
необходимые параметры. Однако, здесь мы будем использовать прерывания, вызов
которых осуществляется посредством инструкции ассемблера int
.
Например, для вывода символов на экран в C мы используем функцию похожую на:
display(noofchar, attr);
Эквивалентно этому, оперируя средствами ассемблера и BIOS, мы напишем:
int 0x10
Перед тем как вызвать прерывание BIOS, нам нужно загрузить данные в заранее определённом формате в регистры процессора. Предположим, мы используем прерывание 0x13, предназначенное для чтения/записи с дискеты. Прежде чем вызвать его, мы должны определить адрес в оперативной памяти, куда будут загружены данные. Также мы должны передать информацию о номере устройства (fd0 - 0x00, fd1 - 0x01, hda - 0x80, hdb - 0x81 и т.д.), цилиндре, секторе и количестве копируемых секторов. Эти данные должны быть загружены в определённые регистры. Всё это вам станет понятно после того, как вы прочтёте описание работы кода загрузочного сектора, который мы разработаем чуть позже.
Есть одна очень важная деталь, о которой вы должны знать -- одно и тоже
прерывание может использоваться для различных целей. Всё это зависит от номера
функции, который указывается в регистре ah
(иногда
ax
). К примеру, прерывание 0x10
может быть
использовано как для вывода на экран строки, так и для получения координат
курсора. Если мы запишем в регистр ah
значение 0x03
,
то тем самым при вызове прерывания 0x10 мы выберем функцию, используемую для
получения координат курсора. Для вывода строки на экран мы записываем в регистр
ah
значение 0x13
, которое является номером функции
вывода строки на экран.
На этот раз наш исходный код состоит из двух программ на ассемблере и одной
на C. Первый файл (на ассемблере) -- это код загрузочного сектора. В нём
хранятся инструкции, копирующие второй сектор флоппи-диска в сегмент памяти
0x500
(абсолютный адрес 0x5000)[2]. Операция выполняется при помощи прерывания BIOS
0x13. После этого код загрузчика передаёт управление по адресу
0x500:0
(сегмент -- 0x500, смещение -- 0). Код второго файла на
ассемблере выводит на экран сообщение, используя прерывание BIOS
0x10
. Программа на C предназначена для записи исполняемого кода
программ на ассемблере в 1-й и во 2-й сектора дискеты.
Используя прерывание 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
содержит значение
смещения в сегменте, по которому будут загружены данные.
Далее мы записываем...
dl
(принцип кодирования описан в
пункте 1.3)
dh
ch
cl
al
Итак, мы загружаем второй сектор нулевой дорожки устройства номер 0 (что
соответствует приводу флоппи-дисков на 1.44Мб) по адресу
0x500:0
.
Значение 2 в регистре ah
указывает на номер функции прерывания.
Мы выбираем функцию номер 2, которая используется для чтения данных с диска
(жёсткого или гибкого).
Теперь мы вызываем прерывание 0x13 и последней командой передаём управление
коду, загруженному по адресу 0x500:0
.
Второй сектор будет содержать следующий код :
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
(номер строки и колонки соответственно).
Переходим ко второй части программы. В регистр...
cx
записываем количество символов в строке, выводимой на
экран[4]
bx
заносим значение номера видео страницы и код атрибута
выводимых на экран символов (0x00 и 0x07). Мы планируем использовать белый
цвет символов (0x7) на чёрном фоне (0x0).
bp
пишем адрес строки [5]
ax
записываем номер функции для вывода на экран строки и код
подфункции, определяющий, что атрибут показываемой строки будет взят из
регистра bl
Начало сообщения содержит два байта со значениями 13 и 10, что соответствует
нажатию клавиши enter
. Эти два кода идут вместе, 13 -- код возврата
каретки (Carriage Return, CR), 10 -- код перевода строки (Line Feed, LF). Сама
строка содержит 24 символа. Символы CR и LF трактуются функцией 0x13 прерывания
0x10 как управляющие и поэтому не высвечиваются. Теперь вызываем прерывание. И
последней инструкцией "вешаем" компьютер.
Код программы на 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]
Вы можете взять исходники примеров здесь
Удалите у файлов расширение txt
и введите
make
в ответ на приглашение оболочки или вы можете откомпилировать файлы самостоятельно. В этом случаем введите
as86 bsect.s -o bsect.o ld86 -d bsect.o -o bsect
и повторите тоже самое для sect2.s
. Сборку write.c
выполните командой :
cc write.c -o write
и вставив дискету в дисковод выполните программу write
.
После загрузки с дискеты вы можете любоваться строкой на экране. И всё это благодаря прерываниям BIOS. В следующей статье из этой серии я надеюсь написать о том как переключать процессор в защищённый режим. А до тех пор, пока!
Кришнакумар -- студент последнего курса 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.