osdev.orgРазработка операционных систем
  ОБРАБОТЧИК ПРЕРЫВАНИЙ (7 августа 2012 года)

Оглавление | Оригинал 


 Архитектура x86 [русская Wiki] - система управляемая прерываниями. Внешние события вызывают прерывания - рабочий поток прерывается и вызывается обработчик прерываний (Interrupt Service Routine - ISR).

 Такие события могут вызываться аппаратным или программным обеспечением. Например аппаратное прерывание от клавиатуры: каждый раз когда вы нажимаете клавишу, клавиатура вызывает IRQ1 (запрос прерывания 1 - Interrupt Request 1) и вызывается соответствующий обработчик. Таймеры и диски запрашивают другие возможные источники аппаратных прерываний.

 Програмное обеспечение управляет прерываниями используя триггерные коды операций int; например сервис предоставляемый MS-DOS называется программным тригером INT21h и позволяет передавать соответствующие параметры в регистры процессора.

 Для того чтобы система знала как вызвать обработчик прерываний, смещения ISR хранятся в таблице дескрипторов прерываний (Interrupt Descriptor Table
[русская Wiki]) когда вы находитесь в защищённом режиме или в таблице векторов прерываний (Interrupt Vector Table) когда вы находитесь в реальном режиме [русская Wiki].

 ISR вызывает непосредственно CPU, а протокол вызова ISR отличается от вызова, например функций Си. Самое главное, что ISR завершаются кодом операции iret, в то время как обычные функции Си завершаются к кодом ret или retf. Однако, очевидно, что-то пошло не так, поскольку это наиболее "популярная" тройная ошибка среди программистов ОС.

Содержание
1. Проблема
2. Решения
 2.1. Чистый Ассемблер
 2.2. Двух-стадийное обертывание ассемблера
 2.3. Директивы прерываний специфичные для компилятора
  2.3.1. Borland C
  2.3.2. Watcom C/C++
  2.3.3. Голые функции
   2.3.3.1. Visual C++
   2.3.3.2. clang-llvm
  2.3.4. gcc/g++
 2.4. Asm Goto
3. Смотрите так же
 3.1 Внешние ссылки

Проблема
 Многие люди пытаются избегать Ассемблера и хотят как можно больше использовать свой любимый язык высокого уровня. GCC [русская Wiki] (как и другие компиляторы) позволяет добавлять ассемблерные вставки, поэтому многие программисты склонны к написанию ISR в следующем виде:

/* Так писать обработчик прерываний НЕ НУЖНО           */
void interrupt_handler(void)
{
    __asm__("pushad"); /* Сохраняем регистры.          */
    /* некоторая реализация */
    __asm__("popad");  /* Востанавливаем регистры.       */
    __asm__("iret");   /* Это будет тройная ошибка! */
}   

 Это не работает. Компилятор добавляет код обработки стека перед  и после вашей функции, что приводит к коду напоминающему примерно этот:

push   %ebp
mov    %esp,%ebp
sub    $<размер локальных переменных>,%esp
pushad
# Здесь код Си
popad
iret
# 'leave' !если! используют !локальные! переменные, вместо 'pop %ebp'.
leave
ret

 Должно быть очевидно, как это влияет на стек (ebp помещается в стек, но никогда не извлекается). Не следует так делать. Вместо этого используйте следующие варианты.

Решения
Чистый Ассемблер
 Изучите Ассемблер для написания обработчика прерываний.

Двух-стадийное обёртывание Ассемблера
 Напишите обёртку Ассемблера вызывающую функцию Си для выполнения реальной работы, а затем выполняющую iret.

/* filename : isr_wrapper.asm */
.globl   _isr_wrapper
.align   4
 
_isr_wrapper:
    pushad
    call    _interrupt_handler
    popad
    iret

/* filename : interrupt_handler.c */
void interrupt_handler(void)
{
    /* здесь что-то делается */
}

Директивы прерываний специфичные для компилятора
 Некоторые компиляторы для некоторых процессоров позволяют объявлять директивы для обработки прерываний, предлагая #pragma interrupt или специальные макросы. Borland C, Watcom C/C++, Microsoft C 6.0 и Free Pascal Compiler 1.9.* и выше позволяют это, в то время как GCC этого не делает. Visual C++ и clang-llvm этого не делает предлагая альтернативу в виде голых функций (Naked Functions):

 Borland C

