📜 ⬆️ ⬇️

I write toy OS (about interruptions)


This article is written in the form of a post for a blog. If it turns out to be interesting to you, it will be continued.

The last four months have been devoting my free time to writing a toy OS for x86_64. The source code is here .

The general idea (so far very far from implementation) is the following: a single 64-bit address space with ever-living threads (like in Phantom OS); virtual machine that provides security code execution. Currently implemented:
')
1. kernel boot using multiboot loader (GRUB);
2. text VGA mode (16 colors, kprintf);
3. simple interface to customize the display of pages;
4. ability to handle interrupts in C;
5. identification of the processor topology (sockets, cores, threads) and their launch;
6. A working prototype of the crowding out SMP scheduler with priority support;

Let us skip the description of multiboot-boot and work with VGA-mode (I didn’t write about this, maybe, lazy). I don’t want to write about displaying pages either, I'm afraid it will be boring (maybe another time). Let's talk about interrupt handling.

Typically, interrupt handlers, like any other critical code, are written in assembly language. I don’t really like assembler, preferring to write as much code as possible on C. Therefore, I made several macros that allow you to conveniently write interrupt handlers on C. Of course, this solution negatively affects performance, but the power of modern computers allows this luxury (we take the brackets out real time).

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



Actually, this picture corresponds to protected mode (I did not find a quality picture for long mode), but, apart from small details, the principle is absolutely the same. The remaining user thread registers remain intact, so the handler must save them on the stack. Since our handler is written in C, we have to save a full 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 will lose many optimizations, in the second case, we generally level out the benefits of writing handlers in C, since we will not be able to use any standard functions. So, use the fxsave and fxrstor instructions to quickly save / restore the 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 the error_code is the 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 easy writing of 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) \ DEFINE_INT_HANDLER(name) 

The first macro defines the signature of the handler function. The second is a wrapper that preserves and restores registers. This scheme allows you to call a single handler function for multiple interrupts. I use this for standard errors when several interrupts dump a stack frame. As can be seen from the code, the handler accepts an additional data argument; accordingly, different interrupts can transfer their data to one handler. Finally, the last macro for the abbreviated spelling of the pair: handler + wrapper, when the handler is sharpened for 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 attaching 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 you about interrupt handling in Vietnam .

Source: https://habr.com/ru/post/175749/


All Articles