UCORE operating system experiment notes – re understanding interrupt

Time:2019-12-26

In the last article, Lab 1, the experimental notes of UCORE operating system, I have recorded the use of interrupt in detail. The focus of that article on interrupts is how to use IDT, interrupt descriptors, interrupt vector tables, and so on. In this article, I will focus on another area, that is, how to save and restore the scene in the process of interruption.

What will the CPU do after receiving the interrupt signal

  1. After executing each instruction of the current program, the CPU will confirm whether the interrupt controller (such as 8259A) sends the interrupt request during the execution of the previous instruction. If so, the CPU will read the interrupt vector corresponding to the interrupt request from the bus when the corresponding clock pulse arrives;

  2. The CPU finds the interrupt descriptor corresponding to the interrupt vector in the IDT according to the obtained interrupt vector (based on this index). The segment selector of the interrupt service routine is stored in the interrupt descriptor;

  3. CPU uses the segment selector of interrupt service routine found by IDT to obtain the corresponding segment descriptor from GDT. The segment descriptor stores the segment base address and attribute information of interrupt service routine. Then CPU gets the starting address of interrupt service routine and jumps to the address;

  4. According to the DPL information of the segment descriptor of CPL and interrupt service routine, the CPU will confirm whether the privilege level conversion has occurred. For example, if the current program is running in the user state and the interrupt program is running in the kernel state, it means that there is a privilege level conversion. At this time, the CPU will obtain the kernel stack address of the program from the TSS information of the current program (the starting address of the information in the memory is in the TR register), which includes the values of SS and ESP in the kernel state, and immediately switch the stack currently used by the system to New kernel stack. This stack is the one to be used by the interrupt service program. Then, the user state SS and ESP used by the current program are pressed into the new kernel stack to be saved;

  5. The CPU needs to start saving the current interrupted program’s field (i.e. the value of some registers), so that the interrupted program can be resumed in the future. The kernel stack should be used to save the relevant field information, that is, to press in the EFLAGS, CS, EIP, errorcode (if there is an exception with an error code) information used by the interrupted program;

  6. The CPU uses the segment descriptor of the interrupt service routine to load the address of its first instruction into the CS and EIP registers to start executing the interrupt service routine. This means that the previous program is suspended and the interrupt service program officially starts to work.

The above contents are directly excerpted from UCORE experimental instruction. In the previous article, I focused on the first three steps and the last step. In this article, I will focus on steps 4 and 5.

Detection of privilege level conversion

I personally think step 4 and step 5 should happen before the CPU jumps to ISR (interrupt service routine), so it is more appropriate to put step 3 after step 5, and then I will explain why I think so. When the CPU obtains the interrupt descriptor in the IDT, it will check the conversion of privilege level, as shown in the following figure:

UCORE operating system experiment notes - re understanding interrupt

When the CPU obtains the interrupt descriptor, the CPU will compare the DPL of the interrupt descriptor with the CPL of the current segment selector to determine whether the privilege level conversion is needed. At the same time, it will do some column detection work, for example, for hard interrupts, CPL must be greater than or equal to DPL, because privilege level is converted to higher privilege level or even level. For soft interrupts, the privilege level after conversion cannot exceed the privilege level before conversion, which is to prevent user code from triggering interrupts at will. For different situations of CPL and DPL, we need to use TSS to switch the kernel stack. I will open a separate article on TSS later.

Changes in the kernel stack

An important function of step 4 and step 5 is to press various registers into the inner core stack. Pressing these registers can not only save the scene, but also let ISR know all kinds of interrupt information, so these two steps are very important. Let’s see which registers the CPU must push into the kernel stack:

UCORE operating system experiment notes - re understanding interrupt

This is a schematic diagram of stack space changes after the privilege level conversion. There are two differences for the interrupts without the privilege level conversion. First, it only uses one stack, that is, the procedure and handler use the same stack; second, the CPU does not need to press SS and esp. In addition, CS, EIP and error code (if any) are required for both cases. The reason why I said that step 3 should be after step 5 is here. If you jump to ISR first, then the pushed EIP is the EIP in ISR, not the EIP before interruption. Therefore, we should complete steps 4 and 5 before step 3.

Trapframe and ISR

In addition to the various registers to be pressed in by the CPU, we also need to press in some other registers to save the field and provide ISR interrupt information. In UCORE, we use the structure trapframe to pass the saved registers to ISR. Let’s take a look at trapframe:

/* registers as pushed by pushal */
struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;          /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

The registers in pushregs are all the registers in pushal that need to be pushed into the stack. With this data structure, we can obtain the interrupt information after the interrupt and pass it to ISR, which will perform the corresponding operations according to the incoming trapframe.
Let’s see how to assign trapframe and pass trapframe to ISR:

.globl vector2
vector2:
  pushl $0
  pushl $2
  jmp __alltraps

The above code is interrupt vector 2. In step 6, the CPU will execute the instructions here. It first pushes in 0 and 2. 0 is the error code (for interrupts without error code, ISR will press in 0 as the error code; if there is error code for interrupts, 0 will not be pressed here). 2 is the interrupt vector number. Note that before that, the CPU has pressed EFLAGS, CS, EIP and error code (if any). After pressing error code and interrupt vector number, CPU will jump to \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\. Trap () will process the interrupt according to the contents of trapframe.

.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame
    # therefore make the stack look like a struct trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

This code pushes all the registers that the interrupt needs to save into the kernel stack.

 # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

This code sets the data segment and additional segment at this time as the data segment of the kernel (ISR is located in the kernel).

 # push %esp to pass a pointer to the trapframe as an argument to trap()
    pushl %esp

    # call trap(tf), where tf=%esp
    call trap

This code first pushes the value of% ESP into the kernel stack, and the value of% ESP will be used as the parameter of function trap(), then we call trap. By pressing the information of various registers into the stack and taking the address at the top of the stack as the address of trapframe, we complete the assignment of trapframe. After the trap() function receives the trapframe, it can make corresponding processing according to the interrupt type. Let’s look at the situation in the stack at this time:

UCORE operating system experiment notes - re understanding interrupt

Because the stack grows from high address to low address, the blue part of the stack has the highest EFLAGS address and the lowest EDI address. This is also consistent with the elements in trapframe. The tf_eflags address is the highest (how not to consider tf_esp, tf_ss), while the reg_edi address is the lowest. So we can use the old ESP address to treat the blue part of the stack as trapframe.

# pop the pushed stack pointer
    popl %esp

    # return falls through to trapret...
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

When trap () is finished, we need to restore the register to the state before the interrupt. Here, we just need to pop up the contents of the kernel stack and save them to the corresponding registers. Finally, EIP, CS and EFLAGS are recovered by calling the IRET instruction. If there is privilege level conversion, we need to pop up SS and ESP saved before. So far, the whole process of interruption is over.

Recommended Today

Redis (1)

Redis About redis Remote dictionary server (redis) is a key value storage system. Redis is an open source log type, key value database written in ANSI C language, complying with BSD protocol, supporting network, memory based and persistent, and providing API in multiple languages. It is often referred to as a data structure server, because […]