Как стать автором
Обновить

Пишу игрушечную ОС (о прерываниях)

Время на прочтение4 мин
Количество просмотров50K

Данная статья написана в форме поста для блога. Если она окажется вам интересной, то будет продолжение.

Последние четыре месяца посвящаю свободное от работы время написанию игрушечной ОС для x86_64. Исходный код лежит здесь.

Общая задумка (пока весьма далёкая от реализации) следующая: единое 64-битное адресное пространство с вечно живущими нитями (как у Phantom OS); виртуальная машина, обеспечивающая безопасность исполнения кода. На данный момент реализованы:

1. загрузка ядра при помощи multiboot-загрузчика (GRUB);
2. текстовый VGA-режим (16-цветов, kprintf);
3. простой интерфейс настройки отображения страниц;
4. возможность обработки прерываний на C;
5. идентификация топологии процессоров (сокеты, ядра, потоки) и их запуск;
6. работающий прототип вытесняющего SMP-планировщика с поддержкой приоритетов;

Пропустим описание multiboot-загрузки и работы с VGA-режимом (об этом не писал, разве что, ленивый). Про отображение страниц тоже не хочу писать, боюсь это будет скучно (может, в другой раз). Давайте лучше поговорим об обработке прерываний.

Обычно обработчики прерываний, как любой другой критичный код, пишутся на ассемблере. Не очень люблю ассемблер, предпочитая как можно больше кода писать на C. Поэтому я сделал несколько макросов, позволяющих удобно писать обработчики прерываний на С. Конечно же, такое решение негативно сказывается на производительности, но мощность современных компьютеров позволяет такую роскошь (выносим за скобки системы реального времени).

В момент прерывания в long mode процессор формирует в стеке обработчика (это может быть как пользовательский, так и отдельно выделенный стек) фрейм, содержащий сохранённые регистры:



Вообще-то, это картинка соответствует protected mode (не нашёл качественную картинку для long mode), но, не считая мелких деталей, принцип абсолютно тот же. Остальные регистры пользовательского потока остаются нетронутыми, поэтому обработчик должен их сохранить в стеке. Поскольку наш обработчик написан на C, то приходится сохранять полный комплект регистров, включая 512 байт FPU/MMX/SSE. Конечно, можно запретить компилятору генерировать SIMD-код для всего ядра или только для функций, работающих внутри прерываний. В первом случае мы лишимся многих оптимизаций, во втором – вообще нивелируем пользу от написания обработчиков на С, так как не сможем пользоваться никакими стандартными функциями. Итак, пользуемся инструкциями fxsave и fxrstor для быстрого сохранения/восстановления регистров FPU/MMX/SSE.

Вот структура нашего стекового фрейма:

struct int_stack_frame {
  uint64_t r15, r14, r13, r12, r11, r10, r9, r8;
  uint64_t rdi, rsi, rbp, rdx, rcx, rbx, rax;
  uint8_t fxdata[512];
  uint32_t error_code;
  uint64_t rip;
  uint16_t cs;
  uint64_t rflags, rsp;
  uint16_t ss;
};

Первая часть полей до error_code – вручную сохранённые регистры, вторая – регистры, автоматически сохранённые процессором. Обратный порядок обусловлен тем, что стек растёт сверху вниз. Теперь определим макросы для удобного написания обработчиков.

#define DEFINE_INT_HANDLER(name)                                        \
  static NOINLINE                                                       \
  void handle_##name##_int(UNUSED struct int_stack_frame *stack_frame,  \
                           UNUSED uint64_t data)

#define DEFINE_ISR_WRAPPER(name, handler_name, data)                 \
  static NOINLINE void *get_##name##_isr(void) {                     \
    ASMV("jmp 2f\n.align 16\n1: andq $(~0xF), %rsp");                \
    ASMV("subq $512, %rsp\nfxsave (%rsp)");                          \
    ASMV("push %rax\npush %rbx\npush %rcx\npush %rdx\npush %rbp\n"); \
    ASMV("push %rsi\npush %rdi\npush %r8\npush %r9\npush %r10");     \
    ASMV("push %r11\npush %r12\npush %r13\npush %r14\npush %r15");   \
    ASMV("movq %%rsp, %%rdi\nmovabsq $%P0, %%rsi" : : "i"(data));    \
    ASMV("callq %P0" : : "i"(handle_##handler_name##_int));          \
    ASMV("pop %r15\npop %r14\npop %r13\npop %r12\npop %r11");        \
    ASMV("pop %r10\npop %r9\npop %r8\npop %rdi\npop %rsi");          \
    ASMV("pop %rbp\npop %rdx\npop %rcx\npop %rbx\npop %rax");        \
    ASMV("fxrstor (%rsp)\naddq $(512 + 8), %rsp");                   \
    void *isr;                                                       \
    ASMV("iretq\n2: movq $1b, %0" : "=m"(isr));                      \
    return isr;                                                      \
  }

#define DEFINE_ISR(name, data)             \
  DEFINE_INT_HANDLER(name);                \
  DEFINE_ISR_WRAPPER(name, name, data)     \
  DEFINE_INT_HANDLER(name)

Первый макрос определяет сигнатуру функции обработчика. Второй – обёртка сохраняющая и восстанавливающая регистры. Подобная схема позволяет вызывать одну функцию-обработчик на несколько прерываний. Я это использую для стандартных ошибок, когда несколько прерываний делают дамп стекового фрейма. Как видно из кода, обработчик принимает дополнительный аргумент data, соответственно, разные прерывания могут передавать свои данные в один обработчик. Наконец, последний макрос для сокращённого написания пары: обработчик + обёртка, когда обработчик заточен под одно единственное прерывание.

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

В результате написать обработчик и привязать его к прерыванию становится тривиальной задачей:

DEFINE_ISR(foo) {
// обычный C-код обработки прерывания
// доступны struct int_stack_frame *stack_frame и uint64_t data
}

set_isr(INT_FOO_VECTOR, get_foo_isr());

Вот и всё что я хотел рассказать о Вьетнаме обработке прерываний.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Продолжать?
88.59% Да1227
11.41% Нет158
Проголосовали 1385 пользователей. Воздержались 294 пользователя.
Теги:
Хабы:
Всего голосов 116: ↑111 и ↓5+106
Комментарии17

Публикации

Истории

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
19 сентября
CDI Conf 2024
Москва
20 – 22 сентября
BCI Hack Moscow
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
24 сентября
Astra DevConf 2024
МоскваОнлайн
25 сентября
Конференция Yandex Scale 2024
МоскваОнлайн
28 – 29 сентября
Конференция E-CODE
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн