понедельник, 15 июля 2013 г.

PhantomEx: Прерывания защищенного режима

В архитектуре процессоров x86 предусмотрены особые ситуации, когда процессор прекращает выполнение текущей программы и немедленно передает управление программе-обработчику, специально созданной для обработки подобной ситуации.

Такие особые ситуации делятся на два типа: прерывания и исключения, в зависимости от того что вызвало эту ситуацию - запрос внешнего устройства или команда, выполняемая самим процессором.

Исключения в свою очередь делятся на ошибки, ловушки и остановы
  • Ошибки - проявляются перед выполнением команды, поэтому обработчик такого исключения получит в качестве адреса возврата адрес ошибочной команды.
  • Ловушки - происходят сразу после выполнения команды, так что обработчик получит в качестве адреса возврата адрес следующей команды. 
  • Остановы - могут происходить в любой момент и вообще не предусматривать средств возврата управления в программу. 
Адреса обработчиков прерываний в реальном режиме располагаются в самом начале адрессного пространства по адресу 0x0000:0x0000. Каждый элемент этой таблицы представляет собой дальний адрес обработчика прерываний в формате сегмент:смещение, или четыре нулевых байта, если обработчик не установлен - так называемая таблица векторов прерываний.

В защищенном режиме всё немного иначе и сложнее. Рассмотрим всё по порядку.

1. Таблица дескрипторов прерываний (IDT)


В защищенном режиме эта самая структура, хранящая адреса обработчиков может располагаться где угодно в памяти, и содержит она не просто адреса обработчиков, а дескрипторы трех типов - шлюз прерывания, шлюз ловушки и шлюз задачи. Эта таблица носит название Interrupt Descriptors Table - таблица дескрипторов прерываний (IDT).

Шлюзы прерываний и ловушек используются для вызова обработчиков прерываний и исключений типа ловушки и имеют следующий формат

Таблица 9. Формат шлюза прерывания и ловушки.

БайтНазначение
1-015-0 биты смещения
3-2Селектор сегмента
4биты 4-0: 00000 или (для шлюза вызова) число двойных слов, которые будут скопированы из стека вызывающей задачи в стек вызываемой.
биты 7-5: 000
5Байт доступа:

биты 3-0: тип шлюза (4, 5, 6, 7, C, E, F)
бит 4: 0
биты 6-5: DPL - уровень привилегий дескриптора
бит 7: бит присутствия сегмента
7-631-16 биты смещения (0 для 16-битных шлюзов и шлюза задачи)

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

Всего существует 256 допустимых номеров прерываний, соответственно IDT должна содержать 256 дескрипторов. И таблицу эту крайне важно определить, так как если происходит прерывание, а для него нет записи в IDT, то процессор просто перезагрузит компьютер.

Отдельный разговор о первых 32 прерываниях - это специальные прерывания, связанные с работой процессора.

Таблица 10. Специальные прерывания процессора


Номер
прерывания
Описание
0Деление на ноль
1Прерывание отладки
2Немаскируемое прерывание
3Прерывание точки останова
4
Переполнение при выполнении команды INTO
5Прерывание выхода за границы данных
6Прерывание неправильного кода операции
7Прерывание отсутствия сопроцессора FPU
8Прерывание двойной ошибки (код ошибки помещается в стек)
9Нарушение сегментации памяти сопроцессором
10Неправильный TSS (код ошибки помещается в стек)
11Отсутствие сегмента (код ошибки помещается в стек)
12Ошибка стека (код ошибки помещается в стек)
13Ошибка общей защиты (код ошибки помещается в стек)
14Ошибка системы страничной адресации (код ошибки помещается в стек)
15Неизвестное прерывание
16Ошибка сопроцессора FPU
17Прерывание контроля выравнивания
18Прерывание связанное с общей работой процессора
19-31Зарезервировано

Крайне важно, для нормальной работы ядра, чтобы эти прерывания имели записи в IDT.

2. Реализация механизма обработки прерываний.


Теперь вернемся к практике, и реализуем простейший механизм обработки прерываний в нашем ядре.

Прежде всего добавим в файл descriptor_tables.h описание структур для работы с IDT.

Листинг 26. Структуры для работы с IDT (файл descriptor_tables.h)

/*------------------------------------------------
// Запись в таблице дескрипторов прерываний (IDT)
//----------------------------------------------*/

struct idt_entry_struct
{
 
  u16int    base_low;  /* Младшее слово адреса обработчика */
  u16int    selector;  /* Селектор сегмента ядра */
  u8int     allways0;  /* Всегда нули */
  u8int     flags;     /* Байт флагов доступа */
  u16int    base_high; /* Старшее слово адреса обработчика */
 
}__attribute__((packed));

typedef    struct    idt_entry_struct    idt_entry_t;

/*----------------------------------------------
// Структура указателей размещения IDT
//--------------------------------------------*/

struct    idt_ptr_struct
{

  u16int    limit; /* Размер таблицы IDT*/
  u32int    base;  /* Адрес первой записи IDT */
 
}__attribute__((packed));

typedef    struct    idt_ptr_struct idt_ptr_t;

Как видим, в этих определениях нет ничего сверхъестественного. Теперь как и в случае с GDT необходимо определить функции для работы с этими структурами, создать саму IDT и загрузить её адрес в регистр IDTR.

Листинг 27. Модифицируем файл descriptor_tables.c

/* Таблица дескрипторов прерываний */
idt_entry_t  idt_entries[256];
/* Структура указателей размещения IDT */
idt_ptr_t    idt_ptr;

/* Загрузка рагистра IDTR - внешняя ассемблерная функция */
extern void idt_flush(u32int);
/* Инициализация IDT */
static void init_idt(void);
/* Добавление записи в таблицу IDT */
static void idt_set_gate(u8int, u32int, u16int, u8int);

Создаем таблицу IDT из 256 записей и указатели на её размещение в памяти. Рассмотрим реализацию функций, начнем с ассемблерного кода загрузки регистра IDTR, который размещаем в файле gdt.s

Листинг 28. Код загрузки регистра IDTR (файл gdt.s)

idt_flush:

/* Загрузка IDTR указателем полученным из стека */
/* переданным в качестве параметра */
    mov    4(%esp), %eax /* берем параметр из стека */
    lidt    (%eax)       /* загружаем регистр IDTR */
    ret

Реализуем функции создания записи в IDT и инициализации самой таблицы

Листинг 29. Создание записи и инициализация IDT (файл descriptor_tables.s)

/*-----------------------------------------------
// Создание записи в IDT
//---------------------------------------------*/

void idt_set_gate(u8int num,       /* номер прерывания */
                  u32int base,     /* адрес обработчика */
                  u16int selector, /* селектор сегмента ядра */
                  u8int flags)     /* флаги доступа */
{
  idt_entries[num].base_low = base & 0xFFFF;
  idt_entries[num].base_high = (base >> 16) & 0xFFFF;
 
  idt_entries[num].selector = selector;
  idt_entries[num].allways0 = 0;
 
  idt_entries[num].flags = flags; /* | 0x60 - для пользовательского */
                                  /* режима */ ;

}

/*-----------------------------------------------
// Инициализация IDT 
//---------------------------------------------*/

void init_idt(void)
{
  /* Инициализация структуры указателя размером и адресом IDT */
  idt_ptr.limit = sizeof(idt_entry_t)*256 - 1;
  idt_ptr.base = (u32int) &idt_entries;
 
  /* Очистка памяти */
  memset(&idt_entries, 0, sizeof(idt_entry_t)*256);   
 
  /* Создание записей в таблице на первые 32 прерывания */ 
  idt_set_gate(0, (u32int) isr0, 0x08, 0x8E);
  idt_set_gate(1, (u32int) isr1, 0x08, 0x8E);
  idt_set_gate(2, (u32int) isr2, 0x08, 0x8E);
  .
  .
  .
  idt_set_gate(31, (u32int)isr31, 0x08, 0x8E);       
 
  /* Загрузка регистра IDTR */
  idt_flush((u32int) &idt_ptr);
}

Здесь выполняется ровно тоже самое, что делалось при инициализации таблицы GDT. Вызовы функции создания записи в IDT не показаны полностью, так как совершенно идентичны за исключением передаваемого в них номера прерывания и адреса обработчика.

Рассмотрим подробнее значение передаваемого селектора и байта доступа

Селектор: 0x08 = 00001000b - это селектор сегмента кода ядра, где выполняется обработчик прерывания.

Байт доступа: 0x8E = 10001110b - младшая тетрада, 0x0E - тип дескриптора - 32-разрядный шлюз прерывания (см. таблицу 7); в старшей тетраде взведен флаг присутствия.

Функция memset(...) - это аналог общеизвестной программистам C/C++ функции установки определенного значения во всех ячейках определенной передаваемым указателем области памяти. Она так же "самописная", и её лучше расположить в модуле common.c с прототипом в заголовке common.h. Реализация этой функции чрезвычайно проста

Листинг 30. Функция инициализации памяти (файл common.c)

/*--------------------------------------------------
// Инициализация памяти одним значением  
//------------------------------------------------*/

void memset(void* ptr, u8int value, u32int size)
{
  /* Преобразуем указатель к массиву байтов */
  u8int* b_ptr = (u8int*) ptr;
  int i = 0;
  /* Записываем переданное значение в каждый байт */
  for (i = 0; i < size; i++)
    b_ptr[i] = value; 
}


Обработчики прерываний так же необходимо создать. Для этого сотворим вот такой код на ассемблере.

Листинг 31. Обработчики прерываний (файл interrupt.s)

/*-------------------------------------------------
// Макрос для обработчика без возврата кода ошибки
//-----------------------------------------------*/
.macro ISR_NOERRCODE isr_num

.global    isr\isr_num

isr\isr_num:

    cli                     /* Запрет всех прерываний */
    push    $0              /* Проталкиваем 0 в стек */
                            /* этот ноль - фиктивный код ошибки */
    push    $\isr_num       /* Сохраняем в стек номер прерывания */
    jmp    isr_common_stub  /* Передаем управление обработчику */
   
.endm
   
/*-----------------------------------------------------------
// Макрос для обработчика прерывания с возвратом кода ошибки
//---------------------------------------------------------*/

.macro ISR_ERRCODE isr_num

.global    isr\isr_num

isr\isr_num:

    cli      
              /* Запрет всех прерываний */
    push    $\isr_num       /* Номер прерывания  - в стек */
    jmp    isr_common_stub  /* Переходим к обработчику */
   
.endm

/*-------------------------------------------------
//    Обработчики на первые 32 прерывания
//-----------------------------------------------*/

ISR_NOERRCODE 0
ISR_NOERRCODE 1
ISR_NOERRCODE 2
ISR_NOERRCODE 3
ISR_NOERRCODE 4
ISR_NOERRCODE 5
ISR_NOERRCODE 6
ISR_NOERRCODE 7

ISR_ERRCODE 8

ISR_NOERRCODE 9
.
.
.
ISR_NOERRCODE 31

/*-------------------------------------------------
// Общая части обработчика прерываний
//-----------------------------------------------*/
.extern isr_handler

isr_common_stub:

      pusha                /* Проталкиваем в стек все регистры */
                           /* общего назначения (РОН) */

   
      mov    %ds, %ax      /* Спасаем в стеке селектор */ 
      push   %eax          /* сегмента данных */
     
      mov    $0x10, %ax    /* Загружаем сегмент данных ядра */
      mov    %ax, %ds
      mov    %ax, %es
      mov    %ax, %fs
      mov    %ax, %gs
     
      call   isr_handler   /* Вызываем наш обработчик */
     
      pop    %eax          /* Восстанавливаем оригинальный */
      mov    %ax, %ds      /* селектор сегмента данных */
      mov    %ax, %es
      mov    %ax, %fs
      mov    %ax, %gs
     
      popa                 /* Выталкиваем РОН из стека */
      add    $8, %esp      /* Убираем из стека код ошибки */
                           /* и помещаем туда номер ISR */
      sti                  /* Вновь разрешаем все прерывания */
      iret
                                /* Возвращаемся из обработчика */
                           /* при этом из стека выталкиваются */
                           /* значения регистров CS, EIP, EFLAGS, */
                           /* SS и ESP */

Нам необходимы 32 практически идентичных обработчика, существующих в двух вариантах - для прерываний без возврата кода ошибки и с возвратом кода ошибки. Даже с применением "копипаста", написание этого кода будет достаточно утомительно, поэтому код обработчиков сформируем воспользовавшись  макросредствами GNU Assembler. Приведенные в коде макросы вызываются затем в коде, ниже, создавая все 32 обработчика. Этим мы экономим время и страхуемся от возможных и очень вероятных ошибок при "копипасте". В качестве параметра в макрос передается номер прерывания isr_num, который потом подставляется в нужное место кода.

