Protected Mode and the IDT

Introduction

The MSDOS system uses IVT (Interrupt Vector Table) to hold the interrupt vectors that are called whenever some action occurs: like an interrupt is generated. But modern execution environments, like the protected mode, require more complex data structures. Therefore, the protected mode uses IDT, which is almost the same as IVT in MSDOS, except that the IDT entry is a little bit different than the IVT entry. In real mode, there isno memory protection mechanisms and the IVT was used to call the interrupt service routine that could be provided by the kernel.

Let's present the three types of interrupts that are in use today:

  • hardware interrupts or external interrupts

    • maskable interrupts (can be ignored or masked)
    • non-maskable interrupts (must be handled immediately)
  • software interrupts or programmed exceptions

  • exceptions (occurred errors that happened while the processor is trying to execute an instruction)

    • faults (program is restarted before the instruction that generated the fault: divide by zero)
    • traps (program is restarted after the instruction that generated the trap: int 3)
    • aborts (program cannot be restarted)

The non-maskable interrupts must be handled as soon as they happen, because they are usually critical, like a hardware failure, division by zero, access to a bad address or something else. Maskable interrupts must be handled sometime in the future; the IRQ (Interrupt Requests) can be categorized under maskable interrupts.

An interrupt request is a hardware signal sent to the processor that temporarily stops a running program and allows a special program, an interrupt handler, to run instead. Interrupts are used to handle events such as data receipt from a modem or network, a key press or a mouse movement. The interrupt request level is the priority of an interrupt request [1].

The hardware interrupts (maskable and non-maskable) as well as exceptions (faults, traps, aborts) are handled by the ISR stored in the IDT table. In real mode, even the software interrupts ere handled by the service routines stored in the IVT table, which is also the case today; but to make the switch from user to kernel mode, a new SYSENTER command was introduced (which was not available in real mode). Let's describe that a little bit more…

When using a system call, we usually specify the system call number that will be called in eax register: then we can use the int instruction to make the system call. When we invoke the system call with the int instruction, it must storethe following registers on the stack: SS, ESP, EFLAGS, CS and EIP. Then we can return to the caller program with the iret instruction that does exactly the opposite: it pops the values from the stack in reverse order, which returns the processor to the exact state as it was at the point where the int instruction was called. When the interrupt is being called, we must read the value of the interrupt service routine ISR from the memory and jump there.

Because of reading this initial address of the ISR routine from memory, the execution of interrupts is slower than using the sysenter instruction. This is because with the sysenter instruction, the address of the ISR routine is already known and is stored in the Model Specific Register (MSR), which can only be accessed in kernel mode. Before the sysenter instruction is executed, the EIP is put into EDX and the ESP is put into ECX register: the sysexit instruction reads the values back from those two registers when returning from the sysenter call. The system call routine must never change the values of those two registers, but if necessary, it must store them on the stack and then restore them back into original state.

We've just seen that we can use two mechanisms to call the system calls on x86 machines: one is by using standard software interrupts by using int instruction, the other is by using sysenter instruction, which is a little bit faster. This is the reason that sysenter instruction is the preferred way of handling software interrupts, but hardware interrupts and exceptions (faults, traps and aborts) still need the IDT table.

The IDT Table

In modern computer systems, each processor has its own IDT table and IDTR register. Because of this, each processor can execute its own interrupt service routine whenever some interrupt or exception occurs. Each of the IDTs tables can hold only 256 entries, each of 8 bytes in size. Let's perform a quick example where we'll try to determine if each processor truly has its own IDTR register value.

Let's use the r command to print the values of the IDTR and IDRL registers. We can see that the IDTR register points to the address 0x8003f400 and is 0x7FF bytes long. The 0x7FF is exactly 2048/8=256 entries (we're subtracting 8 from the number of bytes, because each entry is 8 bytes long).

image0

Let's dump the entries at the base address, which we can do with the dd command:

image1

Notice that we used the dd command and not the !dd command, which means that we're dealing with linear address and not a physical address. Let's take a look at the first 4 descriptors (remember that each of them is 8 bytes long). The first 4 registers are the following (the first number being the address):

  • 0008e25c 80538e00
  • 0008e3d4 80538e00
  • 0058113e 00008500
  • 0008e7a4 8053ee00

But we don't have to do that by hand, because there's a very useful command, ! idt, that can be used. If we execute the "!idt -a" command, the whole IDT table will be printed, as shown below:

image2

Hmm, okay something went wrong before the IDT was printed? It seems like some DLL wasn't found and therefore cannot be loaded. Let's execute the .chain command to list all loaded debugger extensions. The output of the .chain command can be seen below:

image3

We can immediately see that there's a text next to the "WdfKd.dll" that says "Not loaded". This happens because we didn't install the debugging tools. We can download the executable from the address http://www.microsoft.com/en-us/download/details.aspx?id=8279 and install the required libraries. Make sure that you select the "Debugging Tools" in the installation wizard, as can be seen on the picture below, because this will actually install the debugging tools (note that this is not enabled by default and you have to manually enable it):

image4

