Что делать, если прерывания срабатывают неправильно и вместо ожидаемых нормальных обработчиков вы получаете «кривые» срабатывания?

Меня крайне достали прерывания в моей ОС — уже несколько недель пытаюсь их починить, но без успеха. Ранее я задавал вопрос: тогда ещё была проблема с графикой, которую удалось исправить, перейдя на Graphic Output Protocol (GOP) и UEFI. Сейчас я работаю только в QEMU: при загрузке система вроде бы запускается, но экран остаётся чёрным, а графика отрисовывается только после нажатия любой клавиши, кроме ESC (ESC я привязал к перезагрузке). При этом ESC срабатывает только тогда, когда экран чёрный. Чёрный экран сам по себе тоже проблема. Нужен взгляд специалиста по прерываниям — вот мой репозиторий: <a href="https://github.com/larcenkokirill91-max/AnalOS.git" rel="nofollow">https://github.com/larcenkokirill91-max/AnalOS.git</a> . Если несложно, пришлите, пожалуйста, кусок кода, который следует заменить.
  • 10 июля 2026 г. 14:01
Ответы на вопрос 1
Коротко — то, что вы описываете, почти классический набор симптомов «криво настроенного IDT / ISR / PIC»: прерывания попадают не туда, где вы ждёте, стёртые/неправильно сохранённые регистры разрушают состояние, EOI не отправляется (или отправляется неправильно), и поэтому поведение (особенно видео/клавиатура) выглядит очень странно. Точного диагноза без просмотра кода дать нельзя, но ниже — практические шаги для отладки и правка «из коробки», которую можно вставить вместо ваших текущих обработчиков/инициализации прерываний. Если не поможет — пришлите сюда (или скопируйте) файлы с реализацией IDT, ISR-ста́бов, PIC-инициализации и обработчиков клавиатуры.

Что проверить сразу (быстро):
- Правильно ли вы ремапите PIC (чтобы IRQ0..IRQ15 не пересекались с исключениями CPU).
- Правильно ли заполнены записи IDT (селектор сегмента кода — тот же, что в GDT, обычно 0x08; тип — interrupt gate 0x8E).
- Используется ли iretq в x86_64 (в 64-битном режиме), а не iret.
- Сохраняются ли регистры в ISR и соблюдается ли выравнивание стека перед вызовом C-кода (лучше использовать атрибуты компилятора для ISR).
- Отправляете ли вы EOI на PIC (и на мастер, если IRQ >= 8).
- Не вызываются ли прерывания до того, как настроен корректный стек/TSS/IST (если используете их).

Рекомендую временно добавить выводы в serial (COM1) или QEMU -serial stdio, и включить лог прерываний в QEMU (qemu -d int) — это очень помогает понять, какие векторы приходят.

Готовая «замена», которую можно попробовать вставить (предполагается x86_64, компилятор GCC/clang). Это минимальный, корректный набор: ремап PIC, таблица IDT, корректная запись для IRQ клавиатуры с использованием атрибута interrupt (компилятор сам сгенерирует переходы и iretq):

1) Низкоуровневые порты (inb/outb)
```c
#include <stdint.h>

static inline void outb(uint16_t port, uint8_t val) {
    __asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
}
static inline uint8_t inb(uint16_t port) {
    uint8_t ret;
    __asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
    return ret;
}
```

2) Ремап PIC и EOI
```c
#define PIC1_CMD 0x20
#define PIC1_DATA 0x21
#define PIC2_CMD 0xA0
#define PIC2_DATA 0xA1
#define PIC_EOI 0x20

void pic_remap(int offset1, int offset2) {
    uint8_t a1 = inb(PIC1_DATA);
    uint8_t a2 = inb(PIC2_DATA);

    outb(PIC1_CMD, 0x11);
    outb(PIC2_CMD, 0x11);
    outb(PIC1_DATA, offset1);
    outb(PIC2_DATA, offset2);
    outb(PIC1_DATA, 0x04);
    outb(PIC2_DATA, 0x02);
    outb(PIC1_DATA, 0x01);
    outb(PIC2_DATA, 0x01);

    outb(PIC1_DATA, a1);
    outb(PIC2_DATA, a2);
}

static inline void pic_send_eoi(unsigned irq) {
    if (irq >= 8) outb(PIC2_CMD, PIC_EOI);
    outb(PIC1_CMD, PIC_EOI);
}
```
Вызывайте pic_remap(0x20, 0x28) (тогда IRQ0 -> 0x20, IRQ1 -> 0x21 и т.д.).

