среда, 10 июля 2013 г.

PhantomEx: Спецификация Multiboot - делаем заготовку ядра

Итак, настал момент написать что-то похожее на ядро операционной системы. Как я уже говорил, наше ядро будет запускаться загрузчиком GRUB2, а для этого ему необходимо удовлетворять некоторым требованиям.

1. Спецификация мультизагрузки.

 

В настоящее время GRUB2 поддерживает две версии спецификации мультизагрузки: Multiboot и Multiboot2. Полностью с ними можно ознакомится по приведенным ссылкам, нам это не требуется пока, и чтобы не загромождать заметку, приведу лишь те первоначальные сведения, которые позволят нам стартовать.


Для наших целей воспользуемся спецификацией Multiboot. Для того чтобы GRUB2 понял, что наше ядро поддерживает мультизагрузку, оно должно содержать заголовок следующего формата

Таблица 1. Заголовок Multiboot

Смещение Поле Значение поля
0x00MBOOT_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
//-----------------------------------------------*/
.set MBOOT_HEADER_MAGIC, 0x1BADB002
.set MBOOT_HEADER_FLAGS, 0x00000001
.set MBOOT_CHECKSUM, -(MBOOT_HEADER_MAGIC+MBOOT_HEADER_FLAGS)

/* Указываем что функция main - внешняя и расположена в другом
 объектном модуле */
.extern main

/* Секция - заголовок мультизагрузки */
.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

Это и есть код нашей заготовки. Всё что мы сделали: поместили в нужное место заголовок мультизагрузки, отключили прерывания и вызвали точку входа в код ядра - функцию 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


Теперь остается только загрузить его и проверить как оно работает. Но об этом в следующей статье, а то я устал немного... :)