OS1: un núcleo primitivo en Rust para x86. Parte 2. VGA, GDT, IDT

Primera parte


El primer artículo aún no tuvo tiempo de enfriarse, pero decidí no mantenerlo intrigante y escribir una secuela.


Entonces, en el artículo anterior hablamos sobre vincular, cargar el archivo del núcleo e inicialización primaria. Le di algunos enlaces útiles, le dije cómo se ubica el núcleo cargado en la memoria, cómo se comparan las direcciones virtuales y físicas en el momento del arranque y cómo habilitar el soporte para el mecanismo de la página. Por último, el control pasó a la función kmain de mi kernel, escrito en Rust. ¡Es hora de seguir adelante y descubrir qué tan profunda es la madriguera del conejo!


En esta parte de las notas, describiré brevemente mi configuración de Rust, en términos generales, hablaré sobre la salida de información en VGA y en detalle sobre cómo configurar segmentos e interrupciones . Pregunto a todos los interesados ​​bajo el corte, y comenzamos.


Configuración de óxido


En general, no hay nada particularmente complicado en este procedimiento, para más detalles puede contactar al blog de Philippe . Sin embargo, me detendré en algunos puntos.


Stable Rust aún no admite algunas características necesarias para el desarrollo de bajo nivel, por lo tanto, para deshabilitar la biblioteca estándar y construir sobre Bare Bones, necesitamos Rust todas las noches. Tenga cuidado, una vez después de actualizar a la última, obtuve un compilador completamente inoperativo y tuve que retroceder al más estable. Si está seguro de que su compilador funcionaba ayer, pero se actualizó y no funciona, ejecute el comando, sustituyendo la fecha que necesita


rustup override add nightly-YYYY-MM-DD 

Para detalles del mecanismo, puede contactar aquí .


A continuación, configure la plataforma de destino para la que vamos. Me basé en el blog de Philip Opperman, muchas de las cosas en esta sección le fueron quitadas, desarmadas por huesos y adaptadas a mis necesidades. Philip está desarrollando para x64 en su blog, originalmente elegí x32, por lo que mi target.json será ligeramente diferente. Lo traigo completamente


 { "llvm-target": "i686-unknown-none", "data-layout": "em:ep:32:32-f64:32:64-f80:32-n8:16:32-S128", "arch": "x86", "target-endian": "little", "target-pointer-width": "32", "target-c-int-width": "32", "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float" } 

La parte más difícil aquí es el parámetro " diseño de datos ". La documentación de LLVM nos dice que estas son opciones de diseño de datos, separadas por "-". El primer personaje "e" es responsable de la identidad india, en nuestro caso es poco endian, como lo requiere la plataforma. El segundo carácter es m, "distorsión". Responsable de los nombres de los personajes durante el diseño. Dado que nuestro formato de salida será ELF (ver script de compilación), seleccionamos "m: e". El tercer carácter es el tamaño del puntero en bits y ABI (interfaz binaria de aplicación). Aquí todo es simple, tenemos 32 bits, por lo que audazmente ponemos "p: 32: 32". El siguiente son los números de coma flotante. Informamos que admitimos números de 64 bits de acuerdo con ABI 32 con alineación 64 - "f64: 32: 64", así como números de 80 bits con alineación por defecto - "f80: 32". El siguiente elemento son los enteros. Comenzamos con 8 bits y pasamos a la plataforma con un máximo de 32 bits: "n8: 16: 32". El último es la alineación de la pila. Incluso necesito números enteros de 128 bits, así que deja que sea S128. En cualquier caso, LLVM puede ignorar este parámetro de forma segura, esta es nuestra preferencia.


En cuanto a los parámetros restantes, puedes echar un vistazo a Philip, él explica todo bien.


También necesitamos Cargo-xbuild, una herramienta que le permite compilar de forma cruzada el núcleo de óxido al construir bajo una plataforma de destino desconocida.
Instalar


 cargo install cargo-xbuild 

Lo recogeremos así.


 cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib 

