# Обработка прерываний
# ISR - Interrupt Service Routine
Архитектура x86 - это система, управляемая прерываниями. Внешние события вызывают прерывание — прерывается нормальный поток управления и вызывается процедура обработки прерываний (ISR).
Такие события могут быть вызваны аппаратным или программным обеспечением. Примером аппаратного прерывания является клавиатура: каждый раз, когда вы нажимаете кнопку, клавиатура запускает IRQ1 (Запрос на прерывание 1), и вызывается соответствующий обработчик прерывания. Таймеры и завершение запроса на диск являются другими возможными источниками аппаратных прерываний.
Прерывания, управляемые программным обеспечением, запускаются кодом операции int; например, службы MS-DOS, вызываются программным обеспечением, запускающим INT 21h и передающим соответствующие параметры в регистрах процессора.
Чтобы система знала, какую процедуру обработки прерываний вызывать при возникновении определенного прерывания, смещения для ISR хранятся в Interrupt Descriptor Table, когда вы находитесь в Protected Mode, или Interrupt Vector Table, когда вы находитесь в Real Mode.
ISR вызывается непосредственно процессором, и протокол для вызова ISR отличается от вызова, например, функции C. Самое главное, ISR должен заканчиваться кодом операции iret (или iretq в Long Mode — да, даже при использовании синтаксиса Intel), в то время как обычные функции C заканчиваются ret или retf. Очевидное, но тем не менее неправильное решение приводит к одной из самых "популярных" тройной ошибки среди программистов ОС.
# Когда вызываются обработчики
# x86
Когда процессор вызывает обработчики прерываний, процессор помещает эти значения в стек в следующем порядке:
EFLAGS -> CS -> EIP
Значение CS дополняется двумя байтами, чтобы сформировать двойное слово.
Если тип шлюза не является прерыванием, процессор очистит флаг прерывания. Если прерывание является исключением, процессор отправит код ошибки в стек в виде двойного слова.
Процессор загрузит значение селектора сегментов из связанного дескриптора IDT в CS.
# x86-64
Когда процессор вызывает обработчики прерываний, он изменяет значение в регистре RSP на значение, указанное в IST, и если его нет, стек остается прежним. В новый стек процессор помещает эти значения в следующем порядке:
SS:RSP (original RSP) -> RFLAGS -> CS -> RIP
CS дополняется, чтобы сформировать четырехсловие.
Если прерывание вызывается из другого кольца, SS устанавливается в 0, что указывает на нулевой селектор. Процессор изменит регистр RFLAGS, установив биты TF, NT и RF равными 0. Если тип прерывание, процессор очистит флаг прерывания.
Если прерывание является исключением, процессор отправит код ошибки в стек, дополненный байтами, чтобы сформировать четырехсловие.
Процессор загрузит значение селектора сегментов из связанного дескриптора IDT в CS и проверит, является ли CS допустимым селектором сегментов кода.
# Проблема
Многие люди избегают ассемблера и хотят сделать как можно больше на своем любимом языке высокого уровня. GCC (а также другие компиляторы) позволяют добавлять встроенный ассемблер, поэтому многие программисты испытывают соблазн написать ISR, подобный этому:
/* Как НЕ НУЖНО писать обработчик прерываний */
void interrupt_handler(void) {
asm("pushad"); /* Сохранение регистров */
/* Делаем что-нибудь */
asm("popad"); /* Восстанавливаем регистры */
asm("iret"); /* Ура! Ура! Тройная ошибка */
/* Думаем о своём поведении и переписываем код правильно */
}
Это не будет работать. Компилятор не понимает, что происходит. Он не понимает, что регистры и стек должны сохраняться между операторами asm; оптимизатор, скорее всего, повредит функцию. Кроме того, компилятор добавляет код обработки стека до и после вашей функции, что вместе с iret приводит к коду ассемблера, похожему на этот:
push %ebp
mov %esp,%ebp
sub $<размер локальных переменных>,%esp
pushad
# C код пишем здесь
popad
iret
# 'leave' если вы используете локальные переменные, 'pop %ebp' для остальных случаев.
leave
ret
Должно быть очевидно, как это портит стек (ebp выталкивается, но никогда не выскакивает). Не делай этого.
# Решения
# Чистый Ассемблер
Узнайте достаточно об ассемблере, чтобы написать в нём обработчики прерываний 😃
# Двухэтапный враппер ассемблера
Напишите оболочку ассемблера, вызывающую функцию C для выполнения обработки, и только затем выполните iret.
/* Файл: isr_wrapper.s */
.globl isr_wrapper
.align 4
isr_wrapper:
pushad
cld /* Код C, следующий за sysV ABI, требует, чтобы DF был очищен при выполнении функции */
call interrupt_handler
popad
iret
/* Файл: interrupt_handler.c */
void interrupt_handler(void) {
/* Делаем что-нибудь */
}
# Директивы прерываний специфичные для компилятора
Некоторые компиляторы для некоторых процессоров имеют директивы, позволяющие объявлять обычное прерывание, предлагая #pragma interrupt или выделенный макрос. Clang 3.9, Borland C, Watcom C/C++, Microsoft C 6.0 и GCC предлагают это. Visual C++ предлагает альтернативу, показанную в разделе Naked-функции:
# Clang
Начиная с версии 3.9, он поддерживает атрибут прерывания для x86/x86-64.
struct interrupt_frame {
uword_t ip;
uword_t cs;
uword_t flags;
uword_t sp;
uword_t ss;
};
__attribute__ ((interrupt))
void interrupt_handler(struct interrupt_frame * frame) {
/* Делаем что-нибудь */
}
# Borland C
void interrupt interrupt_handler(void) {
/* Делаем что-нибудь */
}
# Watcom C/C++
void _interrupt interrupt_handler(void) {
/* Делаем что-нибудь */
}
# Naked-функции
Некоторые компиляторы могут использоваться для создания процедур прерывания, но требуют, чтобы вы вручную обрабатывали операции стека и возврата. Для этого требуется, чтобы функция создавалась без эпилога или пролога. Это называется сделать функцию naked — это делается в Visual C++ путем добавления атрибута _declspec(naked) к функции. Вам необходимо убедиться, что вы включаете операцию возврата (например, iretd), поскольку это часть эпилога, который компилятору теперь было поручено не включать.
Если вы собираетесь использовать локальные переменные, вы должны настроить фрейм стека так, как ожидает компилятор; однако, поскольку ISR не являются реентерабельными, вы можете просто использовать статические переменные.
# Microsoft Visual C++
Visual C++ предоставляет макрос ассемблера __LOCAL_SIZE, который уведомляет вас, сколько места требуется объектам в стеке для функции.
void _declspec(naked) interrupt_handler() {
_asm pushad;
/* Делаем что-нибудь */
_asm {
popad
iretd
}
}
# GCC / G++
В документации GCC (opens new window) говорится, что, используя атрибуты функций GCC, они добавили возможность писать обработчики прерываний в интерфейсе C с помощью _attribute_((interrupt)). Так что вместо:
/* ЧЁРНАЯ МАГИЯ - категорически не рекомендуется! */
void interrupt_handler() {
__asm__("pushad");
/* Делаем что-нибудь */
__asm__("popad; leave; iret"); /* ЧЁРНАЯ МАГИЯ! */
}
Вы можете использовать:
struct interrupt_frame;
__attribute__((interrupt)) void interrupt_handler(struct interrupt_frame* frame) {
/* Делаем что-нибудь */
}
В документации для GCC говорится, что если используется атрибут прерывания, инструкция iret будет использоваться вместо ret на архитектурах x86 и x86-64. В нем также говорится: "Поскольку GCC не сохраняет состояния SSE, MMX и x87, параметр GCC-mgeneral-regs-only должен использоваться для компиляции обработчиков прерываний и исключений."
# Чёрная магия
Посмотрите на неисправный код выше, где правильный exit-код функции C был пропущен, что испортило стек. Теперь рассмотрим этот фрагмент кода, где код выхода добавляется вручную:
/* ЧЁРНАЯ МАГИЯ - категорически не рекомендуется! */
void interrupt_handler() {
__asm__("pushad");
/* Делаем что-нибудь */
__asm__("popad; leave; iret"); /* ЧЁРНАЯ МАГИЯ! */
}
Ассемблерный код будет выглядеть примерно так:
push %ebp
mov %esp,%ebp
sub $<размер локальных переменных>,%esp
pushad
# C-код где-то здесь
popad
leave
iret
leave # мёртвый код
ret # мёртвый код
Это предполагает, что leave является правильной обработкой конца функции - вы выполняете код возврата функции "вручную", а обработку, сгенерированную компилятором, оставляете как "мертвый код". Излишне говорить, что такие предположения о внутренних компонентах компилятора опасны. Этот код может сломаться на другом компиляторе или даже на другой версии того же компилятора. Поэтому он настоятельно не рекомендуется и указан только для полноты картины.
# Assembly Goto
Начиная с версии 4.5, GCC поддерживает оператор "asm goto". Он может быть использован для создания ISR в качестве функций, которые возвращают правильный адрес точки входа ISR.