paint-brush
x86 Handling Exceptions by@mvuksano
3,437 reads
3,437 reads

x86 Handling Exceptions

by Marko VuksanovicAugust 25th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Exceptions can happen at any point in time during operation of the processor. Division by zero and page faults are examples of exceptions. We will focus on one of the simpler exceptions - division by zero. After working through this article you should be able to extend the code to handle other exceptions, interrupts or traps. The code is available at Gitlab: //gitlab.com/mvuksano/kvm-playground/tree/master/07-exceptions.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - x86 Handling Exceptions
Marko Vuksanovic HackerNoon profile picture

In previous article we got our CPU into 64 bit mode and were able to run 64 bit programs. Our vCPU is already very useful but it doesn't deal well with issues like division by zero, page faults or other exceptional cases it might encounter.

Division by zero and page faults are examples of exceptions. Exceptions can happen at any point in time during operation of the processor and they signify an event that needs attention of the processor.

Because the idea of this series of posts is learning we will focus on one of the simpler exceptions - division by zero. After working through this article you should be able to extend the code to handle other exceptions, interrupts or traps.

Triggering interrupts

Division by zero can be solved in software by first checking if divider is equal to zero. If it is, we use INT instruction to trigger an exception. It can also be triggered by CPU automatically. In case of software generated interrupt it's up to us to decide which interrupt handler to use to deal with divide by zero issue. When CPU triggers the handler it choses handler at predefined location.

Exception handlers are normally found via so called Interrupt Descriptor Table or IDT for short. IDT contains up to 256 entries and each of those entries is 16 bytes in size in 64 bit mode. When using software interrupts we can choose any of the entries to place our exception handler but it is customary to have "divide by zero" error as the first entry (index = 0). It also allows us to use the same handler for software generated interrupt as well as hardware generated one.

When an interrupt is invoked it looks up the corresponding entry in IDT to find the location of interrupt handler. As you will see a little bit later, IDT entry defines which code segment to use. In our case we only have one code segment so it may not seem so important but It does imply that we need to set up a so called Global Descriptor Table (GDT). GDT is used by CPU to select code segment specified in an IDT entry.

Building on the work from previous articles we will define a GDT, then we will define IDT and a handler. Finally we will load a program to trigger the exception handler.

Full code is available at Gitlab: . As you read this article I suggest you follow the code too. It should help you learn these concepts faster.

Global Descriptor Table

Global descriptor table is a data structure that is present on x86 processors and it is used to configure characteristics of various memory areas. It is used in real and protected mode but it is almost not used in long mode. In long mode GDT is generally set up once during startup and never changed again.

GDT was used to configure characteristics of different memory segments. Memory segments are selected via so called segment selectors which are in turned stored in segment registers. For example, CS or code segment register could contain value 0x8. 0x8 is segment selector as it "selects" 0x8 byte in GDT which, in this example, should be location of code segment descriptor.

In long mode segmentation is disabled but CPU still uses some fields from segment selector (e.g. Descriptor Privilege Level or DPL field from Code Segment). For example,

int
instruction will also throw General Protection Exception in case referenced code segment is not valid. This means that we can't totally ignore GDT and have to do a little bit of setup.

To enable CPU to handle interrupts, exceptions and traps we need to set up two entries in our GDT:

  1. Null descriptor
  2. Code Segment descriptor

GDT entries are 8 bytes in size.

Null descriptor is a sequence of zeros. GDT always has null descriptor as its first entry.

Code segment descriptor is used, in long mode, to establish processor's operating mode and executing privilege level. Majority of bits in CS descriptor are ignored but there are a few that we need to set. The following image is from Intel manual and shows which bits mean what and which ones are ignored:

First 5 bytes as well as the last byte need to be set to 0. This means we really only care about two bytes in the "middle" - the ones that fall in range 8-15 and 16-23.

For byte that starts at position 40 (indicated by number 8 in top row in the image above):

  • Bit P needs to be set to 1 to indicate that this segment is present
  • DPL (Descriptor Privilege Level) should be set to 0x00. This is the highest privilege level.
  • Bits 12 is always set to 1
  • Type for code segment should be set to 0xa (You can check table 3-1 from Intel Manual for other code and data segment types)

And for bits 51 - 54 (4 bits starting at number 20 in top row in the image above):

  • For 64 bit code segment bit L needs to be set.
  • When L bit is set, D bit has to be 0
  • Bit G should not be relevant in 64 bit mode. It tells how field in CS is interpreted but given limit field is ignored in 64 bit mode we should be able to set this to anything. In this example I have set this bit.

Given the above information our first byte (we'll call it

type
byte) can be set to 0x9a and the second byte (we'll call it
flags
byte) should be set to 0xa0.

All other bytes should be set to 0.

Define GDT entries

There are numerous ways to define GDT entries. The following is just one possible solution.

We will define two structs

struct gdtNullEntry
and
struct gdtEntry
.

gdtNullEntry
can be defined using a struct that contains 8 bytes all set to zero. For example:

struct gdtNullEntry {
	unsigned char always0[8];
} __attribute__((packed));

gdtEntry
is a little bit more complex. It has 5 byte padding at the beginning and 2 byte padding at the end. In the middle are two bytes called
type
and
flags
:

struct gdtEntry {
	unsigned char padding_1[5];
	unsigned char type; 
	unsigned char flags;
	unsigned char padding_2[2];
} __attribute__((packed));

