Rambler's
Top100

Разработка операционных систем. Выпуск 9

Автор: lonesome [TSH/Digital Daemons]
Дата: 31.05.2003
Раздел: Разработка ОС

ПРЕДЫДУЩИЙ ВЫПУСК      СЛЕДУЮЩИЙ ВЫПУСК

Разработка операционных систем

Выпуск 9 от 2003-05-27

Сегодня в номере:

Переходим на Си.

Сегодня мы впервые используем этот замечательный язык в разработке нашей системы. Как вы помните, написанный в предыдущем выпуске загрузчик обладает возможностью загружать и выполнять код, находящийся в файле kernel.bin и скомпонованный по адресу 0x200000 (очевидно, что эти два параметра элементарным образом можно изменить, но, в дальнейшем, я буду предполагать, что они равны первоначальным значениям). Запуск файла производится копированием его по адресу 0x200000 и передачей управления на начало кода. Отсюда следует, что ни один из форматов исполняемых файлов (за исключением "сырого" бинарного) нам не подходит - наш загрузчик не умеет распознавать заголовок файла.

Но нам и не обязательно использовать формат вроде ELF или PE - компоновщик ld, который я буду использовать, поддерживает создание сырых бинарников с помощью опции --oformat binary. Второй важный параметр, который нам понадобится - указание адреса, по которому будет произведена компоновка. Фактически все наше ядро располагается в сегменте кода, поэтому необходимо указать только его адрес: -Ttext 0x200000 (так уж повелось, что секция (сегмент) кода называется 'text').

И еще один момент: при создании объектного кода, который в дальнейшем будет компоновкой приведен к бинарному, мы указываем GCC опцию -ffreestanding. Теперь он не будет делать глупых предположений по поводу функций стандартной библиотеки (например не будет предполагать, что printf - это именно printf (const char *fmt, ...))

А теперь пришло время рассказать о первой ловушке, которая будет нас подстерегать. Представим например, что наше ядро выглядит вот так:

void main()
{
   printf ("Hello World!\n");
}

Что сделает GCC при компиляции этого файла? Он вынесет константу "Hello World!\n" в начало файла. Получится, что в начале kernel.bin (а ведь туда мы и передаем управление!) у нас будет не код, а непонятно что. Работать оно конечно не будет

Избавиться от этого просто - достаточно при компоновке кода первым указывать компоновщику файл, который наверняка скомпилировался должным образом (например, полученный из ассемблерного исходника). Выглядеть этот "переходник" может достаточно просто:


[BITS 32]
[EXTERN kernel_main]
[GLOBAL _start]
_start:
	mov esp, 0x200000-4
	call kernel_main
	

Перед тем, как отдать управление функции kernel_main мы к тому же и устанавливаем стек (конечно, не обязательно делать это вот здесь - в самый последний момент, можно и в загрузчике).

Стандартная библиотека

Вторая трудность, которая нас ожидает - отсутствие функций стандартной библиотеки. Конечно, как только мы получили возможность использовать Си, так и чешутся руки вывести на экран какой-нибудь хелловорлд, но... нечем. Обычно, функция printf бралась из библиотеки Си операционной системы, ну а сейчас ей взяться неоткуда. Будем писать ее сами, ну а заодно и несколько других функций, без которых жизнь наша будет подобна ночному кошмару. Та часть функций, которые мы напишем и будем использовать сегодня, предназначена для функционирования телетайпного устройства (tty). Еще раз напомню, что в нем:
1) можно использовать контрольные символы (например '\n') для перемещения курсора
2) автоматически производится контроль за экраном (например при выходе курсора за границы экрана происходит сдвиг экрана вверх).
Создать tty-устройство на базе видеопамяти несложно и ниже приведена его реализация:


#define VIDEO_WIDTH 80    //ширина экрана
#define VIDEO_HEIGHT 25   //высота экрана
#define VIDEO_RAM 0xb8000 //адрес видеопамяти

int tty_cursor;    //положение курсора
int tty_attribute; //текущий аттрибут символа


//Инициализация tty
void init_tty() 
{
  tty_cursor = 0;
  tty_attribute = 7;
}

//Смена текущего аттрибута символа
void textcolor(char c)
{
  tty_attribute = c;
}

//Очистка экрана
void clear()
{
  char *video = VIDEO_RAM;
  int i;

  for (i = 0; i < VIDEO_HEIGHT*VIDEO_WIDTH; i++) {
    *(video + i*2) = ' ';
  }
  
  tty_cursor = 0;
}


