Those engaged in kernel research are often familiar with the concept of interrupt. We often come into contact with a lot of terms related to interrupts, which are classified according to software and hardware:

Hardware CPU related:

  • IRQ
  • IDT
  • cli&sti

Software operating system related:

  • APC
  • DPC
  • IRQL

I have always had a little understanding of this part of the interrupt, and how to work together between the operating system and the CPU is also very vague. Recently, I took some time to carefully sort out this piece of knowledge. If it is inappropriate, please point out the expert, thank you in advance!

This article aims to answer the following questions:

  • What is the relationship between IRQ and IRQL?
  • How does Windows virtualize the IRQL interrupt mechanism at the software level
  • Both APC and DPC are software interrupts. Since they are interrupts, where are the processing routines in the corresponding IDT entries?

0x00 Intel 80386 processor interrupt

First of all, let us forget about Windows. Starting with the 80386 processor at the very beginning, let's see how Intel handles interrupts when designing it.

Let's take a look at what this CPU, which was born in 1985, looks like:
在这里插入图片描述
Take a look at the protruding pins, the following is its pin labeling diagram:
在这里插入图片描述
Note the two pins marked with a red circle. These two are the two pins reserved for interrupts by the 80386 processor. Among them, INTR is a maskable interrupt input port, and NMI is a non-maskable interrupt input port.

So how is the interrupt input to the processor? There are so many external devices, and this has only one pin (for the time being only consider the maskable interrupt), here it is necessary to equip the CPU with a secretary that manages the interrupt-the programmable interrupt controller PIC. What work does this secretary need to do? The interrupts of peripheral devices enter the central processing unit from it, so it is responsible for receiving interrupt signals from peripherals and initiating interrupt requests to the CPU according to the priority. The first PIC role was played by a chip code-named 8259A, which looks like this:
在这里插入图片描述
The following is its pin diagram:

在这里插入图片描述
A total of 8 pins IR0-IR7 are responsible for connecting external devices, and each IR port of the 8259A PIC is connected to an IRQ line for receiving interrupt signals from peripherals. INT is responsible for connecting the INTR pin of the CPU to initiate an interrupt request to the CPU. Generally, two 8259A chips are used for cascading, one is connected to the CPU, called the master, and the other is connected to the IR2 pin of the master PIC, called the slave, so that a total of 8+7=15 external chips can be connected. Set up. As shown below:
在这里插入图片描述
In the 8259A, the default priority is that the interrupt request priority of the master IR0 is the highest, the master IR7 is the lowest, and all the interrupt request priorities of the slave IR0-7 are equivalent to IR2. Therefore, the priority of the IRQ line from high to low is IRQ0, IRQ1, IRQ8-15, IRQ3-7. This is the default condition and can be changed programmatically.

There are several important registers inside the 8259a chip:

Interrupt request register: IRR, 8bit, corresponding to IR0-IR7, when the corresponding pin generates an interrupt signal, this bit is set to 1.

Interrupt service register: ISR, 8bit, corresponding to IR0-IR7. When the interrupt corresponding to the pin is being processed by the CPU, this bit is set to 1.

Interrupt mask register: IMR, 8bit, corresponding to IR0-IR7, when the corresponding bit is 1, it means to shield the interrupt signal generated by this pin.

There is also an interrupt priority arbiter: PR. When the interrupt pin has a signal, it combines the IRQ number that generated the interrupt and the current interrupt information recorded in the ISR to decide whether to interrupt the new interrupt according to the priority. The signal is reported to the CPU to generate interrupt nesting.

The following are the peripherals connected to the 15 IRQ lines:
在这里插入图片描述
Now let's take a look at how the secretary works with the CPU.

