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


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

    Последние четыре месяца посвящаю свободное от работы время написанию игрушечной ОС для 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());
    

    Вот и всё что я хотел рассказать о Вьетнаме обработке прерываний.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Продолжать?

    Поделиться публикацией

    Похожие публикации

    Комментарии 17

      +2
      Спасибо! Конечно же продолжайте!
        0
        Несомненно продолжайте, изложено все очень и очень доступно.
          –26
          Вообще все просто и понятно!)))
          Видите сколько много комментариев?!
          У кого есть вопросы по статье — задавайте.
          Автор, думаю, с удовольствием на них ответит.
          PS: я точно понял две вещи: 1.автору — РЕСПЕКТ!!!, 2. мой мозг еще не готов усваивать этот левел.
            +1
            выдыхайте :)
              –2
              Ну да… 2-й пункт — действительно неверный. Уже готов! )))
              –2
              Заминусовали. Бугагага
              +1
              у меня такой вопрос: а разве компилятор не сохраняет большинство изменяемых регистров самостоятельно при входе в функцию (кроме регистров общего назначения)?
              А то получается, что при каждом вызове другой функции он должен всё сохранять, но не встречал такого в дизассемблере. Т.е. достаточно ли дополнительно сохранить EAX...EDX, а не всё-всё-всё?

              Еще одная интересная статья по входу в прерывания
                0
                Обычно функция сохраняет в стеке лишь те регистры, которые использует, чтобы вернуть старые значения перед выходом.
                  +2
                  Некоторые регистры функция может свободно изменять. Это чётко прописано в ABI. Например, для x86_64 читаем:

                  Registers %rbp, %rbx and %r12 through %r15 “belong” to the calling function and the called function is required to preserve their values. In other words, a called function must preserve these registers’ values for its caller. Remaining registers “belong” to the called function. If a calling function wants to preserve such a register value across a function call, it must save the value in its local stack frame.
                  0
                  В случае с прерываниями по-любому придётся сохранять регистры, чтобы можно было единообразно их изменять/копировать, например, во время планирования.
                  0
                  Мне вот интересен вопрос безопасности через виртуализацию. Разве это не более затратная операция чем переключение контекстов? Одно дело, когда виртуализируешь что-либо сознательно и готов мириться с некоторыми потерями производительности, но сделать это частью архитектуры системы — мне это кажется дерзким ходом :)
                    –16
                    А откуда возьмутся приложения под Вашу ОС?
                      +3
                      а вот автор возьмет, и posix level реализует — и эта «проблема» отпадет;)))
                      +2
                      Есть ещё такой интересный проект Skelix OS
                      Он куда-то пропал на долгое время а недавно снова вернулся.
                        0
                        Хочу использовать Вашу ОС как пример для своих студентов. Можно ли получать от Вас информационную поддержку по некоторых вопросах.
                          +2
                          Легко. Только пока это совсем не ОС, а, скорее, simple OS-like program.
                          0
                          Позвольте взглянуть на статью с чисто практической точки зрения)
                          Здесь немало людей, которым интересно что-то новое, и которые хотят в первом же абзаце услышать простой и ясный ответ на: «ради чего это делается? „

                          ну правда, надо бы попросить каждую статью от всех авторов начинать ответами на стандартные вопросы:

                          1.я — профессионал/любитель, у меня была вот такая конкретная проблема. я хотел сделать стандартными средствами, смотрел здесь и здесь, но не нашел аналогов или меня они не устроили по этим и этим причинам. Поэтому выкладываю для конструктивной критики свою поделку.
                          2.это вот такая штука. она нужна для вот этого.
                          конец первого абзаца.

                          под катом:
                          1.может применяться вами еще вот здесь и здесь.
                          2.работает вот так. она справляется настолько-то успешно. сравнение моей поделки и уже существующих аналогов.
                          3.а вот это и это осталось реализовать.
                          4.опрос “как считаете, это нужная вещь? вы будете ее покупать?»

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое