I’ll flatter myself by saying that this post is going to be highly technical and esoteric, so my friend who asked me not to post anything too technical is not going to like it. Before we begin walking through the endless lines of codes that explain The Two Tables, I’d suggest that you get yourself familiarized with the “basic” Intel x86 architecture. Basic means, the core basics and not what you study in the engineering textbooks ;).
Once again this is a general article and the things written here are scattered all over the net. If you find similarities then that is because this is the “standard” way of initalizing and using GDTs and IDTs.
I personally took help from the Intel’s software programming guide, a book called Understanding the Linux Kernel and OSDev’s wiki.
The GDT and the IDT are descriptor tables. They are arrays of flags and bit values describing the operation of either the segmentation system (in the case of the GDT), or the interrupt vector table (IDT).
We’ll start with the GDT… so here it goes 🙂
Global Descriptor Table (GDT)
<wiki> “The Global Descriptor Table or GDT is a data structure used by Intel x86-family processors starting with the 80286 in order to define the characteristics of the various memory areas used during program execution, for example the base address, the size and access privileges like executability and writability. These memory areas are called segments in Intel terminology.” </wiki>
In less convoluted way, the x86 architecture has two ways of memory protection, namely: paging and segmentation. With segmentation, every memory access is evaluated with respect to a segment. That is, the addresses are calculated relative to the segment’s base address. With paging, the memory is split into constant size (size == 4kB usually) blocks called “pages”. These pages are then mapped to the physical memory and then they are called “frames”. If we do not map, we get virtual memory. Understood? Good, lets continue ;).
Paging is better and more frequently used but segmentation is in-built into the processor. We cannot circumvent segmentation as such so we create a table that holds the address of all the segments and make it global so that every part of the kernel can access it. We call this table the GDT.
GDT consists of 8-byte entry each representing a “descriptor” called segment descriptor. It can hold other things as well like, the TSS (Task State Segment… duh!!) call gates etc. If you read the wiki snippet, it says that GDT provides access privileges. Well, I do not know how much more easily can I put it, but the x86 architecture has 4 ring levels defined ranging from 0-3 with ring level 0 being the most privileged.
The segment descriptor amongst other things has a bit to check the ring level. Most modern OSs (including my own) use just 2 levels, 0 and 3. Level 0 is also called the supervisor mode and privileged commands like `cli’ and `sti’ can only be made in ring level 0. I mean, just imagine a normal user clearing the IF flag in the eflags register… it would create a havoc!!
Well, that was the crude theory. I know it is not at all helping but it will be more clear if you read the Intel’s programmer’s manual. Lets just dive into the actual code taken from Black (yes, I’m full of hubris ;)).
unsigned short lower_lim; //lower 16-bits of the limit
unsigned short lower_base_lim; //lower 16-bits of the base
unsigned char base_mid; //next 8-bits of the base
unsigned char access; //access field
unsigned char granularity; //granularity field
unsigned char base_high; //last 8-bits of the base
this C’s way of representing the GDT. The only strange thing is the __attribute__ thing. Its a less used feature of C that tells the compiler NOT to do any code optimizations. Lets see the various field of a descriptor.
P – is the descriptor present? (1 == yes)
DPL – Descriptor Privilege Level (which ring?)
DT – Descriptor Type (kernel mode or user mode)
T – Type (code segment or data segment?)
G – Granularity (0 == 1 byte, 1 == 1 kByte)
D – operand size
0 – always 0
A – for system use ad is always set to 0
to tell the processor, where the new GDT resides, we need one more structure… a pointer to our GDT. Black defines this as:
unsigned short lim; //upper 16-bits of all GDT entries
unsigned int base; //address of the first GDT entry
}__attribute__((packed)); //again, we do not want compiler optimization
to initalize the GDT, we need to define an `init’ function that calls another function to set the values of the GDT.
Black defines two functions `install_gdt()’ and `gdt_set_entry()’. install gdt() calls gdt_set_entry() to set 5 entries namely: NULL, kernel code segment, kernel data segment, user code segment and user data segment. Following is the direct copy paste from the Black’s source:
void gdt_set_entry(num, base, limit, access, gran)
unsigned long base;
unsigned long limit;
unsigned char access;
unsigned char gran;
//setup the descriptor base address
gdt_entry[num].lower_base_lim = (base & 0xFFFF);
gdt_entry[num].base_mid = (base >> 16) & 0xFF;
gdt_entry[num].base_high = (base >> 24) & 0xFF;
//setup descriptor limits
gdt_entry[num].lower_lim = (limit & 0xFFFF);
gdt_entry[num].granularity = (limit >> 16) & 0xFF;
//setup granularity and access flags
gdt_entry[num].granularity |= (gran & 0xF0);
gdt_entry[num].access = access;
//setup GDT pointer and limit
gptr.lim = (sizeof(struct gdt) * 5) – 1;
gptr.base = &gdt_entry; //TODO: need to figure this out
//setup the NULL descriptor
gdt_set_entry(0, 0, 0, 0, 0);
//setup the second GDT entry for CS
/* the base address is 0. Use a granularity of 4kB.
* 32-bit opcodes are used
gdt_set_entry(1, 0, 0xFFFFFFFF, 0x9A, 0x0F);
//setup the third entry for DS
/* essentially same as above, just the type changes to DS */
gdt_set_entry(2, 0, 0xFFFFFFFF, 0x92, 0x0F);
/* setup user space code and data segments */
gdt_set_entry(3, 0, 0xFFFFFFFF, 0xFA, 0x0F);
gdt_set_entry(4, 0, 0xFFFFFFFF, 0xF2, 0x0F);
gdt_load(); //defined in start-up code
The code is quite straightforward if you study it for a while. Starting from entry 1, the code calls gdt_set_entry repeatedly with a change in the access field. These access fields define which ring the segment resides in. I hope that the code above is obviated. The gdt_load() function is defined in the “kernel decompression” routine and is written in assembly. The code is again, a copy paste from Black.
; install a new GDT
extern gptr ; defined in gdt.c
mov ax, 0x10 ; offset of the data segment in the GDT
jmp 0x08: end1 ; far jump to code segment
ret ; return to C code
this code is again really straightforward. The GDT is loaded using the `lgdt’ “command” with the GDT pointer (structure defined above). It then loads the segment selector of the data segment and then does a far jump to the code segment. Each GDT entry is 8 bytes and the kernel code segment is the second entry after NULL, therefore the offset for it is 0x08. The kernel data segment is entry number 3 so its offset is 0x10. The far jump implicitly changes the code segment.
This was all about the GDT. It is quite easy to understand as well as code. Provided that you’re proficient enough with C and assembly. Not erudite but neither totally naive, if you know what I mean.
In the next post we’ll take a walk through the IDT theory and the codes, again directly copied from Black’s source.
May the force be with you
PS: I know that GDT and IDT should not be under the heading of “bootloader” but it was necessary to maintain the continuity of the previous post. 🙂 *innocent smile*