Aspectos Avanzados de Arquitectura de Computadoras Aspectos Avanzados de Arquitectura de Computadoras (AAAC) 2012 Obligatorio 2 Facultad de Ingeniería Instituto de Computación Departamento de Arquitectura de Sistemas Objetivo Familiarizarse con la arquitectura IA-32 de Intel. Herramientas Este obligatorio se desarrollará en un entorno linux. Se utilizará la misma máquina virtual (imagen vmware) que en el Obligatorio 1, donde se encuentran las herramientas necesarias: • nasm (Netwide Assembler http://nasm.sourceforge.net/) • gcc (compilador) • ld (linker) • objdump (object files dump) También se necesita el bochs (máquina virtual versión debug). En la página del curso se encuentra disponible un paquete de instalación y un instructivo de cómo instalarlo. Se pide El obligatorio consta de resolver los problemas planteados a continuación, documentando tanto la solución como los obstáculos encontrados, utilizando las herramientas dadas para trabajar con la arquitectura. Recomendamos inspeccionar el formato solicitado para la entrega para evitar confusiones. Parte A Se debe implementar un boot loader, cuyo cometido es arrancar una PC, inicialmente en modo real, cargar de un disquete booteable a memoria un micro-kernel, luego pasar a modo protegido, para finalmente ejecutar el micro-kernel. El micro-kernel debe borrar la pantalla, luego mostrar el mensaje "OBLIGATORIO 2- AAAC", y finalmente correr en un loop infinito que no realiza tarea alguna. Boot Loader Al arrancar el procesador, comienza ejecutando la BIOS en modo real, la cual configura el hardware básico y carga el primer sector del disquete de booteo en la dirección 0x7C00 de memoria RAM. En este primer sector del disquete debe residir el código del boot loader, de forma tal que se ejecute cuando la BIOS transfiere el control a la posición antes mencionada. La primera labor del boot loader es cargar desde el segundo sector del disquete de booteo a memoria RAM el micro-kernel. Para esto se debe usar la interrupción 0x13 de la BIOS (ver [1, Página 1 de 9 Aspectos Avanzados de Arquitectura de Computadoras Sección 13.2.5]), para resetear y luego cargar a memoria los sectores de disco donde se encuentra su implementación del micro-kernel. Tener en cuenta que las interrupciones de la BIOS sólo pueden ser usadas mientras el procesador esté en modo real. La segunda labor del boot loader es pasar a modo protegido; una forma de hacer esto se detalla en el siguiente fragmento de código, extraído de [2, 3]1 [BITS 16] ; We need 16-bit intructions for Real mode [ORG 0x7C00] ; The BIOS loads the boot sector into memory location 0x7C00 cli ; Disable interrupts, we want to be alone xor ax, ax mov ds, ax ; Set DS-register to 0 - used by lgdt lgdt [gdt_desc] ; Load the GDT descriptor mov eax, cr0 or eax, 1 mov cr0, eax ; Copy the contents of CR0 into EAX ; Set bit 0 ; Copy the contents of EAX into CR0 jmp 08h:clear_pipe ; Jump to code segment, offset clear_pipe ; Index 1, TI 0, RPL 00: 1000b = 08h [BITS 32] clear_pipe: mov ax, 10h mov ds, ax ; We now need 32-bit instructions ; Save data segment identifyer ; Move a valid data segment into the data ; segment register ; Move a valid data segment into the stack ; segment register ; Move the stack pointer to 06000h mov ss, ax mov esp, 06000h Donde gdt_desc es una etiqueta de memoria que referencia al GDT descriptor [4]. A continuación se presenta una definición posible de la GDT, en la cual se definen dos segmentos (además del segmento nulo que es obligatorio), uno de código y otro de datos, solapados, que abarcan toda la memoria: gdt: ; Address for the GDT gdt_null: dd 0 dd 0 ; Null Segment gdt_code: dw dw db db db db ; ; ; ; ; ; ; 0FFFFh 0 0 10011010b 11001111b 0 gdt_data: dw 0FFFFh dw 0 Code segment, read/execute, nonconforming Límit 0FFFFXh Base |0FFFFh = 0 Base >> 8 |0ffh = 0 present, max priv.,1,exec, non conf, read, 0 gran=4k, 32bit, 00,X=0fh Base 0 ; Data segment, read/write 1 En [2, 3] se encuentra una guía paso a paso del proceso de boot. Página 2 de 9 Aspectos Avanzados de Arquitectura de Computadoras db db db db 0 10010010b 11001111b 0 gdt_end: ; Used to calculate the size of the GDT gdt_desc: dw gdt_end - gdt - 1 dd gdt ; The GDT descriptor ; Limit (size) ; Address of the GDT Finalmente se debe saltar al lugar de memoria donde se encuentra en memoria el microkernel. En los dos últimos bytes del sector que contiene el código ensamblado del boot loader, debe colocarse la palabra 0x0AA55, firma que identifica al sector como booteable para la BIOS: times 510-($-$$) db 0 dw 0AA55h ; Fill up the file with zeros ; Boot sector identifyer Micro-Kernel La principal labor de esta parte del obligatorio, consiste en borrar y luego escribir un mensaje en una pantalla en modo texto. En este modo la pantalla se encuentra mapeada a memoria como una matriz de 80 columnas y 25 filas a partir de la dirección 0xB8000. La matriz se organiza por filas y cada elemento ocupa dos bytes: el primero corresponde al código ASCII del carácter desplegado en pantalla y el segundo a sus atributos de formato (como el color y el color de fondo). Consultar [1, Capítulo 23] por una descripción más detallada del modo de funcionamiento de una pantalla en modo texto. El micro-kernel puede implementarse tanto en lenguaje ensamblador como en lenguaje C. En el primer caso, tanto el boot loader como el micro-kernel se ensamblan usando el programa nasm (pueden implementarse como dos módulos separado o como un único módulo, a elección). Si se utiliza C, el código del micro-kernel se debe compilar (utilizando gcc), linkeditarlo, y luego, mediante el comando objcopy, convertirlo a formato binario "crudo" (sin ciertas secciones sólo útiles para un sistema operativo). A continuación se muestra un ejemplo de un script de ensamblado, compilado y linkeditado de esta parte del obligatorio: #!/bin/sh nasm -f bin bootloader.asm -o bootloader.bin -l bootloader.list gcc -c main.c ld -e main -Ttext 0x7E00 -o kernel.o main.o objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin El parámetro “-Ttext 0x7E00”, indica al linker dónde va a residir en memoria RAM el código ejecutable, que debe coincidir con la dirección en la cual efectivamente es cargado por el boot loader. Ver más detalles en el Apéndice A. Mediante el comando objdump se puede observar el código ensamblador generado y la ubicación de las etiquetas en el código del micro-kernel. objdump -D kernel.o Página 3 de 9 Aspectos Avanzados de Arquitectura de Computadoras De la misma forma, para observar el código generado por el ensamblador para el boot loader se puede revisar el archivo bootloader.list generado por el script previo. Armado del disco booteable Se muestra a continuación un ejemplo de un script para generar la imagen del disquete booteable que será utilizado en la máquina virtual bochs: rm boot.img dd if=/dev/zero of=boot.img bs=512 count=18 dd if=bootloader.bin of=boot.img bs=512 count=1 conv=sync,notrunc dd if=kernel.bin of=boot.img bs=512 count=14 seek=1 conv=sync,notrunc Este script está armado para un kernel.bin cuyo tamaño es menor a 14*512 bytes (14 sectores). El número de sectores que tenga su kernel.bin debe ser tomado en cuenta en el boot loader, de forma de cargar el número de sectores del micro-kernel necesarios a memoria. Parte B En esta parte se debe extender el boot loader y kernel de la PARTE A para crear un entorno de ejecución con dos tareas que ejecutan concurrentemente. Cada tarea, que llamaremos Tarea 1 y Tarea 2, consiste de un ciclo infinito que en cada iteración incrementa y muestra en pantalla el valor de un contador entero (se puede incluir en cada iteración un ciclo anidado que repita código vacío a los efectos de enlentecer la ejecución y poder visualizar el avance del contador en pantalla). El kernel alternará la asignación de la CPU a cada una de las tareas en períodos regulares de tiempo. El usuario puede suspender / reanudar la ejecución de la Tarea 1 oprimiendo la tecla '1' y análogamente puede suspender / reanudar la ejecución de la Tarea 2 oprimiendo la tecla '2'. Mientras una tarea está suspendida, el kernel no le asigna tiempo de CPU para su ejecución. Las dos tareas no pueden estar suspendidas simultáneamente; se ignorará cualquier intento de suspender una tarea mientras la otra se encuentra suspendida. Para implementar el kernel se deberán programar manejadores de interrupción para el timer y el teclado (ambas en modo protegido). Para una descripción del manejo de teclado puede consultar [1, Capítulo 20]. Recomendamos leer [1, Sección 17.4.1], donde se trata el manejo del controlador programable de interrupciones 8259A, módulo externo al CPU utilizado para el manejo de las diversas interrupciones de hardware. En particular es útil consultar los mecanismos para: • indicar qué interrupciones de hardware pueden interrumpir y cuáles no, • comunicar al controlador 8259A (desde el manejador de interrupciones implementado) que la interrupción fue atendida y por ende puede volver a interrumpir por ese canal. Las tareas no tendrán acceso a la memoria de video. Por esta razón, el kernel debe implementar un servicio de impresión en pantalla para desplegar una cadena especificada por parámetro en una posición de pantalla también especificada por parámetro. El entorno de ejecución debe cumplir con las siguientes características: • Cada tarea cuenta con sus propios segmentos de código, datos y stack, que deben estar definidos con nivel de privilegio 3 (el menos privilegiado). • Cada tarea cuenta con su propia LDT, en la cual están definidos todos los segmentos a los que accede. Página 4 de 9 Aspectos Avanzados de Arquitectura de Computadoras • El código del kernel ejecuta con nivel de protección 0 (el más privilegiado). En general todos los segmentos salvo los que son específicos para las tareas deben definirse con nivel de privilegio 0. • Las rutinas de interrupción deben ser atendidas en el mismo contexto de la tarea interrumpida (una interrupción no genera un cambio de tarea). • Todos los segmentos de código deben ser nonconforming. • El servicio de impresión en pantalla se accede a través de un call gate descriptor definido en la LDT de cada tarea y los parámetros deben pasarse por stack. • La disposición en memoria de las distintas partes de la tarea debe ser la siguiente: ... ... 1000h-1FFFh Espacio reservado para segmentos de código y datos de la Tarea 1. 2000h-2FFFh Stack para nivel de privilegio 3 de la Tarea 1. 3000h-3FFFh Stack para nivel de privilegio 0 de la Tarea 1. 4000h-4FFFh Espacio reservado para segmentos de código y datos de la Tarea 2. 5000h-5FFFh Stack para nivel de privilegio 3 de la Tarea 2. 6000h-6FFFh Stack para nivel de privilegio 0 de la Tarea 2. ... ... 7C00h - XXXX Boot loader y kernel. ... ... B8000h Memoria de video El código de la Tarea 1 y la Tarea 2 debe escribirse en lenguaje C con ensamblador embebido. El kernel puede incluir código C pero no es un requisito. El kernel (eventualmente junto con el boot loader) y cada una de las tareas se compilan como tres módulos independientes que se almacenan cada uno en una región específica del disquete (a libre elección). El boot loader debe cargar desde el disquete a memoria tanto el kernel como las tareas. Por documentación sobre cómo escribir código ensamblador embebido en C consultar [5, Sección 6.41]. Tener en cuenta que el lenguaje utilizado por gcc para compilar código ensamblador embebido en C sigue la sintaxis de AT&T, que es diferente a la sintaxis de Intel utilizada por Nasm. En el Apéndice B se incluye un resumen de las principales diferencias. Una referencia completa de la sintaxis utilizada por gcc se encuentra en [6]. Descripción de la entrega Formato de la entrega Se debe entregar un único archivo llamado obligatorio2.tar.gz, en el formato utilizado por los comandos tar y gzip de Unix, con el siguiente contenido: 1. Los archivos correspondientes al código fuente de su solución, repartidos en dos directorios parteA y parteB, que deben contener los fuentes correspondientes a cada parte. A su vez, para cada parte se debe entregar: Página 5 de 9 Aspectos Avanzados de Arquitectura de Computadoras ◦ Un script con nombre make.sh, que arme la imagen del disquete booteable con su solución. ◦ Archivo bochsrc de configuración del bochs. 2. La documentación del laboratorio en formato PDF con denominación obligatorio2.pdf, conteniendo una descripción clara de todos los aspectos relevantes de su solución. Es muy importante respetar: 1. El nombre de directorios y archivos debe ser exactamente el pedido, respetando mayúsculas y (falta de) espacios. 2. El archivo entregado debe tener el formato gzip-tarball. 3. El formato del archivo de documentación debe ser pdf. La entrega se realizará a través del sitio Web del curso. Fecha de entrega La fecha límite para realizar la entrega es el domingo 27 de mayo a las 23:30 horas. El sistema soporta múltiples entregas. Se recomienda que se realice una entrega con tiempo para verificar que el sistema le permite entregar correctamente. Se recomienda especialmente que no deje para último momento su única entrega y que realice una con al menos contenidos parciales de forma de asegurar que no quede sin entregar. Observaciones. No se aceptará bajo ninguna circunstancia una entrega realizada pasada la fecha límite estipulada. Esto incluye (pero no se restringe) a: entregas por email y entregas en formatos físicos. En caso de que una entrega no cumpla los criterios pautados en cuanto a los formatos y mecanismos establecidos, la misma puede no ser considerada como tal y descartada. Observación sobre instancias de no individualidad. Está prohibido utilizar código de otros estudiantes, de otros años, de cualquier índole, o hacer público código, corto o largo, general o específico, a través de cualquier medio (news, correo, papeles sobre la mesa, etc.). Los estudiantes que a juicio de los docentes no hayan cumplido con esta norma perderán el curso. Además todos los casos serán enviados a los órganos competentes de la Facultad, lo cual puede acarrear sanciones de otro carácter y gravedad. Página 6 de 9 Aspectos Avanzados de Arquitectura de Computadoras Apéndice A Descripción de áreas de un ejecutable Cuando se escribe código C que será cargado en una posición prefijada de memoria, puede ser necesario, al momento de linkeditar, explicitar la ubicación que tendrán diferentes secciones que forman un ejecutable. Existen seis secciones (ordenadas según posición estándar en memoria): • .text: En esta área se encuentra definido el código ejecutable. En arquitectura x86, normalmente es señalado por el segmento CS • .rodata: Datos de solo lectura, variables declaradas como const y constantes de cadena (por ejemplo el texto “ OBLIGATORIO 2-AAAC” ). Señalado por los segmentos DS y ES. • .data: Esta área contiene variables globales y estáticas, inicializadas al ser definidas. Señalado por los segmentos DS y ES. • .bss: Contiene las variables globales y estáticas, sin inicializar. Señalado por los segmentos DS y ES. • Heap: En esta área se guardan las regiones de memoria creadas en forma dinámica. Por ejemplo, al ejecutar un new o un malloc. No tiene un tamaño fijo y crece hacia zonas más altas de la memoria. Señalado por los segmentos DS y ES. • Stack: En esta área se guardan los registros de activación de funciones y procedimientos, que guardan entre otras cosas, la dirección de retorno y las variables locales. Inicialmente se pone en la posición más alta disponible de la memoria. No tiene un tamaño fijo y crece hacia las zonas más bajas de memoria. Es referenciado por el segmento SS. Un ejemplo en código: int i; // Variable global no inicializada=> .bss char * cadena; // Variable global no inicializada=> .bss char a='a'; // Variable global inicializada=> .data ; el valor 'a' se guarda en .rodata int main(){ int j; //Variable local => registro de activación de la función main static int estático; // Variable estatica no inicializada => .bss static int orig=5; // Variable estatica inicializada => .data cadena= malloc(sizeof(char)*4);//pide 4 caracteres en el HEAP printf("Esto es j: %d", j); //"Esto es j: %d" se guarda en .rodata } Página 7 de 9 Aspectos Avanzados de Arquitectura de Computadoras Apéndice B Principales diferencias entre la sintaxis de AT&T y la de Intel. • Los valores inmediatos se preceden por el símbolo $ en la sintaxis de AT&T • Los nombres de los registros se preceden por el símbolo % en la sintaxis de AT&T • Los operandos, en general, se colocan en orden inverso en las dos sintaxis. • El tamaño de los operandos en la sintaxis de AT&T se determina por el último carácter del nombre de la instrucción: b – byte (8 bits), w – word (16 bits), l – long (32 bits). Por ejemplo, la instrucción addl $1, %eax, suma uno al registro eax. • Saltos/llamadas far se codifican en AT&T como ljmp/lcall $segmento, $desplazamiento. Página 8 de 9 Aspectos Avanzados de Arquitectura de Computadoras Referencias [1]R. Hyde, “The art of assembly language”, [en línea], 1996, [citado Abril de 2012], disponible en Web: http://courses.ece.uiuc.edu/ece390/books/artofasm/artofasm.html. [2] G. Brunmar, “The booting process”, [en línea], Julio, 2003, [citado Abril de 2012], disponible en Web: http://www.osdever.net/tutorials/view/the-booting-process. [3] G. Brunmar, “The world of Protected mode”, [en línea], Julio, 2003, [citado Abril de 2012], disponible en Web: http://www.osdever.net/tutorials/view/the-world-of-protected-mode. [4] Intel Corporation, “Intel® 64 and IA-32 Architectures Software Developer's Manual”, Volume 3A: System Programming Guide, Part 1, [en línea], Marzo, 2012, [citado Abril de 2012], disponible en Web: http://www.intel.com/design/processor/manuals/253668.pdf. [5] GNU Project, “Using the GNU Compiler Collection (GCC)”, [en línea], [citado Abril de 2012], disponible en Web: http://gcc.gnu.org/onlinedocs/gcc/index.html [6] GNU Project, “Using as”, [en línea], [citado Abril de 2012], disponible en Web: http://sourceware.org/binutils/docs/as/ Página 9 de 9