Necesitaba un manifiesto para el correcto funcionamiento de Make, ya que comienza desde el directorio raíz y el núcleo se encuentra en el directorio del núcleo.


De las características del manifiesto, solo puedo resaltar crate-type = ["staticlib"] , que proporciona un archivo enlazable a la salida. Lo alimentaremos en LLD.


kmain y configuración inicial


Según las convenciones de Rust, si creamos una biblioteca estática (o un archivo binario "plano"), la raíz de la caja debería contener el archivo lib.rs, que es el punto de entrada. En él, con la ayuda de los atributos, se configuran las características del lenguaje y también se encuentra el atesorado kmain.


Entonces, en el primer paso tendremos que deshabilitar la biblioteca estándar. Esto se hace con una macro.


 #![no_std] 

Con un paso tan simple, nos olvidamos de inmediato de la memoria dinámica de subprocesos múltiples y otras delicias de la biblioteca estándar. Además, incluso nos privamos de la macro println !, por lo que tendremos que implementarla nosotros mismos. Te diré cómo hacerlo la próxima vez.


Muchos tutoriales en algún lugar de este lugar terminan con la salida de "Hello World" y sin explicar cómo vivir. Nosotros iremos por el otro lado. En primer lugar, necesitamos establecer segmentos de código y datos para el modo protegido, configurar VGA, configurar interrupciones, lo cual haremos.


 #![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd); ...... } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {} } 

¿Qué está pasando aquí? Como dije, apagamos la biblioteca estándar. También anunciaremos dos módulos muy importantes: depuración (en la que escribiremos en la pantalla) y arco (en el que vivirá toda la magia dependiente de la plataforma). Utilizo la función Rust con configuraciones para declarar las mismas interfaces en diferentes implementaciones arquitectónicas y las uso al máximo. Aquí me detengo solo en x86 y luego hablamos solo de eso.


Declaré un controlador de pánico completamente primitivo, que Rust requiere. Entonces será posible modificarlo.


kmain acepta tres argumentos y también se exporta en notación C sin distorsión de nombre para que el enlazador pueda asociar correctamente la función con la llamada de _loader, que describí en el artículo anterior. El primer argumento es la dirección de la tabla de páginas PD, el segundo es la dirección física de la estructura GRUB, de donde obtendremos la tarjeta de memoria, el tercero es el número mágico. En el futuro, me gustaría implementar tanto el soporte Multiboot 2 como mi propio gestor de arranque, por lo que utilizo un número mágico para identificar el método de arranque.


La primera llamada de kmain es la inicialización específica de la plataforma. Vamos adentro La función arch_init se encuentra en el archivo arch / i686 / mod.rs, es pública, específica para x86 de 32 bits y tiene este aspecto:


 pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); } } 

Como puede ver, para x86, la salida, la segmentación, las interrupciones y la paginación se inicializan en orden. Comencemos con VGA.


Inicialización VGA


Cada tutorial considera que es su deber imprimir Hello World, por lo que encontrará cómo trabajar con VGA en todas partes. Por esta razón, iré lo más brevemente posible, me enfocaré solo en las fichas que hice yo mismo. Sobre el uso de lazy_static, lo enviaré al blog de Philippe y no lo explicaré en detalle. const fn aún no se ha lanzado, por lo que todavía no se pueden realizar inicializaciones maravillosamente estáticas. Y agregaremos un bloqueo de giro para que no resulte un desastre.


 use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } }); } 

Como sabe, el búfer de pantalla se encuentra en la dirección física 0xB8000 y tiene un tamaño de 80x25x2 bytes (ancho y alto de la pantalla, byte por carácter y atributos: colores, parpadeo). Como ya hemos habilitado la memoria virtual, el acceso a esta dirección se bloqueará, por lo que agregamos 3 GB. También desreferenciamos un puntero sin formato, que no es seguro, pero sabemos lo que estamos haciendo.
Tal vez, lo interesante de este archivo es solo la implementación de la estructura de Writer, que permite no solo mostrar caracteres en una fila, sino también desplazarse, ir a cualquier lugar de la pantalla y otras pequeñas cosas bonitas.


