Урок 2. Приступаем к работе.

UPD. В данный урок были внесены изменения связанные с созданием образа диска для ВМ с ядром и загрузчиком. Теперь содержание статьи соответствует действительности.

2.1. Код для загрузки

Итак, настало время писать код. Хотя основной язык разработки ядра - С, есть некоторые модули, которые просто необходимо написать на языке ассемблера. Один из таких модулей - код инициализации и загрузки ядра.



;
; boot.s -- Kernel start location. Also defines multiboot header
; Based on Bran's kernel development tutorial file start.asm
;

MBOOT_PAGE_ALIGN    equ 1<<0    ; Загрузить ядро и модули по границе страницы
MBOOT_MEM_INFO        equ 1<<1    ; Запросить от загрузчика информацию о памяти
MBOOT_HEADER_MAGIC    equ 0x1BADB002    ; Специальный флаг для загрузчика
; NOTE: мы не используем MBOOT_AOUT_KLUDGE. Это означает что GRUB  не сообщит 
; адрес таблицы символов
MBOOT_HEADER_FLAGS    equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO
MBOOT_CHECKSUM        equ -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)

[BITS 32]        ; загрузчик сам переводит процессор в защищенный режим

[GLOBAL mboot]    ; чтобы 'mboot' был доступен из кода на C
[EXTERN code]    ; начало секции .text
[EXTERN bss]    ; начало секции .bss
[EXTERN end]    ; конец последней загружаемой секции

mboot:
    dd    MBOOT_HEADER_MAGIC    ; GRUB будет искать это значение на каждой
                            ; 4-кБ границе в файле ядра
    dd    MBOOT_HEADER_FLAGS    ; Сообщает загрузчику опции загрузки ядра
    dd    MBOOT_CHECKSUM        ; Контрольная сумма первых двух полей

    dd    mboot                ; адрес текущего дескриптора
    dd    code                ; адрес начала секции .text
    dd    bss                    ; адрес конца секции .data
    dd    end                    ; адрес конца всех секций
    dd    start                ; адрес точки входа

[GLOBAL start]                ; объявляем метку точки вход глобальной
[EXTERN kmain]                ; адрес функции main

start:
    push    ebx                ; загрузить в стек адрес структуры, полученной от загрузчика
    
    ; запускаем ядро
    cli                        ; запрещаем прерывания
    call    kmain            ; вызываем функцию kmain
    jmp        $                ; Бесконечный цикл, чтобы процессор не начал выполнять 
                            ; код (мусор), находящийся после кода ядра.

 

2.2. Разбираемся в коде.

В приведенном выше коде только четыре строки являются, собственно, исполняемым кодом.
    push ebx
    cli
    call kmain
    jmp $
Все остальное - это описание заголовка для загрузчика.

2.2.1. Загрузчик

Загрузчик GRUB соответствует стандарту, описываемому в Multiboot Specification. Наше ядро тоже должно соответствовать этому стандарту, чтобы мы могли переложить часть работы по настройке окружения на загрузчик.
Например, если ядро требует загрузки в режиме VESA (что, кстати, является плохим признаком), Вы можете проинформировать об этом загрузчик и он позаботиться о настройке соответствующего окружения.
Чтобы сделать ядро совместимым со стандартом, Вам необходимо добавить куда-нибудь в ядро специальный заголовок  (вообще-то он должен располагаться в первых 4 кБ файла ядра).
    dd    MBOOT_HEADER_MAGIC    
    dd    MBOOT_HEADER_FLAGS    
    dd    MBOOT_CHECKSUM    
    dd    mboot
    dd    code
    dd    bss
    dd    end
    dd    start
Используемые константы определены выше.

MBOOT_HEADER_MAGIC — Специальный идентификатор. Показывает, что ядро соответствует требованиям Multiboot Specification.

MBOOT_HEADER_FLAGS — Мы просим GRUB, чтобы он выровнял по границе страницы все секции ядра (MBOOT_PAGE_ALIGN), а также сообщил нам некоторую информацию о памяти (MBOOT_MEM_INFO). В некоторых случаях также использую константу MBOOT_AOUT_KLUDGE, но т.к. мы используем для нашего ядра формат файла ELF, а не a.out, эту опцию указывать не следует.

MBOOT_CHECKSUM — Поле добавлено для контроля ошибок.

mboot — адрес заголовка

code,bss,end,start — все эти символы определены линковщиком. Мы используем их, чтобы сообщить загрузчику куда могут быть размещены различные секции нашего ядра.

При загрузке GRUB помещает указатель на структуру с необходимой нам информацией в регистр EBX. Он может быть использован для доступа к окружению, которое для нас подготовил GRUB.

2.2.2. Вернемся к коду

Итак, сразу после загрузки, наш код помещает указатель на структуру, подготовленную загрузчиком, в стек, запрещает прерывания, вызывает функцию 'kmain' (которую мы еще не определили) и уходит в бесконечный цикл.
Все хорошо, но код до сих пор не линкуется.

2.3. Добавляем немного кода на C.

Чтобы легко перейти от кода на языке ассемблера к коду на С необходимо знать некоторые правила.
  1. Аргументы в функцию передаются через стек.
  2. Параметры проталкиваются в стек справа-налево.
  3. Значение, возвращаемое функцией, помещается в регистр EAX.
И этого достаточно, чтобы легко вызывать функции, написанные на ассемблере из кода на С, и наоборот.

К примеру вызов функции:
d = func(a,b,c);
на языке ассемблера будет выглядеть так:
push [c]
push [b]
push [a]
call func
mov [d],eax

Все просто.
Как Вы можете убедиться, строка
push ebx
в коде выше есть ни что иное, как передача значения из EBX как параметра функции kmain().

2.3.1. Код на С.

// main.c -- Defines the C-code kernel entry point, calls initialisation routines.

int kmain(struct multiboot *mboot_ptr)
{
    // All our initialisation calls will go in here
    return 0xDEADBABA;
}

Это наш первый вариант функции kmain(). Как Вы можете видеть она принимает один аргумент - указатель на структуру, подготовленную для нас загрузчиком GRUB. Мы определим ее позже.

Все что делает сейчас наша функция kmain() - это возвращает константу 0xDEADBABA.

2.4. Компиляция и сборка проекта.

UPD. В данный параграф были внесены изменения связанные с созданием образа диска для ВМ с ядром и загрузчиком.

Так как мы добавили новый файл в наш проект, мы должны внести изменения в наш Makefile. Отредактируйте следующие строки:
SOURCES = boot.o main.o
CFLAGS = -nostdlib -nostdinc -fno-builtin -fno-stack-protector
Эти строки нужны, чтобы GCC не пытался собрать наше ядро вместе со стандартной библиотекой C из Вашей *nix-системы.

Теперь Вы можете собрать наше ядро и попробовать запустить его. В папке src/ выполним команду:
$ make
После этого у вас в этой папке должен появиться бинарный файл kernel. Этот файл необходимо записать на образ, созданный нами в предыдущем уроке. Способ описан здесь и здесь.
Если мы загрузимся с этого образа (например в VirtualBox), то увидим стандартное меню загрузчика GRUB.
.
Меню загрузчика GRUB
Отсутствие этого меню означает только одно: где-то была допущена ошибка.
При выборе пункта меню или по истечении времени таймера GRUB загрузит наше ядро в память и передаст ему управление.

Код уже на github.

В следующий раз мы выведем на экран текст лицензии GPL.

Комментариев нет:

Отправить комментарий