Now suppose we hit a keyboard button and an interrupt event is generated on the keyboard. This event informs the main PIC through the IRQ1 line. The main PIC sends an electrical signal to the INTR on the CPU side through INT after some internal judgment processing. After the CPU has executed the current instruction, it checks that the INTR has a signal, indicating that an interrupt request is coming, and then checks that the IF in eflags is not zero, which means that the interrupt is currently allowed, and then sends a signal to the PIC's -INTA to tell it to save the current The vector number of the second interrupt is sent over. After the main PIC receives the signal on the -INTA pin, it outputs the interrupt vector number of this interrupt to the data bus through the D0-D7 pins (this simplifies the interaction process, there are actually two INTA signal transmissions). After the CPU gets this number, it can look for an interrupt service routine (ISR) from the IDT for processing. Everyone knows the rest.

How does the interrupt vector number in PIC come from? How do each IRQ correspond to each item in IDT? Here it is decided by the programmability of the interrupt controller.

PIC is called Programmable Interrupt Controller in its entirety, so what aspects of its programmability are embodied? Reference 2 "i8259A Interrupt Controller Analysis One" has a more detailed description, which generally includes the interrupt vector number in the IDT table of the interrupt corresponding to the IRQ line of the designated master-slave chip, the interrupt mode and priority of the 8259a interrupt controller Mode, interrupt nesting mode, interrupt mask mode, interrupt end mode, etc., all of which can be specified by operating system programming. The specific programming format is described in reference material 3 "i8259A Interrupt Controller Analysis Two".

Going back to the previous question, how does the interrupt on the IRQ line correspond to the entry in the IDT? When the operating system is initializing, it will specify the start vector of the PIC chip by programming the 8259a chip (read and write I/O ports) The lower three digits are required to be 0, and the starting vector number is aligned according to 8. The reason for this is that when an interrupt occurs, the lower three digits will automatically fill in the corresponding IRQ number, so that it can be added to the starting vector number It is directly sent to the data bus to be obtained by the CPU. Specifically in Windows, when the system is initialized, the PIC is programmed as follows: Specify the starting interrupt vector number of the master slice as 0x30, and specify the starting interrupt vector number of the slave slice as 0x38. In this way, the 15 peripherals connected through the interrupt controller will be flatly mapped to the range of 0x30-0x40 in the IDT. The hal!HalpInitializePICs is used to program the 8259a chip during the initialization of the Windows kernel. The code in ReactOS is as follows:

在这里插入图片描述
Among them, 0x20, 0x21 are the IO ports of the master chip, and 0xa0, 0xa1 are the IO ports of the slave chip:
在这里插入图片描述
PRIMARY_VECTOR_BASE is defined as:
在这里插入图片描述
The programming method of the specific 8259a is to read and write the IO port and set the corresponding control command, without further study. Let's look at what is specified when programming Windows 8259a.

  • 1. The working mode of the main chip is specified as cascading, and the interrupt mode is electrical signal edge triggering.
  • 2. The interrupt vector mapping base address of the main chip IRQ is specified: 0x30
  • 3. Specify the cascading method of the main chip to use its own IRQ2 pin
  • 4. The working mode of the main film is specified as 80x86 mode, and the interrupt end mode is normal end mode
  • 5. The working mode of the slave chip is cascaded, and the interrupt mode is electrical signal edge triggering.
  • 6. The interrupt vector mapping base address of the slave IRQ is specified: 0x38
  • 7. The working mode of the slave chip is specified. The cascade mode is the IRQ2 pin of the master chip.
  • 8. The working mode of the slave chip is specified as 80x86 mode, and the interrupt end mode is normal end mode

So far we can know that on the computer using the 8259A interrupt controller, the 15 peripheral maskable interrupts connected through the IRQ line are linearly mapped to a range in the IDT by the operating system. It is 0x30-0x40 in Windows (PS: 0x20-0x2F in Linux). At the same time, it specifies that the interrupt mode of the interrupt controller is edge trigger, and the end mode is ordinary end mode (that is, the CPU side needs to inform whether the interrupt processing is over And set the corresponding bit, it cannot be set automatically).

Windows IRQL on 0x02 8259a

Let's take a look at IRQL.