Escritor vga
 pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer, } impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } } } 

Al rebobinar, simplemente copia secciones de memoria del tamaño del ancho de la pantalla hacia atrás, completando con espacios en blanco una nueva línea (así es como hago la limpieza). Las llamadas de salida son un poco más interesantes: de ninguna otra manera que trabajar con puertos de E / S, es imposible mover el cursor. Sin embargo, todavía necesitamos entrada / salida a través de puertos, por lo que se entregaron en un paquete separado y se envolvieron en envoltorios seguros. Debajo del spoiler a continuación se encuentra el código del ensamblador. Por ahora, es suficiente saber que:


  • Se muestra el desplazamiento absoluto del cursor, no la coordenada.
  • Puede enviar al controlador un byte a la vez
  • La salida de un byte ocurre en dos comandos: primero escribimos el comando en el controlador, luego los datos.
  • El puerto para los comandos es 0x3D4, el puerto de datos es 0x3D5
  • Primero, imprima el byte inferior de la posición con el comando 0x0F, luego el superior con el comando 0x0E

out.asm

Presta atención al trabajo con variables pasadas en la pila. Dado que la pila comienza al final del espacio y reduce el puntero de la pila cuando se llama a la función, para obtener parámetros, un punto de retorno, etc., debe agregar el tamaño del argumento alineado con la alineación de la pila al registro ESP, en nuestro caso 4 bytes.


 global writeb global writew global writed section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret 

Configuración de segmento


Llegamos al tema más desconcertante, pero al mismo tiempo más simple. Como dije en un artículo anterior, la organización de memoria de páginas y segmentos se mezcló en mi cabeza, cargué la dirección de la tabla de páginas en el GDTR y agarré mi cabeza. Me tomó varios meses leer el material lo suficiente, digerirlo y poder realizarlo. Puede que haya sido víctima del ensamblador de libros de texto de Peter Abel. El lenguaje y la programación para la PC de IBM ”(¡un gran libro!), Que describe la segmentación para el Intel 8086. En esos momentos agradables, cargamos los 16 bits superiores de una dirección de veinte bits en el registro de segmento, y esa era la dirección en la memoria. Resultó ser una cruel decepción que comenzando con i286 en modo protegido, todo está completamente mal.


Por lo tanto, la teoría básica es que x86 admite un modelo de memoria segmentada, ya que los programas más antiguos solo podían superar los 640 KB y luego 1 MB de memoria.


Los programadores tuvieron que pensar en cómo colocar el código ejecutable, cómo colocar los datos y cómo mantener su seguridad. El advenimiento de la organización de la página hizo innecesaria la organización segmentada, pero se mantuvo con el propósito de compatibilidad y protección (separación de privilegios para el espacio del kernel y el espacio del usuario), por lo que sin ella no sería en ninguna parte. Algunas instrucciones del procesador están prohibidas cuando el nivel de privilegio es más débil que 0, y el acceso entre el programa y los segmentos del núcleo provocará un error de segmentación.


Hagámoslo nuevamente (con suerte en el último) sobre la traducción de direcciones
Dirección de línea [0x08: 0xFFFFFFFF] -> Verificar permisos de segmento 0x08 -> Dirección virtual [0xFFFFFFFF] -> Tabla de páginas + TLB -> Dirección física [0xAAAAFFFF]


Un segmento se usa solo dentro del procesador, se almacena en un registro de segmento especial (CS, SS, DS, ES, FS, GS) y se usa exclusivamente para verificar los derechos para ejecutar el código y transferir el control. Es por eso que no puede simplemente tomar y llamar a la función del núcleo desde el espacio del usuario. El segmento con el descriptor 0x18 (tengo uno, el suyo es diferente) tiene derechos de nivel 3, y el segmento con el descriptor 0x08 tiene derechos de nivel 0. Según la convención x86, para proteger contra el acceso no autorizado, un segmento con menos privilegios no puede llamar directamente a un segmento con gran derechos a través de jmp 0x08: [EAX], pero está obligado a utilizar otros mecanismos, como trampas, puertas, interrupciones.


