воскресенье, 25 августа 2013 г.

PhantomEx: Обработка процессорных исключений

Говоря о разработке ядра ОС, нельзя обойти стороной эту тему, так как надежная работа ядра определяется именно тем, как оно умеет обрабатывать возникающие в процессе исключительные ситуации.

Ошибочные действия выполняемые ядром или прикладной программой, в общем случае не должны приводить к прекращению его работы - большинство таких исключительных ситуаций в той или иной степени могут быть разрешены соответствующим обработчиком. Кроме того, некоторые механизмы реализуемые операционной системой могут быть реализованы как раз через обработку соответствующих исключений CPU. Например, реализация подкачки страниц памяти с HDD, известный как "своппинг", может быть реализован только с использованием обработчика #PF - исключения страничной памяти.

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



1. Немного теории


Под исключения CPU выделены прерывания 0x00 - 0x1F, и именно поэтому мы сдвигали номера аппаратных прерываний IRQ на 32 позиции, перенастраивая PIC. У Зубкова довольно неплохо описаны все программные прерывания CPU, поэтому обратимся к этой книге и рассмотрим все прерывания по порядку.

INT 00 - ошибка #DE "Деление на ноль"
Вызывается командами DIV и IDIV, если делитель - ноль, или происходит переполнение.

INT 01 - исключение #DB "Отладочное прерывание"
Вызывается как ловушка при пошаговой трассировке (флаг TF = 1), при переключении на задачу с установленным отладочным флагом или срабатывании точки останова во время доступа к данным, определенной в отладочных регистрах.
Вызывается как ошибка при срабатывании точки останова по выполнении команды с адресом, определенным в отладочных регистрах.

INT 02 - прерывание NMI
Немаскируемое прерывание. Так называются прерывания, которые нельзя запретить сбросом флага IF в регистре EFLAGS. Такие прерывания можно запретить манипуляциями с портами ввода/вывода 0x70 и 0x71, однако пока что подробно это рассматривать мы не будем.
Данное прерывание может быть сгенерировано, например при сбое в микросхеме памяти, то есть при критических отказах в работе оборудования.

INT 03 - ловушка #BP "Точка останова"
Вызывается однобайтной командой INT3.

INT 04 - ловушка #OF "Переполнение"
Вызывается командой INTO, если флаг OF = 1.

INT 05 - ошибка #BR "Переполнение при BOUND"
Вызывается командой BOUND при выходе операнда за допустимые границы.

INT 06 - ошибка #UD "Недопустимая операция"
Вызывается когда процессор пытается выполнить недопустимую команду или команду с недопустимыми операндами. Часто возникает при неверном задании адреса возврата из процедуры, например при переключении задач программным способом, из-за того что процессор начинает выполнять "белиберду" расположенную по неверному адресу возврата.

INT 07 - ошибка #NM "Сопроцессор отсутствует"
Вызывается любой командой FPU кроме WAIT, если бит EM регистра CR0 установлен в 1, и командой WAIT если MP и TS установлены в 1.

INT 08 - ошибка #DF "Двойная ошибка"
Вызывается при одновременном возникновении двух исключений, которые не могут быть обслужены последовательно. К ним относятся #DE, #TS, #NP, #SS, #GP и #PF. Обработчик этого исключения получает код ошибки, который всегда равен нулю.
Если при вызове обработчика #DF произошло ещё одно исключение, процессор отключается, и может быть выведен из этого состояния сигналом NMI или перезагрузкой.

INT 09 - зарезервировано
Эта ошибка вызывалась сопроцессором 80387, если происходило исключение #PF или #GP при передаче операнды команды FPU.
Старые компьютеры с процессорами 80286/80386 дополнительно допускали установку отдельного математического сопроцессора FPU, выполнявшего операции с плавающей запятой. Эти сопроцессоры соответственно обозначались как 80287 и 80387. Начиная с процессора Intel 486 DX сопроцессор FPU стал частью основного процессора, войдя в него в виде модуля FPU.

INT 0Ah - ошибка #TS "Ошибочный TSS"
Вызывается при попытке переключения на задачу с ошибочным TSS. Обработчик этого исключения должен вызываться через шлюз задачи. Обработчик получает код ошибки. Формат кода ошибки будет рассмотрен ниже. Бит EXT кода ошибки установлен если переключение пыталось выполнить аппаратное прерывание, использующее шлюз задачи. Индекс ошибки равен селектору TSS, если TSS меньше 67h байт, селектору LDT если LDT отсутствует или ошибочен, селектору сегмента стека, кода или данных, если если ими нельзя пользоваться (из-за нарушения защиты или ошибок в селекторе).

