Prácticas de Periféricos F. Javier Gil Gabriel J. Garcı́a Gómez DFISTS Curso 2009-10 1 1. Sobre las prácticas Esta sección fué escrita en el momento en que decidı́ cambiar el modelo de prácticas, que anteriormente era un modelo de enunciados abiertos. La mantengo porque las consideraciones que hice entonces siguen siendo instructivas. 1. Durante el curso pasado detecté algunas deficiencias en programación. Estas deficiencias se han ido acentuando con el paso de los años. En el curso pasado se rebasó, hacia abajo, el umbral que marca la diferencia entre poder o no poder hacer las prácticas. La causa fundamental, a mi juicio, ha sido el abandono de los lenguajes de programación de bajo nivel en favor de lenguajes de más alto nivel. De Pascal se pasó a C, y luego a C++ y Java. Como resultado, se han perdido competencias de programación a bajo nivel, que en la programación de sistemas son imprescindibles. Algunas de las deficiencias o errores tı́picos encontrados entre los alumnos del curso pasado son los siguientes: Desconocimiento de las operaciones de bits Desconocimiento de la representación interna de los números. Por ejemplo, creencia de que el número ’5’ ocupa un byte mientras que el número ’1234’ ocupa 4 bytes. Incapacidad para usar correctamente los punteros: declararlos, asignarlos, usar moldeadores de tipo con ellos, uso de punteros a punteros, manejo de memoria dinámica, punteros a funciones. Desconocimiento de la diferencia entre números de 8 bits, tipo de dato ’carácter’, código ASCII y figura asociada para su representación en pantalla. Manejo deficiente de cadenas de caracteres y su relación con el tipo ’puntero a carácter’ 2. A estos problemas causados por el cambio en el paradigma de programación se une otro, y es el cambio en el paradigma del entorno de programación. Muchos alumnos no comprenden que las herramientas de alto nivel funcionan sobre sistemas completos. Cuando se programa a bajo nivel, el sistema aún no existe y por tanto no existen aplicaciones sofisticadas, como depuradores de código o editores elaborados, para ayudar en la tarea. 2 Es preciso que el alumno se familiarice con un entorno de programación más austero. Y es preciso que el alumno se familiarice con herramientas que han sido diseñadas para funcionar bien en entornos austeros: compiladores en lı́nea de órdenes, editores ligeros, ensamblador y desensamblador. 3. Otro problema, consecuencia de los anteriores, viene del hecho de que se han abandonado lo que siempre fueron consideradas ’buenas prácticas’ de programación. En particular, he encontrado con inusitada frecuencia los siguientes defectos: Código con formato deficiente e incluso sin formato alguno Muy deficiente factorización del código, con funciones de cientos de lı́neas. Desconocimiento de la existencia de distintas capas en que se organiza el código del sistema. Esto lleva, por ejemplo, a pretender usar la función printf() dentro del controlador de pantalla. Una actitud casi unánime de precipitación hacia el teclado, cuando el trabajo importante se hace antes, con papel y lápiz. Uso de abstracciones innecesarias. Por ejemplo, cuando se necesita una pila, que no es más que un vector cuyo primer elemento se usa como ı́ndice, casi todos los alumnos de prácticas en el curso anterior usaron una lista doblemente enlazada, que es algo ası́ como diez veces más compleja que la representación obvia de una pila. 4. Todo esto me obliga a replantear el contenido y objetivos de las prácticas de Periféricos, una vez constatadas estas deficiencias. Durante los años anteriores, he usado preferentemente un pequeño sistema operativo (mı́nimo) con una arquitectura que permitı́a escribir los controladores de dispositivo en C, sin tener que recurrir al ensamblador. Este sistema podı́a ejecutar varias tareas y permitı́a ’entrar’ en el controlador de pantalla, teclado y reloj, reasignar interrupciones y ejercitarse con un par de métodos de acceso al hardware, como son acceso a través de puertos y acceso a través de memoria convencional. Una vez cruzado hacia abajo el umbral de las competencias de programación en lenguaje C 1 , es preciso dedicar al menos parte del curso 1 En ninguna caso se vea aquı́ un reproche a los alumnos: no es responsabilidad de ellos que se abandonase primero el ensamblador, luego el lenguaje Pascal (ideal para adquirir 3 a ejercitar los conceptos y la forma de pensar para programar a bajo nivel, y esperar, en las sesiones finales, poder escribir alguna aplicación sencilla de sistema. Esto tiene otra implicación práctica, y es que ya no puedo plantear un enunciado abierto con las lı́neas generales de lo que se quiere hacer y el resto quede al arbitrio del alumno, sino que será preciso plantear enunciados ’cerrados’. Cada sesión consistirá en una serie de puntos que habrá que completar. Siendo ası́, esto implica a su vez la necesidad de realizar un examen final de prácticas, que consistirá en un ejercicio de programación a bajo nivel sobre algún aspecto de los tratados en el curso. 5. Con estas consideraciones presentes, sigue el contenido de cada una de las sesiones en que se desarrollarán las prácticas. 2. Sesión I 1. Entorno. Las prácticas se realizarán en entorno DOS. Las razones son que existen herramientas de buena calidad para él, que está disponible bien de forma nativa o bien mediante emulación tanto en Windows como en Linux y que al no proteger el hardware es posible el acceso a los puertos y a la memoria asignada a dispositivos. 2. Compilador. Se instalará el compilador ’Turbo C’ de la compañı́a Borland, que en su versión 2.01 se encuentra en el dominio público. ’Turbo C’ incluye tanto un compilador como un entorno para editar, compilar y depurar. 3. Editor. Como se ha dicho, el compilador propuesto incluye un entorno de programación con su editor. No obstante, recomendamos el uso de un editor externo y el uso del compilador en lı́nea de órdenes ’tcc’ 4. Tareas. Las tareas a desarrollar durante esta primera sesión serán: 1. Crear un entorno de programación portable. Es decir, un directorio que contenga las herramientas que van a usarse durante el curso y del que buenos hábitos de programación), y después el lenguaje C para pasar a C++ y luego a Java. Os recomendamos el artı́culo Volviendo a lo básico, cuyo enlace se encuentra en la página del Prof. Gil 4 pueda tenerse una copia de seguridad, por ejemplo, en una llave de memoria usb. La base del entorno puede ser: a) El entorno DOS que ofrece Windows b) Un emulador DOS como DOSBOX, corriendo sobre Windows. Ésta es la opción que recomiendo. c) Un sistema DOS auténtico con el cual arrancar el sistema desde disco flexible 2. Descargar e instalar el compilador ’Turbo C’. 3. Descargar e instalar el ensamblador, necesario para alguna situación concreta. Este ensamblador será ’Turbo Assembler’ también de Borland. 4. Opcional: descargar e instalar un editor ’decente’, como ’vi’ que se encuentra disponible para DOS, tanto en 16 como en 32 bits. Aunque la elección de un editor es un asunto de gustos personales y una cuestión para nosotros irrelevante, no está de más recordar la importancia de elegir bien una herramienta que se usará a diario, durante mucho tiempo, en varios entornos distintos y para una gran variedad de tareas. 5. Familiarizarse con la ayuda del compilador. 6. Escribir, compilar y ejecutar algún programa sencillo con el entorno creado. Ajustar los parámetros del editor para asegurarse de que el código que se escribe tiene el formato deseado y de que éste formato será el mismo durante todo el curso. 7. Guardar la instalación para la sesión siguiente. 3. Sesión II 1. C a bajo nivel. Comenzaremos por escribir algunas funciones para manipulación de bits, necesarias para después escribir y leer de los puertos. Estos son los prototipos: char UTIL_caracter_consultar_bit(char *p, char n) void UTIL_caracter_bit_a_0(char *p, char n) void UTIL_caracter_bit_a_1(char *p, char n) 5 La primera devuelve 1 o 0 según que el bit n del carácter apuntado por p sea 1 o 0. La segunda establece a 0 el bit n del carácter apuntado por p. La tercera establece a 1 el bit n del carácter apuntado por p. Tras ésto, se escribirán otras tres funciones similares que trabajen con enteros en lugar de caracteres. Se llamarán igual, salvo el cambio de ’caracter’ por ’entero’ y tomarán como primer argumento un puntero a entero 2. La segunda parte de la práctica consiste en el uso de la macro MK FP() para apuntar un puntero a carácter a la primera posición de la memoria de video, ubicada en (0xb800,0) En esta posición comienza un vector de 4000 bytes = 80 columnas x 25 filas x 2 bytes por carácter. El primer byte de un carácter es el código ascii del mismo. El segundo son los atributos. Los cuatro bits bajos del byte de atributos codifican el color del texto. Los bits 4, 5 y 6 codifican el color del fondo. Se escribirá un pequeño programa que escriba directamente en memoria de video un rectángulo de 16 filas y 8 columnas un carácter cualquiera escrito con todas las combinaciones posibles de texto y fondo. 4. Sesión III 1. Punteros en la programación de sistemas Las tareas a desarrollar durante esta sesión serán: 1. declarar punteros a un carácter, a un entero y a un entero largo 2. reservar memoria para ellos 3. imprimir los tamaños de los tres punteros 4. imprimir los propios punteros (no el contenido del lugar al que apuntan). Aquı́ puede ser útil el formato %p de printf(). 5. sumar 1 a cada puntero y repetir el paso anterior. Interpretar qué ha ocurrido 6. declarar un vector de 100 caracteres 7. escribir un carácter A en la posición 23 8. escribir el entero 1004 en las posiciones 45-46 9. leer el carácter de la posición 23 y el entero de las posiciones 45-46 6 10. declarar un puntero a un vector de enteros y comprobar su uso 11. declarar un vector de punteros a entero, y comprobar su uso 12. declarar un puntero a un puntero a carácter, reservar memoria para él y escribir en el lugar apuntado un puntero a la pantalla de vı́deo 13. declarar un puntero a una función que devuelve un entero 14. declarar un vector de punteros a funciones que devuelven un entero 5. Sesión IV 1. Acceso a la ROM para obtener los patrones de bits de los caracteres. En esta sesión pondremos en práctica algunos de los conceptos vistos en las sesiones anteriores explorando uno de los aspectos de la memoria de vı́deo. Como sabemos, la memoria de vı́deo VGA en modo 80x25, color, comienza en la direccin (0xb800,0), o lo que es igual, en la dirección fı́sica 0xb8000000. Es un vector de 80x25x2=4000 caracteres agrupados de parejas (carácteratributo). Nos centraremos en el código que escribimos para el carácter y cómo se transforma en un patrón de bits que en pantalla representa la figura del carácter. Por ejemplo, al escribir A en memoria de vı́deo no escribimos el carácter A en realidad, sino el valor 65. Este valor indexa una tabla que se encuentra en ROM y que contiene el ((dibujo)) de la letra A. Cada dibujo consta de 16 bytes, y como cada byte consta de 8 bits, tenemos una matriz de 16 filas x 8 columnas. El patrón de ((unos)) representa el dibujo del carácter. Por ejemplo, el carácter = se codifica ası́: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01111110 01111110 00000000 00000000 01111110 01111110 -> -> -> -> -> -> -> -> -> -> -> -> -> 0 0 0 0 0 0 0 126 126 0 0 126 126 7 00000000 00000000 00000000 -> -> -> 0 0 0 La práctica consistirá en acceder a la tabla donde están los patrones de los caracteres, leerlos y volcarlos a un archivo de texto donde puedan ser consultados. Para visualizar mejor los patrones, los ceros pueden representarse mediante espacio en blanco, y los unos mediante algún carácter ((lleno)), como por ejemplo #. 2. Acceso a la tabla. Existe una función de la BIOS que ofrece la dirección de la tabla. Las funciones de la BIOS se invocan de la siguiente forma: unos valores convenidos se escriben en los registros de la CPU, se llama a la función adecuada y ésta deja el resultado en los registros de la cpu. Nuestro compilador ofrece algunas maneras de hacer esto. La más sencilla es: #include <dos.h> struct REGPACK rp ; rp.r_ax=0x1130; rp.r_bx=0x0600; intr(0x10,&rp); REGPACK es una estructura predefinida cuyos miembros son variables que hacen las veces de pseudoregistros. El compilador generará el código preciso para 1. salvar los registros de la cpu 2. tomar los valores de los pseudoregistros y colocarlos en los registros verdaderos 3. llamar a la función correspondiente de la BIOS, cuyo resultado queda almacenado igualmente en los registros de la cpu 4. copiar los registros verdaderos en los pseudoregistros Consultar la ayuda del compilador para ver qué contiene struct REGPACK. En nuestro caso, a las funciones de la BIOS de vı́deo se accede a través de la interrupción 0x10. 8 IMPORTANTE: estas no son interrupciones hardware, sino interrupciones software. El nombre de ((interrupción)) con que se las designa es muy desafortunado. Pensemos en ellas simplemente como funciones pre-escritas en la ROM. Como resultado de la interrupcin, el segmento y el desplazamiento donde se encuentra la tabla se depositan en los registros ES,BP. Por tanto, declaramos un puntero a carácter y lo hacemos apuntar a la dirección indicada: char *p=(char *)MK_FP(rp.r_es,rp.r_bp); La tabla contiene 256 caracteres x 16 bytes/caracter = 4096 bytes = 4k Usando la función que se escribió hace dos sesiones para consultar un bit de un byte, generar para cada carácter su patrón de ceros y unos usando los caracteres ’ ’ y ’#’ y escribir ese patrón en un archivo de disco que pueda después ser consultado mediante un editor cualquiera. La legibilidad mejora si se escribe el propio carácter antes del patrón y se deja una lı́nea en blanco entre patrón y patrón. Por ejemplo A #### ## ## ## ## ## ## ## ## ## ## ###### ###### ## ## ## ## ## ## ## ## ## ## ## ## B .... 9 3. Modificar la tabla. La tabla en ROM puede copiarse a RAM, ((decirle)) a la tarjeta la dirección de la copia y, puesto que se encuentra en RAM, poder modificarla. Es la forma de activar juegos de caracteres nuevos. Una forma sencilla de comprobar el cambio es alterando algún carácter. Por ejemplo, si p es un puntero que apunta a la nueva tabla, alteramos el patrón de bits de la letra A colocándole una barra horizontal en la parte superior. Ası́: *(p+65*16)=255 El código que establece la nueva tabla de caracteres es el siguiente: /* activar nuevo juego de caracteres */ reg.r_ax=0x1110; reg.r_bx=(16<<8)|0; reg.r_cx=256; reg.r_dx=0; reg.r_es=FP_SEG(*p); reg.r_bp=FP_OFF(*p); intr(0x10,&reg); Ahora bien, por alguna razón (antigüedad del compilador, emulación del DOS, algún cambio en la BIOS...) el código anterior no funciona correctamente, por lo que es preciso llamar a la interrupción directamente, insertando el fragmento en ensamblador siguiente: /* activar nuevo juego de caracteres, usando ensamblador */ asm push ax ; asm push bx ; asm push cx ; asm push dx ; asm push es ; asm push bp ; asm mov ax, 1110h ; asm mov bx, 1000h ; asm mov cx, 0100h ; asm mov dx, 0 ; asm les bp, p ; asm int 10h ; asm pop bp ; asm pop es ; asm pop dx ; asm pop cx ; asm pop bx ; asm pop ax ; 10 6. Sesión V 1. Continuando con las llamadas a funciones de la BIOS, en esta práctica usaremos la interfaz ya conocida para realizar algunas tareas: 1. Alteración del tiempo de espera y ritmo de repetición del teclado. Interrupción 0x16 Entrada: ah=0x03 al=0x05 bh=retardo bl=factor de repeticion Las tablas de valores para bh y bl se encuentran en las transparencias de clase 2. Comprobacio’n del estado del teclado Entrada: ah=0x02 Salida: al=estado del teclado bit 0 1 2 3 4 5 6 7 1 1 1 1 1 1 1 1 -> -> -> -> -> -> -> -> tecla derecha may. pulsada tecla izquierda may. pulsada tecla control pulsada tecla alt pulsada bloq. desp. pulsado bloq. num. pulsado bloq. may. pulsado insert pulsado Puede escribirse un pequeño bucle que llame ininterrumpidamente a la interrupción y muestre continuamente el estado del teclado en pantalla 3. Fecha y hora: lectura del reloj de tiempo real. Interrupción 0x1a 11 Entrada: ah=0x02 Salida: ch=hora cl=minuto dh=segundo Se tendrá en cuenta que los valores para las horas, minutos y segundos está codificados en formato BCD. Por ejemplo, si las 16 horas se codifican usando los cuatro bits bajos de ch para representar el ’6’ y los cuatro bits altos para representar el ’1’. 4. Fijar modo de vı́deo. Interrupción 0x10 Entrada: ah=0x00 al= 0x01 40*25, 16 colores 0x03 80*25, 16 colores 0x05 gra’fico, 320*200 pixels, 4 colores 0x13 gra’fico, 320*200, 256 colores ...etc. El modo 0x13 es especialmente interesante. La resolución no es muy alta, pero con 256 colores pudieron en su dı́a escribirse muchos buenos juegos. Algunos de ellos se han portado para jugarse en los móviles. Además, 320*200*1 byte/pixel = 64000 bytes. Es decir, la pantalla cabe ı́ntegra en un segmento, y puede accederse a cualquier pixel simplemente sumando un desplazamiento al puntero que apunta al primer pixel. La dirección de la pantalla de vı́deo en este modo es 0xa000. Cada lı́nea consta de 320 pixels = 320 bytes luego para acceder al pixel que está en la fila f columna c hay que sumar al puntero un desplazamiento 320*f+c. Escribir una función que active un pixel y que tome como argumentos la fila, la columna y el color que se desee. 7. Sesión VI 1. Acceso a la tabla de vectores de interrupción. Se leerán los valores depositados en dicha tabla, y se aprenderá a modificarlos de forma que los 12 eventos de hardware provoquen la ejecución de rutinas escritas por el propio alumno. Tareas a desarrollar durante esta sesión: 1. Acceder mediante punteros a la TVI. Se usará MK FP para acceder a dicha tabla. 2. Uso de cli y sti: necesidad del lenguaje ensamblador e interfaz ofrecida por ’Turbo C’ 3. Escritura de una pequeña RSI propia que acceda mediante inportb() al puerto de teclado y lea los códigos de las teclas. 4. Modificación de la entrada correspondiente al teclado en la TVI para que apunte a la rutina propia. 5. Restauración de la rutina original 8. Sesiones VII-IX 1. Objeto. El objeto de estas tres sesiones será el de escribir una versión de la orden dir, que muestre en pantalla los archivos y directorios de un disco, junto con información relevante como tamaño, fecha y atributos. Por seguridad, trabajaremos con discos flexibles de 1.44MB. Ahora bien, dado que muchos trabajáis ya con portátiles que carecen de unidad de disco flexible, podrı́amos trabajar con alguna de las alternativas siguientes: trabajar en los ordenadores del laboratorio, usando las unidades de disco de estos ordenadores trabajar mediante emulador (vmware, dosbox, etc) montando como unidad de disco flexible una imagen trabajar desde windows o linux con una imagen, a la que se accede abriéndola como un archivo cualquiera trabajar en DOS pero usando un disco RAM, que se creará en el arranque. Requiere el programa RAMDRIVE Por simplicidad, usaremos una imagen de un disco flexible de 1.44 MB. Una imagen no es más que un archivo de disco que se abre con fopen() y se cierra con fclose(). El archivo se ha generado leyendo uno por uno los sectores de 512 bytes del disco original y escribiéndolos uno a continuación de 13 otro en el archivo. Por tanto, la imagen se considera como un conjunto consecutivo de bloques de 512 bytes. Por consiguiente, leer un sector n equivale a desplazarse 512 × n a partir del origen del archivo mediante lseek() y hacer una lectura de un 512 bytes mediante fread(). Recomendamos que hagáis la práctica sobre Linux, con el compilador gcc, ya que en años anteriores tcc ha dado algunos problemas relacionados con la gestión del buffer intermedio que usa fread(). Pero recuérdese que gcc es un compilador de 32 bytes (1 entero=4 bytes) mientras que tcc es un compilador de 16 bytes (1 entero=2 bytes). 2. Fundamentos. Un disco es una colección de sectores. En el caso de los discos flexibles de 1.44MB se trata de una colección de 2880 sectores de 512 bytes. De este conjunto de sectores se reserva un espacio para implementar en él una base de datos de sectores, que indique qué sectores se encuentran libres u ocupados y qué sectores pertenecen a qué archivos (o a la inversa, de qué sectores consta un archivo dado). A esta base de datos es a lo que se conoce como ((sistema de archivos)). Hay muchas formas de implementarla: fat12, fat16, fat32, ext2-3, reiserfs, minix, vfat, ntfs, ufs, iso ... La elección influye de forma determinante en el rendimiento de las operaciones de acceso a disco, aunque después haya otros factores relevantes que tiendan a difuminar las diferencias, como por ejemplo la existencia de grandes cachés, tanto en la propia unidad de disco como en el propio sistema operativo. 3. FAT. El sistema FAT, en sus distintas versiones, es una de las formas de implementar la base de datos de sectores. FAT proviene de File Allocation Table, es decir, tabla de localización de ficheros. La idea consiste en tener una lista enlazada por cada archivo. Si el disco tiene N sectores (o bien agrupaciones de un número fijo de sectores que llamamos ((bloques))), entonces la lista contendrá N elementos. Como el número de elementos es fijo, se pueden almacenar en un vector. Cada elemento del vector contiene un número. Si el número es de x bits, entonces tenemos una ((FAT x)). Por ejemplo, en referencia a la figura, supongamos que un determinado archivo ocupa los sectores 1,2,6,9. Entonces, la FAT tiene el aspecto que se muestra: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ... --------------------------------------------------------------14 | | 2 | 6 | | -1| | 9 | | | 0 | | | | | ... --------------------------------------------------------------El primer sector del archivo es el número 1. Entonces, en la entrada 1 de la FAT se escribe el número del siguiente sector ocupado por el archivo, en este caso el 2. En la entrada 2 se escribe el número del siguiente sector ocupado, que es el 6. En la entrada 6, el número del siguiente sector, que es el 9. Como el 9 es el último sector, en esa entrada escribimos un número especial, el 0 u otro que se convenga, que indica el final. Los espacios en blanco en la figura pueden estar ocupados por las listas que correspondan a otros archivos distintos. Por otro lado, un número especial convenido (por ejemplo el -1) puede indicar que el sector correspondiente a esa entrada está libre. Por ejemplo, el sector 4 de nuestra figura está libre, y puede asignarse a un archivo nuevo que se cree o a uno ya existente si crece su tamaño. Surge la pregunta ¿cómo sabemos cuál es el primer sector de un archivo dado? En el ejemplo anterior, ¿cómo sabemos que el primer sector ocupado por el archivo es el número 1? O dicho de otra forma ¿cómo sabemos dónde empieza la lista para un archivo dado? Lo sabemos porque aparte del espacio reservado para la FAT ha de haber otro espacio reservado, donde se escriba, esencialmente, el nombre de cada archivo y el primer sector que ocupa. Entonces, dado un nombre, se busca en este espacio (que se llama ((directorio raı́z))) y allı́ se consulta cual es ese primer sector. Al conjunto de los distintos espacios contenidos en un disco es a lo que se llama ((volumen)). Los datos que guardamos en un disco ocupan sólo una parte del volumen. El resto del espacio, se emplea en ((apuntar)) dónde han ido a parar los datos. 4. FAT 12. Vayamos a los detalles del sistema FAT-12, que es el usado por MS-DOS en discos de 1.44MB. Un disco de esta capacidad es un ((volumen)), y ese volumen se organiza en varias partes: sector de arranque FAT una o más copias de la FAT directorio raı́z datos 15 La tabla siguiente indica qué rango de sectores lógicos ocupa cada sección. Los sectores lógicos se indexan desde 0: Sector o rango 0 1-9 10-18 19-32 33-2879 Contenido Sector de arranque FAT Copia de la FAT (por seguridad) Directorio raı́z Datos 4.1. Sector de arranque. Es el primer sector del disco (número 0). De entre los 512 bytes estos campos son relevantes: Desplazamiento 0x0B 0x0D 0x0E 0x10 0x11 0x13 0x15 0x16 0x18 0x1A Contenido bytes por sector sectores por bloque sectores reservados número de ejemplares de FAT entradas del D.R. número sectores del volumen código tipo de disco número de sectores por FAT sectores por pista cabezales Tamaño 2 1 2 1 2 2 1 2 2 2 Dividiendo el número total de sectores (0x13) entre el número de sectores por bloque se tiene el número de unidades de asignación, y de ahı́ puede deducirse el número de bits usados en la FAT. En nuestro caso ya sabemos que son 12. 4.2. FAT. Las dos primeras entradas (=24 bits = 3 bytes) están reservadas. El resto de entradas pueden contener los siguientes valores (para FAT-12): 0x000 0xff0-0xff6 0xff7 0xff8-0xfff otro libre reservado sistema bloque defectuoso último bloque de un archivo siguiente bloque de un archivo La FAT obviamente indica sectores de datos. Como los sectores 0 al 32 no contienen datos, la entrada 0 de la FAT indicarı́a el sector lógico 33. Pero, 16 como las dos primeras entradas de la FAT están reservadas, es la tercera entrada de la FAT (número 2) la que hace referencia al sector 33. Por consiguiente, la entrada X de la FAT hace referencia al sector lógico S=X+33-2. Ası́, la entrada 2 al sector lógico 33, la 3 al 34 y sucesivamente. 4.3. Directorio raı́z. Está formado por entradas de 32 bytes, en número predeterminado (posición 0x11 del sector de arranque). Por este motivo, pudiera darse el caso de que aún quedando espacio libre en disco un archivo no pudiese crearse, al encontrarse lleno el directorio raı́z. Ésta es la estructura de cada entrada del D.R.: Desplazamiento 0x00 0x08 0x0B 0x0C 0x0E 0x10 0x12 0x16 0x18 0x1A 0x1C Contenido Nombre del archivo Extensión del archivo Atributos del archivo Reservado Hora de creación Fecha de creación Fecha del último acceso Hora última modificación Fecha última modificiación Primer bloque del archivo Tamaño del archivo (bytes) Tamaño 8 3 1 2 2 2 2 2 2 2 4 El primer byte del nombre puede tener algunos valores especiales, indicando: Valor 0x00 0x2E 0xE5 Indica última entrada del directorio archivo se refiere al directorio actual archivo se borró En el byte de atributos del archivo, cada bit tiene el siguiente significado: bit 0 1 2 3 4 5 6 7 significa sólo lectura oculto archivo de sistema etiqueta de volumen subdirectorio bit de backup sin usar sin usar 17 La palabra de hora tiene la siguiente estructura bit o rango 0-4 5-10 11-15 significa segundos/2 minuto hora Y para la palabra de fecha bit o rango 0-4 5-8 9-15 significa dı́a del mes mes año, relativo a 1980 4.4. Explicación de los subdirectorios. El bit 4 del byte de atributos indica que la entrada corresponde a un subdirectorio. El tamaño se establece siempre a cero. Las entradas de un subdirectorio tienen la misma estructura que las entradas del directorio raı́z, es decir, entradas de 32 bytes. Un directorio se trata como un archivo normal, y por tanto en 0x1A se encuentra el número del primer bloque que contiene al subdirectorio. Si los bloques son de un sector, es decir, 512 bytes, tenemos 16 entradas por subdirectorio. Cuando se agotan, se añaden nuevos sectores, de la misma forma a como se aumentarı́a el tamaño de un archivo cualquiera. Cuando se crea un subdirectorio, se crean automáticamente dos entradas, la entrada ((.)), que apunta a su mismo bloque y la entrada ((..)), que apunta al subdirectorio padre. 4.5. Funciones relevantes. La función 0x02 de la interrupción 0x13 de la BIOS se usa para leer sectores de la unidad de disco flexible, y colocar el resultado de la lectura en un buffer intermedio. Entradas ........ ah=0x02 dl=nu’mero de unidad dh=nu’mero de cara ch=nu’mero de pista cl=nu’mero de sector al=nu’mero de sectores a leer es:bx= puntero lejano al buffer Salida 18 ...... acarreo=0; en este caso ah=0 y al=nu’mero de sectores lei’dos acarreo=1; en este caso ah=co’digo de error Una unidad de disco flexible de 1.44MB tiene 2880 sectores lógicos, distribuidos en las dos caras. En cada cara hay 80 pistas, y en cada pista 18 sectores. Ası́, dado un número de sector lógico, es preciso encontrar la cara, pista y sector en que se encuentra antes de hacer la llamada. Lo dejo como ejercicio (sencillo). Téngase en cuenta que la BIOS numera las caras desde 0, las pistas también desde 0 pero los sectores dentro de una pista desde 1. Si se va a trabajar con una imagen de un disco, abriéndola como un archivo ordinario, se requieren las funciones fopen(), fread() y fseek(); consúltese la bibliografı́a o el manual. Es importante desactivar todo buffer intermedio para las operaciones de lectura mediante la función setbuf(). También es importante abrir el archivo en modo binario. 4.6. Interfaz. Con objeto de simplificar la práctica, la interfaz de la orden dir será sencilla: No tomará argumento alguno, sino que mostrará en pantalla el contenido completo del disco, marcando de alguna forma los directorios con objeto de separar visualmente los archivos pertenecientes a un directorio de los pertenecientes a otro. 4.7. Tres observaciones finales. Trabajamos con entradas de 12 bits. Esto implica que en la FAT hay dos entradas por cada 3 bytes, o lo que es lo mismo, que cada entrada ocupa un byte y medio. No es preciso leer la FAT si lo que se desea es sólo hacer dir del directorio raı́z, pero sı́ que hay que leerla si se quieren listar los subdirectorios. Por tanto, pensad bien cómo localizar la entrada x en la FAT y cómo leerla. Ya que las entradas son de 12 bits, cada dos entradas ocupan 3 bytes. Si representamos con dı́gitos o letras cada bit de los tres bytes, tal y como muestra la figura: 76543210 abcdefgh xyztuvwp la primera entrada se forma tomando los cuatro bits bajos del byte intermedio y usándolos como bits altos del primer byte: efgh76543210. La segunda entrada se forma tomando los cuatro bits altos del byte intermedio y usándolos como bits bajos del tercer byte: xyztuvwpabcd. Por otro lado, la estructura de un subdirectorio es igual que la estructura del directorio raı́z. Seguir la pista a los subdirectorios, no importa hasta qué profundidad, es entonces sencillo si se hace recursivamente. 19 Para terminar: las funciones que escribáis para esta práctica son todo lo que necesitáis para hacer otras operaciones, que resultarán casi ((gratis)). Por ejemplo, listar los sectores que ocupa un archivo concreto, buscar un archivo, calcular el espacio libre que queda en el disco o calcular el tamaño total de los archivos contenidos en un subdirectorio. Desfragmentar el disco o comprobar su consistencia lleva algo más de trabajo. 4.8. Ejemplo de lectura del primer sector en C e interpretación de sus contenidos. /* lectura del primer sector de un disco fat-12 para leer sus campos. Compilador tcc 2.01 */ #include <stdio.h> #include <fcntl.h> #include <io.h> /* fabricante y version */ void fab_y_ver(char *buffer){ int j; char *p=buffer+0x03; printf("\nFabricante y version: "); for(j=0;j<8;++j) printf("%c",*(p+j)); printf("\n"); return; } /* bytes por sector */ int bps(char *buffer){ int *p=(int *)(buffer+0x0b); return(*p); } /* sectores por pista */ int spb(char *buffer){ char *p=buffer+0x0d; return((int)(*p)); } 20 /* sectores reservados */ int sr(char *buffer){ char *p=buffer+0x0e; return((int)(*p)); } /* copias de la fat */ int copias_fat(char *buffer){ char *p=buffer+0x10; return((int)(*p)); } /* entradas del directorio raiz */ int entradas_dr(char *buffer){ int *p=(int *)(buffer+0x11); return(*p); } /* numero de sectores */ int numero_sectores(char *buffer){ int *p=(int *)(buffer+0x13); return(*p); } /* tipo de disco */ int tipo_disco(char *buffer){ unsigned char *p=buffer+0x15; return((int)(*p)); } /* sectores ocupados por las fat */ int sectores_fat(char *buffer){ int *p=(int *)(buffer+0x16); return(*p); } /* sectores por pista */ int sectores_pista(char *buffer){ int *p=(int *)(buffer+0x18); return(*p); } 21 /* numero de cabezales */ int cabezales(char *buffer){ int *p=(int *)(buffer+0x1a); return(*p); } main() { char buffer[512]; char *p; int *n; int fd=open("floppy.img",O_RDONLY,0); lseek(fd,0L,0); read(fd,buffer,512); fab_y_ver(buffer); printf(" bytes por sector: %4d\n",bps(buffer)); printf(" sectores por bloque: %4d\n",spb(buffer)); printf(" sectores reservados: %4d\n",sr(buffer)); printf(" copias de la FAT: %4d\n",copias_fat(buffer)); printf(" entradas del d.r.: %4d\n"\’entradas_dr(buffer)); printf(" total de sectores: %4d\n"\~numero_sectores(buffer)); printf("codigo tipo de disco: %4d\n",tipo_disco(buffer)); printf(" sectores por fat: %4d\n",sectores_fat(buffer)); printf(" sectores por pista: %4d\n",sectores_pista(buffer)); printf(" cabezales: %4d\n",cabezales(buffer)); close(fd); } 9. Sesión X Esta sesión se plantea como una aplicación trivial de las funciones de lectura de la FAT desarrolladas en la práctica anterior. Se trata de representar en pantalla el estado de todos los sectores de datos del disco. Por ejemplo, 22 cada sector se puede representar mediante un bloque sólido de un color distinto: rojo para los defectuosos, amarillo para los ocupados y verde para los libres. El único problema que puede presentarse es que en modo texto de 80 columnas y 25 filas disponemos de sólo 2000 caracteres, que no son suficientes para todo el disco. Hay cuatro soluciones, y podéis elegir la que más os convenga: 1) pasar a un modo texto de mayor resolución usando la función adecuada de la BIOS; b) Usar el modo de 80 × 25 habilitando algún mecanismo de desplazamiento que permita ver el disco completo; c) Pasar a modo gráfico de 320 × 200 y representar cada sector con un pixel, o un cuadradito de cuatro pixels, o como se crea conveniente; d) En lugar de escribir en pantalla, escribir lı́neas a un archivo de disco, que luego puede editarse. 10. Sesión XI En la sección 12.7.2 de los apuntes se encuentra una descripción de cómo se codifican los colores en el modo texto de la VGA. En particular, cómo el byte de atributos indexa la tabla DAC cuyas entradas a su vez apuntan a los registros de paleta que contienen las componentes de rojo, verde y azul de cada atributo. También se da una pequeña librerı́a para acceder y cambiar estos valores. Una vez leı́da y entendida esa sección, se escribirá el siguiente pequeño programa: Dibújese un rectángulo sólido en el centro de la pantalla. Por ejemplo, de 8 × 8 caracteres, en el color que se desee. Resérvense tres pares de teclas. Cada pareja de teclas, sirve para modificar una de las componentes de color del atributo elegido para el bloque (rojo, verde, azul). Una tecla de la pareja incrementa el valor, la otra lo decrementa. Accionando las teclas se variarán interactivamente las componentes, y se verá cómo el color del recuadro en pantalla varı́a de forma continua. 11. Sesión XII Esta práctica consiste en unos pocos ejercicios en lenguaje PostScript. En toda distribución Linux se incluye un intérprete de este lenguaje llamado Ghostscript. El intérprete se presenta como un terminal donde se teclean 23 instrucciones y una ventana que representa la salida del programa. Los ejercicios propuestos son los siguientes: 1. Escribir una función que tome como parámetro un número n y dibuje un polı́gono regular de n lados inscrito en una circunferencia. Se pasarán también como parámetros las coordenadas del centro de la circunferencia y su radio. 2. Escribir un pequeño programa que genere una hoja de papel milimetrado. Una hoja de papel milimetrado tiene divisiones de 1 cm., medio centı́metro y 1 mm, con grosores distintos: menor para las divisiones de milı́metro y mayor para las de un centı́metro, siendo intermedio para las de medio. 3. Escribir un pequeño programa que genere una hoja de papel polar, o semilogarı́tmico (a elección). 4. Escribir un programa en C que lea un archivo de texto y genere un programa PostScript. El archivo texto contendrá dos columnas, con parejas de valores (x, y). El programa C leerá esos valores y los introducirá en una matriz (hará una primera pasada para conocer el número de lı́neas en el archivo, y por tanto las dimensiones de la matriz que ha de usar). A continuación, escribirá en un archivo de texto un programa PostScript. Ese programa PostScript dibujará un recuadro en la página, cuya esquina inferior izquierda tendrá coordenadas (6, 11), en centı́metros, y cuya esquina superior derecha tendrá coordenas (15, 17), en centı́metros. Una vez hecho esto, dibujará la gráfica a partir del conjunto de valores (x, y), uniendo cada punto con el siguiente por medio de una lı́nea. Se trata esencialmente de hacer una transformación lineal que lleve del rango (xmin , xmax ) al rango (6, 15) (para las coordenadas x) y del rango (ymin , ymax ) al rango (11, 17) (para las coordenas y). 24