Los segmentos y sus tipos (código, datos, escaleras, compuertas) deben describirse en la tabla de descriptores globales GDT, la dirección virtual y el tamaño de los cuales se carga en el registro GDTR. Al cambiar entre segmentos (por simplicidad, supongo que es posible una transición directa), debe llamar a la instrucción jmp 0x08: [EAX], donde 0x08 es el desplazamiento del primer descriptor válido en bytes desde el comienzo de la tabla , y EAX es el registro que contiene la dirección de transición. El desplazamiento (selector) se cargará en el registro CS, y el descriptor correspondiente se cargará en el registro sombra del procesador. Cada descriptor es una estructura de 8 bytes. Está bien documentado y su descripción se puede encontrar tanto en OSDev como en la documentación de Intel (consulte el primer artículo).


Resumo Cuando inicializamos GDT y ejecutamos la transición jmp 0x08: [EAX], el estado del procesador será el siguiente:


  • GDTR contiene una dirección GDT virtual
  • CS contiene el valor 0x08
  • Se copió un identificador de la dirección [GDTR + 0x08] en el registro de sombra CS de la memoria
  • El registro EIP contiene la dirección del registro EAX

El descriptor cero siempre debe estar sin inicializar y el acceso a él está prohibido. Me detendré en el descriptor TSS y su significado con más detalle cuando hablemos de subprocesamiento múltiple. Mi tabla GDT ahora se ve así:


 extern { fn load_gdt(base: *const GdtEntry, limit: u16); } pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit); } static mut GDT: [GdtEntry; 7] = [ //null descriptor - cannot access GdtEntry::new(0, 0, 0, 0), //kernel code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //kernel data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //TSS - for interrupt handling in multithreading GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0), GdtEntry::new(0, 0, 0, 0), ]; 

Y aquí está la inicialización, de la que hablé mucho más arriba. La carga de dirección y tamaño de GDT se realiza a través de una estructura separada, que contiene solo dos campos. La dirección de esta estructura se pasa al comando lgdt. En los registros de segmento de datos, cargue el siguiente descriptor con un desplazamiento de 0x10.


 global load_gdt section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS .reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret 

Entonces todo será un poco más fácil, pero no menos interesante.


Interrupciones


En realidad, es hora de darnos la oportunidad de interactuar con nuestro núcleo (al menos para ver lo que presionamos en el teclado). Para hacer esto, debe inicializar el controlador de interrupción.


Digresión lírica sobre el estilo de código.


Gracias a los esfuerzos de la comunidad y específicamente a Philip Opperman, la convención de llamadas de interrupción x86 se ha agregado a Rust, que le permite escribir controladores de interrupciones que ejecutan iret. Sin embargo, conscientemente decidí no seguir esta ruta, ya que decidí separar el ensamblador y Rust en diferentes archivos y, por lo tanto, funciones. Sí, estoy usando irrazonablemente la memoria de pila, soy consciente de esto, pero todavía está condimentando. Mis manejadores de interrupciones están escritos en ensamblador y hacen exactamente una cosa: llaman casi los mismos manejadores de interrupciones escritos en Rust. Acepte este hecho y sea indulgente.


En general, el proceso de inicialización de interrupciones es similar a la inicialización de un GDT, pero es más fácil de entender. Por otro lado, necesitas mucho código uniforme. Los desarrolladores del sistema operativo Redox toman una hermosa decisión, utilizando todas las delicias del lenguaje, pero fui "en la frente" y decidí permitir la duplicación de código.


Según la convención x86, tenemos interrupciones, pero hay situaciones excepcionales. En este contexto, la configuración para nosotros es prácticamente la misma. La única diferencia es que cuando se produce una excepción, la pila puede contener información adicional. Por ejemplo, lo uso para manejar la falta de una página cuando trabajo con un grupo (pero todo tiene su tiempo). Tanto las interrupciones como las excepciones se procesan desde la misma tabla, que usted y yo debemos completar. También es necesario programar el PIC (Controlador de interrupción programable). También hay APIC, pero aún no lo he descubierto.


Al trabajar con PIC no haré muchos comentarios, ya que hay muchos ejemplos en la red sobre cómo trabajar con él. Comenzaré con los controladores en ensamblador. Todos son completamente idénticos, por lo que eliminaré el código del spoiler.


IRQ
 global irq0 global irq1 ...... global irq14 global irq15 extern kirq0 extern kirq1 ...... extern kirq14 extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret 

Como puede ver, todas las llamadas a las funciones de Rust comienzan con el prefijo "k", por distinción y conveniencia. El manejo de excepciones es exactamente el mismo. Para las funciones de ensamblador, se selecciona el prefijo "e", para Rust, "k". El manejador de fallas de página es diferente, pero al respecto, en las notas sobre administración de memoria.


Excepciones
 global e0_zero_divide global e1_debug ...... global eE_page_fault ...... global e14_virtualization global e1E_security extern k0_zero_divide extern k1_debug ...... extern kE_page_fault ...... extern k14_virtualization extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret 

Declaramos manipuladores de ensamblador:


 extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug(); ...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1(); ...... fn irq14(); fn irq15(); } 