INT 0Bh - ошибка #NP "Сегмент недоступен"
Вызывается при попытке загрузить в регистр CS, DS, ES, FS или GS селектор сегмента, в дескрипторе которого сброшен бит присутствия (загрузка SS вызывает в этом случае #SS), а так же при попытке использовать шлюз, помеченный как отсутствующий, или при загрузке таблицы локальных дескрипторов командой LLDT (загрузка при переключении задач приводит к #TS). Если операционная система реализует виртуальную память на уровне сегментов, обработчик этого исключения может загрузить отсутствующий сегмент в память, установить бит присутствия и вернуть управление.
Обработчик этого исключения получает код ошибки. Бит EXT кода ошибки устанавливается если причина ошибки - внешнее прерывание, бит IDT устанавливается, если причина ошибки - шлюз из IDT, помеченный как отсутствующий. Индекс ошибки равен селектору отсутствующего сегмента.

INT 0Ch - ошибка #SS "Ошибка стека"
Вызывается при попытке выхода за пределы сегмента стека во время выполнения любой команды, работающей со стеком, - как явно (PUSH, POP, ENTER, LEAVE), так и не явно (MOV EAX, [BP+12]), а так же при попытке загрузить селектор сегмента, помеченного как отсутствующий, не только во время исполнения команд работающих со стеком, но и вовремя переключения задач, вызова и возврата из процедуры, работающей на другом уровне привилегий. 
Обработчик этого исключения получает код ошибки. Код ошибки равен селектору сегмента, вызвавшего ошибку, если она произошла из-за отсутствия сегмента или переполнении нового стека в межуровневой команде CALL. Во всех остальных случаях код ошибки - ноль.

INT 0Dh - исключение #GP "Общая ошибка защиты"
Все ошибки и ловушки не приводящие к другим исключениям, вызывают #GP - в основном при нарушении привилегий.
Обработчик этого исключения получает код ошибки. Если ошибка произошла при загрузке селектора в сегментный регистр, код ошибки равен этому селектору, во всех остальных случаях код ошибки - ноль.

INT 0Eh - ошибка #PF "Ошибка страничной адресации"
Вызывается, если в режиме страничной адресации программа пытается обратится к странице, помеченной как отсутствующая или привилегированная.
Обработчик этого исключения получает код ошибки, имеющий формат, отличающийся от других исключений
бит 0: 1, если причина ошибки - нарушение привилегий
           0, если было обращение к отсутствующей странице

бит 1: 1, если выполнялась операция записи; 0, если чтения;
бит 2: 1, если операция выполнялась из CPL = 3; 0, если CPL < 3
бит 3: 0, если ошибку вызвала попытка установить зарезервированный бит в каталоге страниц.

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

INT 0Fh - зарезервировано

INT 10h - ошибка #MF "Ошибка сопроцессора"
Вызывается, только если бит NE в регистре CR0 установлен в 1 при выполнении любой команды FPU, кроме управляющих команд WAIT/FWAIT, при условии, что в FPU произошло одно из исключений FPU.

INT 11h - ошибка #AC "Ошибка выравнивания"
Вызывается только если бит AM в регистре CR0 и флаг AC из EFLAGS установлены в 1, если CPL = 3 и произошло невыравненное обращение к памяти. (Выравнивание должно быть по границе слова при обращении к слову, по границе двойного слова при обращении к двойному слову и т.д.)
Обработчик этого исключения получает код ошибки, равный нулю.

INT 12h - останов #MC "Машинно-зависимая ошибка"
Вызывается, начиная с процессоров Pentium, при обнаружении некоторых аппаратных ошибок с помощью специальных машинно-зависимых регистров MCG_*. Наличие кода ошибки, так же как и способ вызова исключения зависит от модели процессора.

INT 13h - 1Fh - зарезервировано Intel для будущих исключений

INT 20h - 0FFh - выделены для использования программами, в частности INT 21h используется MS DOS для вызова системных функций, INT 80h используется ядром Linux для реализации системных вызовов.

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

Таблица 19. Формат кода ошибки
БитНазначение
15-3Биты 15-3 селектора, вызвавшего исключение
2TI - установлен, если причина исключения - дескриптор, находящийся в LDT, сброшен, если в GDT
1IDT - установлен, если причина исключения, дескриптор, находящийся в IDT
0EXT - установлен, если причина исключения - аппаратное прерывание

На основе этих справочных данных можно попытаться написать простейшие обработчики исключений CPU.

2. Реализация обработчика #PF


В качестве иллюстрации для данной статьи я выбрал обработчик исключения #PF, как субъективно наиболее интересный вариант, учитывая что мы используем режим страничной адресации и рано или поздно займемся реализацией подкачки страниц с HDD.

Для обработки исключений  создадим отдельный модуль, гле реализуем все обработчики

Листинг 81. Модуль обработки прерываний CPU (cpu_isr.h)

/*--------------------------------------------------------------
 *
 *         Обработка прерываний CPU
 *
 *------------------------------------------------------------*/

#ifndef            CPU_ISR_H
#define            CPU_ISR_H

/* Номера прерываний CPU */
#define            INT_0         0x00
#define            INT_1         0x01
#define            INT_2         0x02
#define            INT_3         0x03
#define            INT_4         0x04
#define            INT_5         0x05
#define            INT_6         0x06
#define            INT_7         0x07
#define            INT_8         0x08
#define            INT_9         0x09
#define            INT_10        0x0A
#define            INT_11        0x0B
#define            INT_12        0x0C
#define            INT_13        0x0D
#define            INT_14        0x0E
#define            INT_15        0x0F
#define            INT_16        0x10
#define            INT_17        0x11
#define            INT_18        0x12

/* Маски флагов для обработки кода ошибки */
#define            EXT_BIT       (1 << 0)
#define            IDT_BIT       (1 << 1)
#define            TI_BIT        (1 << 2)
#define            ERR_CODE_MASK 0xFFF8;

/* Необходимые заголовочные файлы */
#include        "common.h"
#include        "isr.h"


/*--------------------------------------------------------------
//        Прототипы обработчиков прерываний CPU
//------------------------------------------------------------*/


/* INT 0Eh - #PF ошибка страничной адресации */
void page_fault(registers_t regs);

#endif


Самое важное здесь - мы объявляем прототип функции обработки исключения #PF. приведем так же и код его реализации

Листинг 82. Реализация обработчика #PF (cpu_isr.c)

/*-----------------------------------------------------------------
 *
 *           Обработка исключений CPU       
 *
 *---------------------------------------------------------------*/


#include        "cpu_isr.h"

/*-----------------------------------------------------------------
//        #PF - ошибка страничной адресации
//---------------------------------------------------------------*/

void page_fault(registers_t regs)
{
  u32int fault_addr = read_cr2();       /* Читаем адрес из регистра CR2 */

  int present = !(regs.err_code & 0x1); /* Страница отсутствует в памяти */
  int rw = regs.err_code & 0x2;         /* Страница только для чтения */
  int user = regs.err_code & 0x4;       /* Обращение с нарушением привилегий */
  int reserved = regs.err_code & 0x8;   /* Перезаписан зарезервированный флаг */
 
  print_text("Page fault: ");

  if (present)
    print_text("NOT PRESENT, ");

  if (rw)
    print_text("READ ONLY, ");

  if (user)
    print_text("USER MODE,  ");

  if (reserved)
    print_text("WRITING TO RESERVED BITS, ");
 
  print_text("at address ");
  print_hex_value(fault_addr);
  print_text("\n");

  while (1);
}

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

Листинг 83. Функция чтения регистра CR2 (regs.s)

/*-------------------------------------------------------------
//      Function for read CR2
//-----------------------------------------------------------*/
     
.global    read_cr2

read_cr2:
   
      mov    %cr2, %eax
      ret


И добавим в common.h прототип этой функции

extern u32int read_cr2(void);

для вызова её из кода на C. Теперь в коде функции main() необходимо выполнить регистрацию нашего обработчика

Листинг 84. Регистрация обработчика исключения #PF (main.c)

/*-----------------------------------------------------------------
//        Точка входа в ядро
//---------------------------------------------------------------*/

int main(multiboot_header_t* mboot, u32int initial_esp)
{
    .
    .
    .
    /* Инициализация GDT и IDT */
    init_descriptor_tables();

    /* Регистрация обработчика #PF */
    register_interrupt_handler(INT_14, &page_fault);
    .
    .
    .
    return 0;
}


Теперь всё готово, и можно попробовать выполнить тест. В каком-нибудь из имеющихся у нас потоков ядра объявим указатель на не отображенную область памяти

Листинг 85. Некорректное обращение к памяти (main.c)

/*--------------------------------------------------------
//        Поток #1
//------------------------------------------------------*/

void task01(void)
{
     u32int* ptr = (u32int*) 0xA0000000;

     ptr[0] = 0x1111;
     print_text("I'm thread #1\n");   

     while (1);
}


В результате имеем такую картину


По сути мы произвели обращение к виртуальному адресу, который не отображен нами в физическую область памяти, что и вызвало исключение. Если мы уберем некорректное обращение к памяти, то всё будет в порядке

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

Заключение


Сегодня мы реализовали код обработки исключений процессора, на примере исключения страничной адресации #PF. В исходных текстах учебного примера, доступных в разделе Загрузки добавлены обработчики большинства из рассмотренных нами в теории исключений, кроме того изменен код функции переключения задач по материалам предыдущей статьи.