//Вывод одного символа в режиме телетайпа
void putchar(char c)
{
  char *video = VIDEO_RAM;
  int i;

  switch (c) {
  case '\n': //Если это символ новой строки
    tty_cursor+=VIDEO_WIDTH;
    tty_cursor-=tty_cursor%VIDEO_WIDTH;
    break;
    
  default:
    *(video + tty_cursor*2) = c;
    *(video + tty_cursor*2+1) = tty_attribute;
    tty_cursor++;
    break;
  }

  //Если курсор вышел за границу экрана, сдвинем экран вверх на одну строку
  if(tty_cursor>VIDEO_WIDTH*VIDEO_HEIGHT){
    for(i=VIDEO_WIDTH*2;i<=VIDEO_WIDTH*VIDEO_HEIGHT*2+VIDEO_WIDTH*2;i++){
      *(video+i-VIDEO_WIDTH*2)=*(video+i);
    }
    tty_cursor-=VIDEO_WIDTH;
  }
}

//Вывод строки, заканчивающейся нуль-символом
void puts(const char *s)
{
  while(*s) {
    putchar(*s);
    s++;
  }
}


А теперь о том, как заставить это все работать. Предположим, что tty у вас находится в файле ktty.c, "переходник" для си-кода - в startup.asm, а главная функция ядра такого содержания:

void kernel_main()
{
  init_tty();
  clear();

  puts("We use C, isn't this great?\n");

  for(;;);
}

находится в файле kernel.c

Сборка ядра производится таким образом:
gcc -ffreestanding -c -o ktty.o ktty.c
gcc -ffreestanding -c -o kernel.o kernel.c
nasm -felf -o startup.o startup.asm
ld --oformat binary -Ttext 0x200000 -o kernel.bin startup.o ktty.o kernel.o

Не правда ли, набивать все это в консоли достаточно утомительно? О том как процесс сборки упростить - читайте в следующем абзаце.

Программа 'Make'

Программа 'make' может управлять компиляцией основываясь на зависимостях (например: объектный файл kernel.o зависит от исходного файла kernel.c). Лучше всего принцип ее действия рассмотреть на конкретном Makefile (он будет предназначен для компиляции кода рассмотренного выше).
#Определим переменные OBJECTS и CFLAGS
#(к ним можно будет обращаться как $(OBJECTS) и $(CFLAGS)
OBJECTS=startup.o kernel.o ktty.o
CFLAGS=-ffreestanding -c

#цель 'all' - "глобальная" цель всех наших пертурбаций -
#получение образа дискеты image.bin из файла bootsect.asm
#(я предполагаю, что файл sb.bin был вставлен в конце bootsect.asm
#директивой incbin 'sb.bin')
#для успешного выполнения 'all' должны присутствовать файлы
#bootsect.asm и sb.bin (они указываются после двоеточия)
#(это и есть зависимости)
#действие для выполнения (nasm -fbin -o image.bin bootsect.asm)
#указано строчкой ниже

all: bootsect.asm sb.bin
	nasm -fbin -o image.bin bootsect.asm


#для создания sb.bin нам нужны sb.asm и kernel.bin
sb.bin: sb.asm kernel.bin
	nasm -fbin -o sb.bin sb.asm

#для создания kernel.bin нам нужны файлы, указанные в переменной OBJECTS
kernel.bin: $(OBJECTS)
	ld --oformat binary -Ttext 0x200000 -o kernel.bin $(OBJECTS)

#для создания startup.o нам нужен только startup.asm
startup.o: startup.asm
	nasm -felf -o startup.o startup.asm

#для создания kernel.o - нужен kernel.c
kernel.o: kernel.c
	gcc $(CFLAGS) -o kernel.o kernel.c

#ну а для ktty.o - ktty.c
ktty.o: ktty.c
	gcc $(CFLAGS) -o ktty.o ktty.c

Сохраните вышеприведенный текст в файле 'Makefile' в той же директории, где находятся остальные файлы системы. Теперь для сборки достаточно запустить программу make находясь в этой же директории. Вот как будет выглядеть результат запуска:

nasm -felf -o startup.o startup.asm
gcc -ffreestanding -c -o kernel.o kernel.c
gcc -ffreestanding -c -o ktty.o ktty.c
ktty.c: In function `clear':
ktty.c:18: warning: initialization makes pointer from integer without a cast
ktty.c: In function `putchar':
ktty.c:31: warning: initialization makes pointer from integer without a cast
ld --oformat binary -Ttext 0x200000 -o kernel.bin startup.o kernel.o ktty.o
nasm -fbin -o sb.bin sb.asm
sb.asm:151: warning: uninitialised space declared in .text section: zeroing
nasm -fbin -o image.bin bootsect.asm

Как видите, make поочередно провела все этапы компиляции, причем именно в том порядке, какой был необходим (т.к. она руководствовалась указанными в Makefile зависимостями)

Исключения IA-32

В теоретической части этого выпуска мы поговорим об исключениях, присутствующих в процессорах IA-32. Ниже приведена полная их таблица для процессора Pentium 4 (примечание: все исключения обратно-совместимы).

Номер исключения Описание
0 Деление на нуль (Divide Error Exception или #DE )
Тип исключения: fault
1 Отладочное прерывание (Debug Exception или #DB)
Тип исключения: trap или fault - смотря как вызвано
2 NMI - немаскируемое прерывание
3 Точка останова (Breakpoint Exception или #BP)
Тип: trap
Примечание: используется отладчиками, т.к. инструкция INT3 занимает один байт (0xCC)
4 Переполнение (Overflow Exception или #OF)
Тип: trap
Примечание: вызвается если при выполнении инструкции INTO (Interrupt on overflow) флаг переполнения OF установлен
5 Выход за допустимые границы при BOUND (Bound Range Exceeded Exception или #BR)
Тип: fault
Примечание: вызвается если операнд инструкции BOUND выходит за границы массива
6 Неправильная инструкция (Invalid Opcode Exception или #UD)
Тип: fault
Примечание: вызывается при попытке выполнить несуществующую инструкцию или инструкцию с недопустимыми операндами
7 Математический сопроцессор не доступен (No Math или #NM).
Тип: fault
Примечание: вызывается при попытке выполнить инструкцию FPU, если его использование запрещено (проверяются несколько флагов CR0)
8 Двойная ошибка (Double Fault Exception или #DF)
Тип: abort
Примечание: вызывается, если при вызове обработчика для исключения случилось еще одно исключение. Если при вызове #DF опять случится исключение, то процессор получит сигнал RESET# (обычно это приводит к перезагрузке системы).
9 Зарезервировано
Примечание: на 386 это было Coprocessor Segment Overrun
10 (0xA) Ошибочный TSS (Invalid TSS или #TS)
11 (0xB) Несуществующий сегмент (Segment Not Present или #NP)
Тип: fault
Примечание: вызывается при обращении к сегменту (или дескриптору какого-либо шлюза), бит P которого установлен в 0.
12 (0xC) Ошибка стека (Stack Fault Exception или #SS)
Тип: fault
Примечание: вызывается при превышении лимита сегмента стека или загрузке несуществующего (P=0) дескриптора в SS
13 (0xD) Общее исключение защиты (General Protection Exception или #GP)
Тип: fault
Примечание: основное исключение защищенного режима. Не перечесть всех случаев в которых оно вызывается :)
14 (0xE) Ошибка страничной адресации (Page Fault Exception или #PF)
Тип: fault
Примечание: в регистре CR2 находится адрес, обращение к которому вызвало ошибку
15 (0xF) Зарезервировано
16 (0x10) Ошибка сопроцессора (FPU Error или #MF)
Тип: fault
17 (0x11) Ошибка выравнивания (Alignment Check Exception или #AC)
Тип: fault
Примечание: вызывается при невыровненном обращении к памяти непривилегированным (CPL=3) кодом если установлены флаги AC в EFLAGS и AM в CR0
18 (0x12) Машинно-зависимая ошибка (Machine Check Exception или #MC)
Тип: abort
19 (0x13) Ошибка SSE/SSE2 (SIMD Floating Point Exception или #XF)
Тип: fault
20 - 31 (0x14 - 0x1F) Зарезервированы

Outro

На сегодня все, уважаемые подписчики.
Как всегда, мой почтовый ящик открыт для вас:
lonesome@lowlevel.ru
Также вы можете задавать интересующие вас вопросы в форуме lowlevel.ru
Предыдущие выпуски рассылки вы можете найти по этому адресу:
http://subscribe.ru/archive/comp.soft.prog.osdev
Всего наилучшего!
Lonesome


ПРЕДЫДУЩИЙ ВЫПУСК      СЛЕДУЮЩИЙ ВЫПУСК


Rambler's Top100