At the end, I still ended up copying the wdfkd.dll file from C:Program FilesWindows Kits8.0Debuggersx86winexp directory to C:Program FilesWindows Kits8.0Debuggersx86winxp directory.

image5

We can see that now, the wdfkd.dll is found. If you have an idea how to convince Windbg to also look in the winexp directory and find the missing DLL by itself, I would greatly appreciate it. Now we can execute the "idt -a" command again and this time it will execute without warnings, as we can see on the picture below:

image6

We've just used the !idt command to dump the interrupt service routine's addresses, which are executed when certain interrupt or exception occurs. Below, I've listed some of the routines stored in the IDT table:

nt!KiTrap0F
nt!KiGetTickCount
nt!KiCallbackReturn
nt!KiSetLowWaitHighThread
nt!KiDebugService
nt!KiSystemService
hal!HalpClockInterrupt
nt!KiUnexpectedInterrupt2

The important routine in the IDT table is at the offset 0x2e, which refers to the KiSystemService routine that provides an entry point into the kernel. Remember that on newer systems the Windows uses sysenter instruction to get into the kernel mode, while on older systems the 0x2e is still used (but newer systems have support of 0x2e as well for backward compatibility). In the end, it doesn't really matter whether the int 0x2e or sysenter is used to invoke a certain system call, because both methods call the same routine.

Let's take a look at the interrupt descriptor format, which can be seen below (note that the picture was taken from [2]):

image7

Each descriptor is 8 bytes long, where the first part of the picture above presents the upper 4 bytes of the descriptor and the bottom 4 bytes present the lower bytes of the descriptor. We can see that we have two offsets that together form a 32-bit address of the interrupt service routine: it doesn't really matter whether the hardware, software interrupt or an exception was triggered, the same ISR is being called. The segment selector is the kernel selector, which will be used to read instructions from the segment selector that contains the code of the ISR routine. We also have some other fields that are quite important, like present bit, but we won't look at those here. If you're interested in the details, take a look at [2], which describes the format of the interrupt descriptor in details.

Here, we can clearly see the distinction between real and protected mode. In real mode, we had an Interrupt Vector Table IVT containing a pointer to the interrupt service routine ISR, but in protected mode we have the Interrupt Descriptor Table IDT that holds descriptors. Each descriptor has various elements, among which are also the protection elements, preventing the code from user-mode to call the code in kernel-mode directly. If you think about it, this really makes a lot of sense: the code in user-mode can't directly call the code in kernel mode (which was possible in real mode), because of the protection mechanisms in place. The only places the user application is allowed to call the kernel code is at the predefined locations defined in the IDT. But because all of the interrupt service routines are safe, the user-space code can't actually call arbitrary code in the kernel-mode, just predefined code.

Conclusion

For the end, let's present two pictures that should be used for reference if we ever forget about all that stuff and we would quickly want to remember. Let's first present the overview of how interrupts work in real mode. This can be seen on the picture below:

image8

In real mode, we're using the int instruction to call whichever interrupt service routine we want. The IVT table is located at 0x0000 address in memory, so the address of the appropriate entry in the IVT table can be simply calculated by multiplying the number x with the size of a single IVT entry. Then, the right IVT entry should be read and the appropriate interrupt service routine called. This is pretty straightforward and pretty easy to understand, but is essential in understanding how the interrupts work in protected mode.

In protected mode, the int instruction is still used to call the right ISR. But the address of the ISR is calculated somewhat more complex. At first, the right address of the IDT entry in the interrupt descriptor table is calculated. Previously, we saw that we're actually reading the descriptor from the IDT table, which contains multiple elements. Among the elements are also the SegmentSelectors that points to the entry in the global descriptor table (together with the GDTR register that contains the base address of the GDT table). The descriptor from the GDT table then contains the Base Address of the code segment. The base address of the code segment together with the Offset from the IDT entry form a whole address that points directly to the ISR that will be called upon exception invocation. But the descriptors also contain the Priv elements that define whether the descriptor is a user or kernel descriptor. We can see the whole concept presented on the picture below:

image9

Keep this process in mind when dealing with int or sysenter instruction, because the concepts are important if we want to really understand how the operating system internals work.

The most important thing to remember is that the user applications can call only predetermined code in the kernel-mode. Restricting the user applications to only those functions in kernel keeps the whole operating system more stable and more secure, because the user mode applications can't mess with kernel mode data structures or kernel code (which can lead to an OS crash).

References:

[1] Wikipedia, Interrupt request, http://en.wikipedia.org/wiki/Interrupt_request.

[2] Descriptors, http://courses.engr.illinois.edu/ece391/references/descriptors.pdf.

[3] WHAT ARE INTERRUPTS? http://www.rcollins.org/articles/pmbasics/tspec_a1_doc.html.

[4] X86: protected mode, GDT, IDT,

https://stackoverflow.com/questions/8883357/x86-protected-mode-gdt-idt.

[5] Protected Mode Basics, Robert Collins, http://www.rcollins.org/articles/pmbasics/tspec_a1_doc.html.

Comments