Series directory

  • introductory
  • The preparatory work
  • BIOS starts in real mode
  • GDT and Protected Mode
  • Exploration of Virtual Memory
  • Load and enter the kernel
  • Display and print
  • The global descriptor table GDT
  • Interrupt handling
  • Virtual memory improvement
  • Implement heap and malloc
  • The first kernel thread
  • Multithreading switch
  • The lock is synchronized with multithreading
  • Implementation of a process
  • Enter user mode
  • A simple file system
  • Load the executable program
  • Implementation of system calls
  • The keyboard driver
  • To run a shell

interrupt

Interrupts play a very important role in the CPU, the response to the hardware, task switching, exception processing are inseparable from the interrupt. It is not only the driving force of all things, but also the source of all evils that bring us all kinds of pain.

The problem of interrupts runs through the whole kernel, and its difficulty lies in its disruption of the flow of code execution and its unpredictability. This is just a preliminary framework for interrupt processing, which will always follow you, and it is also a touchstone to test Kernel design and implementation.

Concept to prepare

There is often confusion about interrupt, exception, hard interrupt, soft interrupt and so on, and the use of these terms in both Chinese and English is also somewhat inconsistent. For the sake of consistency, let’s make a statement about the use of terms:

  • The term interrupt is used as a general concept, that is, it includes all types of interrupts and exceptions;