As we have seen from the front, the hardware level has provided good support for interrupt processing, and the operating system needs to do two things: First, when initializing, program the PIC to set its working mode and map the IRQ, so that These interrupts correspond to each item in the IDT, and secondly, implement the interrupt service routines in these IDTs. It seems that this is enough, so what is the set of IRQL that Windows makes?

Take a look at the definition of IRQL in the book "Windows Internals":
在这里插入图片描述
When writing drivers, I often come into contact with the concept of IRQL. It implements the interrupt priority system in Windows. High-priority interrupts can always be processed first, while low-priority interrupts have to wait for high-priority interrupts to be processed. It will be processed after processing. How can this set of mechanisms virtualized by software manage the priority of hardware? How is this achieved?

Let's solve two problems first:

1. What is the relationship between IRQ and IRQL? 、After using KeRaiseIrql to increase the current IRQL, why can it be guaranteed not to be disturbed by low-priority interrupts?

For the first question, in a computer using the 8259a interrupt controller, IRQL=27-IRQ, which is a linear relationship.

Regarding the second question, the book "Windows Internals" answers this:
在这里插入图片描述
Let's look at the implementation of Windows in detail:

IRQL is a completely virtualized concept. In order to realize this virtual mechanism, Windows completely virtualizes an interrupt controller. It is in KPCR:

+0x024 Irql         : UChar  //IRQL
+0x028 IRR          : Uint4B  //虚拟中断请求寄存器
+0x02c IrrActive    : Uint4B  //虚拟中断在服务寄存器
+0x030 IDR          : Uint4B  //虚拟中断屏蔽寄存器

As mentioned in the first part of the previous section, 15 interrupt sources connected through two 8259a chips are mapped to a range in the processor IDT. Specifically, for Windows, it is in the range of 0x30-0x40. The interrupt handling routine (ISR) described by the interrupt descriptors in these 15 IDTs is different from the KiTrap03 corresponding to int 3 and the KiTrap0E corresponding to int 0e. The code pointed to by their ISR is located in the DispatchCode of the respective interrupt object KINTERRUPT. The following is the definition of this structure:

typedef struct _KINTERRUPT {
    CSHORT Type;
    CSHORT Size;
    LIST_ENTRY InterruptListEntry;
    PKSERVICE_ROUTINE ServiceRoutine;
    PVOID ServiceContext;
    KSPIN_LOCK SpinLock;
    ULONG TickCount;
    PKSPIN_LOCK ActualLock;
    PVOID DispatchAddress;
    ULONG Vector;
    KIRQL Irql;
    KIRQL SynchronizeIrql;
    BOOLEAN FloatingSave;
    BOOLEAN Connected;
    CHAR Number;
    UCHAR ShareVector;
    KINTERRUPT_MODE Mode;
    ULONG ServiceCount;
    ULONG DispatchCount;
    ULONG DispatchCode[106];
} KINTERRUPT, *PKINTERRUPT;

The code in DispatchCode is based on a template. The ISR processing is the same as KiTrap03. First, the trap frame is created, and then the address of the KINTERRUPT object where you are located is obtained. After these two parameters are obtained, the KiInterruptDispatch or KiChainedDispatch is started. (If multiple KINTERRUPT structures are registered for the interrupt to form a linked list, use this function) for interrupt dispatch. In these two specific dispatches, HalBeginSystemInterrupt will be called first, and then the actual processing of the corresponding interrupt will be executed, and finally HalEndSystemInterrupt will be executed to complete the interrupt processing. Let's focus on these two functions below.

BOOLEAN
HalBeginSystemInterrupt(
    IN KIRQL Irql
    IN CCHAR Vector,
    OUT PKIRQL OldIrql);

The input parameter Irql represents the IRQL corresponding to the interrupt that occurred this time, and Vector represents the interrupt vector number. As mentioned earlier, both of these parameters are taken from the KINTERRUPT object where DispatchCode is located.

