Данная статья написана в форме поста для блога. Если она окажется вам интересной, то будет продолжение.
Последние четыре месяца посвящаю свободное от работы время написанию игрушечной ОС для 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());
Вот и всё что я хотел рассказать о
Only registered users can participate in poll. Log in, please.
Продолжать?
88.59% Да1227
11.41% Нет158
1385 users voted. 294 users abstained.