I am writing a toy OS (about interrupts)

    This article is written as a blog post. If you find it interesting, then there will be a sequel.

    For the past four months, I have been devoting free time to writing a toy OS for x86_64. The source code is here .

    The general idea (so far quite far from implementation) is as follows: a single 64-bit address space with eternally living threads (like Phantom OS); A virtual machine that provides security for code execution. Currently implemented:

    1. loading the kernel using a multiboot loader (GRUB);
    2. text VGA mode (16-colors, kprintf);
    3. simple interface for displaying pages;
    4. the ability to handle interrupts in C;
    5. identification of processor topology (sockets, cores, threads) and their launch;
    6. a working prototype of the preemptive SMP scheduler with priority support;

    Let's skip the description of multiboot-loading and working with VGA-mode (I didn’t write about this, except, lazy). I don’t want to write about page display either, I'm afraid it will be boring (maybe another time). Let's talk about interrupt handling better.

    Typically, interrupt handlers, like any other critical code, are written in assembler. I don’t really like assembler, preferring to write as much code as possible in C. Therefore, I made several macros that make it convenient to write interrupt handlers in C. Of course, this solution negatively affects performance, but the power of modern computers allows such a luxury (we put the brackets of the system real time).

    At the time of interruption in long mode, the processor forms a frame in the handler stack (this can be either a custom or a separately selected stack) containing the saved registers:

    Actually, this picture corresponds to protected mode (I did not find a high-quality picture for long mode), but, apart from small details, the principle is absolutely the same. The remaining registers of the user stream remain intact, so the handler must save them on the stack. Since our handler is written in C, we have to save a complete set of registers, including 512 bytes of FPU / MMX / SSE. Of course, you can prevent the compiler from generating SIMD code for the entire kernel, or only for functions that work inside interrupts. In the first case, we lose many optimizations, in the second - we generally neutralize the benefit of writing handlers in C, since we will not be able to use any standard functions. So, we use the fxsave and fxrstor instructions to quickly save / restore FPU / MMX / SSE registers.

    Here is the structure of our stack frame:

    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;

    The first part of the fields before error_code is manually saved registers, the second is the registers automatically saved by the processor. The reverse order is due to the fact that the stack grows from top to bottom. Now we define macros for conveniently writing handlers.

    #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)     \

    The first macro defines the signature of the handler function. The second is a wrapper that saves and restores registers. Such a scheme allows you to call one function handler for several interrupts. I use this for standard errors when multiple interrupts dump the stack frame. As you can see from the code, the handler takes an additional argument, data, respectively, different interrupts can pass their data to one handler. Finally, the last macro for abbreviated spelling of a pair: handler + wrapper, when the handler is sharpened by one single interrupt.

    A wrapper is a function that returns a pointer to the beginning of the processing code located in its own body. Read more about this trick here .

    As a result, writing a handler and binding it to an interrupt becomes a trivial task:

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

    That's all I wanted to tell about Vietnam handling interrupts.

    Only registered users can participate in the survey. Please come in.


    • 88.6% Yes 1223
    • 11.3% No 157

    Also popular now: