Operating Systems Development Series | |
Operating Systems Development - Errors, Exceptions, Interruptions This series is intended to demonstrate and teach operating system development from the ground up. Please note: This tutorial covers software interrupt handling, not hardware interrupt handling. If you are looking for hardware interrupts, please see our 8259A PIC tutorial. The software side of handling hardware interrupts is discussed here.
IntroductionWelcome back! :)In the last tutorial we have covered the ground basics and design of our system. Yes, pretty basic and easy so far, right? You might be wondering when we will start getting into the system-level code again. Well.... *ahem* ...welcome back :) This tutorial will cover a very important concept: Error Handling . Error Handling involves alot more then simply handling problems, but catching them as well. This is where Exception Handling comes in. Because Exception Handling requires interrupts , we will also cover interrupt handling as well. Interrupts are architecture dependent. Because of this, we will be developing an interface for managing interrupts through our uber 1337, yet very empty (at the moment) Hardware Abstraction Layer , and interfacing with our Kernel to install our own Trap Gates which will be used to catch processor exception errors, and allow us to prevent triple faults now, and forever, while remaining completely hardware independent. Sound fun? So, heres whats up:
...Alot of stuff going on here, so lets get started, shall we? Errors, Errors, ErrorsOkay, lets face it: No one is perfect. With computers, this is even truer. As we are working in the wonderful world of kernel land, things are even worse as a simple error can cause unpredictable software to hardware problems.I am sure a lot of our readers have already experienced this through Triple Faults. In applications programming, we are not working directly with the hardware. Because of this, there are less problems that can result to errors. In kernel land, things are a bit different. Triple faults are caused do to errors with our instructions or data. If there is a problem that the processor cannot resolve, it reboots the system before it gets worse. Triple faults and no error handling is very bad in OS development, as the problems can get much worse?rom data corruption to hardware failures, to even completely destroying the system. Knowing the importance of error handling is critical in resolving these, and insuring that our system stays stable to its end release. Exception Handling?xception Handling comes in two flavors: A programming language construct (For example, standard C++? try/catch/throw keywords. Some compilers also include additional keywords like _except; or mechanisms like SEH, or VEH). The other flavor is the one we are interested in: Hardware mechanisms that are designed to change (?nterrupt? the current flow of execution. The condition that changes this flow of execution is called an exception. Exceptions should only be used to signal error (exceptional) conditions, and not for conditionals that are used for normal operation.When an exception occurs, the flow of execution changes as a subroutine (the ?xception handler? is executed. This allows the subroutine to handle the error condition in some way. Normally, the current state will be saved before the handler is called. This will allow the handler to continue execution later, if possible. Remember that exceptions are designed from the hardware. i.e., They are hardware mechanisms. This is similar to hardware interrupts, and the bases of interrupt handling, as they are related. Because of this, in order to understand exception handling from the hardware, we need to look at interrupts. Lets look at that next. Interrupt HandlingInterruptsAn Interrupt is an external asynchronous signal requiring a need for attention by software or hardware. It allows a way of interrupting the current task so that we can execute something more important.Not to hard. Interrupts provide a way to help trap problems, such as divide by zeros. If the processor finds a problem with the currently executing code, it provides the processor alternative code to execute to fix that problem. Other interrupts may be used to provide a way to service software as routines. These interrupts can be called by any software from within the system. This is used alot for System API's, which provide a way for ring 3 applications to execute ring 0 level routines. Interrupts provide alot of use, especially as a way of receiving information from hardware that may change its state at asynchronous times. Interrupt TypesThere are two types of interrupts: Hardware Interrupts and Software Interrupts. In the 8259A PIC tutorial, we have covered hardware interrupts. This tutorial focuses on software interrupts.Hardware InterruptsA hardware interrupt is an interrupt triggered by a hardware device. Normally, these are hardware devices that require attention. The hardware Interrupt handler will be required to service this hardware request.This tutorial does not cover hardware interrupt handling, as that is hardware specific. For the x86 architecture, hardware interrupts are handled by programming the 8259A Programmable Interrupt Controller (PIC). Please see our 8259A PIC tutorial for more information on hardware interrupt handling. Spurious Interrupt This is a hardware interrupt generated by electrical interference in the interrupt line, or faulty hardware. We do NOT want this! Software InterruptsThis is where the fun stuff is at!Software Interrupts are interrupts implemented and triggered in software. Normally, the processor's instruction set will provide an instruction to service software interrupts. For the x86 architectures, these are normally INT imm, and INT 3. It alsu uses IRET and IRETD instructions. INT imm and INT 3 instructions are used to generate an interrupt, while the IRET class of instructions are used to return from Interrupt Routines (IRs). For example, here we generate an interrupt through a software instruction: These instructions may be used to generate software interrupts and execute Interrupt Routines (IR)'s through software. As you know, software interrupts were available in real mode. However, as soon as we made the jump to protected mode, the Interrupt Vector Table (IVT) became invalid. Because of this, we cannot use interrupts. Instead, we have to make our own. We will cover software interrupt handling in this tutorial. Interrupt Routines (IRs)An Interrupt Routine (IR) is a special function used to handle an Interrupt Request (IRQ).When the processor executes an interrupt instruction, such as INT, it executes the Interrupt Routine (IR) at that location within the Interrupt Vector Table (IVT). This means, it simply executes a routine that we define. Not to hard, huh? This special routine determins the Interrupt Function to execute normally based off of the value in the AX register. This allows us to define multiple functions in an interrupt call. Such as, the DOS INT 21h function 0x4c00. Remember: Executing an interrupt simply executes an interrupt routine that you created. For example, the instruction INT 2 will execute the IR at index 2 in the IVT. Cool? IRs are commonly also referred to as Interrupt Requests (IRQs) . However, the naming convention of IRs are still used within the ISA bus, so understanding both names is important. Interrupt Requests (IRQs)An Interrupt Request (IRQ) refers to the act of interrupting an event by signaling the system either through the Control Bus IR line or through one of the 8259A Programmable Interrupt Controller (PIC) IR lines.For systems with a single 8259 PIC, there are 8 IRQ lines, labeled IR0 IR7. For systems with 2 8259 PICs, there are 16 possible IRQ? labeled IR0 IR15. On the system ISA bus, These lines are labeled as IRQ0 IRQ15. Newer intel based systems integrate an Advanced Programmable Interrupt Controller (APIC) device that allows 255 IRQs per controller. For more information about IRQs, please see either the 8259A PIC tutorial or the APIC tutorial. What this means is that the 8259A PIC can signal the processor to generate a software interrupt call through a hardware device by activating the processors IR line, and the processor to execute the correct interrupt handler. This allows us to handle hardware device requests through software. Please see the 8259A PIC tutorial for more information on this...It is very important to understand this. Interrupt Service Routines (ISRs)Interrupt Service Routines (ISRs) is an Interrupt Handler. These are important to understand, so lets look closer.Interrupt HandlersAn interrupt handler is an IR for handling interrupts and IRQs. In other words, they are callback methods that we define for handling both hardware and software interrupts.There are two types of ISRs: FLIH , and SLIH . First Level Interrupt Handler (FLIH) A FLIH is considered to be part of the lower half of a device driver or kernel. These interrupt handlers are platform specific, and usually service hardware requests, executing similar to Interrupt Routines (IRs) and Interrupt Requests (IRQs). They have short execution time. Their primary duty is to service the interrupt, or to record platform specific information which is only available at the time of the interrupt (As it is running in a lower level.) It may also schedule or execute a SLIH, if needed. Second Level Interrupt Handler (SLIH) These interrupt handlers are longer lived then FLIHs. In this way, it is similar to a task or process. SLIHs are normally executed and managed by a kernel program, or by FLIHs. Nested Interrupt Handlers When an interrupt handler is executed and the Interrupt Flag (IF) is set, interrupts can still be executed during the current interrupt. This is known as a nested interrupt. Interrupts in Real ModeInterrupts in Real Mode are handled through the Interrupt Vector Table (IVT). The Interrupt Vector Table (IVT) is a list of Interrupt Vectors. There are 256 Interrupts in the IVT.IVT MapThe IVT is located in the first 1024 bytes of physical memory, from addresses 0x0 through 0x3FF. Each entry inside of the IVT is 4 bytes, in the following format:
Okay, Lets take a look at the IVT. The first few interrupts are reserved, and stay the same.
Not to hard. Each of these interrupts are located at a base address within the IVT. Interrupts in Protected ModeAs we are developing a protected mode operating system. This will be important to us. As you know, we cannot access the IVT in protected mode do to a lot of reasons. Because of this, we cannot access or use any more interrupts. So, instead, we need to create our own....And it all starts with the Interrupt Descriptor Table. Interrupt Descriptor Table (IDT)The Interrupt Descriptor Table (IDT) is a special table used by the processor for the management of IRs. Its use depends on the mode of the processor. The IDT itself is an array of 256 descriptors , simular to the LDT and GDT.Real ModeIn Real Mode, The IDT is also known as the IVT. Please see the description of the IVT in the above sections for more information.Protected ModeThe way the IDT works in protected mode is very different then that of Real Mode (This is one of the many reasons why we cannot use the IVT in protected mode.) The IVT is still used, however.The IDT is an array of 256 8 byte descriptors stored consecutively in memory and indexed by an interrupt vector within the IVT. We will take a look at these descriptors, descriptor types, and the details of the IDT next. Interrupt Descriptor: StructureA descriptor for an IDT takes the following formats. Some of the format changes depending on what type of descriptor this is.
Thats it!? Yep--Thats all there is to it ;) All we need to do is fill in our IDT, and install it, just like what we done with the GDT. The IDT is alot more simpler then the IDT, so its even easier :) The above list is the complete descriptor format. We only need to worry about developing an interrupt gate for now, so we will only focus on that. Interrupt Descriptor: ExampleJust like with the GDT, we will create an example at the bit level to help describe exactally how everything works.First, lets look at an example interrupt descriptor. This is going to be in shown in assembly so we can get a better view of everything. Yep--Thats all there is to a descriptor. Thats not that hard, is it? Lets see how this relates to our table above by breaking it down and seeing each bit: This is our descriptor, but in binary form. For the most part this is easy as most of it is all 0's. The first two bytes is our m_baseLow member shown in the above code. Looking at the table above, we can see that this is the first 16 bits of the descriptor. Because this is an interrupt gate, this represents bits 0-15 of the base address of the IR . This means, if this was our field, our IR would be located at address 0. (This normally would NOT be the case, as the location of the IR varies. This works for this example, though.) The next 2 bytes is our m_selector field. This is bytes 16-31 of the descriptor. Looking at our table, we can see that this represents our segment selector. Our interrupt handler containes code, so it should be using one of our code selectors. This is defined at offset 0x8 within the GDT, so that is our segment selector. The next few bits are not used. We can see that bits 31-35 are not used, while bits 36-38 must be 0 for interrupt gates. Because of this, we can saftley say bits 31-38 are 0. This is the size of a byte, which is our m_reserved member. The next byte is where the interesting stuff happens. Lets break it down, bit by bit--literally: Okay...Right now we are at bit 39. Looking at our table above, we can see bits 39-41 must be 0D110. If the D bit is set, this is a 32bit descriptor. It is equal to 01110, so it is indeed a 32bit descriptor. The next two bits (00 above) are bytes 42-45 of the descriptor, which represents the privledge level (DPL). It is 00, so the DPL is to execute at ring 0. The final two bytes within our example are the last two bytes within the above table. This is the high 16 bits of the IR base address (Which, in our case, is 0.) This is the m_baseHi member displayed above. As you can see, there really is not that much going on here. The selector is always going to be that of the code selector within the GDT (0x8 for our needs); then all we need to do is set the flag bits and the IR base address within m_baseLow and m_baseHi . We will see a complete example a little later which will help in understanding everything. IDTR Processor RegisterThe IDTR register is the processor register that stores the base address of the IDT.The IDTR register has the following format:
Simple enough, huh? Notice that the base address of our created IDT is stored in this register. The processor uses this register to determin where our IDT is located at. Knowing this format is very important, as it containes both the limit and base address. Because of this, simply giving it the base address of our idt will NOT work. This is useually resolved by creating a new structure in the format shown above like this:
Oh, wait...How do we even access this register? Oh right... LIDT Instruction - Loading our IDTThis instruction is used to store a new address of an IDT into the IDTR register. This instruction can only be used if the Current Protection Level (CPL) is 0 (Ring 0). It is very easy to use:Thats all there is to it. As long as idt_base is the base address of the IDT, this will copy the address into IDTR. SIDT Instruction - Storing our IDTThis instruction is used to store the value in IDTR into a 6 byte memory location. This instruction may be used in both ring 0 and ring 3 applications.
How Interrupts Work: DetailFinding the interrupt procedure to callWhen an interrupt or exception is fired, the processor uses the exception or interrupt number as an index into the IDT. As you know, our IDT is nothing more then an array of 256 descriptors of the format shown above. The processor performs the calculation IDTR.baseAddress + index * 8 , where 8 is the size of a descriptor (Remember that descriptors are 8 bytes in size?), and index is the interrupt number. IDTR.baseAddress is the base address of the IDT stored within the upper bits of IDTR. This allows the processor to retrieve the base address of the descriptor index for the interrupt handler. If the value of the calculation is greater then the IDT limit size (stored in IDTR.limit), the processor will execute a General Protection Fault (GPF) as this will result into a call beyond the size of the IDT.Remember that the descriptor is either an interrupt, trap, or task gate. If the index points to an interrupt or trap gate, the processor calls the exception or interrupt handler. This is done simular to CALLing a call gate. If the index points to a task gate, the processor executes a task switch to the exception or interrupt handler task simular to a CALL to a task gate. The information and addresses for the handler are stored within this descriptor. When the processor performs the switch: Executing the handler
It is very important to know how the stack is pushed when our interrupt handler is called, and what exceptions also push error codes. We will look at this next. Inside of our interrupt handlerBecause the location of our interrupt handler is stored within the descriptor, the processor is now able to execute our handler.As you know, when the processor executes our handler, it pushes some extra information on the stack. If our handler is running in the same ring level as ours (As it will be), then we must remember that the processor will push EFLAGS, CS, EIP and an Error code on our current stack. This allows us to continue execution if we are able to. Putting all of this together, when our handler is called, our stack will be set up like this: We use this information to return back from our handler, and to determin what caused the exception (If there is an error code.) Inside of our interrupt handler: Error code formatIf an error code is pushed on the stack when our handler is called, we can use its information to help in determing the error.It has the following format:
Error codes are not pushed on the stack for exceptions that are generated externally (via the INTR,LINT0,LINT1 pins), or INT n instruction. The error code format is different for page fault exception errors. We will look at that in the next section. Returning from a handlerAll handlers must use either IRET or IRETD instructions to return. IRET is simular to RET except that it restores the saved EFLAGS (that was pushed on the stack when the handler was getting executed), and the IOPL field in EFLAGS is only set to 0 if the current protection level (CPL) is 0. The IF flag is also changed only if the CPL is less then or equal to the IOPL.If a stack switch occured when executing the handler, IRET switches back to the interrupted procedures stack as well. x86 ExceptionsExceptions: ListingAll of the exceptions are defined as the first few interrupts within the IVT or IDT. Here is the complete list of generated exceptions from the x86 class of processors.
IRQ 0 and the System TimerAs you know, if we enter protected mode all interrupts must be disabled. If we have not done this, our system will triple fault immediately on the next clock tick. Why is this?The System Timer , useually a form of the 8253 Programmable Interval Timer (PIT) uses IRQ 0 to let us know when a clock tick happens. This device is configured this way by the system BIOS. But WAIT! *looks at above table*, Isn't that the Divide by 0 error? Bingo. Because the tables are now invalid because we switched to protected mode, Who knows where this will lead us. Because of this, an immediate triple fault on the next system tick, and the reason we must disable interrupts before switching. We should also note that the 8253 Programmable Interval Timer (PIT) is a hardware device . Notice how, using the table above, it will fire an exception (IRQ 0)? How will we know its an actual error, or just a simple tick? Lets take a look closer... Remapping the 8259A Programmable Interrupt Controller (PIC)The 8259A PIC is a standard controller used to control hardware interrupts. Hardware microcontrollers signal the PIC on their respective IR line that connects to the PIC. This allows the PIC to "know" a hardware device needs attention, and to signal the processor to fire an interrupt to handle the devices request.In our above example, the 8253 PIT was signalling the 8259A PIC to handle a system tick in this manner, which caused IRQ 0 (Remember that the 8253 PIT uses IRQ 0) to fire--which caused a triple fault as it was also a) a divide by 0 exception, and b) invalid code as we have not written it yet. To resolve this problem, we will need to reprogram the 8259A PIC Microcontroller to remap the hardware devices to use different IRQs. Please keep in mind that we *can* still use software interrupts if the IF is 0 (interrupts disabled), as IF only applies to hardware interrupts. However, if we want to re-enable hardware interrupts, we must reprogram the PIC. The 8259A PIC is a fairly complex microcontroller to program. Luckly, most of its modes do not apply to us. The demo at the end reprograms the PIC and re-enable interrupts. In order to completely get the most out of this tutorial, it is recommended to read our 8259A Programmable Interrupt Controller tutorial. DemoSKIP TO DEMO CONCLUSIONNote: These images have been scaled down to better fit the screen.
The first screenshot displays the kernel initializing the HAL. The second screenshot displays what happens when an interrupt is fired. Notice how our default handler catches the interrupt. This demo is fairly complex, as we have covered a lot of material in this tutorial. This demo installs a new Global Descriptor Table (GDT) for the kernel to use and an Interrupt Descriptor Table (IDT). It also creates a nice interface that we can use to handle software interrupts. Note that we do NOT cover hardware interrupts yet. In the next tutorial, we will be adding both 8259A PIC and 8253 PIT microcontroller interfaces to the HAL. This will allow us to catch hardware interrupts, enable hardware interrupts, and provide ourself with a system timer. It will be fun :) Lets cover the demo a little more so that we can see everything working.
Hardware AbstractionThis demo includes alot of extra files that we have not seen until now. Because of this, it is kind of like a code dump, which is what I want to avoid here. Alot of it is very simple and things that we have looked at and even implimented in our bootloader. Some of it (Inside idt.h and idt.cpp may be new to you, and covers what we have learned here: The Interrupt Descriptor Table (IDT) .This is also the beginning of our Hardware Abstraction Layer (HAL) ! As you know, I have been stressing hardware abstraction, and the importance of it, sense this series begun. You will see why soon as we continue to build on the Hal. You might even see the pluses of keeping the Hal completely independent of the kernel here! Without further ado, lets look at the beginnings of the primary interface for the HAL. Hal - include/hal.h - Platform independent interface for the HALThis is the interface between the HAL and the kernel. It is part of the standard include directory, and is completely separate from its implimentation. All routines are declared extern as the header file is meant to be used by any implimentation that defines the routines inside of it. The implimentations are architecture specific, but the interface is in no way coupled to any specific implimentation, making it completely hardware independent.While the implimentations themselves are architecture specific, we can simply build the implimentations for different architectures. Because each implimentation uses this common interface, and we can support dynamic loading (like hal.dll), we can either a)link what static hal implimentation to use when building for different architectures, or b)Build the different Hals independently, and choose which HAL to use at startup. Because they all use the same interface (Hal.h), we dont need any changes in the kernel to use different implimentations (and hence different hardware setups.) Cool? There are currently only two functions in it. We will add more when we need to: I will most likley change the prototypes of these routines to allow startup and shutdown paramaters. In any case, these are very generic routines that is meant to provide a way to setup and shutdown the hardware, if needed, for the implimentation. There are a couple of very simple layers of software within the hal for the gdt, idt, and cpu, and hal.cpp. Because all they do is initialize the layer below it (Hal.cpp calls the cpu initialize routine, which calls the gdt and idt initialize methods), I am not going to post it here as it may add more complexity in this tutorial then is required. Instead, lets focus on the bulk of the hal: The gdt setup code, The idt setup code (This containes the bulk of what we looked at in this tutorial), and the kenrel's main() routine. Cool? We will not cover the GDT in detail. Plaese see Tutorial 8 for a complete description of it. Hal - hal/gdt.h - global descriptor tableDescriptor Tables ...again!Yes, the GDT has come back to haunt you!!! ...yes, YOU!!Anywhoo... the GDT is quite a complex structure, huh? As you know, a GDT is an array of descriptors. What was the format of a GDT descriptor again? Right, okay then...
Okay, okay I'll stop now :) But serously, Intel could have made the structure more nicer, don't you think? :) Building the C structureWe can hide this structure behind a nice C style structure using C's built in types. Knowing that the first 15 bits is the segment limit (Size of an uint16_t), thats data member one . The next 16 bits is bits 0-23 of the base address, and that can be either expressed as 1 uint16_t or 2 uint8_t's. Thats data member two and/or three . The next 16 bits (bits 41-56 of GDT) is 16 bits. This is the bulk of the ugly structure that containes flag values, and can be represented, of course, using either 2 uint8_t's or 1 uint16_t. Thats the next data member. The last byte is our base address. Thats the last data member!Looking at the above, that ugly structure can be represented in 4 to 5 nice members within a structure. Here is our structure. Try to compare this structure with the above description and table to see where everything fits in. Also, remember that this structure is packed to 1 byte, so it is guaranteed to be 64 bits in size. Easy enough! There are alot of bit flags that can be set to help build the flags bytes within the structure. Please see the header file to see them all, and notice how they work. Basically, we would bitwise OR the bit flags that we want to set. You will see us do this in the next section. gdtr abstractionRemember from tutorial 8 , we have covered protected mode, the gdt, and gdtr? gdtr Is the processors internal register used to point to the GDT to be used. It is a 48 bit pointer that must follow the following format:
Here you can also see our new GDT and _gdtr, which will be used for refrence when setting up the processors GDTR register. gdt_install(): Installs a gdt into gdtrThis routine is a very simple one. All it does is use the lgdt instruction to load GDTR with our gdtr pointer. We do not need to do any far jumps here, though, as CS should never change.
gdt_set_descriptor(): Sets up a new descriptor in the gdtThis routine is used to install a new descriptor in the GDT. For the most part, it is not too hard; the ugly code is when we get to setting up the flags.
i86_gdt_initialize() - initializes the gdtThis brings everything together. All it does is set up our GDTR structure, installs some default descriptors into our GDT, and finally installs the GDT. To make things easier, this GDT is the same one we have used for our bootloader. The base address is 0, the limit (Maximum addressable address) is 4GB (0xffffffff). All of the flags are defined in gdt.h . They are defined to increase readability and to get rid of ugly magic numbers. It should be much easier to see what the descriptors are for with the flags!
Hal: Interrupt Descriptor TableTHIS is where the fun stuff is at! The IDT interface is within the idt.h and idt.cpp source files.hal.h - idt_descriptorThis is the structure for an interrupt descriptor. Compare this format with the descriptor format we looked at in this tutorial, and you should notice that they follow exactally the same format:Lets look at what each member represents, and where at within the interrupt descriptor:
idt.cpp - idtrSimular to how we set up the gdtr structure, we also have one for idtr. Notice how this structure followes the exact structure for the idtr register.Okay... Remember that the IDT is nothing more then an array of interrupt descriptors? With this, _idtr is here for refrence; it stores the current information in the processors IDTR register for our use. Basically, all we need to do from here is to set up the IDT, and _idtr; then install the IDT! Not to hard :) idt_install() - installs a new IDTThis is used to install the IDT into IDTR, no more, and no less. It is a helper method used to abstract the inline assembly language (Which is compiler dependent) behind a common interface to help with portability between compilers.
i86_default_handler() - default interrupt handlerOur IDT interface will provide a way to install our own interrupt handling routines directly into the IDT, which is as cool as it can get! Because there are 256 interrupts, there are 256 interrupt handlers. Odds are, we will not be using every one of them early on. So, what happens if an interrupt is generated that the kernel does not yet handle?That is what this is for! This is a basic unhandled exception handler that our IDT interface will install (You will see this later on.) All it does is, if being built for debug mode, prints out an error. It then halts the system.
Returning from an interrupt... C and C++ automatically pops the values off the stack and issues a RET instruction when returning from an IR. This is bad! Because of this, we need to issue our own way of returning through an IRET instruction. geninterrupt() - generrate interrupt callThis is a little tricky. This is another helper method provided to abstract the inline assembly language behind a common interface for better portability for more compiliers. However, it also hides the challenge of generating an abritary interrupt call.The problem is that of the OPCode for an interrupt (INT instruction) only has one format: 0xCDimm, where imm is an intermediate value. Because of this, we cannot use any registers nor memory locations in the INT instruction; as there is no OPCode form that accepts that (Invalid instruction.) So, how do we fix this? There are alot of different ways, of course. I opted to use a fast and small solution: self modifying code. Basically, all we need to do is modify the second byte of the INT OPCode. Knowing it is always two bytes (First being 0xCD, second is the interrupt number to call) it is quite an easy solution:
i86_install_ir () - installs interrupt handler into IDTThis is a little tricky, but not too hard. Remember that the baseLo and baseHi members of our structure contain the high and low bits of our Interrupt Routine (IR) ? So, all we need to do is get the address of the IR function, and store its high and low bits. This is done here by means of a function pointer .We pass in a function pointer as a parameter. This routine gets the address of the function the pointer points to, and masks out the low and high bits, storing it into the structure at _idt [i] , where i is the descriptor offset (the Interrupt number) in the IDT. There is some bueaty in this. Remember that, when an interrupt is generated, the processor pushes some information on the stack for us? This information will now be in the paramaters list when our routine is called! Cool, huh? This also means, however, that we need to be careful as only some interrupts push error codes, others do not. i86_idt_initialize () - Initialize IDT InterfaceNow, lets bring everything together. The following code sets up IDTR, sets our default interrupt handler to catch all interrupts (This is so we only need to define the needed interrupts in the Kernel); and finally installs the IDT using the above methods.The bit flags used for setting up the IDT are defined in idt.h and are provided to make the code more readable and easier to modify.
Demo ConclusionThis demo is a bit complex, I am to admit. At least we got the ugly neccessities out of the way! You will see that, if you issue any INT instruction, the default handler will be called. If you install your own interrupt handlers, try to experiement with them - both the ones with the error codes, and the ones without them. You will see the interrupts being fired. Anytime you call geninterrupt() or an INT instruction, you will see that the correct interrupt handler (Or, if the interrupt handler was not defined, the default handler) is executed.To keep this tutorial from getting much more complex, I decided to NOT handle hardware interrupts yet. We will cover this in the next tutorial, as well as developing the code for the 8253 Programmable Interval Timer (PIT) for use as the Kernels System Timer as well as for the 8259A Programmable Interrupt Controller , which is needed for hardware interrupts. Study the demo well and how everything works. Modify a few things; try to register your own interrupt handlers using i86_install_ir() and generrating the interrupts. To do this, all we need to do is: I decided to leave out the paramater lists for the interrupt handlers as the format may change. So, in order to access paramaters, we would need to access it through ESP. I might decide to give it paramaters later on to make things easier, though. Demo Download Here (MSVC++) ConclusionA lot of fun stuff this time, huh? We covered a lot of ground in this tutorial. We have looked at a lot of important topics, covered exception and interrupt handling, and have re-enabled interrupts in our system. This might even be the last time we ever see a triple fault. Woohoo!I am to admit this tutorial is a bit complex. Isn't OS programming fun? ^_^ In the next tutorial, we will be starting to develop our kernel even more. We will be handling timing through the 8254 Programmable Interval Timer (PIT) microcontroller , which will be covered simular to the 8259A PIC tutorial. Afterwords, we plan on moving onto more memory management and process management. ...We might even develop a basic debugging text based console to spice things up a bit ;) Until next time, ~Mike (); Questions or comments? Feel free to Contact me . |