Interrupt Descriptor Table

Interrupt descriptor table (IDT) is a table of descriptors that are used map interrupts to interrupt handlers. This table has up to 256 entries and each of the entries is 16 bytes large. Address of IDT is stored in idtr register.

In case of an exception CPU will consult the table and call interrupt handler associated with the exception. There are a few different types of exceptions but we won't discuss them in this article. Hopefully, I'll find some time in the future to talk a bit more about it.

In 64 bit mode, IDT entries are encoded using the following data structure:

Offset is memory address at which interrupt handler can be found.

Segment selector is used to tell CPU which code segment to use when looking for interrupt handler. As we already mentioned when talking about GDT, in 64 bit mode CS is not used for address calculations. It is used to enforce privilege checks which we won't discuss at this point. For now we are working at the highest privilege level - 0.

Type field is used to tell CPU what kind of handler this descriptor points to. Depending on the type CPU might do slightly different things. In this example we will use what is called an "interrupt gate" and type field for that gate is 0xe.

Interrupt stack table (IST) is used to enable interrupt to always execute on a known good stack. For now we will set it to 0x0.

As with GDT entires, there are numerous ways to define IDT entries.

In this example I'll use the following data structure to encode an IDT entry:

struct idt_entry
{
    unsigned short base_lo;
    unsigned short sel;
    unsigned char always0;
    unsigned char flags;
    unsigned short base_hi;
    unsigned char padding[8];
} __attribute__((packed));

The first two bytes (

base_lo
) are the two least significant bytes of the address where the interrupt handler is located. The next two bytes are used to specify which code segment descriptor to use. In our case we have only one code segment and it can be found starting at byte 8 in GDT.

always0
byte must always be set to 0.
flags
is used to encode a bit more information about segment selector (higher 4 bytes) and specify what kind of interrupt handler this descriptor points to (lower 4 bytes).

In our case this byte will have value

0x8e
. The highest 8 bytes will be set to 0 in our case.
base_hi
represents 3rd and 4th byte of interrupt handler address.

Create and loading GDT and IDT

So far we have defined data structures that can be used to create GDT and IDT. Creating the tables is a matter of initializing those data structures and copying them into memory at correct locations:

void createGdtTable(void *mem) {
	struct gdtNullEntry nullEntry;
	struct gdtEntry csEntry = {
		.type = 0x9a,
		.flags = 0xa0
	};

	memcpy(mem, &nullEntry, 8);
	memcpy(mem + 8, &csEntry, 8);
}

GDT entries are 8 bytes in size so

createGdtTable
function copies nullEntry at the location pointed to by
mem
while
csEntry
is copied right after.

To create the IDT table we use

createIdtTable
function:

void createIdt(void *mem) {
	struct idt_entry e = {
		.base_lo = 0x9000,
		.sel = 0x8,
		.flags = 0x8e,
	};
	for(int i = 0; i < 256; i++) {
		memcpy(mem + i * 16, &e, 16);
	}
}

This function populates 256 IDT entries and points them to the same handler. Keep in mind that IDT entries are 16 bytes in size compared to GDT which are 8 bytes large.

In the main function we use

createGdtTable
and
createIdtTable
to place GDT and IDT at desired memory locations. For this tutorial I've chosen to place GDT at memory location 0x0 and IDT at 0xc000.

Also keep in mind that these are virtual addresses which happen to be identity mapped in this set of articles.

createGdtTable(mem);
createIdt(mem + 0xc000);

Interrupt handler function

Next missing piece in the puzzle is an actual interrupt handler:

BITS 64
mov rax, 'H'
mov edx, 0x3f8
out dx, al
hlt

The interrupt handler is super simple - it just outputs letter H and exits the VM.

Run a program and trigger handler

There are multiple ways to trigger an interrupt handler. In this example we will use

int
instruction. It is used to trigger an interrupt handler from a program.

The instruction itself takes a number which tells which of the handlers to use.

int 0
for example will trigger the first interrupt (interrupts are zero indexed).

Calling something like

int 57
would trigger 58th interrupt.

We will use the following program to trigger the interrupt:

BITS 64
mov esp, 0xe000
mov rax, 'N'
mov edx, 0x3f8
out dx, al

int 0 
hlt

An astute reader will notice that the first instruction in this program actually sets stack pointer (ESP) to 0xe000. The reason why we need to set a stack pointer to some known good value is because when the interrupt handler is called CPU will place return address on the stack.

The return address is (in this case) next instruction to execute. Following that we output character "N" and trigger interrupt handler 0.

Our interrupt handler just halts the VM and doesn't do anything but in normal operation an interrupt handler would continue executing a program.

When you run the program you should see the following output:

And that is it! You now have a vCPU that is configured to handle interrupts and exceptions

Conclusion

In this series of articles we've worked our way and configured a CPU that runs in 64bit mode and can handle interrupts and exceptions. This is good enough base for running some workloads in a VM as well as a good staring point to write your own OS.

If you followed the my series you can see that there are quite a few moving pieces to put together. Hopefully now you have enough knowledge to drill down even further and adapt this code to your needs. Stay tuned to learn what other cool stuff you can do with VMs and CPUs.

References

  • [1] Section 5.5, “Privilege Levels”, for a description of the relationship of the DPL to the CPL
바카라사이트 바카라사이트 온라인바카라