======================================================= Introducción a la programación en lenguaje ensamblador para procesadores Intel serie x86 y compatibles (VI) ======================================================= Por nmt numit_or@cantv.net ========================================================= MODO PROTEGIDO: INTRODUCCIÓN A LOS REGISTROS DE SISTEMA Y A LAS ESTRUCTURAS DE DATOS EN ENSAMBLADOR. ========================================================= -------------------------------------------------------- CONTENIDO · Modo protegido: preliminares · NOTA: Sistemas multitareas · Multiprogramación y protección · Gestión de memoria en sistemas multiprogramación · Segmentación · Paginación · Segmentación paginada · Memoria y privilegios en el Intel 80386 · Estructuras en lenguaje ensamblador · Estructuras en NASM · Segmentación en el 80386 · Selectores · Descriptores · Interrupciones · Excepciones · Conmutación de tareas · Nuevas instrucciones · Instrucciones privilegiadas · Instruciones para la gestión de Entrada/Salida · Pequeña nota sobre algunas intrucciones de operaciones lógicas · Conmutación entre modos de funcionamiento · Definición de los segmentos · Acceder a los 4 GB · NOTA SOBRE LA LÍNEA A20 · Definición de los selectores · Establecimiento de las tablas de descriptores y de los selectores · Constantes útiles para definir tablas de descriptores · NOTA SOBRE EL DESCRIPTOR NULO · Pasar a modo protegido · Determinación del tipo de procesador y revisión del modo actual · Ejemplo de conmutador de modo real a modo protegido · Análisis de load.asm · El modo plano real · A modo de recapitulación · Apéndice: · NOTA SOBRE EL DESCRIPTOR NULO · NOTA SOBRE BOCHS ----------------------------- Modo protegido: preliminares ----------------------------- Aunque el 80386 fue el primer procesador de la serie ix836 en aumentar el tamaño de los registros de uso geeral de 16 a 32 bits, ya a partir de 80286 los procesadores de esta serie pueden correr en modo protegido. Antes del 80286, los ix86 sólo podían correr en modo real ¿Qué significa esto? ¿qué diferencia hay entre el modo real y el modo protegido? ¿qué se entiende aquí por modo? En su manual del 80386, Intel afirma que la implementación del modo protegido en este procesador tiene como objeto ofrecer un buen apoyo a las eleboración de programas, suministrando un entorno con eficientes mecanismos para la depuración de programas. Pero la historia de los sistemas operativos nos informan sobre la finalidad última de la implementación de sitemas de protección en los procesadores Multiprogramación y protección ------------------------------ El 80386 puede programarse para que se comporte de diversas maneras. De acuerdo al modo en que corra, favorecerá o no requerimientos del sistema: un software de sistema operativo carece de viabilidad sin un hardware que le de soporte. Cuando el MIT diseñó a comienzos de la década de 1960 un sistema capaz de soportar múltiples usuarios conectados a él al mismo tiempo --es decir, con soporte para multiprogramación--, no había todavía suficientemente difundido un hardware de protección necesario. Por ese motivo, la propuesta del MIT no alcanzó popularidad. La multiprogramación necesita software de protección ¿Por qué? porque si varios usuarios realizan trabajos sobre un sistema, estarán corriendo varias tareas al mismo tiempo, cada una con su propio código y sus propios datos. Un sistema así no debe permitir que las tareas que realiza un usuario afecte las de los demás: sería una verdadera catásrofe que los datos de un usuario se vieran modificados por las tarea de otro. Para evitar esto, debe implementarse algún mecanismo que proteja los datos de cada usuario. Este mecanismo debe ser proporcionado por el propio procesador. Pero un sistema multiprogramación exige algo más que protección. Si se trabaja con un solo procesador, su tiempo de trabajo debe ser repartido entre las diversas tareas que se ejecutan, ya que en principio un procesador sólo puede atender una tarea a la vez. Para facilitar este proceso, se parte la memoria del sistema y se asigna una parte a cada tarea. Una vez dividida en partes la memoria, es más fácil asignar a cada tarea un espacio propio y una serie de atributos de protección. Hay que observar también que no sólo deben correr tareas de usuario: también deben correr tareas propias del sistema que administra el proceso general. Evidentemente, estas tareas deben tener un privilegio mayor que todas las demás, deben estar por encima de las tareas de los usuarios. El mecanismo de protección debe poder discriminar y establecer privilegios de tareas y de acceso a memoria. *NOTA: Sistemas multitareas* Decimos que un sistema es mulitareas si puede controlar la ejecución simultánea de varias aplicaciones. No es necesario que se ejecuten todas al mismo tiempo, sino que el procesador atienda a cada tarea con la rapidez suficiente para dar la impresión de que es así. Para hacer eso, el sistema debe ubicar en memoria las múltiples "tareas", habilitar un espacio que ellas puedan compartir con datos y código comunes a todas ellas y administrar el tiempo de ejecución de cada tarea. Cuando esto ocurre, cuando los datos y las instrucciones de varias tareas distintas son localizadas en la memoria al mismo tiempo, decimos que nuestro sistema es multiprogramación. Un sistema multitareas no debe ser necesariamente multiprogramado. Los sistemas multiprogramación deben manejar una lista con las tareas pendientes, asignar a cada tarea una zona de memoria exclusiva --el área local de la tarea--, disponer una zona de memoria común y compartida por todas las tareas --el área global; el acceso al área local de la tarea debe estar restringido a la propia tarea: ninguna tarea podrá acceder al área local de otra, pero todas las tareas deberían poder acceder al área global. Si el sistema da acceso a varios usuarios a un mismo procesador, decimos que este sistema es multiusuario. Para apoyar un sistema multitareas, el procesador debe soportar tres conceptos: · Memoria virtual que permita el empleo de una memoria superior a la que realmente posee el sistema. · Conmutación rápida de tareas. · Sistema de protección para evitar posibles interferencias en el espacio de memoria privado de cada tarea. Cuando el sistema soporta accseo de varios usuarios al procesador, se le llama sistema multiusuario. Un sistema de este tipo es más crítico ya que cada ususario puede ser multitarea, lo que significa otro nivel de complejidad. Gestión de memoria en sistemas multiprogramación ------------------------------------------------ Para manejar simultáneamente varias tareas al mismo tiempo, los sistemas multiprogramación ubican varias tareas o procesos en la memoria al mismo tiempo. Luego irá pasando el control de una tarea a otra según lo vayan requiriendo las circunstancias y de manera tal que se tenga la impresión de que todas las tareas corren realmente al mismo tiempo. Este proceso exige un mecanismo de gestión de memoria que lo facilite. Para ellos se han concebido dos mecanismos: segmentación y paginación. Uno tercero combina ambos. *Segmentación* La gestión por segmentación divide la memoria en porciones de diferentes tamaños llamadas segmentos. Cada una d e estas particiones es tratada como una unidad lógica u objeto que se puede referenciar por un nombre asociado a la localidad de memoria donde se ubica, y que posee atributos que definen el tipo de su contenido, su función y su tamaño. La segmentación permite localizar un espacio de memoria mayor al que dispone la memoria real del sistema. Esto lo logra por reubicación dinámica de las tareas. Si se requiere que se ejecute una tarea y la memoria física está ocupada, el sistema puede usar el espacio del segmento de una tarea que no esté activa en ese momento o que no tenga una prioridad alta; guardará todo su contexto (los valores con los que trabaja, su estado actual) y cargará el segmento que ocupa esa tarea por el otro que ha solicitado una localidad "libre". La tarea que ha sido temporalmente descargada, luego podrá ser repuesta, no necesariamente en la misma posición que antes, si se solicita. Para la relocalización dinámica se emplean direcciones con dos campos: uno para la dirección del segmento y otra para la ubicación del elemento dentro del segmento. La dirección del segmento varía con su reubicación en la memoria pero el otro valor en la dirección referenciada, el desplazamiento, siempre es fija. Cuando los programas son desplazados, sólo hay que cambiar los registros de segmento para ajustarlos al cambio de lugar. Los elementos se localizan sumando al nuevo valor del segmento el desplazamiento, cuyo valor es siempre el mismo. La reubicación dinámica también permite que más de un programa o proceso comparta adecuadamente parte de su código: basta sólo ajustar los registros de segmento en las diversas tareas para que apunten al mismo segmento que será compartido. La segmentación proporciona un método efectivo para el manejo del espacio de direcciones virtuales: se trata de un concepto más lógico que físico. Pero tiene algunos inconvenientes. Un segmento de código de un programa será tan grande como el código que contenga; un segmento que contenga un array de datos será tan grande como lo sea dicho array. El manejo de bloques de datos de tamaño distinto exige procedimientos más complejos y costosos, lo que se agrava por el hecho de que los discos almacenan datos en bloques de igual tamaño. Por ejemplo, se requiere más tiempo para encontrar un espacio de memoria cuyo tamaño se adecúe al del segmento que sea necesario cargar. Irán apareciendo huecos no ocupados de diferentes temaños que tienden a aparecer entre segmentos sucesivos dando lugar a fragmentación externa, que resulta en un manejo ineficiente de la memoria. También está la situación en las que sólo se usa una pequeña porción de un gran segmento: como tiene que cargarse todo el segmento para el manejo de esta pequeña porción del segmento, habrá desperdicio de memoria. *Paginación* La gestión de memoria por paginación divide el espacio físico en porciones de igual tamaño llamados páginas. Con este sistema se busca superar los inconvenientes de la segmentación en la reubicación dinámica de objetos en memoria. Las páginas deben cargarse desde el disco a la memoria en bloques de tamaño constante llamados marcos de página, que tienen el mismo tamaño que las páginas. Estos marcos son alineados en el límite de un valor igual al tamaño de una página: sus direcciones iniciales siempre son múltiplos enteros del tamaño fijo de la página. Siempre podrá colocarse una página nueva en cualquier marco de página disponible. La localización de una página en la memoria física es más simple que hallar un segmento. Una dirección de página puede dividirse en por lo menos dos campos. Uno es un índice a un marco de página. Para encontrar el marco con este valor se multiplica por el tamaño constante de un marco de página. El otro valor puede ser un desplazamiento dentro del marco de página alelemento refrenciado. El sistema de paginación también permite compartir páginas de memoria declarando algunas páginas de código compartibles por más de una tarea y haciendo que las entradas de las tablas de página de las dibversas tareas apunten al mismo marco. Pero compartir páginas no es tan sencillo como compartir segmentos: si se quiere compartir un código o un array de datos que estén conformados por más de dos páginas, entonces hay que usar tres entradas separadas para cada una de las páginas del área compartida; si se tratara de un segmento, será suficiente una entrada simple para facilitar el direccionamiento del área compartida. Por otro lado, aunque la paginación parece eliminar espacios vacíos, ya que se ubican en los marcos disponibles, que son de su mismo tamaño, la paginación tiende a producir fragmentación interna: algunos bloques pueden tener menos tamaño que el marco de página, quedando un hueco dentro del marco cuando se carga ese bloque. * Segmentación paginada * Para superar las desventajas de cada modo de manejo de la memoria, se ha implementado una combinación de ambos: segmentación paginada. En este esquema, cada segmento es dividido en páginas y es referenciado por el procesador a través de una tabla de páginas para dicho segmento. Como consecuencia, cada segmento tendrá siempre un tamaño que será múltiplo del tamaño de la página. Las páginas de un mismo segmento, contiguas en la memoria virtual, no tienen que aparecer así en la memoria real ni tienen que aparecer todas a la vez, lo que elimina la necesidad de reubicar todo el segmento cuando se necesita sólo una porción de él y solventa el problema de fragmentación externa de la segmentación. En segmentación paginada la dirección virtual v consta de tres partes: un índice s con el número de entrada dentro de una tabla con descriptores de segmentos; un índice p con el número de entrada dentro de una tabla de proyecciones de páginas; un desplazamiento d en la página indicada en la entrada señalada antes en la entrada en la tabla de proyeciones de página. Un registro especial del procesador contiene la dirección física base de la tabla con los descriptores de segmentos del proceso actual. El campo s de la dirección virtual especifica cuál entrada de esta tabla tiene la dirección física de la base del segmento donde está nuestra tabla de proyecciones de páginas. El campo p de la dirección virtual es un índice que señala en cuál entrada de esta tabla se encuentra la dirección física de nuestra página, y la dirección física de la instrucción se obtiene a esta dirección física de página el valor del campo d de la dirección virtual. Memoria y privilegios en el Intel 80386 --------------------------------------- Si revisamos la arquitectura y rasgos del procesador i80386, encontraremos claramente que fue pensado especialmente para entornos multiprogramación. Primero, puede correr en tres modos: real, protegido y virtual; es decir, entre sus modos de funcionamiento, hay soporte para protección de memoria. Segundo, su MMU (Memory Management Unity: Unidad de Manejo de Memoria) posee dos subunidades, una que permite segmentación de memoria, llamada unidad de segmentación y que traduce direcciones virtuales a direcciones lineales, y otra para paginación; la unidad de paginación, que traduce direcciones lineales a físicas o reales. Tercero, contiene nuevos segmentos de sistema que apoyan la comutación de un modo a otro y la implementación de los diversos modos de funcionamiento. Como hemos señalado, un entorno protegido impide interferencias entre las tareas, entre objetos de una misma tarea y prohibe la ejecución de ciertas instrucciones del procesador si se piden en condiciones p rohibidas. Para ello debe comenzar por especifiar la dirección base, el tamaño y los derechos de acceso de las áreas locales de cada tarea y del área global compartida. También deben estar especificados los objetos en cada una de las áreas. Sobre cada área de la memoria se esteblecen zonas con niveles de privilegio propios. El nivel de privilegio es una medida para la seguridad de una zona: cuanto mayor privilegio haya, mayor la seguridad. Al asignar mayor privilegio a una zona se especifica que los objetos ahí ubicados son bastante fiables, más que los que se hallan en zonas de menor privilegio. La implementación de un entorno protegido en el 80386 divide el espacio de memoria en cuatro niveles de privilegio (PL), desde 0 (mayor privilegio) a 3 (menor privilegio). El nivel 0 se destinará por lo general a los objetos propios de ls istema y el nivel de menor privilegio, el 3, se deja para los objetos de las aplicaciones del usuario. Los niveles de privilegio establecen una serie de reglas de acceso en las que luego profundizaremos. Ahora nos interesa el tema de la gestión de memoria. Uno de los beneficios de que dispone el 80386 son sus registros de 32 bits y un apoyo en su mecanismo en la gestión de memoria que nos permite acceder a un espacio lineal y plano de 4 GB. Vamos a comenzar por acá, por el manejo de memoria en modo protegido, específicamente vamos a centrarnos en la segmentación en modo protegido. Pero antes algunas acotaciones nuevas sobre estructuras de datos en lenguaje ensamblador. Estructuras en lenguaje ensamblador ----------------------------------- Los datos tipo estructura son uno de los más importantes en programación. Hasta ahora, hemos considerado los tipos de datos casi exclusivamente por su tamaño. Al hablar de tipos de datos, estamos agrupándolos de acuerdo a ciertas características que no se reducen exclusivamente a su tamaño: también la función y el modo de acceso forman parte de las características de un tipo de datos. Incluso, una colección de datos puede ser agrupada en un conjunto de acuerdo a su organización y a las operaciones que se definen en ella. Cuando el criterio de clasificación de los datos es su organización decimos que se trata de una estructura de datos; si además, se incluyen definiciones de operaciones sobre la estructura como criterio de clasificación, entonces hablamos de clases. Vamos a referirnos aquí a estructuras de datos, cómo se definen en lenguaje ensamblador. Si estos tipos de datos se clasifican según su organización, entonces están determinados por la posición de sus elementos. A este grupo de elementos se le asigna un nombre, que lo identifica y permite acceder a sus miembros a través de índices; estos índices serían nombres de los elementos de la estructura. En ensamblador, la definición de una estructura tiene el siguiente formato: - TASM - nombre STRUCT nombre_a tamaño ? nombre_b tamaño ? ... nombre_x tamaño ? nombre ENDS Este formato define una estructura compleja de datos que puede ser manipulados como un único tipo de dato. Para usar una estructura de este tipo, debemos declararla, después de su definición, en el segmento de datos: .data nombre1 nombre nombre2 nombre Tenemos aquí datosdel tipo 'nombre': nombre1 y nombre2; nombre1 no tiene datos inicializados; nombre2 si tiene datos inicializados en su definición: 'nombre_a = A', 'nombre_b = B', ..., 'nombre_x = X'. Para asignar valores a los miembros del tipo de variable nombre1, usamos el siguiente formato: nombre_estructura.miembro_estructura "nombre_estructura" es la dirección de la variable de este tipo; "miembro_estructura" es un índice dentro de la estructura la respectivo miembro_estructura. Las siguiente instrucciones asignan valores a los miembros de la estructura "nombre1": mov nombre1.nombre_a, 1 mov nombre1.nombre_b, 2 ... mov nombre1.nombre_x, N Estructuras en NASM ------------------- NASM no ofrece medios intrínsecos para estructuras de datos; con él se usan macros implícitas: las macros STRUC y ENDSTRUC. Para asignar valores a los miembros del tipo de variable nombre1, usamos el símbolo STRUC, que toma un parámetro, el nombre del tipo de dato. Una vez suministrada la macro STRUC, se define la estructura y los campos usando la familia de pseudo instrucciones RESx, y al final se invoca la macro ENDSTRUC para finalizar la definición. Las pseudo instrucciones RESx: RESB, RESW, RESD, RESQ y REST, se emplean para definir datos no inicializados que serán indicados en la sección BSS de un módulo, donde se reserva espacio en memoria, no en el archivo, para este tipo de datos. Cada una de estas instrtucciones almacena un operando, que es el número de bytes, words, double words o lo que haya que reservar. NASM no soporta la sintaxisMASM/TASM para reservar espacio no inicializado: "DW ?". El operando para la pseudo instrrucción RESB–tipo es una expresión crítica: buffer: resb 64 ; reserva 64 bytes wordvar: resw 1 ; reserva un espacio tamaño word realarray resq 10 ; array de diez reales Este tipo de declaraciones se aplica a la definición de estructuras de datos. Por ejemplo, para una estructura llamada mytype que contenga miembros dword, word, byte y una cadena de bytes, se podría escribir: struc mytype mt_long: resd 1 mt_word: resw 1 mt_byte: resb 1 mt_str: resb 32 endstruc que define seis símbolos: mt_long como 0 (desplazamiento desde el comienzo de una estructura mytype al campo dword) mt_word como 4, mt_byte como 6, mt_str como 7, mytype_size como 39, y mytype mismo como. Para mover un dato al segundo miembro de mytipe, debería hacer lago como: mov [mytype+mt_word], 5 que movera 5 a [mytype+4], dirección del segundo miembro de mytype. Para que los miembros de la estructura puedan tener los mismos nombres en más de una estructura, se define la estructura anteponiendo un punto a cada uno de sus miembros: struc mytype .long: resd 1 .word: resw 1 .byte: resb 1 .str: resb 32 endstruc así se definen los desplazamientos a los campos de la estructura como mytype.long, mytype.word, mytype.byte y mytype.str. Una vez definido un tipo de estructura, lo siguiente será declarar instancias de ella en el segmento de datos. Para ello, NASM provee el mecanismo ISTRUC. Para declarar una estructura de tipo mytype en un programa, se escribe algo como: mystruc: istruc mytype at mt_long, dd 123456 at mt_word, dw 1024 at mt_byte, db 'x' at mt_str, db 'hello, world', 13, 10, 0 iend La función de la macro AT es usar el prefijo TIMES para hacer avanzar la posición del ensamblado al punto correcto del campo especificado de la estructura, y luego declarar los datos especificados. Por lo tanto los campos de la estructura deben ser declarados en el mismo orden especificado en la definición de la estructura. Si el dato que va en un campo de la estructura requiere que se especifique más de una línea, el resto de las líneas de la fuente pueden ir después de la línea AT. Por ejemplo: at mt_str, db 123,134,145,156,167,178,189 db 190,100,0 Se puede comenzar el campo de la estructura en la siguiente línea: at mt_str db 'hello, world' db 13,10,0 En NASM no se puede usar el siguiente código para hacer referencia a los elementos de una estructura: mov ax,[mystruc.mt_word] mt_word es una constante como cualquier otra, así que la sintaxis correcta es como mencionamos arriba: mov ax,[mystruc+mt_word] o mov ax,[mystruc+mytype.word] Vamos a tener ocasión de usar estos tipos de datos. Volvamos a nuestro estudio del modo protegido en el 80386. Segmentación en el 80386 ------------------------ Desde el punto de vista de programación, un segmento es un tipo de dato o de objeto caracterizado por ser un bloque de memoria de tamaño variable con información de la misma clase. Los segmentos son un objeto principal en un sistema con protección. La división de la memoria en segmentos ofrece un soporte eficiente para un estilo de programación estructurada, donde el código y los datos de los programas son agrupados en módulos lógicos de acuerdo a su importancia, función y calidad de acceso. Un segmento puede ser concebido para que contenga datos, pueda ser leído y escrito, pueda ser compartido su acceso por varias tarea. O puede ser pensado para que contenga código, sea ejecutable y no pueda ser accedido desde otras tareas. Los procesadores ix86 incorporan segmentación como principal sistema de gestión de memoria. Dividen el espacio de un programa en por lo menos tres segmentos: uno de datos, uno de código y otro para la pila. Para localizar o identificar un elemento dentro del un segmento, se emplean dos direcciones: la base b del segmento y el desplazamiento d dentro del segmento. Para soportar este tipo de direccionamiento, el 80386 dispone de seis registros de segmento de 16 bits: el registro de segmento de código CS, el del segmento de datos CS, el de la pila SS, y tres registros extra: ES, FS y GS. Cada vez que se quiera acceder a un segmento, su dirección base debe estar en alguno de estos registros. Para localizar un elemento específico en el segmento, se le suma a la base b el desplazamiento d, cuyo valor puede estar en un registro de uso general, puede ser un valor inmediato o un valor en alguna localidad de la memoria. Así que la dirección lógica de todo elemento de memoria está formada por un puntero de dos campos: Selector s: valor de 16 bits en un registro de segmento y que identifica la dirección de la base del segmento. Desplazamiento d: un valor de 32 bits que se añade a la base del segmento para el elemento referenciado. Esta división en dos campos permite dinamizar la ubicación de los segmentos: en los programas, los desplazamientos nunca cambian, permanecen constantes; pero cuando un programa es cargado en memoria para su ejecución, no tiene por qué ser ubicado en una misma dirección: la base puede cambiar de acuerdo al estado actual de la memoria. Los segmentos flotan en un espacio de memoria virtual. Una dirección lógica es una localidad de memoria considerada desde el punto de vista del programador; por lo tanto es el tipo de direcciones que genera un programa. Para ubicar el dato, el sistema debe traducir dicho valor lógico en una localidad de la memoria física. Como la gestión por segmentación ordena los bloques en orden consecutivo, uno después de otro, el procesador suma la base y el desplazamiento para obtener un valor absoluto conocido como dirección lineal, que indica la localidad de memoria física donde se encuentra el elemento refrenciado: Dirección lineal = Selector + Desplazamiento En modo real, que es el modo por defecto en que corre el 80386, un segmento queda identificado por su base y su tamaño, que puede ser de hasta 64 KB. El espacio de memoria de un s egmento en modo real está limitado por el tamaño de los registros: 16 bits. En modo protegido, el selector ya no apunta directamente a la base de un segmento: ahora es un índice dentro de una tabla de punteros: el registro de segmento "selecciona" uno de l os punteros de dicha tabla y elpuntero seleccionado es la dirección base del segmento. El puntero al segmento puede tener un tamaño de 32 bits, así que un segmento puede ser ubicado en un espacio de memoria por encima de 1 MB. Y como el 80386 tiene segmentos de 32 bits, en teoría, los segmentos pueden tener un tamaño de hasta 2^32 = 4GB. En modo protegido, los segmentos quedan caracterizados por tres parámetros: base: dirección lineal del comienzo del segmento límite: tamaño del segmento derechos de acceso: atributos del segmento como tipo, nivel de privilegio e indicadores de estado. Uno puede pensar que es imposible localizar un segmento de digamos 1GB en un espacio de memoria física de 32 MB. Pero el 80386 implementa otro método de gestión de memoria además de la segmentación que permite manejar segmentos de este tamaño: paginación; en modo protegido, los segmentos pueden subdividirse en bloques del mismo tamaño, llamados páginas. Se implementa un método de relocalización dinámica parecido al de la segmentación para relocalizar páginas de segmento en vez de segmentos completos, según sea necesario hacer uso de los elementos de ciertas programas. Cuando se paginan los segmentos, debe aplicarse un segundo nivel de traducción: ya las direcciones lineales obtenidas a partir de las direcciones lógicas no coinciden con las de la memoria física y deben ser traducidas para localizar el elemento referenciado; las direcciones lineales deberán ser traducidas a direcciones físicas de memoria. El proceso de traducción de direción lineal a física de página, es más complejo en el 80386 que la traducción de memoria lógica a lineal: la dirección lineal de 32 bits es dividida en tres campos: directorio de páginas, tabla de páginas y desplazamiento. 31 22 21 12 11 0 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º º º º º DIR º PAGE º OFFSET º º º º º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ - Formato de una dirección lineal - El directorio de páginas contiene en cada una de sus entradas la dirección de una tabla sde páginas. La dirección de la base del directorio se encuentra en uno de los registros de control: CR3. El campo de directorio de páginas se usa como un índice dentro del directorio cuya base está en CR3. En la entrada indicada se encuentra la base de una tabla de páginas el campo tabla de páginas de la dirección lineal se usa como un índice dentro de la tabla de páginas para encontrar una entrada donde se encuentra la dirección base de la página donde se encuentra el elemento referenciado: el campo desplazamiento de la dirección lineal se usa como índice dentro de la página para encontrar el elemento. PAGE FRAME ÉÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍ» ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º DIR º PAGE º OFFSET º º º ÈÍÍÍÍÍÑÍÍÍÍÍÊÍÍÍÍÍÑÍÍÍÍÍÊÍÍÍÍÍÑÍÍÍͼ º º ³ ³ ³ º º ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄĺ PHYSICAL º ³ ³ º ADDRESS º ³ PAGE DIRECTORY ³ PAGE TABLE º º ³ ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» ³ ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º º ³ º º ³ º º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ ³ º º ³ ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹  ³ º º ÀÄĺ PG TBL ENTRY ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹ ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹ Àĺ DIR ENTRY ÇÄÄ¿ º º ÌÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͹ ³ º º º º ³ º º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ ³ ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ  ³  ÉÍÍÍÍÍÍÍ» ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ º CR3 ÇÄÄÄÄÄÄÄÄÙ ÈÍÍÍÍÍÍͼ - Traducción de dirección líneal a física - * Selectores * En modo protegido llamamos selectores a los registros de segmentos. Uno de los aspectos que cambia en modo protegido es el par segmento:desplazamiento (offset), usada para direcciones lejanas: es reemplazado por par selector:desplazamiento. Aunque el tamaño de los registros de segmento siguen siendo el mismo (16bits), su implementación es nueva. Ahora son llamados selectores y se usan como índices dentro de tablas del sistema llamadas GDTs (global descriptor table: tabla de descriptores globales) o la LDT (local descriptor table: tabla de descriptores locales). Cada una de las entradas de estas tablas consta de un record de 64 bits con información que describe los atributos de un segmento y su localización. TABLA DE DESCRIPTORES GLOBALES TABLA DE DESCRIPTORES LOCALES ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º M º ³ º M ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ | | | | | | | | ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º N + 3 º ³ º N + 3 ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º N + 2 º ³ º N + 2 ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º N + 1 º ³ º N + 1 ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ ÌÍÍÍÍÍÍÑÍÍÍÍÍØÍÍÍÍÍÑÍÍÍÍÍ͹ º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º N º ³ º N ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ | | | | | | | | ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» ÉÍÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÑÍÍÍÍÍÍ» º ³ ³ ³ º º ³ ³ ³ º ÇÄÄÄÄÄÄÁÄÄ(UNUSED)ÄÁÄÄÄÄÄĶ ÇÄÄÄÄÄÄÁÄÄÄÄÄÅÄÄÄÄÄÁÄÄÄÄÄĶ º ³ º º ³ º ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ ÈÍÍÍÍÍÍÍÍÍÍÍÍÏÍÍÍÍÍÍÍÍÍÍÍͼ   ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» ³ ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» ³ º GDTR ÇÄÄÙ º LDTR ÇÄÄÙ ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ - Tablas de descriptores - El selector de 16 bits se divide en 3 partes. 15 4 3 0 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÑÍÑÍÍÍ» º ³T³ º º ÍNDICE ³ ³RPLº º ³I³ º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÏÍÏÍÍͼ TI - TABLE INDICATOR: Indicador de tabla RPL - REQUESTOR'S PRIVILEGE LEVEL: nivel de privilegio requerido - Formato de un Selector - bit descripción 15-3 = ÍNDICE: desplazamiento dentro de la GDT o de la LDT 2 = TI: está activo (en 1) si el descriptor está en una LDT, sino está en una GDT 1-0 = RPL (requested priviledge level: nivel de privilegio requerido): El desplazamiento (bits 15-3) indica cual entrada, de una de las nuevas tablas del sistema contiene, el descriptor que indica la ubicación y propiedades de un segmento. Los bits 2-0 describen atributos especiales del descriptor. Si el bit 2 (TI) está establecido (está en 1) el descriptor se encuentra en una LDT, sino está en la GDT. Los bits 1-0 son el RPL, que describe el nivel de protección que amerita el segmento. Como son dos bits, pueden haber hasta 2^2=4 niveles de privilegio, desde cero a tres. En el 80386, cero es el nivel de privilegio más alto, tres el más bajo. El nivel más alto se reserva a los segmentos que contengan códigos o datos más seguros y esenciales para el funcionamiento del sistema. Los bits 15-3 señalan cuál de las entradas contiene el descriptor del segmento que buscamos. Como cada descriptor tiene un tamaño de 8 bytes, el ÍNDICE debe ser multiplicado por 8 para hallar el descriptor. Los selectores, en conjunción con otros registros especiales, permiten localizar la ubicación de los descriptores de segmento. 15 0 31 0 DIR. ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» LÓGICA º SELECTOR º º DESPLAZAMIENTO º ÈÍÍÍÑÍÍÍÍÍÍÍÍÍÑÍͼ ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÑÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ ÚÄÄÄÄÄÄÙ  ³ ³ TABLA DESCRIPTORA ³ ³ ÉÍÍÍÍÍÍÍÍÍÍÍÍ» ³ ³ º º ³ ³ º º ³ ³ º º ³ ³ º º ³ ³ ÌÍÍÍÍÍÍÍÍÍÍÍ͹ ³ ³ º DESCRIPTOR º BASE ÉÍÍÍ» ³ ÀĺDE SEGMENTO ÇÄÄÄÄÄÄÄÄÄÄÄÄÄĺ + ºÄÄÄÄÄÄÙ ÌÍÍÍÍÍÍÍÍÍÍÍ͹ DIRECCIÓN ÈÍÑͼ º º ³ ÈÍÍÍÍÍÍÍÍÍÍÍͼ ³  DIR. ÉÍÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍËÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» LINEAL º DIR º PAG. ºDESPLAZAMIENTOº ÈÍÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÊÍÍÍÍÍÍÍÍÍÍÍÍÍͼ - Traducción de dirección lógica a física - La base de la tabla descriptores se encuentra en un registro especial del 80386; la de la GDT se encuentra en el registro GDTR y la de la LDT en el LDTR. El procesador usa el índice del selector para ubicar la entrada de la tabla de descriptores cuya dirección base está en el GDTR o en LDTR del caso. * Descriptores * Los descriptores son una estructura con información sobre los segmentos que maneja el procesador. Estos descriptores constituyen las entradas de unas tablas que el sistema ubica en la memoria para facilitar el manejo dinámico de los segmentos. Ya dijimos que estas tablas se llaman Tablas Descriptoras. Cada una de las entradas de estas tablas, los llamados descriptores, tienen un tamaño de 8 bytes (64 bits). La información que contiene un descriptor incluye la ubicación y tamaño del segmento, su nivel de privilegio y el modo permitido de acceso. Existen dos tipos de tablas de descriptores: globales y locales. Las primeras contienen información sobre segmentos con información común para todas las tareas. Las locales incluyen información sobre el área de memoria privada de una tarea. La GDT y la LDT pueden mantener hasta 8192 decriptores de 64 bits cada uno, debido a que contamos con los bits 15-3 del selector para direccionarlos. El 1er. descriptor en la GDT esá reservado y nunca puede ser usado para nada (es NULO). Cada estructura de descriptor en las tablas es como a continuación: descriptor struct limit_lo dw ? ; bits de límite 15-0 base_lo dw ? ; bits de base 15-0 base_mid db ? ; bits de base 23-16 type1 db ? ; tipo de selector limit_hi db ? ; bits de límite 19-16 y otra información base_hi db ? ; bits de base 31-24 descriptor ends A continuación el formato general de un descriptor de segmento: Atributos (23-20)·(15-8) / \ / \ 31 24-23 / 20-19 16-15 \ 8-7 0 Dirección -------------- --- --- --- --- -------- --- --- --- --- --- --------- n+4 | base_hi | G |D/B| 0 |AVL|limit_hi| P |DPL| S |tip| A |base_mid | | (31-24) | |(16-19) | | (23-16) | Dirección -------------- --------------- -------- ------------------- --------- n | base_lo | limit_lo | | (15-0) | (15-0) | ------------------------------ -------------------------------------- Cuando todos los bits de base (bits 15-25; 60-63) son puestos juntos, forman un valor de 32 bits que representa el comienzo del segmento en la memoria. Con una dirección de 32 bits puede accederse a 4GB de memoria. El campo limit_hi se reparte así: bit # Nombre Descripción 0-3 límite bits altos de límite 19-16 4 AVL disponible sólo para el programador 5 - reservado (debe ser 0) 6 D tamaño por defecto del segmento 7 G granularidad El límite total es 20 bits (que permite un acceso de hasta un 1MB) pero si el bit G bit está activo (1) entonces el límite es multiplicado por 4096 (4kb) que establece ahora un máximo de cuatro gigabytes (4 GBs). El bit D define el tamaño por defecto del código de segmento, si está establecido es de 32bit sino es de 16 bits. El campo type1 de la estructura del descriptor define más atributos del segmento. bit # Nombre Descripción 4 S Define el tipo de descriptor. Si S=1 entonces es un segmento estándar de código/datos y el resto de la estructura es como sigue: 3 T define si se trata de un segmento de código o de datos. Si T=0, es un segmento estándar de datos y el resto de la estructura es como sigue: 0 A Accedido 1 W escribible 2 E Expandido pero si T=1, un segmento de código y el resto de la estructura es como sigue: 0 A Accedido 1 R Leíble 2 C Conformante Ahora, si S=0, se trata de un descriptor de sistema y el resto de la estructura es como sigue: 0-3 TYPE Define el tipo (LDT, puerta INT o de trampa, etc.) El resto ya no depende de T ni de S: 5-6 DPL Nivel de privilegio del descriptor (descriptor privledge level) 7 P presente Esto parece muy complejo. He aquí lo que los campos pueden significar: S (bit4) : Define si es un segmento de código/datos o un descriptor especial. T (bit3) : Define si es un segmento de código o de datos A (bit0) : Este bit es puesto por el CPU cada vez que este segmento es para programar (bit1): Define si los segmentos de código pueden ser leídos, lo que permitiría instrucciones como: mov ax, cs:[ebx] W (bit1) : Este bit define si los segmentos de datos pueden ser escritos. C (bit2) : Lo explicaremos luego. E (bit2) : Define si el segmento de datos de expansión descendente. DPL (bits5-6) : nivel de privilegio del descriptor (descriptor privledge level) P (bit7) : Siestá activo el descrptor está presente (válido) sino sólo el byte del tipo de descriptor debe mantener la información y el resto es ignorado por el CPU, cargar un selector nopresente es inválido. Campos de protección de los descriptores de segmento DESCRIPTOR DE SEGMENTO DE DATOS 31 23 15 7 0 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÑÍÑÍÑÍÑÍÍÍÍÍÍÍÍÍØÍÑÍÍÍÍÍÑÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º±±±±±±±±±±±±±±±±±³±³±³±³A³ LÍMITE ³±³ ³ TIPO ³±±±±±±±±±±±±±±±±±º º±±±BASE 31..24±±±³G³B³0³V³ 19..16 ³P³ DPL ³ ³±±±BASE 23..16±±±º 4 º±±±±±±±±±±±±±±±±±³±³±³±³L³ ³±³ ³1³0³E³W³A³±±±±±±±±±±±±±±±±±º ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÅÄÁÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º º±±±±±BASE DEL SEGMENTO 15..0±±±±±±±³ LÍMITE DEL SEGMENTO 15..0 º 0 º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ DESCRIPTOR DE SEGMENTO EXECUTABLE 31 23 15 7 0 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÑÍÑÍÑÍÑÍÍÍÍÍÍÍÍÍØÍÑÍÍÍÍÍÑÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º±±±±±±±±±±±±±±±±±³±³±³±³A³ LIMIT ³±³ ³ TIPO ³±±±±±±±±±±±±±±±±±º º±±±BASE 31..24±±±³G³D³0³V³ 19..16 ³P³ DPL ³ ³±±±BASE 23..16±±±º 4 º±±±±±±±±±±±±±±±±±³±³±³±³L³ ³±³ ³1³0³C³R³A³±±±±±±±±±±±±±±±±±º ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÅÄÁÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º º±±±±±BASE DEL SEGMENTO 15..0±±±±±±±³ LÍMITE DEL SEGMENTO 15..0 º 0 º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ DESCRIPTOR DE SEGMENTO DE SISTEMA 31 23 15 7 0 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÑÍÑÍÑÍÑÍÍÍÍÍÍÍÍÍØÍÑÍÍÍÍÍÑÍÑÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º±±±±±±±±±±±±±±±±±³±³±³±³A³ LÍMITE ³±³ ³ ³ ³±±±±±±±±±±±±±±±±±º º±±±BASE 31..24±±±³G³X³0³V³ 19..16 ³P³ DPL ³0³ TIPO ³±±±BASE 23..16±±±º 4 º±±±±±±±±±±±±±±±±±³±³±³±³L³ ³±³ ³ ³ ³±±±±±±±±±±±±±±±±±º ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÅÄÁÄÄÄÄÄÁÄÁÄÁÄÁÄÁÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º º±±±±±BASE DEL SEGMENTO 15..0±±±±±±±³ LÍMITE DEL SEGMENTO 15..0 º 0 º±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±³ º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍØÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ Notas: El LÍMITE de un segmento define el valor máximo al que un desplazamiento en el segmento puede ser accedido (mov al,[1000h] cuando LIMITE=9999h es inválido). La BASE de un segmento define en qué lugar de la memoria comienza (mov al,[1000h] cuando BASE=10000h accederá a la RAM en la dirección 10000h+1000h=11000h). Los segmentos de datos pueden ser leídos desde: SS debe ser cargado con un selector de datos que debe ser para escritura. CS sólo puede ser cargado con un selector de cádigo. DS, ES, FS, GS cpuede ser cargado con un selector datos o de código. Si E (expand down) está activo, cambia la interpretación del LÍMITE: ahora LÍMITE define la dirección más baja que puede ser accedida desde el segmento. Debido a que los segmentos de pila se extienden hacia direcciones inferiores, respecto a la base, el LÍMITE puede ser reducido para dar más espacio a la pila. La LDT es la tabla de descriptores locales (local descriptor table): es lo mismo que la GDT, pero con la excepción de que cada tarea tiene su propia LDT y la GDT es compartida por todo el sistema. La LDT es simplemente una entrada en la GDT, que describe donde está esta LDT. La LDT puede contener cualquier cosa que pueda contener una GDT. Si se necesita usar un selector que está en la LDT, entonces el bit 2 de un selector debe activarse y el descriptor vendrá de la LDT. Cualquiera de las reglas de arriba pueden romperse cuando ocurren INTs (como cuando se dispara una IRQ). Este tipo de interrupciones se llaman excepciones y serán detalladas más abajo. * Interrupciones * El antiguo vector de interrupciones usado en modo real, que ocupaba la localidad 0:0 de la memoria, ya no es usado en modo protegido. Ahora hay un nuevo conjunto de hasta 256 interrupciones posibles que se acceden de forma parecida a como se acceden los segmentos vía la GDT: a través de una tabla de descriptores que, en este caso, contiene punteros descriptores a los servicios o manejadores de las interrupciones; pero ahora sería una tabla con descriptores de interrupciones, conocida como la IDT (Interrupt Descriptor Table): las interrupciones en modo protegido ya no están fijadas a una ubicación única, la localidad 0 de la memoria física, sino que ahora pueden flotar en el espacio de memoria lineal con absoluta libertad. El formato de una entrada de la IDT es un poco diferente al de un descriptor de la GDT: 31 16-15 8-7 0 Dirección ------------------------------ --- --- --- --- --- ---------- n+4 | Desplazamiento (offset16_31) | P |DPL| S |tip|000|zero_byte | | | (flags) | (23-16) | Dirección ------------------------------ ------------------------------ n | Selector (selector0_1) | Desplazamiento (offset0_15) | | (31-16) | (15-0) | ------------------------------ ------------------------------ En ensamblador: interrupt_descriptor struc offset0_15 dw ? ; palabra baja del despl. del manejador selector0_15 dw ? ; selector del segmento zero_byte db 0 ; sinuso en este formato flags db ? ; byte de banderas offset16_31 dw ? ; palabra alta del despl. del manejador interrupt_descriptor ends Cada entrada de la IDT, sus descriptores, son llamados descriptores de puerta porque permiten son una especie de vía de acceso controlada a servicios a los que debería ser imposible acceder por otras vías. Pueden ser de tres tipos: · Puerta de interrupción: transfieren el control a un manejador de interrupción con interrupciones deshabiitadas. · Puerta de trampa: transfieren el control a un manejador de interrupción sin cambiar la badera IF. · Puertas de tareas: permiten pasar el control de una tarea a otra. Las puertas de llamada, como también se conoce a los descriptores de puerta, permiten acceso a los puntos de entrada de un segmento de código de mayor nivel de privilegio, sin detrimento del nivel de seguridad. Los manejadores de interrupciones son los servicios que ofrece el sistema. Es lógico que estén protegidos y ubicados en un nivel de privilegio más alto. Por ese motivo, los programas de aplicación, que corren en un nivel de privilegio menor, acceden a esos servicios a traves de puertas de llamada. La dirección lineal de una IDT se ubica en un registro especial del procesador llamado IDTR. Se pone ahí usando la instrucción LIDT (Load IDT). * NOTA * Windows ofrece servicios alternativos a los del BIOS y a los de DOS, escritos especialmente para modo protegido. Cuando un programa los usa, el CPU revisa la IDT (no la tabla del vector de interrupciones, IVT: Interrupt Vector Table) para obtener su dirección. En algunos caos, la ejecución va a los servicios originales del BIOS o del DOS. En estos casos, la entrada de la IDT apunta a un manipulador (handler) especial que además de cambiar el CPU a modo real, debe convertir todos los punteros del selector a un valor de segmento. Luego el manipulador revisará en la IVT para obtener la dirección del servico en modo real. Para hallar el manipulador (handler) de una int en modo protegido, por ejemplo de la int 80h, se multiplica el número de la interrupción (80h) por 8 (el tamaño de un descriptor) y ya tenemos la ubicación del descriptor en la IDT (la ubicación de la IDT está en el IDTR). El descriptor ubicado, cuya estructura expusimos arriba, es del tipo puerta. Hay un campo de 16 bits para un selector, que se carga en CS cuando se activa la interrupción, y designa un segmento de código; mejor dicho, "selecciona" una entrada en la GDT donde está un descriptor de segmento de código que indica la dirección del handler de la interrupción. Al segmento obtenido, hay que añadir el desplazamiento (offset) en el descriptor de la IDT, y con ello obtenemos la dirección lineal de la entrada al handler de la interrupción. Las primeras 32 interrupciones de la IDT se reservan para excepciones, especie de interrupciones que son llamadas por el procesador para señalar eventos como una ejecución de código inválida, intento de acceso más allá del límite fijado en el campo LIMIT de un descriptor, o cualquier otro evento que no sea válido. * Excepciones * Como ya hemos dicho una excepción o falta es una especie de seña que genera el el procesador cuando ocurre algo erróneo. En realidad son manipuladores de interrupciones que se acceden desde la interrupción 0 a la 01Fh (32). La más popular de las excepciones es la 13h. Esta excepción manipula la mayoría de los errores de programación: · carga del CS (con JMP o CALL) con un descriptor que no sea de un segmento de código. · escritura en un segmento de código · carga de SS con un descriptor que no sea de un segmento de datos que acepte escritura. · acceso a un segmento por encima de su límite. · intento de cambiar a modo protegido directamente, mientras se corre en modo V86. Todavía hay muchas más: INT : DESCRIPCIÓN 0 división por cero: llamada cada vez que una instrucción div/idiv intenta dividir entre 0 1 excepción de depuración: 2 NMI, interrupción no enmascarable (Non-Maskable Interrupt): llamada por cualquier dispositivo de hardware que haya fallado; es esencial para los sistemas de operación (es decir: falla de paridad sobre la RAM) 3 punto de ruptura (breakpoint): llamada cada vez que ocurre la INT3, cuyo código de operación es 0CCh). Este byte de código de operación puede ser insertado "manualmente" en un código para propósitos de depuración. 4 desbordamiento (overflow): llamada cuando se ejecuta la instrucción INTO y se activa la bandera de desbordamiento OF. 5 chequeo de límites (bounds check): ocurre si se ejecuta la instrucción BOUND y el operando supera el límite establecido. 6 código inválido: llamada cuando no es válida la siguiente instrucción en una dirección CS:(E)IP no es una instrucción 80x86 válida. Esto también podría llamarse si la instrucción es mayor a los 15 bytes que puede ocurrir si se usan muchos prefijos. 7 dipositivo no disponible: usada generalmente para optimizar el uso de la FPU. 8 doble falta: generada si ocurre una excepción mientras el sistema está procesando ya otra. Naturalemente, esto puede ocurrir, pero el sistema debe ser muy cuidadoso ahora porque una tercera excepción hará que el procesador se reinicie. 9 80486+ = reservado (80386- = co-pro segment overrun): en el 80486, esta excepción es nueva. En el 80386 señala un error de la FPU. 10 TSS (task state segment) inválido: se genera siempre que se carga una tarea inválida. 11 segmento no presente: se produce si se usa un descriptor que no tiene el bit P (bit de presencia) activo. Esto significa que el segmento no está presente en ese momento en la memoria. Se usa generalmente en el manejo de la memoria virtual. 12 excepción de la pila: esto puede ocurrir de dos maneras: a) cuando SS es cargado con un descriptor que no está presente; b) cuando el límte de la pila es sobrepasado o una instrucción trata de acceder a un operando en el segmento de pila (por ejemplo: mov al,ss:[ebx]) 13 fallo de protección general: si ocurre algo inválido que no pertenece a la categoría de las otras excepciones entonces se produce esta excepción. 14 fallo de página: usada en el sistema de paginación para indicar que se ha solicitado un elemento ubicado en una página que no está cargada en ese momento en la memoria. 15 reservado 16 error del FPU: ocurre cuando la FPU detecta un error en una instrucción que esté ejecutando. 17 chequeo de alineación: es una innovación qgregada en el i80486 que obliga al programador a alinear los datos. Se produce cuando se detecta una referencia a una dirección de memoria que está desalineada. Esto activará el bit 18, agregado al registro de banderas, el bit AC. 18-31 excepciones reservadas 32-255 disponible para el software Como puede observarse, las interrupciones normales ya no son las mismas; por ejemplo, la IRQ#5 genera la INT13, la misma que la falla de protección general. Ahora, en el 386+ el PIC (programable interrupt controller) puede ser proyectado de nuevo para establecer las IRQs en localizaciones diferentes dentro de la tabla de interrupciones. Los antiguos extensores DOS y los sistemas operativos acostumbraban hacer esto, mientras estaban en modo protegido, pero resultaba muy lento: cada vez que el extensor DOS debía regresar a modo real, por alguna razón el PIC tenía que ser reprogramado en el estilo tradicional: IRQ#0 = INT#8 e IRQ#8 = INT #70h. Cuando el control regresaba al modo protegido, nuevamente tenía que reprogramarse el PIC. Esto ya no es así en los extensores y OSs modernos. Entonces ¿cómo un manipulador de IRQ determina si un evento que ha disparado una INT viene de una IRQ, de una INT de usuario o de una excepción? Es muy simple. Se revisan los PICs para determinar si esperan por alguna IRQ. Si es así, salta al manejador de IRQ. Luego chequea dentro del segmento de código y ve si la última operación ejecutada fue la INTx (donde x es el actual manipulador de INT). Si es el caso, entonces el servicio que llama la INT es el que le corresponde; por ejemplo la INT 10h es es la misma que le de una excepción por error de la FPU y si el programa ejecutó la INT 10h para llamar los servicios de video se debería pasar el control al manipulador de la INT 10h. Si estos dos casos fueran falsos, entonces la INT debió haber sido causada por una excepción. Ahora el problema es que toda IRQ toma un tiempo adicional antes de que el procesador tome el control. * Conmutación de Tareas * La conmutación de tareas es la base de la concurrencia en sistemas multitareas. En estos sistemas, el procesador maneja varias tareas al mismo tiempo. Como el procesador no puede hacerlo simultáneamente entonces reparte su tiempo de trabajo entre las tareas activas siguiendo algún criterio de planificación. Para cambiar de una tarea a otra, lo que se hace es usar una nueva LDT, lo cual supone que el procesador revise la GDT y obtenga la dirección de la nueva LDT. El estado de la tarea abandonada debe ser guardado y el de la tarea entrante debe restaurarse: el estado consiste en el contenido de los registros del procesador y del coprocesador, además de varios valores en variables y punteros a memoria. El CPU tiene que mantener un segmento especial para cada tarea llamado Segmento del Estado de la Tarea (TSS: Task State Segment ), donde coloca lo necesario para el cambio. El CPU lleva una cuenta de donde están los TSSs, así que mantiene descriptores para el TSSs en la GDT: la GDT no mantiene sólo descriptores para las LDTs. Ya profundizaremos sobre la planificación de tareas en sistemas de tiempo compartido. * Nuevas instrucciones * Instrucción Descripción Modo LGDT S Load Global Descriptor Table Register. Cargar el registro Ambos de la tabla de descriptores globales. S especififica la localidad de memoria que contiene el primero de los 6 bytes a ser cargado en el GDTR. SGDT D Store the global descriptor table register. Almacenar el Ambos registro de la tabla de descriptores globales. D especifica la localidad de memoria que obtendrá el primero de los 6 bytes a ser almacenado en el GDTR. LIDT S Load the interrupt descriptor table register. Cargar el Ambos registro de la tabla de descriptores de interrupciones. S especififica la localidad de memoria que contiene el primero de los 6 bytes a ser cargado en el IDTR. SIDT D Store the interrupt descriptor table register. Almacenar Ambos el registro de la tabla de descriptores interrupciones. D especifica la localidad de memoria que obtendrá el primero de los 6 bytes a ser almacenado en el IDTR. LMSW S Load the machine status word. Carga la palabra de estado Ambos de la máquina. S especifica la palabra a ser cargada en MSW. Es la instrucción que se usaba para cambiar de un modo a otro; con el 80386 cayó en desuso. SMSW D Store the machine status word. Almacenar la palabra de Ambos estado de la máquina. D es un operando que especifica la localización de la palabra o registro donde el MSW debe ser salvado. LLDT S Load the local descriptor table register. Cargar el Protegido registro de la tabla de descriptores locales. S indica el operando que especifica una palabra a ser cargada en la LDTR. SLDT D Store the local descriptor table register. Almacenar el Protegido registro de la tabla de descriptores locales. D es un operando que specifica la localización de la palabra donde la LDTR será salvada. LTR S Load the task register. Cargar el registro de tareas. S Protegido es un operando que especifica una palabra a ser cargada en el registro de tarea (TR: Task Register). STR D Store the task register. Almacenar el registro de tareas. Protegido D es un operando que specifica la localización de la palabra donde será almacenado el TR. LAR D,S Load access rights byte. Cargar el byte de derechos de Protegido acceso. S especifica el selector para el descriptor cuyo byte de acceso es cargado en el byte superior del operando de D. El byte bajo especificado por D es limpiado. La bandera de cero ZF es activada si la carga se completa exitosamente; sino es limpiada. LSL R16,S Load segment limit. Cargar el límite del segmento. S Protegido especifica el selector para el descriptor cuya palabra es cargada en el *word register operand R16*. La bandera de cero ZF es activada si la carga se completa exitosamente; sino es limpiada. ARPL D,R16 Adjust RPL field of the selector. Ajuste del campo RPL Protegido del selector. D especifica un selector cuyo campo RPL es incrementado para que coincida con el campo PRL en el registro. La bandera de cero ZF es activada si es exitoso el ajuste; en otro caso, es limpiada. VERR S Verify read access. Verificar acceso de lectura. S Protegido especifica el selector para el segmento a ser verificado para operación de lectura. Si es satisfactorio, se activa la bandera de cero ZF; en otro caso es limpiada. VERW S Verify write access. Verificar acceso de estritura. S Protegido especifica el selector para el segmento a ser verificado para operación de escritura. Si es satisfactorio, se activa la bandera de cero ZF; en otro caso es limpiada. CLTS Limpiar la bandera de cambio de tarea. Protegido A continuación algunos ejemplos: 1. LGDT [INIT_GDTR] Carga GDTR con la base y límite apuntado por la dirección INIT_GDTR para crear una GDT en memoria. Esta instrucción es para ser usada durante la inicialización del sistema y antes de cambiar el 80386 a modo protegido. Una vez cargado el contenido actual de la GDTR puede ser salvada en memoria ejecutando la instrucción SGDT: 2. SGDT [SAVE_GDTR] 3. Las instrucciones LMSW y SMSW se usan para cargar y almacenar el contenido de la palabra de estado de la máquina MSW, respectivamente. Hay instrucciones que son usadas para cambiar el 80386 de modo real a modo protegido. Para hacer esto debemos poner el bit menos significante en el MSW a 1. Esto se puede hacer leyendo primero el contenido de la MSW, modificando el LSB (PE), y luego escribiendo el valor devuelta a la parte MSW del CR0. La siguiente secuencia cambia un 80386 que opera en modo real a modo protegido: SMSW AX ;read from the MSW OR AX,1 ;modify the PE bit LMSW AX ;write to the MSW Un secuencia equivalente es: mov ax, cr0 or ax, 1 mov cr0, ax Esta última secuencia es la que debe ser usada; la anterior se considera obsoleta. * Instrucciones Privilegiadas * El procesador 386 se ejecuta en uno de cuatro niveles de privilegio definidos por el nivel CPL en el registro EFLAGS. En el nivel 0, se permiten todas las instrucciones. Cuando se ejecuta en cualquier otro nivel (1-3), las siguientes instrucciones causan un fallo o excepción de protección general: SGDT SIDT STR SLDGT LGDT LIDT LTR LLDGT ARPL LAR LSL VERR VERW LMSW SMSW Además, si se usa la LDT, cada tarea es asignada a un bitmap que representa cuales puertos E/S pueden ser accedidos. Las instrucciones E/S para los puertos que no están permitidos también causan excepciones de protección general. Instrucciones para la gestión de Entrada/Salida ----------------------------------------------- Para comunicarse con ciertos dispositivos del sistema --como el temporizador, el puerto de impresión, la tarjeta de sonido, el controlador de disco o de DMA, etc. -- el procesador hace uso de unos canales asignados en la memoria para ello que se llaman puertos de hardware. Cada uno de estos canales se localiza o identifica con un valor entero y funcionan como una especie de índice o puntero a algún registro de los dispositivos con los que se quiere establecer la operación E/S. Estos registros pueden ser de Entrada (lectura) o de Salida (escritura). Para escribir en los registros de algún dispositivo, habrá que indicar el número del puerto de salida pertinente y la orden o el dato que va a pasársele. Lo mismo para leer un dato de un registro de lectura de un dispositivo: debe indicarse el número del puerto de entrada correspondiente y el almacén donde debe ser devuelto el valor leído. Las principales instrucciones del 8086 para la gestión de los puertos de entrada/ salida del sistema son: IN y OUT, respecivamente. El formato para la instrucción IN es: IN registro_acumulador, puerto y dice al procesador que ponga en AL, AX o EAX, dependiendo del tamaño del operando, el valor en el puerto indicado por el segundo operando, que puede ser un valor inmediato o un valor en el registro DL, DX o EDX. El uso del registro DL, DX, o EDX, permite un uso dinámico de la instrucción. El formato para la instrucción OUT es: OUT puerto, registro_acumulador y ordena que se envíe al puerto indicado por el primer operando, el contenido en AL, AX o EAX, dependiendo del tamaño del operando. De nuevo, el puerto puede ser indicado por un valor inmediato o usando el registro DL, DX o EDX. Algunas direcciones de puertos bien conocidas son: 020h - 023h Registro de la máscara de interrupción 040h - 043h Temporizador/contador 060h Entrada del teclado 061h Varias funciones: salida del teclado, bocina (bits 0 y 1), etc. 064h Estado del buffer del teclado 200h - 20Fh Controlador de juego - Interface MIDI 2F8h - 2FFh Puerto serial COM2 3C0h - 3CFh EGA/VGA 3F0h - 3F7h Contrilador de disco 3F8h - 3FFh Puerto serial COM1 - Un ejemplo - Un ejemplo rápido es la siguiente rutina que usa el puerto 61h para producir un sonido con la bocina del PC. Para ello, usamos dos dispositivos específicos del PC: la Interfaz Programable de Periféricos y el Temporizador Programable de Intervalo. ; ----------------------------------------------------------------------- TITLE BOCINA.ASM ; ----------------------------------------------------------------------- .model tiny .code org 100h main: cli in al, 61h ; Salvar el byte de información en el push ax ; puerto 61h. cli mov cx, 100h call sound ; Llamar a la rutina que hace sonar ; el parlante. in al, 61h ; Desactivar el parlante and al, 11111100b ; 0FCh desactiva el parlante out 61h, al ; enviarlo al puerto pop ax ; Restablecer el byte de información out 61h, al ; original que estaba en el puerto 61h. sti int 20h ; Salir a DOS. ; ----------------------------------------------------------------------- sound: set_ppi: mov al, 10110110b ; canal 2 out 43h, al ; operación y modo 3 set_freq: mov ax, 2711 ; frecuencia en ax: 1.193.180 / 400 out 42h, al ; enviar byte bajo primero mov al, ah ; colocar byte alto en al out 42h, al ; y enviarlo active_spk: or al, 00000011b ; bit 0 y bit 1 activos out 61h, al ; y excitar el parlante con el PPI clean_counter: mov ah, 1 mov cx, 0 mov dx, cx int 1Ah delay_it: mov ah, 0 int 1Ah ; leer de nuevo cmp dl, 10 ; y comparar con el conteo de retardo jne delay_it ; si no es igual, seguir esperando ret end main ; ----------------------------------------------------------------------- Analicemos el programa. Lo primero que hace es obtener un byte de la información que se encuentra en el puerto 61h y guardarla en la pila: in al, 61h push ax ¿Por qué? porque luego va a llamrase a una rutina call sound que seguramente va a cambiar la información original en el registro que comunica con el puerto 61h. ¿Con cuál dispositivo comunica el puerto 61h? Con uno de los registros E/S de la Interfaz Programable de Periféricos, PPI (Programmable Peripherical Interface), un dispositivo programable de E/S general. Dispone de tres registros E/S, a los que pueden accederse a través de tres puertos: 60h, 61h y 62h. Podríamos decir que el puerto 61h comunica con el registro B de E/S. Un cuarto registro, el de control, se accedería a través del puerto 63h. La PPI se usa para almacenar los datos que llegan del teclado (puerto A, 60h), para leer la configuración del ordenador en los conmutadores de la placa base (puerto C, 62h) y para controlar el altavoz (puerto B, 61h). Dado que vamos a trabajar con el altavoz del sistema, usamos el puerto 61h. Ahora revisemos la rutina "sound". Primero ponemos en el registro DX el valor que determina la duración del sonido: sound: mov dx, duracion Antes de excitar el altavoz con el PPI, debemos configurar el PPI. Para no profundizar mucho, digamos que basta con enviar 10110110b (0B6h=182) al puerto 43h, que es el puerto donde debe enviarse la palabra de control que configura el temporizador. La frecuencia que será generada se establece enviando un valor de retardo apropiado al puerto 42h; este valor es: 1.193.180 / freq, donde la constante 1.193.180 es la frecuencia de la señal generada por el reloj que excita el PPI y "freq" es la frecuencia que deseamos genere el PPI. Como el puerto 42h es un registro de 8 bits y el dato que indica la frecuencia es de 16 bits, debe usarse dos instrucciones OUT; al hacer esto primero debe enviarse el byte menos significante (el bajo) y luego el más significante (el alto). sound: set_ppi: mov al, 10110110b ; canal 2 out 43h, al ; operación y modo 3 set_freq: mov ax, 2711 ; frecuencia en ax: 2711 = 1.193.180 / 400 out 42h, al ; enviar byte bajo primero mov al, ah ; colocar byte alto en AL out 42h, al ; y enviarlo Para que el PPI excite el parlante, se activa el bit 1 del puerto 61h, que habilita el PPI, y el bit 0 también del puierto 61h, que comunica la salida del PPI con el parlante. Los otros bits del puerto 61h tienen otros usos, así que no deben modificarse; esta es la razón por la cual salvamos el contenido original del puerto 61h y nunca establecemos los nuevos bits usando la instrucción MOV, sino la intrucción OR. active_spk: or al, 00000011b ; bit 0 y bit 1 activos out 61h, al ; y excitar el parlante con el PPI La instrucción OR es un operador lógico que compara el contenido de los operandos, bit por bit; si uno de los bits del segundo operando es 1, el bit correspondiente en el primer operando es puesto en 1. Por ejemplo: mov al, 01010101 mov bl, 10100011 or al, bl ; AL = 11110111 Luego incluimos código para mantener el tono. Pudimos haber usado una rutina simple como la siguiente, que no merece explicación, ya que lo que hace es realizar gran cantidad de bucles: mov bx, 25 pause1: mov cx, 65535 pause2: dec cx jne pause2 dec bx jne pause1 pero esto haría que la duración del tono variara con la diferencia de velocidad del procesador. Por ese motivo hemos usado el reloj del sistema como cronómetro. La interrupción 1Ah permite leer y establecer este reloj. Si en AH hay un cero, la interrupción devuelve la parte alta del contador en CX y la parte baja en DX; si colocamos uno en AH, podemos establecer la parte del contador pasando este valor en CX y la parte a través de DX: clean_counter: mov ah, 1 ; solicitud de escritura mov cx, 0 ; escribir 0 en la parte alta mov dx, cx ; y en la baja del contador int 1Ah La rutina anterior, limpia el contador. La siguiente espera a que en la parte baja halla un 10: delay_it: mov ah, 0 int 1Ah ; leer de nuevo cmp dl, 10 ; y comparar con el conteo de retardo jne delay_it ; si no es igual, seguir esperando Terminada la pausa, regresamos a la rutina principal y desactivamos el parlante: in al, 61h ; Desactivar el parlante and al, 11111100b ; 0FCh desactiva el parlante out 61h, al ; enviarlo al puerto Finalmente restauramos el puerto 61h original y salimos a DOS: pop ax ; Restablecer el byte de información out 61h, al ; original que estaba en el puerto 61h. sti int 20h ; Salir a DOS. - Instrucciones adicionales - Hay instrucciones adicionales para la entrada/salida de cadenas de caracteres: INSx Transfiere datos desde un puerto E/S, especificado en DX, a un buffer apuntado por ES:DI. El tamaño del registro de índice varía con el tamaño del dato, que se especifica usando B para byte, W para word y D para dword en donde está la x. Así que tendríamos las siguientes instrucciones: INSB, INSW, INSD. El formato sería. INSB reg/mem, DL INSW reg/mem, DX INSD reg/mem, EDX OUTSx Transfiere datos al puerto E/S especificado por DX. El origen del los datos están en DS:SI. El tamaño de los datos se indican reemplazando el sufijo x por B para byte, W para word y D para dword. El valor de SI se aumenta o decrementa con cada ejecucción dependiendo siestá activada o no DF. El formato es: OUTSB DL, reg/mem OUTSW DX, reg/mem OUTSD EDX, reg/mem Ambas instrucciones pueden usarse en conjunción con los prefijos tipo REP: mov cl, 5 mov dl, 61h lea si, string rep outsd dl, si Esta secuencia transfiere cinco caracteres desde 'string' al puerto de salida 61h. Las instrucciones de transferencia a puertos E/S tienen acceso restringido y controlado en modo protegido. Pequeña nota sobre intrucciones de operaciones lógicas ------------------------------------------------------ Otro tipo de intrucciones que vamos a usar un poco más adelante son las que rea- lizan operaciones lógicas. Su estudio detallado lo dejamos para el próximo capítulo. Aquí vamos a emplear tres: AND: operador binario de función AND lógica OR: operador binario de función OR lógica XOR: operador binario de OR eXclusivo Como puede verse, son tres operadores o funciones binarios (de dos operadores). También son binarios por que trabajan con lógica binaria sobre los bits de sus operadores (un bit puede tener dos estados, 1 o 0, de ahí que hablemos de lógica binaria). Una operación AND sólo da VERDAD (1) si todos sus operadores son VERDAD (1). Las instrucciones producen un cambio sobre el primer operador o de destino y afectan algunas banderas de acuerdo a las reglas que definen cada operación. Estas reglas se definen mediante tablas lógicas, pero aquí bastará dar una definición rápida. Una operación AND sólo da VERDAD (1) si todos sus operadores son VERDAD (1). Esta operación compara cada bit de sus operadores, si uno de ellos es FALSO (0), en- tonces el bit correspondiente del destino se pondrá en FALSO (0) y se activará la bandera de cero ZF. La instrucción se usa mucho para borrar uno de los nibles del operando de destino: mov al, 02Fh and al, F0h ; borrar el nible bajo este código dá como resultado AL = 20h Otro ejemplo: mov al, 10011101b mov dl, 00110011b and al, dl dará como resultado AL = 00010001b La operación lógica OR es casi la contraria de AND: da FALSO (0) sólo si todos los operandos también son falsos (0). Se emplea para escribir bits en operandos sin afectar otros: mov al, 02Eh or al, 1 dará AL = 02Fh. Otro ejemplo: mov al, 10011101b mov dl, 00110011b and al, dl dará como resultado AL = 10111111b La operación XOR es parecida a OR, pero es excluyente, es decir, sólo dara valor VERDAD (1) si un operando es FALSO (0) y el otro es VERDAD (1): mov al, 02Eh xor al, 1 dará AL = 0FFh. Otro ejemplo: mov al, 10011101b mov dl, 00110011b and al, dl dará como resultado AL = 10110110b Conmutación entre modos de funcionamiento ----------------------------------------- Los pasos para pasar de modo real a modo protegido son: · crear una GDT válida · opcionalmente, crear una IDT válida · deshabilitar las interrupciones · cargar en el GDTR un puntero a la GDT · si se creó una IDT, cargar en el IDTR un puntero a la IDT · activar el bit PE (protection enable) en el registro CR0. Esto activa la unidad de paginación y pone en marcha la gestión de memoria virtual, basada en el manejo de tablas, y todo el sistema de protección. · realizar un salto lejano (cargar CS e IP/EIP) para entrar a modo protegido (cargar CS con el selector del segmento de código), · cargar los registros DS y SS sobn el selctor del segmento de datos/pila · establecer una pila para el modo protegido · habilitar las interrupciones, si se van a usar. Como en modo protegido se precisa leer un descriptor para aceder a un segmento, se necesita tener una imagen mínima de la GDT en la memoria principal y tener en el registro GDTR su dirección antes de pasar a modo protegido. Debido a que el paso a modo protegido puede provocar interrupciones y excepciones, también es necesario una imagen mínima de la IDT y su dirección en el IDTR antes de la conmutación. Así que la definición de la GDT y de la IDT, lo mismo que la carga de los registros con sus direciones, GDTR e IDTR, debe hacerse en modo real. Para regresar a modo real: · hacer un salto largo a un segmento de código de 16-bits · cargar SS con un selector a un segmento de datos/pila de 16-bits · limpiar el bit PE · hacer un salto largo a una dirección en modo real · cargar los registros DS, ES, FS, GS, y SS con valores en modo real · establecer el IDTR a los valores en modo real (base 0, límite 0xFFFF), · rehabilitar interrupciones Antes de regresar a modo real, CS y SS deben contener selectores que apunten a descriptores que sean apropiados al modo real. Estos tienen un límite de 64 KB, tienen una granularidad de un byte, expansión descendente, se pueden escribir (data/stack segment only), y están presentes (Access byte=1xx1001x). Antes de iniciar el proceso de cambio, es bueno hacer algunas comprobaciones como obtener el tipo de procesador, revisión del modo actual del procesador, etc. * Definición de los segmentos * Debemos comenzar declararando dónde poner todos los segmentos. Con TASM y MASM podemos usar: segment code16 para public use16 ; <- segmento de código y de datos de 16 bits assume cs:code16, ds:code16 ends code16 segment code32 para public use32 ; <- segmento de código y de datos de 32 bits assume cs:code32, ds:code32 ends code32 Con NASM, para binarios que vamos a usar como bootstraps (código de arranque) podemos usar: BITS 16 ORG 0 para la sección en modo real, y BITS 32 para la sección en modo protegido. * Acceder a los 4GB * Como en modo protegido se precisa leer un descriptor para aceder a un segmento, Para acceder a los 4GB de memoria desde modo protegido, hay que habilitar la puerta A20, sino el direccionamiento de memoria quedará limitado a sólo 1 MB. Para habilitar la puerta A20 en los compatibles AT podemos usar las siguientes líneas de código: mov al, 0D1h ; Enviar la orden 0D1h de escritura out 64h, al ; al puerto 64h. mov al, 0DFh ; Escribir 0DFh (1101 1111b) out 60h, al ; en el puerto E/S 60h Y para deshabilitarla: mov al, 0D1h ; Enviar la orden 0D1h de escritura out 64h, al ; al puerto 64h. mov al, 0DDh ; Escribir 0DDh (1101 1101h) out 60h, al ; en el puerto E/S 60h Como en modo protegido se precisa leer un descriptor para aceder a un segmento, Como puede observarse, estas rutinas hacen uso de dos puertos E/S de hardware: los puertos 60h y 64h, que comunican con el controlador hardware del teclado. * NOTA SOBRE LA LÍNEA A20 * Ya hemos comentado que para manipular cualquier dato, el CPU debe tener su dirección de destino, que obtiene a través del bus de direcciones, mientras el dato mismo espera en el bus de datos. Un bus simplemente es un conjunto de líneas que conectan los dispositivos harware del ordenador. El bus de direcciones, por ejemplo, sería un conjunto de líneas reservadas para comunicar al CPU la ubicación de los datos que debe manipular; consta de 20 líneas de señales para treansmitir las direcciones de las posiciones de memoria y de los dispositivos conectados a la tarjeta madre. Así que el bus de direcciones es una especie de registro de 20 bits que usa el CPU para conocer la ubicación de los datos que debe manipular. Las tarjetas para el i8088, tenían 20 líneas de direcciones, así que abracaban un espacio de 2^20 direcciones, es decir, un ámbito de 1MB. A partir del 80286, el bus se amplió a 24 líneas de direcciones, lo que permite especificar 2^24 direcciones, es decir, abarcar un ámbito de 16MB. Para asegurar compatibilidad entre los dos procesadores, IBM decidió agregar hardware extra y conectaron la línea A20 del CPU a una salida del controlador del teclado (KBC), para que fuera posible habilitar o inhibir esa línea. La posibilidad de inhabilitar la línea A20, permitía seguir usando software original para el 8088 en plataformas más avanzadas. Cuando el KBC es programado apropiadamente, inhibe la línea A20 del CPU del bus de direcciones. Esta condición emula el ámbito de memoria del i8088 en un sistema controlado por un 80286. Cuando el KBC es programado para habilitar la línea A20 en el bus de direcciones, se puede acceder a un espacio de direcciones de hasta 16MB. Como la línea de dirección A20 del CPU se halla por defecto inhabilitada, cualquier acceso a memoria que se extienda sobre límites de megabytes impares (1M-2M, 3M-4M, etc.) está inhibida. Independientemente del estado de la compuerta, el programador siempre tiene acceso a todo megabyte de memoria par (0M-1M, 2M-3M, etc.). Para acceder a todo el espacio de direcciones debe habilitarse la línea A20. DOS incluye un controlador, HIMEM.SYS, que habilita esta línea. Pero si estamos escribiendo código que no corre en DOS y queremos acceder a todo el espacio de direcciones, entonces debemos programar el KBC para habilitar la línea A20. La línea A20, controlada por el controlador del teclado, permite o inhibe el acceso a la memoria por encima de 1 Mb. Al quedar inhibida, el sistema emula el direccionamiento de los antiguos PC/XT; cuando está habilitada, el CPU tiene acceso a todo el espacio de direcciones. Como la línea está controlada por el controlador de teclado, revisemos un poco cómo se programa este controlador. El controlador del teclado se programa a través de dos puertos: 60h y 64h. El puerto 60h se usa para el envío y recepción de datos a y desde el controlador. Este puerto puede ser usado para escribir o leer datos en controlador. Pero antes de usarlo, debemos solicitar su uso al controlador enviándole la orden pertinente a través del puerto para envío de órdenes: el 64h. El puerto 64h tiene dos usos: · lectura de estado del controlador · escritura o envío de órdenes al controlador Antes de escribir o leer datos a o desde el controlador del teclado, debemos avisarle enviándole la orden pertinente por el puerto 64h. Si queremos escribir un dato que cambie la configuración del controlador y active algún dispositivo, debe enviársele primero el requerimiento de escritura, que para el puerto de salida del contrtoador es la orden 0D1h; para leer el puerto de salida deberíamos enviar antes la orden 0D0h. Es lo que hacemos en el código de arriba: mov al, 0D1h ; Enviar la orden 0D1h (1101 0001) de escritura out 64h, al ; al puerto 64h. mov al, 0DFh ; Escribir 0DFh (1101 1111b) out 60h, al ; en el puerto E/S 60h Con este código se espera habilitar la línea A20, ya que para hacer esto el bit 1 del puerto de salida del controlador del teclado debe estar activo (1). Así que para deshabilitar la línea A20, sólo limpiamos ese bit enviando 0DDh (1101 1101b) en vez de 0DFh (1101 1111b). Aunque en teoría el código presentado debería funcionar, en la práctica puede fallar. Hay otra forma más segura: Gate_a20: call empty_8042 mov al, 0D1h ; orden de escritura out 64h, al call empty_8042 mov al, ah ; Activar (ah=0DFh) o desactivar (ah=0DEh) out 60h, al ; línea A20 call empty_8042 ret ; ================================================================== ; Esta rutina se asegura de que el requerimiento de envío de órdenes ; al teclado esté vacío o disponible (después de vaciar los buffers ; de salida) para las órdenes que habilitan la línea A20 que tiene ; que ver con los puertos usados para controlar el teclado en PCs. ; ================================================================== empty_8042: call Delay in al, 64h ; puerto de estado test al, 1 ; ¿buffer de salida disponible? jz no_output call Delay in al, 60h ; sí, entonces leerlo jmp empty_8042 ; y lo ignoramos no_output: test al, 2 ; ¿buffer de entrada lleno? jnz empty_8042 ; sí, seguir el bucle ret ; ------------------------------------- ; Breve retardo para operaciones de E/S ; ------------------------------------- Delay: jmp delay_01 delay_01: jmp delay_02 delay_02: ret Como puede observarse, es esencialmente la misma rutina que presentamos antes pero añade código que se asegura que los buffers de salida del controlador del teclado están vacíos antes de enviar órdenes y datos al buffer de salida. Además, la misma rutina sirve para habilitar o deshabilitar la linea A20, dependiendo del valor que se pase a través de AH (0DFh para activar o 0DDh para desactivar). Podemos notar que primero se consulta el contenido del puerto 64h, que da acceso al byte de estado dedl controlador del teclado: in al, 64h ; puerto de estado Luego se revisa si el buffer de salida está vacío comprobando si el bit 0 está activo. Si este es el caso, entonces hay un dato en el buffer de salida y se procede a leerlo hasta que esté vacío. test al, 1 ; ¿buffer de salida disponible? jz no_output call Delay in al, 60h ; sí, entonces leerlo jmp empty_8042 ; y lo ignoramos Ya seguros de que está vacío el buffer de salida, revisamos el bit 1 del byte de estado para asegurarnos que el buffer de entrada no está lleno: si el bit está activo, el buffer de entrada está lleno aún y se sigue esperando hasta que esté vacío de la rutina. no_output: test al, 2 ; ¿buffer de entrada lleno? jnz empty_8042 ; sí, seguir el bucle ret * Definición de los selectores * Otro asunto que debemos resolver es la definición de los selectores. Selector es el nombre que toman los registros de segmentos en MP (CS, DS, ES, FS, GS). Se llaman así porque su función es "seleccionar" el descriptor apropiado para definir un segmento. Tiene un tamaño de 16 bits y tiene la siguiente estructura: 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 --------------------------------------------------- -- -------- | Índice en la tabla de descriptores |TI| RPL | --------------------------------------------------- -- -------- RPL (bits 0-1] es el nivel de privilegio solicitado [Requested Privilege Level]. Si el descriptor en la tabla tiene un nivel de privilegio más alto que el del RPL, se produce una falla de protección general. TI [bit 02] selecciona el tipo de tabla de descriptores: TI=0 para GDT, TI=1 para LDT. El índice [bits 03-15] selecciona una entrada en la tabla de descriptores; su valor es multiplicado por ocho (el tamaño de un descriptor) para establecer la dirección del descriptor buscado en la tabla. Podemos definir unas constantes para la definición de selectores. RPL_0 equ 0 RPL_1 equ 1 RPL_2 equ 2 RPL_3 equ 3 TI_GDT equ 0 TI_LDT equ 4 La siguiente macro ayuda a establecer el índice: SET_DT_INDEX macro selector, XDT, index mov ax, selector lea cx, XDT mov dx, index*8 add cx, dx shl cx, 3 or ax, cx endm (no se puede usar CX para indicar el índice y el segundo parámetro debe ser una etiqueta o nombre en el área de datos) El siguiente código define un selector para un segmento de datos con RPL=0 en una LDT cuya base se indica con la etiqueta "LDT", en la entrada 2: SET_DT_INDEX RPL_0+TI_LDT, LDT, 2 mov ds, ax * Establecimiento de las tablas de descriptores y de los selectores * Para establecer los tipos de segmentos que vamos a usar, debemos confeccionar unas tablas con los descriptores apropiados. Los descriptores básicos son mantenidos en la Tabla de Descriptores Globales [GDT Global Descriptor Table]. Opcionalmente pueden haber Tablas de Descriptores Locales [Local Descriptor Table: LDT], cuyos descriptores sólo son usados por las aplicaciones, no por el sistema. Arriba hemos presentado la escritura de un descriptor de GDT: descriptor struct limit_lo dw ? ; bits de límite 15-0 base_lo dw ? ; bits de base 15-0 base_mid db ? ; bits de base 23-16 type1 db ? ; tipo de selector limit_hi db ? ; bits de límite 19-16 y otra información base_hi db ? ; bits de base 31-24 descriptor ends Otra tabla de descriptores es la Tabla de los Descriptores de las Interrupciones (Interrupt Descriptor Table: IDT) que contiene una entrada o descriptor por interrupción y que dice al procesador dónde encontrar los manipuladores (handlers) de las interrupciones. También presentamos arriba la estructura de un descriptor de IDT. interrupt_descriptor struc offset0_15 dw ? ; palabra baja del despl. del manejador selector0_15 dw ? ; selector del segmento zero_byte db 0 ; sinuso en este formato flags db ? ; byte de banderas offset16_31 dw ? ; palabra alta del despl. del manejador interrupt_descriptor ends Para establecer estas tablas, simplemente definimos las tablas ya inicializadas con sus respectivos valores: GDT_TABLE LABEL BYTE dummy_dscr descriptor <0,0,0,0,0> code32_dscr descriptor <0ffffh,0,0,9ah,0cfh,0> data32_dscr descriptor <0ffffh,0,0,92h,0cfh,0> code16_dscr descriptor <0ffffh,0,0,9ah,0,0> data16_dscr descriptor <0ffffh,0,0,92h,0,0> La línea "GDT_TABLE LABEL BYTE" define una etiqueta para una dirección en el área de datos que va a estar alineada en el límte de un byte. De esta manera podremos hacer referencia a la dirección del comienzo de la GDT usando la etiqueta GDT_TABLE. El primer descriptor es nulo, así que todos sus campos no inicializados en cero. El primer campo de los siguientes descriptores, limit_lo, vale 0ffffh, que indica el tamaño máximo de cada segmento, el segundo y el tercer campo, base_lo y base_mid, que indica la dirección de la base de cada segmento, se inicializan con cero, porque estos valores serán determinados en tiempo de ejecución en el código del programa; el cuarto campo, type1, indica el tipo de segmento: 9Ah = 1001 1010: P = 1 (Presente) bit 7 DPL = 00 (máximo: anillo 0) bits 6-5 S = 1 (segmento estándar bit 4 de código/datos) T = 1 (código, el resto de bit 3 los bits son ARC) C = 0 bit 2 R = 1 (leíble) bit 1 A = 0 bit 0 92h = 1001 0010: P = 1 (Presente) bit 7 DPL = 00 (máximo: anillo 0) bits 6-5 S = 1 (segmento estándar bit 4 de código/datos) T = 0 (datos, el resto de bit 3 los bits son AWC) C = 0 bit 2 W = 1 (escribible) bit 1 A = 0 bit 0 El quinto campo, limit_hi, se divide en dos nibles (nible=4bits): el nible alto tiene información adicional sobre el tipo de segmento y el nible bajo es la parte alta del límite; para los descriptores de segmentos de 32 bits este valor es: 0CFh = 1100 1111: G = 1 (límite X 4K) bit 7 D = 1 (ancho de los bit 6 registros es 32bits) - = 0 bit 5 AVL = 0 bit 4 lim. = 0Fh bits 0-3 El sexto campo, base_hi, es nulo. Así que tenemos dos registros estándar de 32 bits, uno de código, code32_dscr, y uno de datos data32_dscr; y dos registros estándar de 16 bits, uno para código, code16_dscr, y otro para datos, data16_dscr. Luego decimos al CPU dónde están nuestras tablas de descriptores. Para ello, podemos definir una estructura XDT_PTR, que permite declarar tipos de datos XDT_PTR, es decir, punteros a tablas de descriptores: ; definición: XDT_PTR struct xdt_size dw ? xdt_base dd ? XDT_PTR ends ; ... SIZE_OF_GDT EQU sizeof descriptor .data ; declaración (en el área de datos): gdt_reg XDT_PTR ; ... .code mov gdt_reg.xdt_size, SIZE_OF_GDT*5 mov gdt_reg.xdt_base, offset GDT_TABLE La línea A20, controlada por el controlador del teclado, permite o inhibe el La líneas "SIZE_OF_GDT EQU SIZEOF descriptor" emplea la directiva "SIZEOF", que devuelve el tamaño del tipo de dato de su operando. Así que en vez de hacer "SIZE_OF_GDT EQU 8", que permite usar SIZE_OF_GDT para indicar que la GDT tiene un tamaño de 8 bytes (64 bits), usamos SIZE_OF_GDT EQU SIZEOF descriptor que es más intuitiva. No podemos inicializar gdt_reg en el área de datos porque no hay manera de indicar la dirección del comienzo de la GDT, así que lo hacemos en tiempo de ejecución con las líneas: mov gdt_reg.xdt_size, SIZE_OF_GDT*5 mov gdt_reg.xdt_base, offset GDT_TABLE la primera instrucción señala que la GDT tiene 5 entradas y la segunda indica la ubicación de la GDT. Si empleamos NASM, tenemos otras opciones. Podemos definir así nuestra GDT: ; descriptor nulo gdt: dw 0 ; limit 15:0 dw 0 ; base 15:0 db 0 ; base 23:16 db 0 ; type db 0 ; limit 19:16, flags db 0 ; base 31:24 ; descriptor del segmento lineal de datos LINEAR_SEL equ $-gdt ; selector lineal dw 0xFFFF ; limit 0xFFFFF (1 meg? 4 gig?) dw 0 ; base pare éste siempre es 0 db 0 db 0x92 ; presente, anillo 0, datos, expand arriba, escribible db 0xCF ; granularidad 4KB (4 gig limit), 32-bit db 0 ; descriptor del segmento de código SYS_CODE_SEL equ $-gdt ; selector del código del sistema gdt2: dw 0xFFFF dw 0 db 0 db 0x9A ; presente, anillo 0, código, no-conforme, leíble db 0xCF db 0 ; descriptor del segmento de datos SYS_DATA_SEL equ $-gdt gdt3: dw 0xFFFF dw 0 ; db 0 db 0x92 ; presente, anillo 0, datos, expandible arriba, escribible db 0xCF db 0 gdt_end: gdt_ptr: dw gdt_end - gdt - 1 ; límite de la GDT dd gdt ; dirección lineal, física de la dirección address of GDT Esta declaración tiene varias peculiaridades. Una son las constantes: LINEAR_SEL, SYS_CODE_SEL y SYS_DATA_SEL, cada una teniendo un valor igual al índice desde la base de la GDT al respectivo descriptor. La otra peculiaridad es que aquí sí podemos inicializar en el área de datos el puntero a la GDT. El resto de los valores de la inicialización para los descriptores coinciden con los que presen- tamos más arriba. En este caso tenemos trtes descriptores, además del descriptor nulo: dos de datos de 32 bits y uno de código de 32 bits. En ambos casos, tendremos que indicar la dirección de la base para cada segmemto en su respectivo descriptor. * Constantes útiles para definir tablas de descriptores * En nuestra GDT, podemos definir varios tipos de descriptores, de acuerdo a los segmentos que queramos acceder. Para aliviar el trabajo, podemos definir, quizás en un archivo de cabecera aparte, una lista de los tipos estándar de segmentos: ACS_PRESENT equ 0x80 ; segmento presente: 1000 0000 ACS_CSEG equ 0x18 ; segmento de código: 0001 1000 ACS_DSEG equ 0x10 ; segmento de datos: 1000 0000 ACS_CONFORM equ 0x04 ; segmento conformante: 0000 0100 ACS_READ equ 0x02 ; segmento leíble: 0000 0010 ACS_WRITE equ 0x02 ; segmento escribible: 0000 0010 ACS_IDT equ ACS_DSEG ; el tipo de segmento es el mismo tipo ACS_INT_GATE equ 0x06 ; puerta de int para 286: 0000 0110 ACS_INT equ (ACS_PRESENT | ACS_INT_GATE) ; puerta de int presente ACS_CODE (ACS_PRESENT or ACS_CSEG or ACS_READ) ACS_DATA (ACS_PRESENT or ACS_DSEG or ACS_WRITE) ACS_STACK (ACS_PRESENT or ACS_DSEG or ACS_WRITE) Cada una de estas constantes es un valor que tiene activo (en 1) los bits de bandera pertinentes para determinar el tipo de segmento deseado. Con estas constantes podemos declarar así la GDT de arriba: dummy_dscr segment_descriptor <0,0,0,0,0> code32_dscr segment_descriptor <0ffffh,0,0,9ah,0cfh,0> data32_dscr segment_descriptor <0ffffh,0,0,92h,0cfh,0> code16_dscr segment_descriptor <0ffffh,0,0,9ah,0,0> data16_dscr segment_descriptor <0ffffh,0,0,92h,0,0> Obsérvese que en los segmentos de datos el cuarto campo (tipo) es 92h: ACS_PRESENT+ACS_DSEG+ACS_WRITE y en los de código 9ah ACS_PRESENT+ACS_CSEG+ACS_READ Así que también podemos declarar los descriptores de la GDT así: dummy_dscr segment_descriptor <0,0,0,0,0> code32_dscr segment_descriptor <0ffffh,0,0,ACS_PRESENT+ACS_CSEG+ACS_READ,0cfh,0> data32_dscr segment_descriptor <0ffffh,0,0,ACS_PRESENT+ACS_DSEG+ACS_WRITE,0cfh,0> code16_dscr segment_descriptor <0ffffh,0,0,ACS_PRESENT+ACS_CSEG+ACS_READ,0,0> data16_dscr segment_descriptor <0ffffh,0,0,ACS_PRESENT+ACS_CSEG+ACS_READ,0,0> - NOTA SOBRE EL DESCRIPTOR NULO - El descriptor nulo (dummy) no debe ser usado. En Modo Protegido hay mecanismos de protección como el descriptor "inválido" (cero): si un segmento es cargado con un descriptor cero, cada vez que se trate de acceder a la memoria a través de él se producirá un error de protección. El selector nulo es usado como "marcador" y es útil para depuradores si van a revisar si se ha usado un registro de segmento. * Cargar el GDTR y el IDTR * Después de declarar nuestra GDT, cargamos la dirección de la GDT en el registro GDTR con la instrucción LGDT: - TASM - lgdt [fword ds:gdt_reg] - NASM - LGDT gdt_reg Lo mismo vale para la IDT, si existe. La diferencia es que tenemos que cargar el puntero a la IDT en el registro IDTR, no en el GDTR; entonces debemos usar la instrucción: - TASM - lidt [fword ds:idt_reg] - NASM - LIDT idt_reg *Pasar a modo protegido* Ahora deshabilitamos las interrupciones: cli y pasamos a modo protegido: mov eax, cr0 or al, 1 mov cr0, eax (también puede usarse LMSW AX, pero el manual de Intel recomienda la anterior) Lo mismo vale para la IDT, si existe. La diferencia es que tenemos que cargar el Luego hay que realizar un JMP que limpie ciertas áreas del procesador de colas y cualquier otro material ahí guardado para ser usado en modo real: jmp offset start32, code32_idx CS queda cargado con el nuevo descriptor de modo protegido. Si no se hace esto el CPU no establecerá la caché del descriptor y el Instruction Prefetch Queue podría contener instrucciones escritas en una manera sólo válida para el Modo Real. Hecho esto, el CPU está establecido para modo protegido y se inicia la ejecución en un punto donde cargamos los otros registros de segmentos con sus respectivos selectores de modo protegido: - NASM - BITS 32 code32_idx: mov ax, SYS_DATA_SEL ; now in 32-bit pmode mov ds, eax ; EAX works, one byte smaller :) mov ss, eax nop mov es, eax mov fs, eax mov gs, eax xor eax, eax ; zero top 16 bits of ESP mov ax, sp mov esp, eax ... * Determinación del tipo de procesador * Por razones de seguridad, podemos revisar al comienzo sobre cuál procesador corre el programa para asegurarnos de que soporta nuestro método de cambio de modos. Lo expuesto, está pensado para procesadores 80386+; los procesadores 80286 necesitan un método de cambio distinto que no vamos a exponer porque consideramos obsoleto este procesador. Los procesadores anteriores el 80286 de la serie ix86, no sopor- tan cambio de modos: sólo corren en modo real. Si queremos comprobar el modo actual en que corre el procesador, podemos usar el siguiente código, que será suficiente para nosotrros por ahora: ;--- ; vfycpu - asegurarse de que está corriendo el CPU apropiado, en Modo Real ; y la línea A20 está habilitada. ; ; cambia AX,DX ; regresa AL=código de error su algo malo ocurrió ; cod. de error: 040h - no es un CPU de 32-bits (PM y A20 no se probaron) ; 001h - ya en PM (386+, A20 no se probó) ; 0FFh - A20 deshabilitada (CPU de 32-bits en Modo real) ; ; Por Jerzy Tarasiuk ; vfycpu proc pushf ; salvar banderas cli ; asegurarse de que las ISRs no interrumpirán ; ; 1. revisar si es al menos un 386 ; pushf pop ax ; AX=banderas xor ah, 40h ; toggle NT push ax ; pila: modified_flags,original_flags popf pushf ; pila: modified_flags_from_cpu,original_flags pop dx ; DX=BANDERAS pasasads a trevés del CPU mov al,dh xor al,ah jnz vcfail ; CPU inapropiado ; ; 2. asegurarse de que estamos en modo real ; mov eax, cr0 and al, 1 jnz vcfail ; si ya estamos in PM (quizás VM86) ; ; 3. asegurarse que está habilitada la línea ; push ds push es push bx xor bx,bx mov ds,bx mov ax,-1 mov es,ax xor [bx],ah ; cambiar el byte[000000h] mov al,es:[bx+10h] ; obtener el byte[100000h] xor [bx],ah ; cambiar el byte[000000h] xor al,es:[bx+10h] ; comparar el byte[100000h] con su valor previo ; 0 si no ha cambiado, -1 significa [000000h]==[100000h] pop bx pop es pop ds ; vcfail: popf ; restablecer las banderas test al,al ; comprobar el código de error ret vfycpu endp ; -------------------------------------------------------------------------------------- Una forma de obtener con presición el tipo de procesador es usar la instrucción CPUID, que sólo la tenemos a partir de los procesadores ix486+. Por este motivo, antes de usarla hay que realizar algunas pruebas. El algoritmo para su uso sería algo como: · chequear si el CPU soporta CPUID. · Si soporta, es 486, Pentium o superior y lo identificaremos usando CPUID. · Si tenemos un Cyrix con CPUID, es mejor usar registros DIR para una identificación más precisa. · Si el CPU no soporta CPUID, chequeamos si es de la familia Cyrix. · Si el CPU no tiene CPUID y no es Cyrix, debe ser un chip Intel/AMD "clásico. · fin del chequeo - Chequeo de CPUID - Es fácil: se intenta cambiar el valor del bit de CPUID bit (bit 21) en el registro EFLAGS. Si el bit puede ser *toggled, se soporta CPUID y puede ser usada para una identificación más precisa. En los chips Cyrix el bit no puede ser *toggled* si no se permite explicitamente. No nos extenderemos ahora en su uso. - Chequeo de CPU Cyrix - Se realiza una rutina de prueba de división; si el valor de la bandera es 00 o 40h, el CPU es Cyrix. - Chequeo de un intel/amd clásico - Si no podemos obtener la signatura RESET, no podemos hacer mucho aunque los Intel SX y DX pueden distinguirse verificando la presencia de FPU. No profundizaremos ahora en ninguno de estos métodos. Ejemplo de conmutador de modo real a modo protegido --------------------------------------------------- Una forma de obtener con presición el tipo de procesador es usar la instrucción A continuación todo el código de un bootstrap que incluye un módulo (load.asm) que realiza cambio de modo real a modo protegido. Cuando se arranca el sistema con este bootstrap, llegamos primero a un shell que incluye la orden "protek", que dice al sistema que pase el control al código que vemos en load.asm, que realiza el paso a modo protegido. En modo protegido llegamos a un shell que tra- baja sólo con golpes de tecla específicos, que activan interrupciones o conducen de vuelta al shell de modo real. ; ------------------------------------------------------------------- ; IMAGE.ASM ; Disk image %include 'boot.asm' %include 'console.asm' %include 'load.asm' ; para ensamblar con NASM: ; nasm image.asm -o IMAGE.bin ; ------------------------------------------------------------------- ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ----------------------------------------------------------------- ; boots.ASM ; Pequeño bootstrap ; Carga un programa del disco y le pasa el control ; Versión: n u MiT_o r ; ----------------------------------------------------------------- ; ; ---------------------------------------------------------------- ; Hay que decirle al ensamblador que este es el desplazamiento 0. ; Si no es el desplazamiento 0, lo será después del salto. ; ---------------------------------------------------------------- ORG 0 jmp 07C0h:start ; Ir al segmento 07C0 start: ; ------------------------------------- ; Actualizar los registros de segmento ; ------------------------------------- cli ; Deshabilitar las interrupciones mov ax, 07C0h ; Ajustar los registros del mov ds, ax ; segmento de datos mov es, ax ; ------------------- ; Establecer la pila ; ------------------- mov ax, 9000h ; Poner la pila en 90000h mov ss, ax ; SS = 9000h mov sp, 0FFF0h ; SP = 0FFF0h => Stack = 90000h sti ; habilitar interrupciones ; -------------------------------- ; Desplegar mensaje de bienvenida ; -------------------------------- lea si, [__msg] call _szDisplay_ ; -------------------------------- ; Restablecer la unidad de floppy ; -------------------------------- reset: mov cx, 0 mov ax, cx ; reestablecer sistema de disco mov dl, al ; 0 para floppy, cambiar a 80h para discos duros int 13h ; ---------------------------------- ; Subir el 2do. sector a la memoria ; ---------------------------------- read: mov bx, 200h ; desplazamiento de la segunda etapa mov ah, 2 ; Cargar los datos del disco a ES:BX mov al, 5 ; Cargar 3 sectores mov ch, 0 ; Cilindros=0 mov cl, 2 ; Sector=2 mov dh, 0 ; Cabezal=0 mov dl, 0 ; Unidad=0 int 13h ; Leer! jnc ok_load_setup ; si no hubo problemas, se continúa lea si, [_err_] ; despliega mensaje de error call _szDisplay_ mov ah, 16 int 16h jmp read ok_load_setup: lea si, [__msg_] ; despliega mensaje de aviso call _szDisplay_ mov ah, 16 int 16h DB 0xEA ; jump to sector 02 DW 0x7E00 ; offset DW 0x0000 ; segment ; ==================================================================== _szDisplay_: push si _print_string: lodsb test al, al jz _exit_szDisplay_ mov bx, 000Fh mov ah, 0Eh int 10h jmp _print_string _exit_szDisplay_: pop si ret ; -------------------------------------------------------------------- __msg db " --------------------------------------", 13, 10 db "Prueba de dise", 0A4h, "o de bootstrap", 13, 10 db "Por n u MiT_o r", 13, 10 db "----------------------------------------", 0 __msg_ db 13, 10, "Sector 2 copiado en 07C0h:0200h", 13, 10 db "Pulsa una tecla para continuar....", 13, 10, 0 _err_ db 13, 10, "Error al leer desde el disco", 13, 10, 0 times 510-($-$$) db 0 dw 0AA55h ; --------------------------------------------------------------------------- ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; ------------------------------------- ; console.asm ; Pequeño intérprete de órdenes ; (C) n u MiT_o r 2002 ; ------------------------------------- ; ------------------------------------- ; Acepta tres órdenes: ; help: imprime lista de órdenes ; clean: limpia la pantalla ; exit: reinicia del sistema ; ------------------------------------- _init: ; Despliega de bienvenida lea si, [msg1] call _szDisplay _loop: ; Despliega puntero lea si, [puntero] call _szDisplay ; Obtiene la orden lea di, [command] mov [di], byte 0 call _get_command _is_clean: ; ¿Es la orden clean? lea si, [_clean_] call _compare test ax, ax je _is_exit ; Limpiar el monitor call clean_ jmp _loop _is_exit: ; ¿Es la orden "exit" lea si, [_exit_] call _compare test ax, ax je _is_protek ; ¿Es la orden protek? ; reiniciar el sistema _reboot: db 0EAh ; Código de operación para JMP dw 0FFFFh ; Saltar a 0000:FFFFh, que reinicia dw 0 ; el sistema dw 0 dw 0 _is_protek: lea si, [_prot_] call _compare test ax, ax je _is_help ; Pasar a modo protegido jmp _protek _is_help: ; ¿Es la orden "help" lea si, [_help_] call _compare test ax, ax je _no_order _help: lea si, [hmsg] call _szDisplay jmp _loop _no_order: lea si, [msg2] call _szDisplay jmp _loop ; ==================================================================== _szDisplay: push si __print_string: lodsb test al, al jz __exit_szDisplay call __display_char jmp __print_string __exit_szDisplay: pop si ret __display_char: mov bx, 0007h mov ah, 0Eh int 10h ret ; -------------------------------------------------------------------- _get_command: push di mov cx, 12 _loop_: mov ah, 10h int 16h filt_extended_function_keys: test al, al je _loop_ cmp al, 0E0h je _loop_ is_enter_key: cmp al, 0Dh je _exit_get_command dec cx test cx, cx je _excess get_character: stosb call __display_char jmp _loop_ _exit_get_command: xor al, al stosb pop di ret _excess: lea si, [msg3] call _szDisplay mov cx, 12 pop di push di jmp _loop_ ; -------------------------------------------------------------------- _compare: push si push di push bx get_string_lenght: call _strlen mov bx, cx xchg di, si call _strlen xchg si, di cmp bx, cx jne _not_equals _compare_strings: repe cmpsb test cx, cx jne _not_equals mov ax, 1 jmp _exit_compare _not_equals: mov ax, 0 _exit_compare: mov cx, bx pop bx pop di pop si ret _strlen: push di mov al, 0 mov cx, -1 ; cx = -1 = FFFFh = 65535 = infinito repne scasb neg cx dec cx dec cx pop di ret ; -------------------------------------------------------------------- clean_: mov dx, 0 call set_cursor mov cx, 80*25 _print: mov al, 32 ; 32 = 20h = espacio call __display_char loop _print mov dx, 0 call set_cursor ret set_cursor: mov ah, 2 int 10h ret ; ---------------------------------------------------------------------------------- msg1 db "Int",82h,"rprete de ",0A2h,"rdenes", 10, 13 db "Escribe 'help' para ver las opciones", 10, 13, 0 msg2 db 7, 10, 13, "No es una orden del programa!", 10, 13, 0 msg3 db 7, 10, 13, "Introdujo m",0A0h,"s de 12 caracteres", 10, 13 puntero db 10, 13, "> ", 0 hmsg db 10, 13, "Ordenes disponibles en este shell:", 10, 13 db 9, "help: despliega este mensaje", 10, 13 db 9, "clean: limpia el monitor", 10, 13 db 9, "protek: pasa a modo protegido", 10, 13 db 9, "exit: reinicia el sistema", 10, 13, 0 _help_ db "help", 0 _clean_ db "clean", 0 _prot_ db "protek", 0 _exit_ db "exit", 0 command: times 12 db 0 ; ---------------------------------------------------------------------------------- ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ;/**************************************************************************** ; load.asm ; ; Ejemplo 1: implementación simple de un conmutador de ; modo real a modo protegido. ; ;****************************************************************************/ @msg0 db 13, 10 @msg1 db "Vamos a pasar a modo protegido", 13, 10, 0 @msg2 db "Ya estamos en modo protegido: ", 0 @msg3 db "Linea A20 habilitada", 13, 10, 0 @msg4 db "Pulsa 'escape' para volver a modo real...", 0 ALIGN 4 Reset_IDTR dw 400h - 1 ; Limit portion dw 0, 0, 0, 0 Orig_stack .DD_offset dw 0 .DD_segment dw 0 Orig_data dw 0 ;--------------------------------------------------------------- _protek: lea si, [@msg0] call _szDisplay ; ---------------------------------------------------------------------------- ; Habilitar linea A20: necesario para acceder a la memoria por encima de 1 MB ; ---------------------------------------------------------------------------- mov ah, 0DDh call Gate_A20 lea si, [@msg3] call _szDisplay ; -------------------------------------------------------------------- ; Guardar el segmento y la dirección del puntero de pila en modo real ; -------------------------------------------------------------------- mov [Orig_stack.DD_segment], SS ; guardar SS:SP mov [Orig_stack.DD_offset], SP mov ax, ds mov [Orig_data], ax ; -------------------------------------------------------------- ; Establecer la dirección dónde regresar de m. protegido a real ; -------------------------------------------------------------- mov [_offs], word 7C00h+_RMode mov [_segm], cs ; ------------------------------------------------------------------------ ; Cargar la dirección de la GDT en el GDTR. Los descriptores de la GDT ya ; fueron inicializados en nuestra sección de datos ; ------------------------------------------------------------------------ lgdt [gdt_ptr] ; ------------------------------------------------------ ; Deshabilitar las interrupciones (pudo haber sido CLI) ; ------------------------------------------------------ push dword 0 ; poner en 0 EFLAGS (interrupciones, popfd ; deshabilitadas, IOPL=0, bit NT=0) ; -------------------------- ; conmutar a modo protegido ; -------------------------- mov eax, CR0 or al, 1 mov CR0, eax ; ------------------------------- ; ir al código en modo protegido ; ------------------------------- jmp SYS_CODE_SEL:do_pm ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; Rutina _A20_ENABLE: habilita la linea A20, que permite acceder a la memoria ; por encima de 1 MB ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Gate_A20: call empty_8042 mov al, 0D1h ; orden de escritura out 64h, al call empty_8042 mov al, ah ; Activar (ah=0DFh) o desactivar (ah=0DEh) out 60h, al ; línea A20 call empty_8042 ret ; ================================================================== ; Esta rutina se asegura de que el requerimiento de envío de órdenes ; al teclado esté vacío o disponible (después de vaciar los buffers ; de salida) para las órdenes que habilitan la línea A20 que tiene ; que ver con los puertos usados para controlar el teclado en PCs. ; ================================================================== empty_8042: call Delay in al, 64h ; puerto de estado test al, 1 ; ¿buffer de salida disponible? jz no_output call Delay in al, 60h ; sí, entonces leerlo jmp empty_8042 ; y lo ignoramos no_output: test al, 2 ; ¿buffer de entrada lleno? jnz empty_8042 ; sí, seguir el bucle ret ; ------------------------------------- ; Breve retardo para operaciones de E/S ; ------------------------------------- Delay: jmp delay_01 delay_01: jmp delay_02 delay_02: ret ; ------------------------------------------------------------ ; Restablecer los registros de segmento de 16 bits en valores ; compatibles con el modo real ; ------------------------------------------------------------ _RMode: mov ax, [Orig_data] mov ds, ax mov es, ax ; Restablecer la pila SS:SP mov ss, [Orig_stack.DD_segment] ; Restore SS:SP mov sp, [Orig_stack.DD_offset] ; Inhabilitar la linea A20 del bus de direcciones (gate A20 off) mov ah, 0DFh call Gate_A20 ; Limpiar el monitor y regresar call clean_ jmp _loop ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; Fragmento que corre en modo protegido ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: BITS 32 ; Para indicar que la base son 32 bits, no 16. do_pm: ; -------------------------------- ; Definir los selectores de datos ; -------------------------------- mov eax, SYS_DATA_SEL ; ahora estamos en modo protegido de 32-bits mov ss, eax mov ds, eax mov fs, eax mov gs, eax ; ------------------------------------------------------ ; Definir un selector de área de despliegue de video ; ------------------------------------------------------ mov eax, SYS_VIDEO_SEL mov es, eax xor eax, eax ; el puntero de pila ESP de 16 bits mov ax, sp mov esp, eax ; ------------------ ; Limpiar el monitor ; ------------------ mov ecx, SYS_DYSPLAY/2 xor edi, edi mov eax, 0F20h rep stosw ; --------------------------------------------------------------------- ; Desplegar un mensaje que avise que ya terminamos usando punteros de ; direcciones lineales de 32 bits ; -------------------- ------------------------------------------------- lea esi, [ds:@msg2] ; puntero a cadena erminada en cero mov cl, 0Fh ; color xor eax, eax ; No. de linea call xDisplay lea esi, [ds:@msg4] ; puntero a cadena terminada en cero mov cl, 0Eh ; color mov eax, 1 ; No. de linea call xDisplay .a in al, 60h cmp al, 1 jne .a ; ------------------- ; Limpiar el monitor ; ------------------- mov ecx, SYS_DYSPLAY/2 xor edi, edi mov eax, 0720h rep stosw ; -------------------------------- ; restablecer el valor compatible ; -------------------------------- lidt [cs:Reset_IDTR] ; w/ con el modo real. En modo real ; las interrupciones son relocalizables ; a través del registro IDT ; ------------------ ; Pasar a modo real ; ------------------ mov eax, CR0 and al, 0FEh mov CR0, eax ; -------------------------------- ; Regresar al código en modo real ; -------------------------------- db 0EAh ; JMP DWORD CS:_RMode _offs dw 0 dw 0 _segm dw 0 ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: xDisplay: xor edx, edx mov ebx, 80*2 mul ebx mov edi, eax mov ah, cl again: lodsb or al, al je .exit stosw jmp again .exit ret ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ; TABLA DE DESCRIPTORES: GDT ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: [SECTION .data] ; null descriptor SYS_DYSPLAY EQU 80*25*2 gdt: dw 0 ; limit 15:0 dw 0 ; base 15:0 db 0 ; base 23:16 db 0 ; type db 0 ; limit 19:16, flags db 0 ; base 31:24 ; linear data segment descriptor LINEAR_SEL equ $-gdt dw 0xFFFF ; limit 0xFFFFF (1 meg? 4 gig?) dw 0 ; base for this one is always 0 db 0 db 0x92 ; present,ring 0,data,expand-up,writable db 0xCF ; page-granular (4 gig limit), 32-bit db 0 ; code segment descriptor SYS_CODE_SEL equ $-gdt gdt2: dw 0xFFFF dw 0x7C00 ; (base gets set above) db 0 db 0x9A ; present,ring 0,code,non-conforming,readable db 0xCF db 0 ; data segment descriptor SYS_DATA_SEL equ $-gdt gdt3: dw 0xFFFF dw 0x7C00 ; (base gets set above) db 0 db 0x92 ; present,ring 0,data,expand-up,writable db 0xCF db 0 SYS_VIDEO_SEL equ $-gdt gdt4: dw SYS_DYSPLAY ; Tamaño de una pantalla en modo de texto dw 0x8000 ; (base gets set above) db 0x0B db 0x92 ; present,ring 0,data,expand-up,writable db 0xCF db 0 gdt_end: gdt_ptr: dw gdt_end - gdt - 1 ; límite de la GDT dd gdt+0x7C00 ; dir. lineal, física de GDT ; -------------------------- END --------------------------- Una vez compilado este programa obténdremos el archivo IMAGE.IMG. Hasta ahora hemos copiado este tipo de ficheros binarios en los primeros sectores de un disquete y lo hemos ejecutado reinicializando el sistema y pasándole el control a la unidad de floppy durante el arranque. Es algo engorroso realmente. Hay otra opción: usar un emulador. Podemos usar el programa BOCHS, que crea una máquina virtual en la que podemos correr un sistema operativo dentro de otro. Es de suma utilidad en la prueba de sistemas operativos que estemos desarrollando. Para su provecgo acá, puedes seguir las instrucciones que damos en la Nota sobre el uso de BOCHS en el apéndice del capítulo. Análisis de load.asm -------------------- El código de boot.asm (el bootstrap) y de console.asm (la cónsola), ya lo hemos analizado. Nos limitaremos aquí a revisar el código de load.asm: un conmutador de modo. El algoritmo es enrealidad muy simple. El punto de entrada es "protek", y lo primero que se hace es desplegar un mensaje: "Vamos a pasar a modo protegido". _protek: lea si, [@msg0] call _szDisplay De inmediato vamos a nuestro asunto: habilitamos la línea A20 para tener acceso a toda la memoria por encima de 1MB. mov ah, 0DDh call Gate_A20 No analizaremos el procedimiento Gate_A20 porque ya lo hicimos arriba de manera exhaustiva. Luego guardamos el segmento y el puntero de pila (SS:SP), así como el segmento de datos: los vamos a necesitar cuando retornemos a modo real: mov [Orig_stack.DD_segment], SS ; guardar SS:SP mov [Orig_stack.DD_offset], SP mov ax, ds mov [Orig_data], ax Ahora establecemos la dirección dónde regresaremos cuando volvamos de modo protegido a modo real: mov [_offs], word 7C00h+_RMode mov [_segm], cs Hecho esto, cargamos nuestro puntero a la GDT, que ya hemos inicializado: lgdt [gdt_ptr] deshabilitamos las interrupciones push dword 0 ; poner en 0 EFLAGS (interrupciones, popfd ; deshabilitadas, IOPL=0, bit NT=0) y, por fin, conmutamos a modo protegido, tal como expusimos arriba, activando el bit 0 dedl registro de control CR0: mov eax, CR0 or al, 1 mov CR0, eax Pasamos entonces al código de 32 bits en modo protegido mediante un salto incon- dicional que limpiará áreas del procesador de restos útiles sólo para modo real: jmp SYS_CODE_SEL:do_pm El descriptor del segmento de código lo hemos definido así: SYS_CODE_SEL equ $-gdt gdt2: dw 0xFFFF dw 0x7C00 ; (base gets set above) db 0 db 0x9A ; present,ring 0,code,non-conforming,readable db 0xCF db 0 Con la explicación que dimos arriba se entiende claramente. Sólo diremos que como segundo dw, la base, hemos puesto la base donde se carga nuestra imagen en memoria. Como puede verse, usamos como dirección de segmento nuestro selector de código de modo protegido. Antes de el punto de entrada del código de modo protegido colocamos: BITS 32 ; Para indicar que la base son 32 bits, no 16. que especifica código y datos de 32 bits. Inmediatamente damos definición a nuestros selectores; primero definimos los que apuntarán al descriptor del seg- mento de datos del sistema: mov eax, SYS_DATA_SEL ; ahora estamos en modo protegido de 32-bits mov ss, eax mov ds, eax mov fs, eax mov gs, eax y luego uno que apuntará al descriptor del segmento correspondiente al área de vídeo: mov eax, SYS_VIDEO_SEL mov es, eax El descriptor seleccionado por SYS_DATA_SEL es semejante a SYS_CODE_SEL, sólo cambia el tipo, porque es un segmento de datos (0x92: escribible), no de código (0x9A: leíble). Más interesante es el descriptor seleccionado por SYS_VIDEO_SEL: gdt4: dw SYS_DYSPLAY ; Tamaño de una pantalla en modo de texto dw 0x8000 ; (base gets set above) db 0x0B db 0x92 ; present,ring 0,data,expand-up,writable db 0xCF db 0 Su base apunta al inicio del área de video (0x8000) Es este descriptor el que usaremos para despliegue en pantalla. Así que después de definir el puntero de pila de 32 bits para modo protegido: xor eax, eax ; el puntero de pila ESP de 16 bits mov ax, sp mov esp, eax Procedemos a limpiar el monitor: mov ecx, SYS_DYSPLAY/2 xor edi, edi mov eax, 0F20h rep stosw La instrucción "rep stosw" mueve tantas veces como indique el registro ecx (aquí SYS_DYSPLAY/2 veces) el contenido en ax (0F20h: 0F es el código de color para fondo negro --el 00-- y fuentes blancas brillantes --la 0Fh--, y 20h es el códi- go ASCII correspondiente al espacio) a la dirección apuntada en ES:EDI. Al hacer esto, se escriben espacios en blanco en todo el monitor. Cada ejecución de stosw incrementa en dos el valor de EDI, lo que pone un caracter más adelante en el área de video. Un caracter es un byte, pero es necesario otro byte con el atri- buto de color para el caracter; así que finalmente tenemos 2 bytes por caracter, uno para el código ascii y otro para el color. El monitor, en modo de texto, que es el que empleamos, tiene espacio para el despliegue de 80 caracteres por fila, y hay 25 filas disponibles. Esto, junto al hecho de que necesitamos dos bytes por caracter, explica nuestrta de finición de la constante SYS_DYSPLAY: SYS_DYSPLAY EQU 80*25*2 Limpio el monitor, desplegamos un par de mensajes llamando a xDisplay: lea esi, [ds:@msg2] ; puntero a cadena erminada en cero mov cl, 0Fh ; color xor eax, eax ; No. de linea call xDisplay lea esi, [ds:@msg4] ; puntero a cadena terminada en cero mov cl, 0Eh ; color mov eax, 1 ; No. de linea call xDisplay Para usar este procedimiento, pasamos en ESI la dirección de la cadena terminada en cero. El atributo de color lo especificamios en CL y el número dee línea lo indocamos en EAX. Para la segunda cadena hemos escogido el color amarillo bri- llante sobre fondo negro (0Eh). Para leer las cadenas, detenemos la ejecución del programa hasta que pulsemos la tecla "escape": .a in al, 60h cmp al, 1 jne .a De nuevo empleamos el puerto del teclado. Aquí lo que hacemos es leer el buffer de salida para revisar si el código de rastreo de la tecla presionada; si es 1, entonces se presionó la tecla de escape y el programa continuará su ejecución, que conducirá de nuevo a modo real. Volvemos a limpiar, pero con otro atributo de color, el original (07h) mov ecx, SYS_DYSPLAY/2 xor edi, edi mov eax, 0720h rep stosw y restablecemos el vector de interrupciones; recuérdese que el vector de interrupciones debe ser el original de modo real cuando pasamos a modoprotegido y su dirección va a estar en el registro IDTR. Antes de regresar al modo real, nos aseguramos que el IDTR apunte al vector de interrupciones compatible con el modo real en 0000:0000 y con un tamaño de 400h-1 bytes: lidt [cs:Reset_IDTR] ; w/ con el modo real. En modo real ; las interrupciones son relocalizables ; a través del registro IDT Después de esto, sólo limpiamos el bit 0 del registro CR0 y ya estamos de nuevo en modo real: mov eax, CR0 and al, 0FEh mov CR0, eax El siguiente código nos lleva de regreso a la dirección donde indicamos antes que debíamos regresar; como puede observarse es de nuevo un salto incondicional, que debe limpiar las áreas del proceador de materia inútil en modo real: db 0EAh ; JMP DWORD CS:_RMode _offs dw 0 dw 0 _segm dw 0 para _offs, habíamos elegido _RMode, donde se encuentra la siguiente rutina de 16 bits: _RMode: mov ax, [Orig_data] mov ds, ax mov es, ax ; Restablecer la pila SS:SP mov ss, [Orig_stack.DD_segment] ; Restore SS:SP mov sp, [Orig_stack.DD_offset] ; Inhabilitar la linea A20 del bus de direcciones (gate A20 off) mov ah, 0DFh call Gate_A20 ; Limpiar el monitor y regresar call clean_ jmp _loop La rutina restablece los valores originales de modo real para los registros de segmento de datos (ds y es), la pila (ss:sp), deshabilita de nuevo la línea A20. Luego limpiamos y regresamos al bucle principal de la cónsola. El modo plano real ------------------ En el programa que hemos realizado no aprovechamos las virtudes del modo protegido: · espacio de memoria virtual de hasta 4GB · jerarquía de áreas de trabajo o de niveles de privilegio · conmutación de tareas Para explotar las virtudes de los niveles de privilegio y de la conmutación de tareas, tendríamos que dar un paso más hacia adelante. Por ahora vamos a conten- tarnos con un espacio más amplio de memoria. Si esto es lo que nos interesa, no necesitamos correr en modo protegido. Podemos ganar acceso a toda la memoria real disponible en nuestro sistema (que seguramente supera 1 MB), pasando a modo plano real. En este modo, toda la memoria es tratada como un segmento único de 4GB para código, datos y pila. Para pasar a modo plano real, cambiamos primero a modo protegido y establecemos la GDT de manera que haya un segmento que comience en la direción 0000:0000 y que tenga un límite de 4 gigabytes. Luego cambiamos de regreso a modo real man- teniendo la GDT. Para que la operación tenga éxito, previamente debemos habilitar la línea A20 para acceder libremente a toda la memoria alta, por encima de 1MB y no dehabilitarla al regresar al modo real. A continuación el código que hace el cambio modo real plano: cli ; Deshabilitar las interrupciones push ds ; Salvar los selectores de modo real push es xor eax, eax ; Parchear el psuedo-descriptor de la GDT mov ax, ds ; (asumiendo que ds apunta al segmento que contiene la GDT) shl eax, 4 add [GDT+2], eax lgdt [GDT] ; Se carga la GDT mov eax, cr0 ; Cambiamos a modo protegido inc ax mov cr0, eax mov bx, flat_data ; Un único slector en modo protegido mov ds, bx ; Instalamos el límite de 4GB mov es, bx dec ax ; Regresamos a modo real mov cr0, eax pop es ; Reestablecemos los selectores de modo real pop ds sti ; Rehabilitamos las interrupciones . . . %include "gdtn.inc" GDT start_gdt flat_data desc 0, 0xFFFFF, D_DATA+D_WRITE+D_BIG_LIM end_gdt Obsérvese que nunca tocamos el DS en modo protegido. Si no usamos macros ni constantes simbólicas, entonces arriba ponemos entonces: GDT: dummy times 8 byte 0 flat_desc dw 0xF, GDT, 0, 0, 0xFFFF, 0, 0x9200, 0x8F flat_data equ 8 Aqui flata_data pasa a ser una constante cuyo valor es un índice al descriptor de datos de nuestra GDT, y que puede ser usado como parte del record de nuestros selectores. Veamos un par de ejemplos: Ejemplo 1 ---------- ; +------------------------------------------------------------------+ ; | Rutina para activar el modo plano del 386 y superiores (acceso | ; | a 4 Gb en modo real). | ; | | ; | TASM flat386 /m5 | ; | TLINK flat386 /t /32 | ; +------------------------------------------------------------------+ ; | Tomado de: Ciriaco García de Celis: El Universo Digital del IBM | ; | PC, AT y PS/2: http://meltingpot.fortunecity.com/uruguay/978/ | ; +------------------------------------------------------------------+ .386p ; sólo para 386 o superior segmento SEGMENT USE16 ASSUME CS:segmento, DS:segmento ORG 100h test: CALL flat386 ; activar modo flat XOR AX,AX MOV DS,AX MOV EBX,0B8000h ; dirección de vídeo absoluta MOV CX,2000 llena_pant: MOV BYTE PTR [EBX],'A' INC EBX MOV BYTE PTR [EBX],15 INC EBX LOOP llena_pant INT 20h ; fin de programa ; ------------ Esta rutina pasa momentáneamente a modo protegido de ; manera directa (necesita la CPU en modo real). No se ; activa la línea A20 (necesario hacerlo directamente ; o a través de algún servicio XMS antes de acceder a ; las áreas de memoria extendida afectadas). flat386 PROC PUSH DS PUSH ES PUSH EAX PUSH BX PUSH CX MOV CX,SS XOR EAX,EAX MOV AX,CS SHL EAX,4 ; dirección lineal de segmento CS ADD EAX,OFFSET gdt ; desplazamiento de GDT MOV CS:[gd2],EAX ; guardar dirección lineal de GDT CLI LGDT CS:[gdtr] ; cargar tabla global de descriptores MOV EAX,CR0 OR AL,1 ; bit de modo protegido MOV CR0,EAX ; pasar a modo protegido JMP SHORT $+2 ; borrar cola de prebúsqueda ; MOV BX,gcodl ; índice de descriptor en BX MOV DS,BX ; cargar registro de segmento DS MOV ES,BX ; ES MOV SS,BX ; SS MOV FS,BX ; FS MOV GS,BX ; GS AND AL,11111110b MOV CR0,EAX ; volver a modo real JMP SHORT $+2 ; borrar cola de prebúsqueda MOV SS,CX STI POP CX POP BX POP EAX POP ES POP DS RET gdtr LABEL QWORD ; datos para cargar en GDTR gd1 DW gdtl-1 gd2 DD ? gdt DB 0,0,0,0,0,0,0,0 ; GDT gcod DB 0ffh,0ffh,0,0,0,9fh,0cfh,0 gcodl EQU $-OFFSET gdt gdat DB 0ffh,0ffh,0,0,0,93h,0cfh,0 gdtl EQU $-OFFSET gdt flat386 ENDP segmento ENDS END test Esta vez el análisis lo dejaremos al lector. Sólo resaltar que esta rutina corre en DOS y pueden ser usada en consecuencia, aprovechando para DOS punteros de 32 bits y todo un espacio plano de memoria por encima de 1 MB. A modo de recapitulación ------------------------ Ya hemos visto como es el proceso para poner un procesador compatible i80386+ en modo protegido o en modo real plano. Sin embargo, no aprovechamos aún las virtu- des de estos modos. Ello requeriría antes una información adicional, que vamos a tratar a continuación. No todo está perdido, ya que los logros obtenidos aquí nos proporcionan un entorno interesante para la prueba de algoritmos y códigos que vamos a estudiar pronto. Queda pendiente todavía ver cómo se estable una IDT, su uso, implementación de un sistema multitareas y revisar los niveles de privilegio en modo protegido. ================================================================================= APÉNDICE NOTA SOBRE EL DESCRIPTOR NULO ----------------------------- La primera entrada en la GDT se llama descriptor nulo y es úncio en la GDT, ya que tiene TI=0 e INDEX=0. Como el procesador nunca referencia a este descriptor, entonces pueden almacenase datos en su lugar para cualquier propósito. Por ejemplo, para poner un puntero a la GDT misma. La instrución LGDT necesita un puntero de seis bytes a la GDT, y el descriptor NULL tiene 8 bytes que no son accedidos por el CPU --esto lo convierte en el candidato ideal para este propósito. El protocolo normal usado para la un puntero a la GDT, que debe ser cargado en el registro GDTR es: GDT_PTR DW GDT_LENGTH-1 DD PHYSICAL_GDT_ADDRESS Luego en el segmento de código: LGDT GDT_PTR Usar el descriptor nulo como un puntero a la GDT, simplifica el segmento de da- tos, y la conceptualización de la GDT queda así: +-----------------+ | | V | Desplazamiento (Offset) +------------------------+ | GDT | Puntero a la GDT | ---+ 00h +------------------------+ | | 08h +------------------------+ | ... ... ... ... | Luego en el segmento de código: LGDT GDT La variable GDT_PTR ya no es necesaria, ya que el descriptor nulo es usado en su lugar. Al usar el descriptor nulo de est manera tenemos una aproximación más clara al direccionamiento de la GDT. NOTA SOBRE BOCHS ---------------- BOCHS es un emulator para plataforma IA-32; la plataforma de 32 bits de los procesadores compatibles Intel 80386+. Es de distribución libre y hay versiones para Windows y Linux. Lo puedes conseguir en: http://bochs.sourceforge.net/. Una vez que hayas descomprimido e instalado BOCHS, busca el fichero bochsrc.txt, que seguro estará en el directorio dlxlinux, donde se halla la distribución DLX de LINUX que se pasa como una especie de DEMO. Para ejecutar IMAGE.IMG, por ejemplo, puedes copiar este archivo en el mismo directorio de trabajo e BOCHS. También puedes copiar bochsrc.txt en el mismo directorio. Editas bochsrc.txt asegurándote que tenga los siguientes datos: # what disk images will be used floppya: 1_44=image.img, status=inserted floppyb: 1_44=floppyb.img, status=inserted newharddrivesupport: enabled=1 # choose the boot disk. boot: a Lo demás lo dejas igual. Lo que haces acá es decir a BOCHS que en su floppya virtual cargue "image.img" (floppya: 1_44=image.img, status=inserted) e inicie desde "a" (boot: a). Después sólo tienes que ejecutar BOCHS.EXE y pulsar ENTER para aceptar los va- lores por defecto dee BOCHS, y ya, nuestra imagen estará ejecutándose en el entorno de BOCHS. ------------------------------------------------- TO BE CONTINUED -----------> emailme: numit_or@cantv.net webZ: http://mipagina.cantv.net/numetorl869/ http://oberon.spaceports.com/~tutorial/aks/