Developing a monolithic Unix-like OS - GDT & IDT (5)

In the previous article, we implemented a dynamic memory manager.

Today we look at the basics of working in protected mode of the Intel i386 processor.

Namely: the global descriptor table and the interrupt vector table.





Table of contents



Build system (make, gcc, gas). Initial boot (multiboot). Launch (qemu). C library (strcpy, memcpy, strext).

C library (sprintf, strcpy, strcmp, strtok, va_list ...). Building the library in kernel mode and user application mode.

The kernel system log. Video memory Output to the terminal (kprintf, kpanic, kassert).

Dynamic memory, heap (kmalloc, kfree).

Organization of memory and interrupt handling (GDT, IDT, PIC, syscall). Exceptions

Virtual memory (page directory and page table).

Process. Scheduler. Multitasking. System calls (kill, exit, ps).

The file system of the kernel (initrd), elf, and its internals. System calls (exec).

Character device drivers. System calls (ioctl, fopen, fread, fwrite). C library (fopen, fclose, fprintf, fscanf).

Shell as a complete program for the kernel.

User protection mode (ring3). Task Status Segment (tss).



Linear addressing



Intel processors have 2 main operating modes: Protected Mode x32 and IA-32e x64.

In general, Zubkov writes very well and understandably about this, I recommend reading it, although in principle Intel Manual is also possible, it is not complicated, but redundant and large.

They have a separate volume for system programming, I recommend and read it.

There is much more Russian-language information on the first, therefore, we will briefly consider the main points.

There are two types of addressing: linear and page. Linear means that the entire physical space is described continuously and coincides with the physical, since as a rule the bases of segment descriptors are equal to zero, because it is easier.

In this case, for the kernel mode, you need to create three descriptors describing the memory: for code, stack and data. They are distinguished by some hardware protection.

Each such segment has a base of zero and a limit addressed by the maximum size of a machine word. The stack grows in the opposite direction, and for this there is also a flag in the descriptor.

So, with three records of this format we address everything that we need:



/* * Global descriptor table entry */ struct GDT_entry_t { u16 limit_low: 16; u16 base_low: 16; u8 base_middle: 8; u8 type: 4; /* whether code (0b1010), data (0b0010), stack (0b0110) or tss (0b1001) */ u8 s: 1; /* whether system descriptor */ u8 dpl: 2; /* privilege level */ u8 p: 1; /* whether segment prensent */ u8 limit_high: 4; u8 a: 1; /* reserved for operation system */ u8 zero: 1; /* zero */ u8 db: 1; /* whether 16 or 32 segment */ u8 g: 1; /* granularity */ u8 base_high: 8; } attribute(packed);
      
      







Each segment register (cs, ds, ss) has its own descriptor in GDT, so when we write something in the code section, we get an error, because there is written protection in the descriptor.

In order for this to work, we need to load a structure of the following format into the GDTR register:



 /* * Global descriptor table pointer */ struct GDT_pointer_t { u16 limit; u32 base; } attribute(packed);
      
      







The limit is the end of the GDT table minus 1, the base is its beginning in memory.

GDT is loaded into the register like this:



/*

* Load global descriptor table

* void asm_gdt_load(void *gdt_ptr)

*/

asm_gdt_load:

mov 4(%esp),%eax # eax = gdt_ptr

lgdt (%eax)

mov $0x10,%eax

mov %ax,%ds

mov %ax,%es

mov %ax,%fs

mov %ax,%gs

mov %ax,%ss

jmp $0x08,$asm_gdt_load_exit

asm_gdt_load_exit:

ret









And immediately after that, we load the kernel data selectors into the all segment registers, indicating the data descriptor (zero protection ring).

After that, everything is ready to include paging, but more on that later.

By the way, multiboot loaders recommend immediately setting up their GDT, although they do it themselves, they say so more reliable.

See how to do all this technically correctly in the video tutorial.



Interrupt handling



By analogy with GDT, the interrupt table has its own IDTR register, into which you also need to load a similar pointer but already on IDT.

The interrupt table itself is described by the following entries:



 /* * Interrupt table entry */ struct IDT_entry_t { u16 offset_lowerbits; u16 selector; u8 zero; u8 type_attr; u16 offset_higherbits; };
      
      







The interruption gateway usually acts as a type, since we want to process interrupts exactly. We do not consider traps and a call gateway yet, as it is closer to TSS and protection rings.

Let's create an interface for working with these tables with you. They just need to be set up and forgotten once.



 /* * Api */ extern void gdt_init(); extern void idt_init();
      
      







And now we’ll declare the interrupt handlers listed in the IDT records themselves.

First, write the hardware error handlers:



 /* * Api - IDT */ extern void ih_double_fault(); extern void ih_general_protect(); extern void ih_page_fault(); extern void ih_alignment_check(); extern void asm_ih_double_fault(); extern void asm_ih_general_protect(); extern void asm_ih_page_fault(); extern void asm_ih_alignment_check();
      
      







Then the keyboard interrupt handler:



 /* * Api - IRQ */ extern void ih_keyboard(); extern void asm_ih_keyboard();
      
      







It's time to initialize the IDT table.

It looks something like this:



 extern void idt_init() { size_t idt_address; size_t idt_ptr[2]; pic_init(); /* fill idt */ idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault); idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect); idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check); idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard); /* load idt */ idt_address = (size_t)IDT; idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE); idt_ptr[1] = idt_address >> 16; asm_idt_load(idt_ptr); }
      
      







Here we registered three hardware error handlers and one interrupt.

In order for this to work, we need to load a special pointer with the base and limit into the IDTR register:



/*

* Load interrupt table

* void asm_idt_load(unsigned long *addr)

*/

asm_idt_load:

push %edx

mov 8(%esp), %edx

lidt (%edx)

pop %edx

ret









Limits are needed to understand how many records are in the table.

It's time to write a keyboard interrupt handler:



/*

* Handle IRQ1

* void asm_ih_keyboard(unsigned int)

*/

asm_ih_keyboard:

pushal

call ih_keyboard

popal

iretl









Note: hereinafter and throughout the code, the “lower halves” are equivalent to the “upper halves” in Linux. And the "upper", respectively, the opposite. I apologize, the opposite was put in my head: D



Actually it will pass code to a high-level handler.

That, in turn, will call the handler of the lower halves of the corresponding driver that registered the request to handle this interrupt.

In our case, it will be a character device driver.

The lower halves are needed to quickly process interrupts without slowing down the others, and then, when there is time, the processor of the upper halves will gradually perform additional work, because such a processor can already be crowded out (interrupted).



 /* * Api - Keyboard interrupt handler */ extern void ih_keyboard() { printf("[IH]: irq %u\n", 1); u_char status = asm_read_port(KEYBOARD_STATUS_PORT); if (status & 0x01) { char keycode = asm_read_port(KEYBOARD_DATA_PORT); if (keycode < 1) { goto end; } /* call low half (bottom) interrupt handler */ } end: asm_write_port(PIC1_CMD_PORT, 0x20); /* end of interrupt */ }
      
      







Now, when we press the keyboard key, each time we will see the corresponding entry in the kernel system log.



References



Now, open the video tutorial for this article.

And watch the git repository in parallel (you need a lesson5 branch)



Bibliography



1. James Molloy. Roll your own toy UNIX-clone OS.

2. Teeth. Assembler for DOS, Windows, Unix

3. Kalashnikov. Assembler is easy!

4. Tanenbaum. Operating Systems. Implementation and development.

5. Robert Love. Linux kernel Description of the development process.



All Articles