HalBeginSystemInterrupt internally uses IRQL parameters to distribute in a table. In this table, except for individual functions that are different (in fact, it is just an extra layer of judgment), the other table items are the same. It is called HalpDismissIrqGeneric in ReactOS. This function is directly Instead, call its underlined version _HalpDismissIrqGeneric. Here is the core of IRQL priority implementation. The function is not long, the following is the code in ReactOS (in the Windows2000 code, the assembly form is not as intuitive as the C language form used by ReactOS, so the ReactOS code is used for explanation):
在这里插入图片描述
First of all, judge the IRQL corresponding to the interrupt that occurred this time and compare it with the IRQL in the current processor (KPCR). If it is greater than the IRQL of the current processor, it means that a higher priority interrupt has come. At this time, set the KPCR The IRQL is the new higher value, and TRUE is returned later, indicating that the interrupt request needs to be processed. If it is not greater than the IRQL of the current processor, first record the interrupt record to the IRR value of the virtual interrupt controller in the KPCR, and then directly select the mask code corresponding to the current processor IRQL from the KiI8259MaskTable table and write it into the PIC, using To shield those interrupt sources whose IRQL is lower than its own, and return FALSE later, which means that the interrupt request will not be processed. Why not set the mask code when setting the new IRQL of the processor? "Windows Internals" explains it like this:
在这里插入图片描述
The return value of HalpDismissIrqGeneric will be directly used as the return value of HalBeginSystemInterrupt. Take the interrupt dispatch function KiInterruptDispatch as an example to see how it uses this return value:
在这里插入图片描述
It can be seen that if HalBeginSystemInterrupt returns FALSE, it will directly cause the interrupt processing to end early. Only when HalBeginSystemInterrupt returns TRUE, will the actual interrupt handling routine continue to be executed. Finally, KiExitInterrupt will be called to end the interrupt processing process in any case. Take a look at this function. Combining the code of KiInterruptDispatch, it can be seen that only when HalBeginSystemInterrupt returns TRUE, the following if condition will be established, thus entering HalEndSystemInterrupt.
在这里插入图片描述
Finally, take a look at HalEndSystemInterrupt. As mentioned earlier, if the IRQL corresponding to the interrupt is lower than the IRQL of the processor, its ISR will not be executed, but it will be recorded in the IRR of the virtual interrupt controller in KPCR, and wait until the processor is executed. For high IRQL tasks, when it comes to HalEndSystemInterrupt, it will lower the IRQL of the processor and reset the interrupt mask code of the PIC. It is also very important to check the record in the IRR. If the record has a higher IRQL than the lowered IRQL Record, then dispatch the interrupt.

→【Technical Documents】←

Summarize

Finally, summarize the IRQL of Windows in the computer using the 8259a interrupt controller.

First, program the 8259a chip when the system starts, set its working mode, and map 15 interrupt sources (IRQ) to the segment 0x30-0x40 in the IDT.

Second, Windows itself defines an IRQL concept called interrupt request level to describe the priority level of interrupts. IRQL is a DWORD with a total of 32 levels. Windows uses a simple linear relationship to map IRQ and IRQL: IRQL=27 -IRQ.

Third, each ISR of the interrupt descriptor of the segment 0x30-0x40 that is mapped to the interrupt request points to a DispatchCode in the KINTERRUPT structure. This section of DispatchCode uses the interrupt dispatch function KiInterruptDispatch or KiChainedDispatch for interrupt dispatch.

Fourth, the dispatch process is: first use HalBeginSystemInterrupt to judge the IRQL of the interrupt to determine whether the interrupt needs to be processed, if not, set the mask code of the interrupt controller to prevent being disturbed again, and at the same time interrupt the current interrupt Registered in the virtual interrupt controller IRR in KPCR. If necessary, raise the IRQL, and then execute the actual processing routine of the interrupt. After the execution is completed, use HalEndSystemInterrupt to lower the IRQL, and then check whether the IRR has recorded unprocessed interrupts for processing at this time.


代码熬夜敲
210 声望354 粉丝

李志宽、前百创作者、渗透测试专家、闷骚男一位、有自己的摇滚乐队