Desarrollo de un sistema operativo monolítico similar a Unix: GDT e IDT (5)

En el artículo anterior, implementamos un administrador de memoria dinámica.
Hoy cubriremos los conceptos básicos del trabajo en modo protegido del procesador Intel i386.
A saber: la tabla de descriptores globales y la tabla de vectores de interrupción.


Tabla de contenidos


Sistema de construcción (marca, gcc, gas). Arranque inicial (arranque múltiple). Lanzamiento (qemu). Biblioteca C (strcpy, memcpy, strext).
Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Creación de la biblioteca en modo kernel y modo de aplicación de usuario.
El registro del sistema del núcleo. Memoria de video Salida a la terminal (kprintf, kpanic, kassert).
Memoria dinámica, montón (kmalloc, kfree).
Organización de memoria y manejo de interrupciones (GDT, IDT, PIC, syscall). Excepciones
Memoria virtual (directorio de páginas y tabla de páginas).
Proceso. Planificador Multitarea Sistema de llamadas (kill, exit, ps).
El sistema de archivos del kernel (initrd), elf y sus componentes internos. Sistema de llamadas (exec).
Controladores de dispositivos de caracteres. Llamadas del sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
Shell como un programa completo para el kernel.
Modo de protección del usuario (anillo3). Segmento de estado de la tarea (tss).

Direccionamiento lineal


Los procesadores Intel tienen 2 modos operativos principales: Modo protegido x32 e IA-32e x64.
En general, Zubkov escribe sobre esto muy bien y claramente, recomiendo leerlo, aunque en principio Intel Manual también es posible, no es complicado, pero redundante y grande.
Tienen un volumen separado para la programación del sistema, lo recomiendo y lo leo.
Hay mucha más información en ruso sobre el primero, por lo tanto, consideraremos brevemente los puntos principales.
Hay dos tipos de direccionamiento: lineal y de página. Lineal significa que todo el espacio físico se describe continuamente y coincide con el físico, ya que, por regla general, las bases de los descriptores de segmento son cero, porque es más fácil.
En este caso, para el modo kernel, debe crear tres descriptores que describan la memoria: para código, pila y datos. Se distinguen por cierta protección de hardware.
Cada segmento tiene una base de cero y un límite abordado por el tamaño máximo de una palabra de máquina. La pila crece en la dirección opuesta, y para esto también hay una bandera en el descriptor.
Entonces, con tres registros de este formato, abordamos todo lo que necesitamos:

/* * 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); 


Cada registro de segmento (cs, ds, ss) tiene su propio descriptor en GDT, por lo que cuando escribimos algo en la sección de código, obtenemos un error, porque hay una protección escrita en el descriptor.
Para que esto funcione, necesitamos cargar una estructura del siguiente formato en el registro GDTR:

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


El límite es el final de la tabla GDT menos 1, la base es su comienzo en la memoria.
GDT se carga en el registro así:

/*
* 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


E inmediatamente después de eso, cargamos los selectores de datos del kernel en todos los registros de segmento indicando al descriptor de datos (anillo de protección cero).
Después de eso, todo está listo para incluir paginación, pero más sobre eso más adelante.
Por cierto, los cargadores de arranque múltiple recomiendan configurar inmediatamente su GDT, aunque lo hacen ellos mismos, lo dicen de manera más confiable.
Vea cómo hacer todo esto técnicamente correctamente en el video tutorial.

Manejo de interrupción


Por analogía con GDT, la tabla de interrupciones tiene su propio registro IDTR, en el que también debe cargar un puntero similar pero ya en IDT.
La tabla de interrupciones en sí se describe mediante las siguientes entradas:

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


La puerta de enlace de interrupción generalmente actúa como un tipo, ya que queremos manejar las interrupciones específicamente. Todavía no consideramos trampas y una puerta de enlace de llamadas, ya que está más cerca de TSS y anillos de protección.
Creemos una interfaz para trabajar con estas tablas con usted. Solo necesitan configurarse y olvidarse una vez.

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


Y ahora declararemos los manejadores de interrupciones enumerados en los registros IDT.
Primero, escriba los manejadores de errores de hardware:

 /* * 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(); 


Entonces el controlador de interrupción del teclado:

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


Es hora de inicializar la tabla IDT.
Se parece a esto:

 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); } 


Aquí registramos tres manejadores de errores de hardware y una interrupción.
Para que esto comience a funcionar, necesitamos cargar un puntero especial con la base y limitarlo en el registro IDTR:

/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret


Se necesitan límites para comprender cuántos registros hay en la tabla.
Es hora de escribir un controlador de interrupción de teclado:

/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl


Nota: de aquí en adelante y en todas partes del código, las "mitades inferiores" son equivalentes a las "mitades superiores" en Linux. Y el "superior", respectivamente, lo contrario. Pido disculpas, se me ocurrió lo contrario: D

En realidad, pasará el código a un controlador de alto nivel.
Eso, a su vez, llamará al controlador de las mitades inferiores del controlador correspondiente que registró la solicitud para procesar esta interrupción.
En nuestro caso, será un controlador de dispositivo de caracteres.
Las mitades inferiores son necesarias para procesar rápidamente las interrupciones sin disminuir la velocidad de las otras, y luego, cuando hay tiempo, el procesador de las mitades superiores realizará gradualmente un trabajo adicional, ya que dicho procesador ya puede ser desplazado (interrumpido).

 /* * 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 */ } 


Ahora, cuando presionamos la tecla del teclado, cada vez veremos la entrada correspondiente en el registro del sistema del núcleo.

Referencias


Ahora, abra el video tutorial para este artículo.
Y mire el repositorio de git en paralelo (necesita una rama de lección5)

Referencias


1. James Molloy. Haga rodar su propio sistema operativo de clones UNIX de juguete.
2. Dientes. Ensamblador para DOS, Windows, Unix
3. Kalashnikov. ¡Ensamblador es fácil!
4. Tanenbaum. Sistemas operativos Implementación y desarrollo.
5. Robert Love. Kernel de Linux Descripción del proceso de desarrollo.

Source: https://habr.com/ru/post/467289/


All Articles