/* Borland C */
void interrupt interrupt_handler(void)
{
    /* что-то делается */
}

 Watcom C/C++

/* Watcom C/C++ */
void _interrupt interrupt_handler(void)
{
    /* что-то делается */
}

Голые функции (Naked Functions)
 Некоторые компиляторы могут использоваться для создания обработчика прерываний, но требуют ручной обработки операций стека и возврата. Для этого необходимы функции, которые будут сгенерированы без эпилога и пролога. Функции созданные таким образом называются "голыми" - в Visual C++ это делается добавлением функции атрибута _declspec(naked), а в clang-llvm добавлением атрибута __attribute__((naked)). Вам необходимо проверить, что вы включили операцию возврата (такую как iretd) так как компилятору указано не включать эту часть пролога.

 Если вы намерены использовать локальные переменные, вам необходимо установить стек в манере ожидаемой компилятором; однако, поскольку ISR не повторяются вы можете просто использовать статические переменные.

Visual C++
 Visual C++ так же поддерживает ассемблерный макрос __LOCAL_SIZE, который уведомляет вас о том, сколько пространства необходимо объектам в стеке для функции.

/* Microsoft Visual C++ */
void _declspec(naked) interrupt_handler()
{
    _asm pushad;
 
    /* что-то делается */
 
    _asm{
        popad
        iretd
    }
}

clang-llvm
 Как указано в заголовке clang-llvm (9 Июля 2011) clang-llvm поддерживает "голые" функции.

/* clang-llvm */
__attribute__((naked)) void interrupt_handler()
{
    asm ("pushad");
 
    /* что-то делается */
 
    asm (
        "popad" \
        "iretd"
    );
}

gcc/g++
 Ни gcc ни g++ не предлагают способов (на x86 или x86-64) для создания обработчика прерываний на C или C++ без чёрной магии.

 Добавлено == ЧЁРНАЯ МАГИЯ ==

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

/* ЧЁРНАЯ МАГИЯ - настоятельно не рекомендуется! */
void interrupt_handler() {
    __asm__("pushad");
    /* что-то делается */
    __asm__("popad; leave; iret"); /* ЧЁРНАЯ МАГИЯ! */
}

 Результирующий вывод должен выглядеть примерно так:

push   %ebp
mov    %esp,%ebp
sub    $<размер локальных переменных>,%esp
pushad
# здесь код Си
popad
leave
iret
leave # "мёртвый код"
ret   # "мёртвый код"

 Для управления корректным завершением функции использовался leav - вы "от руки" создаёте код возврата функции, а leave генерируемый компилятором является "мёртвым кодом". Излишне говорить, что подобные предположения о внутреннем устройстве компилятора опасны. Этот код может разбиваться на различных компиляторах, или другой версии того же компилятора.  Подобные действия строго не рекомендуются и приведены только для полноты картины.

Asm Goto
 Учитывая все выше приведённые предупреждения вы всё ещё хотите хотите получить решение для написания ISR на C(++) используя gcc? Учитывайте следующее:

// Эта фнкция вызывается для  получения адреса кода обработки ISR когда заполняется IDT
void * interrupt_handler( )
{
        __asm__ __volatile__ goto( "jmp %l[endOfISR]" : : : "memory" : endOfISR );
        __asm__ __volatile__( ".align 16\t\n" : : : "memory" );  // выравнивание по 16 для эффективности - зависит от CPU
startOfISR:
        __asm__ __volatile__( "pushad\t\n" : : : "memory" );
 
        printf( "Hello, world of ISRs!" );
        __asm__ __volatile__( "popad\t\niret\t\n" : : : "memory" );
endOfISR:
        __asm__ __volatile__ goto( "mov %l[startOfISR], %%eax" : : : "memory" : startOfISR );
 
}

 Этот код использует "asm goto" - относительно новую возможность специфичную для GCC (смотрите http://wiki.osdev.org/Inline_Assembly#asm_goto). Если вы думаете, что "goto - это зло", а asm goto ещё хуже - однако в данном случае они могут вам пригодиться.

Смотрите так же
Внешние ссылки
 Прерывания и IRQ на osdever.net
 IRQ и PIT от  James Molloy