Then, under the general concept of interrupt, the following classification is made:

  • Exceptions (exceptionInternal interrupt, it is the internal execution of the CPU encountered error, in English expression it hasexception.fault.trapAnd so on several categories, we generally all collectively calledexception; Such problems are generally not maskable and must be dealt with;
  • Hard interrupts (interrupt) : External interrupts are usually sent by other hardware devices, such as clock, hard disk, keyboard, network card, etc., which can be shielded;
  • Soft interrupt (soft int) : Strictly speaking this is not an interruption, as this is byintSystem call is the most commonly used method of actively triggering instructions, which is the way that users actively request to enter the kernel state. Its handling mechanism is the same as any other interrupt, so it is included in the interrupt.

We then use the English word exception to refer to the first type, CPU internal exceptions and errors, which is not ambiguous. The word interrupt is used specifically for the second class, the hard interrupt, which is the original usage in Intel documentation; As for the third category, we can ignore it because we don’t need to talk about it yet;

As for the Chinese word interrupt, which we use to refer to all of the above types, it is a big concept, and we do not equate it with the word interrupt.

Note that this is purely my personal usage and regulations, just for the convenience of the expression and understanding of the following terms unity.

Interrupt descriptor table

The reason the word Interrupt is ambiguous, I think, is because all of these things are managed in the IDT (Interrupt Descriptor Table), which makes it seem like interrupts are outside the context of the word Interrupt itself, Include exception as well. That’s why I want to use the Chinese word interrupt for the overall concept, and the English word interrupt and exception for the two subconcepts below it.

IDT table item

Returing to the interrupt descriptor table IDT, its main purpose is to define the various interrupt handler functions. The structure of each entry is defined as follows:

struct idt_entry_struct {
  // the lower 16 bits of the handler address
  uint16 handler_addr_low;
  // kernel segment selector
  uint16 sel;
  // this must always be zero
  uint8 always0;
  // attribute flags
  uint8 attrs;
  // The upper 16 bits of the handler address
  uint16 handler_addr_high;
} __attribute__((packed));
typedef struct idt_entry_struct idt_entry_t;

Code links in SRC/interrupt/interrupt. J h, about IDT document can reference here.

Please refer to the above documents for the meaning of each field, and here are two of the more important fields:

  • sel: that is,selectorThis specifies where the interrupt handler function residessegment. Enter interrupt processing after the CPU’scodeSegment registercsIt’s going to be replaced with that value, so this selector has to point tokernelcode segment;
  • DPL: These are the two bits in the ATTRS field that specify the minimum CPU privilege required to be able to call, or access, this handler. For us, we have to make it privileged level 3, which is user level, otherwise we won’t be able to access interrupts in user mode;

Of course, one of the most important things missing from the IDT entry above is the address of the interrupt handler, which we’ll talk about later.

Build the IDT

Then you can define the IDT structure, the code in the SRC/interrupt/interrupt. C:

static idt_entry_t idt_entries[256];

There are 256 entries reserved, which is more than enough to meet our needs.

The first 0 to 31 of these are reserved for exceptions. Starting at item 32, it is used as the interrupt handler. Each interrupt has an interrupt number, which corresponds to the entry in the IDT table. The CPU finds the interrupt handler and jumps to it.

Exception is the following, which I took directly from the wiki:

The 14th page faualt, or page missing exception, will be addressed in the next article on virtual memory improvement. The other Exceptions do not need our attention at this time, as they should not normally occur.

IDT is set to interrupt starting with item 32, for example, item 32 is set to timer interrupt.

Interrupt handler function

Let’s go back to the interrupt handler, or interrupt handler, whose address is written in each entry of the IDT above. They don’t exist yet, so we need to define these functions.

Each interrupt handler is of course different, but there are some common tasks that need to be done before and after entering and leaving these handlers, which are to save and restore the scene before the interrupt occurred, or context. These include various registers, They will be stored in the stack. So the interrupt process looks something like this:

save_context();
handler();
restore_context();

It’s important to note that the saving and restoring of the context is done by the CPU and us, that is, the CPU automatically pushes some registers onto the stack, and we push some registers and other information onto the stack as needed. Together, these two parts make up the interrupt context.

Interrupts the CPU stack

First, let’s look at the registers the CPU automatically pushes into the stack:

Here are two situations:

  • If an interrupt is entered from kernel mode, only the three values in the left figure will be pushed;
  • If the interrupt is coming from user mode, the CPU will push the five values in the figure on the right.

The difference between the two is User SS and User ESP at the top.

The purpose of what the CPU does is clear. It stores the context state of the instruction execution stream. There are two core elements here:

  • Before the interrupt occursinstructionLocation (cseipProvide);
  • Before the interrupt occursstackLocation (ssespProvide);

But why is it necessary to push the current SS and ESP only when a privileged transition occurs (user mode goes to kernel mode)? Because user mode code execution and kernel mode code are carried out in different Stacks, interrupt processing from user mode needs to be transferred to kernel Stack. When the interrupt processing is finished, it will return to the user’s stack, so it is necessary to save the user’s stack information.

The following diagram shows how the stack jumps when an interrupt occurs in user mode:

If the interrupt occurs in the kernel state, the situation becomes much easier, because in the kernel stack, the interrupt is still executed in the same stack, so it behaves a bit like a normal function call (with a slight difference of course) :

We can see that CPU is only responsible for saving registers related to instruction and stack, but does not save registers related to data, which mainly includes several general registers: EAX, ECX, EDX, EBX, ESI, EDI and EBP. Of course, there are several data registers DS, ES, FS, GS, etc.

Why doesn’t the CPU care about these registers? I don’t really know, but it’s a CPU architecture decision. My personal understanding is that the CPU is an instruction executor. It only cares about the flow of instruction execution, which includes the two core elements of instruction and stack. As for data, the upper logic, the logic of the code itself, is responsible for managing it. The design concept here, in fact, is to divide the logic of hardware and software. In fact, it is difficult to define, but also can be understood as a legacy of history, so decided from the very beginning.

The interrupt handler

But anyway, let’s go back to breaking the context. Since the CPU does not save the registers associated with the data, it is up to us to save them.

Let’s look at the interrupt handler code from the top down. First, each interrupt obviously has its own interrupt handler, or ISR (interrupt service routine) :

isr0
isr1
isr2
...

Each ISR * has a common structure, which is defined using the ASM macro syntax:

; exceptions with error code pushed by CPU
%macro DEFINE_ISR_ERRCODE 1
  [GLOBAL isr%1]
  isr%1:
    cli
    push byte %1
    jmp isr_common_stub
%endmacro

; exceptions/interrupts without error code
%macro DEFINE_ISR_NOERRCODE 1
  [GLOBAL isr%1]
  isr%1:
    cli
    push byte 0
    push byte %1
    jmp isr_common_stub
%endmacro

Then we can define all ISR * :

DEFINE_ISR_NOERRCODE  0
DEFINE_ISR_NOERRCODE  1
DEFINE_ISR_NOERRCODE  2
DEFINE_ISR_NOERRCODE  3
DEFINE_ISR_NOERRCODE  4
DEFINE_ISR_NOERRCODE  5
DEFINE_ISR_NOERRCODE  6
DEFINE_ISR_NOERRCODE  7
DEFINE_ISR_ERRCODE    8
DEFINE_ISR_NOERRCODE  9
...

Why are there two definitions of ISR? In addition to storing information about instruction and stack, the CPU will also push an error code for certain exceptions. As to which Exceptions are pushed into the Error Code, you can refer to the table shown above.

Because of this weird inconsistency, for the sake of consistency, we manually add a 0 for exceptions that would not be pushed into the error code.

So to summarize, ISR is the main entry point for interrupt handling, and it does these things:

  • Shut downinterruptNote that this can only mask hard interrupts forexceptionIs invalid;
  • Press in the interrupt number;
  • Jump into theisr_common_stub;

Go to isr_common_stub, where the code is in SRC /interrupt/ idt.s. This is where the data-related register context is saved and restored, and where the actual interrupt handling takes place.

Here’s the first half of it:

[EXTERN isr_handler]

isr_common_stub:
  ; save common registers
  pusha

  ; save original data segment
  mov ax, ds
  push eax

  ; load the kernel data segment descriptor
  mov ax, 0x10
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax

  call isr_handler

And then the second half of it:

interrupt_exit:
  ; recover the original data segment
  pop eax
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax

  popa
  ; clean up the pushed error code and pushed ISR number
  add esp, 8

  ; make sure interrupt is enabled
  sti
  ; pop cs, eip, eflags, user_ss, and user_esp by processor
  iret

There is really only one function isr_common_stub (there is no ret in the first half). Interrupt_exit is just a sign I added because it will be used somewhere else. Of course, in fact, there was no function in the assembly of the concept, essentially are just markers.

We saw that the first half did a couple of things:

  • pushaAll universal registers are saved;
  • Then save the DATA section registerds;
  • Change the data section register to kernel and call the real interrupt handling logicisr_handler(More on that later);

After the interrupt is processed, the recovery phase of the second half does the following things, which is essentially the reverse of the first half:

  • Revert todataSegment register;
  • popaRestore all universal registers;
  • [Fixed] Skip error codes and interrupt numbers on the stack
  • Restore interrupt and return;

So we can draw the stack with the full interrupt occurring, where the green part is the saved interrupt context, including both the CPU’s automatic push and our own push:

The actual core interrupt handling function isr_handler, written in C, is in pink at the bottom:

typedef void (*isr_t)(isr_params_t);

void isr_handler(isr_params_t regs);

The isr_params_t structure is defined as:

typedef struct isr_params {
  uint32 ds;
  uint32 edi, esi, ebp, esp, ebx, edx, ecx, eax;
  uint32 int_num;
  uint32 err_code;
  uint32 eip, cs, eflags, user_esp, user_ss;
} isr_params_t;

The reason isr_handler can use this structure is because the green part of the figure is pressed, and then by calling isr_handler, the green part corresponds exactly to the isr_params_t structure. The red arrow points to the address of the parameter isr_params_t.

With isr_params_t, we can retrieve all the information about interrupts in isr_handler:

void isr_handler(isr_params_t params) {
  uint32 int_num = params.int_num;

  // ...

  // Bottom half of interrupt handler - now interrupt is re-enabled.
  enable_interrupt();

  // handle interrupt
  if (interrupt_handlers[int_num] != nullptr) {
    isr_t handler = interrupt_handlers[int_num];
    handler(params);
  } else {
    monitor_printf("unknown interrupt: %d\n", int_num);
    PANIC();
  }
}

The first half is all about the necessary interaction between the CPU and the interrupt peripheral chip, which you can ignore for now. Isr_handler interrupt handling entry as a general, it is actually a distribution function, it will be based on the interrupt number to find out the real corresponding interrupt handler, these functions we defined in the array of interrupt_handlers:

static isr_t interrupt_handlers[256];

They are set by the register_interrupt_handler function:

void register_interrupt_handler(uint8 n, isr_t handler) {
  interrupt_handlers[n] = handler;
}

The above code is all in SRC /interrupt/, and it’s not much but it’s a bit of a detour that should make sense if you read it carefully.

Turn on clock interrupt

That’s all theory. We need a real break to get the effect out of the way. The ideal interrupt is, of course, a timer interrupt, which is the core interrupt that drives multitasking later. The code to initialize the timer, which is described in SRC /interrupt/timer.c, is mainly related to the hardware port, setting the clock rate and, most importantly, registering the interrupt handler:

register_interrupt_handler(IRQ0_INT_NUM, &timer_callback);
  • IRQ0_INT_NUMThat’s 32, that’s the clock interrupt number;
  • timer_callbackWe can simply do the printing process:
static uint32 tick = 0;

static void timer_callback(isr_params_t regs) {
  monitor_printf("tick = %d\n", tick++);
}

Then you can try to verify:

int main() {
  init_gdt();

  monitor_clear();

  init_idt();
  init_timer(TIMER_FREQUENCY);
  enable_interrupt();

  while (1) {}
}

Run Bochs, and if you’re lucky you’ll see this:

Trigger the exception

A timer is a hard interrupt. Let’s look at an example of an exception, such as page fault, with an interrupt number of 14:

register_interrupt_handler(14, page_fault_handler);
void page_fault_handler(isr_params_t params) { monitor_printf("page fault! \n"); }

How do I cause a Page Fault? Simply access a virtual address that is not mapped in the page table.

int main() {
  init_gdt();

  monitor_clear();

  init_idt();
  register_interrupt_handler(14, page_fault_handler);
    
  int* ptr = (int*)0xD0000000;
  *ptr = 5;

  while (1) {}
}

Run Bochs, and if you’re lucky you’ll see this:

You can see that it’s printing all the time because our page fault handler does nothing but print, it doesn’t really resolve the page fault. When the processing is complete, the CPU will attempt to execute the same memory access instruction that caused the page fault again, so the page fault will be triggered again. For the actual handling of page faults, we’ll leave it to the next article, Virtual Memory.