Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Руководство по созданию простой UNIX-подобной ОС

2. Генезис

Оригинал: "2. Genesis"
Автор: James Molloy
Дата публикации: 2008
Перевод: Н.Ромоданов
Дата перевода: январь 2012 г.

2.1. Код первоначальной загрузки boot

Ладно, пора что-нибудь закодировать! Хотя основная часть нашего ядра написана на языке C, есть ряд вещей, для которых мы просто обязаны использовать ассемблер. Одной из таких является код первоначальной загрузки.

У нас есть следующий код:

;
; boot.s -- Место, откуда начинается ядро. Также определяет заголовок мультизагрузки multiboot.
; За основу взят файл start.asm из руководства по разработке ядра Bran's kernel development tutorial
;

MBOOT_PAGE_ALIGN    equ 1<<0    ; Загружает ядро и модули на границу страницы
MBOOT_MEM_INFO      equ 1<<1    ; Предоставляет вашему ядру информацию о памяти
MBOOT_HEADER_MAGIC  equ 0x1BADB002 ; Магическое значение Multiboot Magic
; ЗАМЕЧАНИЕ: Мы не используем значение 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]                       ; Все инструкции должны быть 32-битовыми.

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

mboot:
  dd  MBOOT_HEADER_MAGIC        ; GRUB будет искать это значение для каждой 
                                ; 4-байтной границы в файле вашего ядра
  dd  MBOOT_HEADER_FLAGS        ; Указывает, как GRUB должен загружать   ваш файл / настройки
  dd  MBOOT_CHECKSUM            ; Чтобы обеспечить, чтобы приведенные выше значения были корректными
   
  dd  mboot                     ; Место размещения дескриптора
  dd  code                      ; Начало секции '.text' (код) ядра.
  dd  bss                       ; Начало секции '.data' ядра.
  dd  end                       ; Конец ядра.
  dd  start                     ; Точка входа в ядро (первоначальный EIP).

[GLOBAL start]                  ; Точка входа в ядро.
[EXTERN main]                   ; Это точка входа в ваш код на C

start:
  push    ebx                   ; Загрузка указателя на место размещения заголовка multiboot

  ; Выполнение ядра:
  cli                         ; Отключаем прерывания.
  call main                   ; вызываем нашу функцию main().
  jmp $                       ; Входом в бесконечный цикл, который останавливает работу
                              ; процессора всякий раз, в память обнаруживается мусор 
                              ; после работы нашего ядра! 

2.2. Разбираемся с кодом первоначальной загрузки boot

В этом модуле, на самом деле, только несколько строк кода:

push ebx
cli
call main
jmp $ 

Все остальное относится к заголовку мультзагрузки multiboot.

2.2.1. Мультизагрузка

Мультизагрузка multiboot является стандартом, которому, как предполагается в GRUB, должно соответствовать ядро. Благодаря этому загрузчик может

  1. точно знать, какая среда нужна ядру, когда происходит загрузка
  2. разрешить ядру запрашивать ту среду, в которой оно должно находиться

Так, например, если ваше ядро должно быть загружено в специальном режиме VESA (что, кстати, является плохой идеей), вы можете сообщить загрузчику об этом, и он может позаботиться об этом за вас.

Для того, чтобы ваше ядро было совместимо со стандартом multiboot, вам нужно где-нибудь в вашем ядре добавить структуру с заголовком (на самом деле, заголовок должен быть в первых 4КБ ядра). Удобно, что в NASM есть команда 'dd', которая позволит вам вставлять в код конкретные константы. Это следующие строки:

dd MBOOT_HEADER_MAGIC
dd MBOOT_HEADER_FLAGS
dd MBOOT_CHECKSUM
dd mboot
dd code
dd bss
dd end
dd start 

Это все. Константы MBOOT_ * определены выше.

MBOOT_HEADER_MAGIC - Магическое число. Указывает, что ядро совместимо со стандартом multiboot.

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

MBOOT_CHECKSUM - Значение этого поля определяется таким образом, что когда суммируются магическое число, флаги и это значение, результат должен быть нулевой. Это поле предназначено для проверки на наличие ошибок.

mboot - Адрес структуры, в которую в настоящее время осуществляется запись. GRUB использует этот адрес для того, чтобы сообщить нам, куда была перемещена эта структура.

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

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

2.2.2. Снова возвращаемся к коду ...

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

Все в порядке, но еще нет кода для выполнения компоновки. Мы еще не определили функцию main()!

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

Организовать взаимодействие между кодом на C и ассемблером необычайно просто. Вы просто должны знать соглашение, используемое для организации вызова. В GCC для архитектуры x86 при организации вызова используется следующее соглашение __cdecl:

  • Все параметры в функцию передаются через стек
  • Параметры, перемещаемые в стек, перечисляются справа налево.
  • Значение, возвращаемое функцией, помещается в EAX.

Так что вызов функции

d = func(a, b, c); 

будет выглядеть следующим образом

push [c]
push [b]
push [a]
call func
mov [d], eax 

Видите? Все просто! Итак, вы видите, что в нашем ассемблерном фрагменте, приведенном выше, команда 'push ebx' 'на самом деле передает значение EBX в функцию main() в качестве параметра.

2.3.1. Код на C

// main.c -- Определяется точка входа в ядро в коде на C, вызов процедур инициализации.
// Сделано для руководства James Molloy

int main(struct multiboot *mboot_ptr)
{
  // Здесь должны находиться все вызовы инициализационных процедур.
  return 0xDEADBABA;
} 

Это наша первая инкарнация функции main(). Как видите, мы передали ей один параметр - указатель на структуру multiboot. Мы определим ее позже (на самом деле, для компиляции нам ее определять не требуется!).

Все, что делает функция, это возвращает константу 0xDEADBABA. Эта константа достаточно необычна, чтобы мы обратили на нее внимание, когда мы запускаем программу второй раз.

2.4. Компилируем, компонуем и запускаем!

Теперь, когда мы добавили в наш проект новый файл, мы должны его добавить также в makefile. Отредактируйте следующие строки:

SOURCES=boot.o
CFLAGS= 

Замените их следующими::

SOURCES=boot.o main.o
CFLAGS=-nostdlib -nostdinc -fno-builtin -fno-stack-protector 

Мы должны остановить компилятор GCC, когда он попытается скомпоновать наше ядро с своими библиотеками C для Linux - это ядро вообще (пока) не будет работать . Для этого используются флаги CFLAGS.

Хорошо, теперь вы можете откомпилировать, скомпоновать и запустить ядро!

cd src
make clean  # Здесь все ошибки игнорируются.
make
cd ..
./update_image.sh
./run_bochs.sh  # Здесь от вас может быть запрошен пароль.

В результате должна начаться загрузка эмулятора bochs, вы увидите на несколько секунд загрузчик GRUB, затем будет работать ядро. Оно ничего не делает, просто замрет, сообщив 'starting up...' ('запуск ...').

Если вы откроете файл bochsout.txt, то в его конце вы увидите что-то вроде следующего:

00074621500i[CPU  ] | EAX=deadbaba  EBX=0002d000  ECX=0001edd0 EDX=00000001
00074621500i[CPU  ] | ESP=00067ec8  EBP=00067ee0  ESI=00053c76 EDI=00053c77
00074621500i[CPU  ] | IOPL=0 id vip vif ac vm rf nt of df if tf sf zf af pf cf
00074621500i[CPU  ] | SEG selector     base    limit G D
00074621500i[CPU  ] | SEG sltr(index|ti|rpl)     base    limit G D
00074621500i[CPU  ] |  CS:0008( 0001| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] |  DS:0010( 0002| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] |  SS:0010( 0002| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] |  ES:0010( 0002| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] |  FS:0010( 0002| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] |  GS:0010( 0002| 0|  0) 00000000 000fffff 1 1
00074621500i[CPU  ] | EIP=00100027 (00100027)
00074621500i[CPU  ] | CR0=0x00000011 CR1=0 CR2=0x00000000
00074621500i[CPU  ] | CR3=0x00000000 CR4=0x00000000
00074621500i[CPU  ] >> jmp .+0xfffffffe (0x00100027) : EBFE 

Обратите внимание, что значением EAX является 0xDEADBABA - значение, возвращаемое из main(). Поздравляем, теперь у вас есть мультизагрузочная заготовка-сборка, совместимая с ассемблером, и вы теперь готовы выводить данные на экран монитора!

Код с примером, используемый в данном руководстве, можно получить здесь.


Назад К началу Вперед