Definimos controladores de óxido que llamamos arriba. Tenga en cuenta que para interrumpir el teclado, simplemente visualizo el código recibido, que obtengo del puerto 0x60; así es como funciona el teclado en el modo más simple. En el futuro, esto se transforma en un controlador completo, espero. Después de cada interrupción, debe enviar al controlador la señal del final del procesamiento 0x20, ¡esto es importante! De lo contrario, no obtendrá más interrupciones.


 #[no_mangle] pub unsafe extern fn kirq0() { // println!("IRQ 0"); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq1() { let ch: char = inb(0x60) as char; crate::arch::vga::VGA_WRITER.force_unlock(); println!("IRQ 1 {}", ch); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq2() { println!("IRQ 2"); outb(0x20, 0x20); } ... 

Inicialización de IDT y PIC. Sobre PIC y su reasignación, encontré una gran cantidad de tutoriales de diversos grados de detalle, comenzando con OSDev y terminando con sitios de aficionados. Dado que el procedimiento de programación opera con una secuencia constante de operaciones y comandos constantes, daré este código sin más explicaciones. , 0x20-0x2F , 0x20 0x28, 16 IDT.


 unsafe fn setup_pic(pic1: u8, pic2: u8) { // Start initialization outb(PIC1, 0x11); outb(PIC2, 0x11); // Set offsets outb(PIC1 + 1, pic1); /* remap */ outb(PIC2 + 1, pic2); /* pics */ // Set up cascade outb(PIC1 + 1, 4); /* IRQ2 -> connection to slave */ outb(PIC2 + 1, 2); // Set up interrupt mode (1 is 8086/88 mode, 2 is auto EOI) outb(PIC1 + 1, 1); outb(PIC2 + 1, 1); // Unmask interrupts outb(PIC1 + 1, 0); outb(PIC2 + 1, 0); // Ack waiting outb(PIC1, 0x20); outb(PIC2, 0x20); } pub unsafe fn init_idt() { IDT[0x0].set_func(e0_zero_divide); IDT[0x1].set_func(e1_debug); ...... IDT[0x14].set_func(e14_virtualization); IDT[0x1E].set_func(e1E_security); IDT[0x20].set_func(irq0); IDT[0x21].set_func(irq1); ...... IDT[0x2E].set_func(irq14); IDT[0x2F].set_func(irq15); setup_pic(0x20, 0x28); let idt_ptr: *const IdtEntry = IDT.as_ptr(); let limit = (IDT.len() * core::mem::size_of::<IdtEntry>() - 1) as u16; load_idt(idt_ptr, limit); } 

IDTR GDTR — . STI — — , , ASCII- -.


 global load_idt section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret 

Epílogo


, , . setup_pd, . , , , .


- GitLab .


Gracias por su atencion!


UPD: 3

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


All Articles