Итак, настал момент написать что-то похожее на ядро операционной системы. Как я уже говорил, наше ядро будет запускаться загрузчиком GRUB2, а для этого ему необходимо удовлетворять некоторым требованиям.
1. Спецификация мультизагрузки.
В настоящее время GRUB2 поддерживает две версии спецификации мультизагрузки: Multiboot и Multiboot2. Полностью с ними можно ознакомится по приведенным ссылкам, нам это не требуется пока, и чтобы не загромождать заметку, приведу лишь те первоначальные сведения, которые позволят нам стартовать.
Для наших целей воспользуемся спецификацией Multiboot. Для того чтобы GRUB2 понял, что наше ядро поддерживает мультизагрузку, оно должно содержать заголовок следующего формата
Таблица 1. Заголовок Multiboot
Таблица 1. Заголовок Multiboot
Смещение | Поле | Значение поля |
0x00 | MBOOT_HEADER_MAGIC | 0x1BADB002 |
0x04
| MBOOT_HEADER_FLAGS | Флаги заголовка |
0x08 | MBOOT_CHECKSUM | Контрольная сумма заголовка |
Рассмотрим эти поля подробнее
- MBOOT_HEADER_MAGIC - так называемое "магическое число". Найдя эту сигнатуру в файле вашего ядра, загрузчик поймет что он имеет дело с ядром, поддерживающим мультизагрузку. Приведенное значение одинаково для всех ядер, использующих спецификацию Multiboot.
- MBOOT_HEADER_FLAGS - флаги, указывающие загрузчику специфические требования и настройки, которые необходимо выполнить при загрузке ядра. Биты 0 - 15 указывают требования к порядку загрузки. Если загрузчик их не поймет, он не будет ничего загружать и выдаст сообщение об ошибке. Биты 16 - 31 указывают дополнительные опции загрузки, и если они не распознаются загрузчиком, то он их просто игнорирует и продолжает работу.
Таблица 2. Флаги заголовков Multiboot
Бит | Назначение |
0 | все загрузочные модули загружаются вместе с операционной системой и должны быть выровнены в памяти по границе страниц размером 4 Кб. |
1
| в ядро, через поля соответствующей структуры, передается информация о доступной памяти |
2 | ядру передается информация о таблице видеорежимов |
Информация относительно остальных флагов в спецификации GNU почему-то опущена (надо сказать эта спецификация и не блещет подробностью изложения).
Итак, нам пока требуется только флаг бита 0, то есть укажем в заголовке значение 0х00000001.
- MBOOT_CHECKSUM - контрольная сумма заголовка. Эту контрольную сумму мы должны указать такой, что сумма всех трех полей заголовка должна давать ноль. Это дает следующую формулу для расчета контрольной суммы
контрольная сумма = -(магическое число + флаги)
Мы выполним автоматический расчет этой контрольной суммы прямо в коде ядра.
Кроме наличия вышеуказанного заголовка есть ещё одно условие - код ядра должен быть выровнен в памяти по границе страниц размером 4 Кб. Загрузчик будет искать "магическое" число именно на границах страниц, при отсутствии такового выравнивания в коде он просто ничего не найдет и выдаст сообщение об ошибке.
Где должен располагаться заголовок Multiboot? Спецификация говорит, что данный заголовок необходимо разместить как можно ближе к началу исполняемого файла ядра, то есть сразу за автоматически добавляемым компилятором заголовком выбраного формата исполняемого файла.
2. Код заготовки ядра
Писать наше "игрушечное" ядро мы будем в основном на языке C, кое-где, по необходимости, используя ассемблер, для выполнения самых низкоуровневых операций, таких как работа с портами ввода/вывода и управляющими регистрами CPU.
Что касается ассемблера, то использовать мы будем GNU Assembler (GAS). Возможно у меня появятся противники в этом вопросе, но GAS наиболее полно интегрирован в общую систему разработки ПО в *nix - подобных ОС и вызывается автоматически компилятором gcc, когда он натыкается на ассемблерный код.
Итак, открываем ваш любимый текстовый редактор (я использую kate, да простят меня аппологеты vim и emacs) и пишем там следующий код
Листинг 1. Код примитивного ядра (файл main.c)
/*-------------------------------------------------------------------
// Код примитивного ядра
//-----------------------------------------------------------------*/
int main(void)
{
/* Тут будет расположен код ядра */
return 0;
}
да-да, не смотрите на меня круглыми глазами, на данном этапе нам этого хватит с головой. Эта функция будет вызвана сразу после загрузки ядра. Тут будет реализован весь функционал нашей "игрушки".
Теперь этот код необходимо "обернуть" в небольшую ассемблерную оболочку
Листинг 2. Ассемблерная "обертка" для мультизагрузки (файл init.s)
/*-------------------------------------------------
//
//
// Код инициализации и мультизагрузки
//
//-----------------------------------------------*/
.code32
/*-------------------------------------------------
// Константы для заголовка Multiboot
//-----------------------------------------------*/
//-----------------------------------------------*/
.code32
/*-------------------------------------------------
// Константы для заголовка Multiboot
//-----------------------------------------------*/
.set MBOOT_HEADER_MAGIC, 0x1BADB002
.set MBOOT_HEADER_FLAGS, 0x00000001
.set MBOOT_CHECKSUM, -(MBOOT_HEADER_MAGIC+MBOOT_HEADER_FLAGS)
.set MBOOT_HEADER_FLAGS, 0x00000001
.set MBOOT_CHECKSUM, -(MBOOT_HEADER_MAGIC+MBOOT_HEADER_FLAGS)
/* Указываем что функция main - внешняя и расположена в другом
объектном модуле */
.extern main
.extern main
/* Секция - заголовок мультизагрузки */
.section .mboot
.int MBOOT_HEADER_MAGIC
.int MBOOT_HEADER_FLAGS
.int MBOOT_CHECKSUM
.section .mboot
.int MBOOT_HEADER_MAGIC
.int MBOOT_HEADER_FLAGS
.int MBOOT_CHECKSUM
/* Секция кода */
.section .text
/* Делаем точку входа глобальной, доступной для компоновщика */
.global init
init:
cli /* Выключаем ВСЕ прерывания */
push %eax /* Заталкиваем в стек */
push %ebx /* регистры общего назначения */
call main /* вызываем main */
hlt /* Останавливаем процессор */
loop: /* Переходим в бесконечный цикл */
jmp loop
.global init
init:
cli /* Выключаем ВСЕ прерывания */
push %eax /* Заталкиваем в стек */
push %ebx /* регистры общего назначения */
call main /* вызываем main */
hlt /* Останавливаем процессор */
loop: /* Переходим в бесконечный цикл */
jmp loop
Это и есть код нашей заготовки. Всё что мы сделали: поместили в нужное место заголовок мультизагрузки, отключили прерывания и вызвали точку входа в код ядра - функцию main().
Следует обратить внимание на синтаксис ассемблера - он носит наименование синтаксис AT&T, и разработан создателями операционной системы UNIX. При желании использовать более привычный синтаксис Intel в начале ассемблерного кода можно поставить директиву .intel_syntax noprefix, однако особой необходимости я в этом не вижу, тем более что при написании макросов для GAS с которыми мы ещё сталкнемся, может возникнуть досадная путаница. Синтаксис AT&T не так сложен в освоении, как может показаться с непривычки, за справочной информацией отсылаю к книге "Assembler для DOS, Windows и UNIX" автора Зубкова С. В.
Кроме того следует обратить внимание на стиль комментариев - это стиль языка C, тот формат сборки который мы будем использовать, не поддерживает однострочных комментариев в стиле C++, о чём компилятор обязательно вам сообщит.
3. Компиляция и компоновка
Теперь нам необходимо собрать наше ядро. Просто выполнить компиляцию - так не пойдет. Помните о том, что нам необходимо обеспечить генерацию необходимого формата исполняемого файла с выравниванием кода по границам 4 Кб страниц.
Это можно выполнить задав правила компоновки для линковщика ld.
Листинг 3. Скрипт управления компоновкой ядра (файл link.ld)
/* Точка входа в код ядра */
ENTRY (init)
SECTIONS
{
/* Адрес, с которого будет начинаться код ядра в памяти */
. = 0x00100000;
/* Секция кода */
.text ALIGN (0x1000) :
{
*(.mboot)
*(.text)
}
/* Секция инициализированных данных */
.data ALIGN (0x1000) :
{
*(.data)
}
/* Секция неинициализированных данных */
.bss :
{
*(.bss)
}
/* Место в памяти, где уже нет кода ядра */
end = .; _end = .; __end = .;
}
Так вот для начала мы должны указать компоновщику где находится точка входа в код ядра - она задана у нас в коде меткой init. Директива ENTRY (init) предназначена именно для этого.
Далее описываем расположение секций в исполняемом файле, и для начала указываем адрес, начиная с которого будет загружаться код ядра - 0x100000, то есть код располагается за границей первого мегабайта ОЗУ, традиционно доступного процессору из так называемого реального режима. Директиdа . - точка имеет очевидный смысл, кратко формулируемый как "это самое место". Так вот "это самое место", то есть начала кода будет расположено в памяти по адресу 0x100000.
Далее у нас секция кода, обозначенная директивой .text, и первое что мы располагаем в этой секции - заголовок Multiboot, обозначенный меткой .mboot (см. Листинг 2). Потом уже пойдет сам код ядра (метка .text после метки .mboot)
Особое внимание обратим на выравнивание, задаваемое директивой ALIGN (0x1000). Указывая её мы сообщаем компоновщику, что код (или данные) необходимо выровнять по границе блоков размером 0х1000 = 4096 байт, то есть как раз запрашиваемые GRUB2 4 Кб.
Следующей указывается секция инициализированных данных - это используемые нашим ядром константы: директива .data, опять же с выравниванием 0x1000. После нее директивой .bss размещается секция неинициализированных данных (переменные), выравнивать которую уже не требуется.
И наконец последней задаем метку end, которая указывает на место в памяти, расположенное сразу за кодом и данными ядра. Её нужно обязательно указать, она пригодится нам, когда мы будем организовывать страничную адресацию.
Очень важно с самого начала понять, как будет организован код ядра и где в памяти будут расположены его секции. Разбору результатов сборки этого кода планируется посвятить целую статью, а пока же продолжим.
Теперь озаботимся сборкой ядра, для этого используем утилиту make, автоматизирующую сборку ПО из исходных кодов. Для утилиты make необходимо написать специальный сценарий - Makefile
Листинг 4. Makefile для сборки ядра (файл Makefile)
#------------------------------------------------------
#
# Правила сборки кода ядра
#
#------------------------------------------------------
# Исходные объектные модули
SOURCES=init.o main.o
# Флаги компилятора языка C
CFLAGS=-nostdlib -nostdinc -fno-builtin -fno-stack-protector -m32 -g
# Флаги компоновщика
LDFLAGS=-T link.ld -m elf_i386
# Флаги ассемблера
ASFLAGS=--32
# Правило сборки
all: $(SOURCES) link
# Правило очистки
clean:
-rm *.o kernel
# Правило компоновки
link:
ld $(LDFLAGS) -o kernel $(SOURCES)
Первым делом указываем какие объектные модули должны быть скомпилированы: переменная SOURCES и файлы init.o и main.o. Эти модули компилятор сформирует на основе имеющихся у нас исходных текстов init.s и make.c, вызвав при этом либо компилятор C, либо ассемблер, в зависимости от расширения исходного файла (*.s - для ассемблера, *.c - для кода на C).
Далее указываются флаги, с которыми будет произведена компиляция С-кода (CFLAGS).
-nostdlib - не используем стандартных библиотек (о Боже!)
-nostdinc - не используем стандартных заголовков (ещё лучше!!!)
-fno-builtin - не использовать встроенных функций
-fno-stack-protector - черт его знает что, но так было указано в источнике, что-то об отсутствии защиты стека.
Перечисленные флаги выключают все добавки, которые компилятор может попытаться запихнуть в ядро - у нас голое железо без операционной системы, так что придется обойтись безо всяких стандартных библиотек и встроенных функций. Далее
-m32 - говорим компилятору, чтобы строил 32-разрядный код. Особенно актуально для разработки в среде 64-битной системы, как, допустим, у меня.
-g - добавляем отладочную информацию в код. Пригодится чуть позже, собственно при отладке.
В переменной LDFLAGS перечисляются флаги компоновщика
-T link.ld - указываем использовать сценарий компоновки из файла link.ld
-m elf-i386 - указываем что необходимо сформировать 32-разрядный исполняемый файл для процессора Intel 80386.
Переменная ASFLAGS содержит флаги ассемблера:
--32 - указывает ассемблеру создать 32-разрядный объектный модуль.
Далее идет основное правило сборки
all: $(SOURCES) link
что означает "компилировать исходные файлы в объектные модули и передать для сборки компоновщику". Правило link имеет следующий вид
link:
ld $(LDFLAGS) -o kernel $(SOURCES)
и это значит что из объектных файлов будет сформирован исполняемый файл kernel в соответствии с флагами линковки.
Правило очистки выполняется при вводе в терминал команды make clean, и просто удаляет результаты сборки с диска. Периодически это требуется, для полного перестроения кода, так как gcc не собирает заново имеющиеся объектные модули.
Всё! Мы готовы к сборке. Все указанные фалы должны лежать в одном каталоге, например с именем ~/myOSkernel/src. Выполняем в терминале
$ cd ~/myOSkernel/src
$ make
ииии....
as --32 -o init.o init.s
init.s: Assembler messages:
init.s: Warning: конец файла не в конце строки; вставлен символ новой строки
cc -nostdlib -nostdinc -fno-builtin -fno-stack-protector -m32 -g -c -o main.o main.c
ld -T link.ld -m elf_i386 -o kernel init.o main.o
Да! Наше ядро таки успешно собрано! Убедимся в наличии файла kernel
$ ls -l
итого 32
-rw-r--r-- 1 maisvendoo users 760 июл 10 17:25 init.o
-rw-r--r-- 1 maisvendoo users 920 июл 10 15:31 init.s
-rwxr-xr-x 1 maisvendoo users 5593 июл 10 17:25 kernel
-rw-r--r-- 1 maisvendoo users 267 июл 10 16:02 link.ld
-rw-r--r-- 1 maisvendoo users 449 июл 10 17:24 main.c
-rw-r--r-- 1 maisvendoo users 1740 июл 10 17:25 main.o
-rw-r--r-- 1 maisvendoo users 454 июл 10 17:24 Makefile
Теперь остается только загрузить его и проверить как оно работает. Но об этом в следующей статье, а то я устал немного... :)
Комментариев нет:
Отправить комментарий