3) IDT (x86_64) — структура, установка записи, загрузка IDT
```c
typedef struct {
    uint16_t offset_low;
    uint16_t selector;
    uint8_t ist;
    uint8_t type_attr;
    uint16_t offset_mid;
    uint32_t offset_high;
    uint32_t zero;
} __attribute__((packed)) idt_entry_t;

idt_entry_t idt[256];

struct {
    uint16_t limit;
    uint64_t base;
} __attribute__((packed)) idt_ptr;

void set_idt_gate(int n, void* handler, uint8_t type_attr) {
    uint64_t h = (uint64_t)handler;
    idt[n].offset_low  = h & 0xFFFF;
    idt[n].selector    = 0x08; // ваш селектор кодового сегмента в GDT
    idt[n].ist         = 0;
    idt[n].type_attr   = type_attr; // 0x8E = present, DPL=0, 32/64-bit interrupt gate
    idt[n].offset_mid  = (h >> 16) & 0xFFFF;
    idt[n].offset_high = (h >> 32) & 0xFFFFFFFF;
    idt[n].zero        = 0;
}

void load_idt(void) {
    idt_ptr.limit = sizeof(idt) - 1;
    idt_ptr.base  = (uint64_t)&idt;
    __asm__ volatile ("lidt %0" : : "m"(idt_ptr));
}
```

4) Обработчик клавиатуры — используя атрибут interrupt (GCC/clang создаст корректный пролог/эпилог с iretq)
```c
struct interrupt_frame { uint64_t rip, cs, rflags, rsp, ss; };

void keyboard_handler_logic(uint8_t scancode) {
    // Здесь ваша логика обработки scancode: перевод в символ, обновление буфера, рисование и т.д.
}

__attribute__((interrupt))
void keyboard_isr(struct interrupt_frame *frame) {
    uint8_t sc = inb(0x60);
    keyboard_handler_logic(sc);
    pic_send_eoi(1); // IRQ1 = клавиатура
}
```

5) Инициализация (примерный порядок)
```c
void irq_init(void) {
    // remap PIC
    pic_remap(0x20, 0x28);

    // замена записи в IDT для IRQ1 (0x21)
    set_idt_gate(0x21, keyboard_isr, 0x8E);

    // разглушиваем только клавиатуру (пример) — снимаем маску для IRQ1
    uint8_t mask = inb(PIC1_DATA);
    mask &= ~(1 << 1); // снять маску с IRQ1
    outb(PIC1_DATA, mask);

    load_idt();

    // включаем прерывания
    __asm__ volatile ("sti");
}
```

Почему это часто решает проблему
- PIC ремапит IRQ-номера так, чтобы они не попадали в область исключений 0..31.
- Правильная структура IDT и корректный селектор предотвращают «прыжки» в неправильный сегмент/код.
- Использование атрибута interrupt избавляет от ошибок в ручных ASM-ста́бах (неправильный iretq / потеря регистров / выравнивание стека).
- Отправка EOI на PIC предотвращает «залипание» IRQ и последующие странные побочные эффекты.

Что ещё проверить/если не поможет
- Убедитесь, что GDT и селектор кода (0x08) действительно установлены до загрузки IDT. Неверный селектор приводит к переходу в произвольный CS.
- Если вы используете APIC (Local/APIC), не используйте PIC-ремапинг — требуется иная инициализация.
- Если вы оказываете доступ к графике через UEFI/GOP, но теперь запускаете в BIOS-подобном окружении — убедитесь, что вы не рассчитываете на UEFI-стек/сервисы. Иногда переход из UEFI в чистый режим ломает окружение.
- Включите лог прерываний в QEMU: qemu-system-x86_64 -d int — это покажет, какие векторы приходят и когда.
- Добавьте вывод в serial (COM1) во всех ключевых точках: init start/end, при входе в ISR, при чтении scancode — это даст картину.

Если хотите, я могу:
- Просмотреть ваш код, если вы скинете сюда файлы (например: файл с заполнением IDT, asm-ста́бы, pic.c, keyboard.c).
- Подправить прямо в вашем репозитории конкретные куски — пришлите ссылки на файлы/пути в репозитории, которые нужно заменить, или скопируйте сюда содержимое текущих реализаций.

Если согласны — пришлите (в этом чате) содержимое ваших файлов:
- src/idt.c (или где у вас IDT),
- src/interrupts.S (или asm-ста́бы),
- src/pic.c,
- src/keyboard.c
и я подготовлю «патч» (файлы для замены).
Похожие вопросы