Обрабатывая прерывание необходимо сохранить состояние все регистров общего назначения процессора (РОН), сохранить селектор текущего сегмента данных, так как мы должны обратится к данным ядра, включив соответствующий сегмент. После этого мы обрабатываем прерывание, и восстанавливаем РОН, восстанавливаем сегмент данных, использовавшийся до прерывания, убираем из стека помещенный туда код ошибки. Таким система должна вернутся к выполнению прерванной задачи. Перед началом обработки прерывания процессор автоматически сохраняет в стеке свое состояние, помещая туда регистры CS, EIP, EFLAGS, SS и ESP. Команда IRET обеспечивает возврат из обработчика, с восстановлением из стека значений перечисленных регистров.

Теперь в интерфейсе к модулю descriptor_tables.h мы можем теперь определить 32 заголовка внешних функций обработчиков прерываний.

Листинг 32. Прототипы внешних функций обработки прерываний (файл descriptor_tables.h)

/* Обработчики прерываний процессора */
extern void isr0(void);
extern void isr1(void);
extern void isr2(void);
.

.
.
extern void isr31(void);


Необходимо написать и наш собственный обработчик прерываний isr_handler() вызываемый в общей части обработчика.

Листинг 33. Заголовочный файл isr.h

#ifndef        ISR_H
#define        ISR_H

#include    "common.h"


/*------------------------------------------------
// Структура для хранения регистров процессора

//----------------------------------------------*/
struct registers
{
 
  u32int    ds;
  u32int    edi, esi, ebp, esp, ebx, edx, esx, eax;
  u32int    int_num, err_code; /* Номер прерывания и код ошибки */
  u32int    eip, cs, eflags, useresp, ss;
 
}__attribute__((packed));

typedef    struct    registers    registers_t;

#endif


Здесь описана структура, с помощью которой обработчику будет передаваться содержимое регистров процессора.

И сам обработчк

Листинг 34. Обработчик прерываний (файл isr.c)

#include    "isr.h"
#include    "text_framebuffer.h"


/*----------------------------------------------------------
// Наш простецкий обработчик, для иллюстрации и тестирования
//--------------------------------------------------------*/

void isr_handler(registers_t regs)
{
  /* Просто пишем на экране что произошло прерывание */
  /* и выводим его номер */

  print_text("unhandled interrupt: ");
  print_hex_value(regs.int_num);
  print_text("\n");
}


Создаем глобальную функцию, чтобы ассемблерный обработчик мог подцепить её как внешнюю. Не делаем пока ничего принципиально полезного, кроме регистрации самого факта обработки ядром прерываний.

Ну и наконец надо добавить код инициализации в функцию инициализации таблиц дескрипторов.

Листинг 35. Инициализация таблиц дескрипторов 
(файл descriptor_tables.c)

/*---------------------------------------------
// Инициализация таблиц дескрипторов
//-------------------------------------------*/

void init_descriptor_tables(void)
{
  /* Инициализируем GDT */
  init_gdt();
  /* Инициализируем IDT */
  init_idt();

ну вот, кажется вроде всё. Разумеется модули isr.o, interrupt.o необходимо добавить в Makefile и пересобрать ядро. Упоминать об этой стандартной для нас процедуре я уже не буду, это уже вполне привычное действие.

3. Проверка скомпилированного кода


Вы помните что для вставки кода обработчиков мы использовали  макросы, неплохо бы проконтролировать, как отработал компилятор, не построил ли он нам какую-нибудь ерунду. Дизассемблируем для этого код ядра.



Как видим, макросы отработали как следует - код всех обработчиков на месте. 

Собственно теперь добавим в функцию main() такой вот код

/* Отступим немного места после вывода GDT */
print_text("\n\n");
/* Сгенерируем программные прерывания */
asm volatile ("int $0x03");
asm volatile ("int $0x04");

То есть сгенерируем программные прерывания. Что мы увидим в итоге?


Увидим мы что прерывания перехватываются нами и обрабатываются. Что ж, это уже неплохо. Обработчик вывел на экран номер перехваченного прерывания. Это те прерывания, которые мы сгенерировали.

Заключение


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