Пишем свою операционную систему. Драйвер текстового экрана
До этого момента мы выводили текст на экран лишь с помощью прямого копирования байт в видео-память. Было бы не плохо реализовать функции вроде printf для более удобного вывода на экран.
Вообще-то я хочу получить в итоге микроядро, и драйверу консоли там не место, однако поскольку эта рассылка имеет цель не только задокументировать ход работы, но и наглядно показать процесс разработки, я пока немного нарушу последовательность и напишу драйвер для ядра. Ничто не мешает в будущем удалить лишние модули
из ядра.
Консоль в некоторых её проявлениях называется телетайпом, я буду придерживаться этой же терминологии. Код драйвера будет располагаться в tty.c. Для начала опишем заголовочный файл tty.h, который определяет доступные другим модулям функции:
Имена функций говорят сам за себя: init_tty - инициализация драйвера, необходимо вызвать до обращения к любым другим функциям. out_char - вывод одиночного символа, поддерживается символ переноса строки '\n' out_string - вывод целой строки символов clear_screen - очистка экрана и перевод курсора в левый верхний угол set_text_attr - смена текущего цвета и фона символов, описание формата цвета будет чуть ниже. move_cursor - перемещение курсора на заданную позицию.
Видео-память
в текстовом режиме представляет собой массив из 2 тысяч слов (для разрешения экрана 80x25 символов). Первый байт слова содержит сам код символа, а второй - его атрибуты (цвет фона и текста). Младшие 4 бита атрибутов символа содержат код цвета текста. Таким образом доступно 16 цветов. Следующие 3 бита содержат код цвета фона (значит существует 8 цветов фона для текста), а самый старший бит включает мерцание символа.
Вот начало файла tty.c:
#include "stdlib.h"
#include "tty.h"
typedef struct {
uint8 chr;
uint8 attr;
} TtyChar;
unsigned int tty_width;
unsigned int tty_height;
unsigned int cursor;
uint8 text_attr;
TtyChar *tty_buffer;
uint16 tty_io_port;
Мы подключаем стандартную библиотеку и описываем несколько внутренних переменных драйвера, а также структуру одиночного символа. Про назначение tty_io_port я расскажу чуть позже. В первую очередь необходимо инициализировать телетайп. Было бы не плохо подхватить старую позицию курсора от BIOS. Также, для перемещения аппаратного курсора (мерцающая горизонтальная черта) нам необходимо узнать tty_io_port. Все эти данные можно извлечь из области данных BIOS, которая расположена по адресу 0x400 - 0x600. Поскольку
1-ый мегабайт примонтирован, мы можем использовать обычное чтение.
Нас интересуют следующие данные из области данных BIOS:
Адрес
Комментарий
0x44A
2 байта. Количество столбцов на экране. Если количество строк постоянно - 25, то количество столбцов теоретически может быть не только 80,
но и 40. Будет красиво прочитать это отсюда.
0x463
2 байта. Базовый порт ввода-вывода для управления контроллером дисплея.
0x450
1 байт. Координата курсора Y
0x451
1 байт. Координата курсора X
Задачей init_tty будет как раз прочитать эти параметры и
сохранить их в нужные переменные:
Вывод символа с обработкой переноса строки. Для сдвига курсора используется move_cursor, чтобы и обновить положение аппаратного курсора, и прокрутить экран в случае, если кончилось место.
Смена текущего цвета текста приводит лишь к обновлению переменной text_attr. По-настоящему цвет будет использоваться уже при выводе символов или очистке экрана:
Эта функция обновляет значение переменной cursor, а также следит за тем, чтобы его позиция не вышла за границу экрана. Если такое случается курсор перемещается на начало последней строки, всё содержимое экрана сдвигается на 1 строку вверх, а последняя строка очищается.
Такой драйвер уже можно использовать для вывода текста на экран, но не хватает одной мелочи - аппаратный курсор остаётся на том же месте, где его оставил BIOS. Надо бы доработать нашу функцию move_cursor.
Для начала
расскажу про порты ввода-вывода. В архитектуре x86 общение с устройствами может осуществляться двумя способами - через память и через порты ввода-вывода. Обращение через память обозначает, что какой-то блок физических адресов не является на самом деле ОЗУ, а все запросы чтения-записи уходят к устройству, которое реагирует на них согласно своим функциям (например, что-то вроде "при записи единицы по такому-то физическому адресу такое-то устройство перейдёт в активный режим"). Помимо этого способа программисту
доступно 65536 портов ввода-вывода. Из каждого из них можно прочитать или записать 1 байт, однако можно объединять их в группы - например, если записать слово в порт 0x100, то его младшая половина уйдёт в 0x100, а старшая в 0x101. Для работы с портами существует две ассемблерных команды - in и out. Они существуют в единственной форме - in al/ax/eax, dx и out dx, al/ax/eax. Аргументы указывать обязательно, но в итоге номер порта хранится непременно в DX, а значение для ввода или вывода в AL/AX/EAX (в зависимости
от того сколько данных мы хотим записать или прочитать).
Контроллер дисплея имеет два порта - tty_io_port и tty_io_port + 1. Первый задаёт номер его внутреннего регистра. После записи туда значения, второй порт содержит значение этого регистра. Если произвести запись во второй порт, значение соответствующего регистра изменится.
Нас интересует два внутренних регистра контроллера - 0x0E и 0x0F. Первый из них хранит старший байт позиции курсора, второй младший байт.
Оказывается ld для win32 не умеет собирать исполняемые ELF файлы, поэтому для успешной компиляции придётся изменить строку LDFLAGS = -melf_i386 на LDFLAGS = -mi386pe.
Один из подписчиков моей рассылки - DragoN - предложил специальный bat-файл для упрощения сборки ОС под Windows:
Вот его комментарии касательно особенностей сборки:
Сборка ОС MyOS под Win32.
Инструменты:
1. FASM - чтобы собрать загрузчик
2. MinGW - из него GCC чтобы собрать ядро
3. MSYS - понадобятся утилиты sh, cp, rm, dd
3. DD - утилита понадобится отдельно, так как не идћт с MinGW, MSYS
4. Bochs
Процесс:
1. Устанавливаем FASM, MinGW, MSYS, DD (я взял отсюда http://www.chrysocome.net/dd), Bochs
2. Настраиваем пути к FASM, MinGW, MSYS, DD, Bochs (переменная окружения PATH)
3. Перезагружаем шелл или тотал командер, чтобы новые пути вступили в силу
4. Запускаем в папке MyOS команду makew, получим:
MyOS\src\boot\boot.bios.bin
MyOS\src\kernel\main.o
MyOS\src\kernel\startup.o
MyOS\src\kernel\stdlib.o
MyOS\src\kernel\kernel.bin
MyOS\src\make_listfs\make_listfs.exe
MyOS\bin\boot.bios.bin
MyOS\bin\kernel.bin
MyOS\bin\make_listfs.exe
MyOS\bin\boot_sector.bin
MyOS\disk\kernel.bin
MyOS\disk\boot.bin
MyOS\disk.img
5. Запускаем bochsrc.bxrc (бочс при установке создаст ассоциацию .bxrc с собой)
Если видим жћлтый Hello, World!, значит ОС запустилась успешно.
Если что то пошло не так, сначала проверяем список файлов по пункту 4, скорее всего чего то не хватает,
и дальше действуем по обстоятельствам.
От себя добавлю, что надо обязательно убрать строку OUTPUT_FORMAT из script.ld.
Заключение
Итак, сегодня мы разработали драйвер текстового экрана. Теперь можно выводить различные информационные сообщения на экран.
В следующем выпуске мы разберём обработку прерываний и ввод с клавиатуры. На этом я планирую закончить эту демонстрацию возможностей и, если ничего не изменится, уже через выпуск мы начнём разрабатывать менеджер памяти.