|
Grado en Ingeniería Informática
Procesadores de Lenguajes
Curso 2022/2023
|
Práctica 13 |
|
Generación de código objeto en el compilador de Tinto |
|
OBJETIVOS |
- Describir el proceso de generación de código ensamblador en el compilador de
Tinto.
|
|
CÓDIGO A UTILIZAR |
Las clases asociadas a la
generación de código ensamblador en el compilador de Tinto están contenidas
en los
paquetes tinto.mips, tinto.mips.instructions
y tinto.mips.registers.
Dentro del
paquete tinto se ha añadido la clase
TintoCompilerOptions. Esta clase desarrolla un contenedor para las
opciones de compilación que se introducen desde la línea de comandos. La
clase TintoCompiler se ha modificado para generar los ficheros en
ensamblador, enlazar todos los ficheros formando una única distribución de
salida y tratar las opciones de compilación.
El código de esta práctica
es el siguiente:
|
|
LA CLASE tinto.TintoCompiler |
Respecto a prácticas anteriores, la clase tinto.TintoCompiler se ha modificado para
desarrollar el proceso de compilación completo. El objetivo es compilar el archivo Main.tinto y todos los archivos importados
desde él para generar el archivo Application.s que contendrá el
código ensamblador de la aplicación.
El método createApplicationFile() se encarga de
crear el archivo Application.s y escribir en él un código común a todas las aplicaciones
(generado por la clase tinto.mips.ApplicationAssembler). El
método createCode() se ha modificado para generar no solo el código
intermedio de cada biblioteca (un objeto
tinto.code.LibraryCodification) sino para generar a partir de éste
el código ensamblador de la biblioteca (un objeto
tinto.mips.LibraryAssembler) y almacenarlo en un archivo con
extensión ".s". Si el modo verbose está activo el código
intermedio se almacena en un fichero con la extensión ".tic". Antes de analizar una biblioteca se busca si existe
el archivo .s correspondiente. Si este archivo ya existe no es
necesario generar de nuevo el código de la biblioteca (y por tanto no se
lanza el método createCode() sobre ella). El método
appendFile() se encarga de leer
un archivo de ensamblador de una biblioteca y añadirlo al archivo
Application.s. El resultado final es un archivo
Application.s que contiene el código ensamblador de todas las bibliotecas incluidas en la aplicación y el código común que
lanza la aplicación.
|
|
LA CLASE tinto.TintoCompilerOptions |
La clase tinto.TintoCompilerOptions desarrolla un
contenedor que almacenaa las opciones de compilado. Estas opciones son las
siguientes:
-
Path, indica directorio de trabajo.
-
-o Name, indica el nombre del fichero de salida (sin la extensión ".s").
-
-I Path, añade un directorio de búsqueda de archivos importados.
-
-v, indica que se generen los ficheros de código intermedio
(por defecto no se generarán).
El constructor de la clase toma como entrada los argumentos
introducidos en la llamada al compilador y calcula a partir de ellos
el valor de los diferentes campos internos de la clase. La clase
incluye métodos para acceder a los valores de estas opciones. El
método getWorkingDir() obtiene el directorio de trabajo en
el que se almacenarán los archivos generados en el proceso de
compilación. Por defecto será el directorio desde el que se ha
lanzado el compilador. El método addIncludeDir() añade un
nuevo directorio a la lista de directorios de búsqueda para los
archivos importados. El método verboseMode() indica si la
opción verbose está activa o no. El método getOutputName()
obtiene el nombre del fichero de salida. Por defecto este nombre es
Application.
La clase contiene además dos métodos dedicados a la búsqueda de archivos.
Esta búsqueda se realiza tanto en el directorio de trabajo como en todos los
directorios de búsqueda añadidos mediante opciones -I. El método
getTintoFile() busca un archivo con extensión ".tinto" en todos estos
directorios. El método getAsmFile() busca un archivo con extensión
".s" en los directorios confirgurados.
|
|
LA REPRESENTACIÓN DEL CÓDIGO ENSAMBLADOR DE MIPS |
El código objeto generado por el compilador de Tinto es código ensamblador del procesador MIPS.
Los registros del procesador se encuentran descrito en el paquete tinto.mips.registers.
Para describir estos registros se han definido las siguientes clases:
-
Register: Clase que describe un registro del procesador MIPS.
La clase contiene un campo con el código del registro y un método getName()
que devuelve el nombre asociado a cada registro.
-
RegisterConstants: Interfaz que define los códigos de los registros
del procesador MIPS.
-
RegisterSet: Clase que contiene la definición de todos los
registros de MIPS como referencias a objetos Register. Cada vez que una instrucción
necesite hacer referencia a un registro de MIPS, se utilizan los campos estáticos de esta clase.
De esta forma se evita crear cientos de referencias repetidas a los mismos registros.
-
DispRegister: Clase auxiliar que permite describir un direccionamiento indirecto, es decir, un desplazamiento
sobre un registro.
Las instrucciones del ensamblador de MIPS se definen en el paquete
tinto.mips.instructions. A continuación se
describen las clases utilizadas para representar este código ensamblador.
-
InstructionSet: Se trata de una interfaz que define códigos
asociados a cada instrucción del procesador MIPS. La interfaz incluye códigos
para todas las instrucciones de MIPS, aunque solo algunas de ellas son
finalmente utilizadas por el compilador de Tinto.
-
Instruction: Clase abstracta que describe una instrucción del procesador
MIPS. Las diferentes subclases de Instruction se diferencian en el modo de direccionamiento
de los datos. El procesador MIPS desarrolla un código ensamblador de tres direcciones donde
el direccionamiento puede ser inmediato (un valor constante), directo (un registro) o indirecto
(un desplazamiento sobre un registro).
-
RRRInstruction: Describe una instrucción MIPS con tres direcciones con direccionamiento
directo (tres registros). Típicamente permite describir instrucciones de operaciones entre dos registros,
almacenando el resultado en el tercer registro (por ejemplo la suma ADDU).
-
RRInstruction: Describe una instrucción MIPS que requiere dos direcciones con direccionamiento
directo (dos registros).
-
RInstruction: Describe una instrucción MIPS con una única dirección con direccionamiento
directo (un registro).
-
RRIInstruction: Describe una instrucción MIPS con tres direcciones con dos direccionamientos
directos (dos registros) y uno inmediato (un valor). Por ejemplo, la suma de un valor (ADDIU).
-
RIInstruction: Describe una instrucción MIPS con dos direcciones con direccionamiento
directo (un registro) e inmediato (un valor). Por ejemplo, la asignación directa de un valor a un registro.
-
LabelInstruction: Describe una instrucción MIPS con una dirección correspondiente a una
etiqueta. Típicamente permite representar saltos incondicionales.
-
RLInstruction: Describe una instrucción MIPS con dos direcciones correspondiente a un registro
y una etiqueta. Típicamente permite representar saltos condicionados al valor de un registro.
-
RRLInstruction: Describe una instrucción MIPS con tres direcciones correspondiente a dos registros
y una etiqueta. Típicamente permite representar saltos condicionados a la comparación entre dos registros.
-
RDRInstruction: Describe una instrucción MIPS con tres direcciones correspondiente a un registro
y un desplazamiento sobre otro registro. Por ejemplo, las instrucciones de acceso a memoria (load y store) son de este tipo.
-
NInstruction: Describe una instrucción MIPS que no necesita ninguna dirección.
-
InstructionFactory: Clase que contiene un conjunto de métodos estáticos para crear las instrucciones
de MIPS. Sólo se han incluido las funciones necesarias para el compilador de Tinto, de manera que existen muchas
instrucciones de MIPS sin su correspondiente método en esta clase.
|
|
GENERACIÓN DE CÓDIGO
ENSAMBLADOR |
Además de las clases dedicadas a representar el código ensamblador de MIPS (comentadas en el apartado anterior), el
paquete tinto.mips contiene tres clases dedicadas a generar el código ensamblador: ApplicationAssembler,
LibraryAssembler y FunctionAssembler.
La clase tinto.mips.ApplicationAssembler permite generar el código ensamblador común a todas las aplicaciones.
Este código consta de una parte dedicada a describir el control de las excepciones (copiado directamente del código ofrecido
por la herramienta PC-Spim) y el código de comienzo de la aplicación (etiquetado como __start) que contiene un salto
a la etiqueta Main_Main correspondiente a la ejecución de la función Main() de la clase Main.
La clase tinto.mips.LibraryAssembler encapsula el código ensamblador generado para un fichero fuente de Tinto.
La clase contiene una lista de objetos FunctionAssembler que son los que generan el código ensamblador de cada método.
El constructor recibe un objeto LibraryCodification con la descripción en código intermedio y construye los objetos
FunctionAssembler a partir de los correspondientes FunctionCodification (que contienen el código intermedio de cada
función). El método más importante de la clase es generateFile() que se encarga de crear el fichero ensamblador
(con extensión ".s") asociado a la biblioteca.
La clase tinto.mips.FunctionAssembler es la encargada realmente de generar el código ensamblador a partir del
código intermedio de cada función descrita en Tinto. La clase contiene los campos label (etiqueta de comienzo del método),
size (tamaño en bytes del registro de activación de la función), callstack (pila utilizada para almacenar el tamaño
la memoria reservada para los argumentos de cada llamada a función) y list (que almacena la lista de instrucciones
ensamblador asociada a la función). El método más importante de la clase es createAssembler() que recibe como entrada la
descripción de la función en código intermedio (FunctionCodification) y genera la lista de instrucciones en ensamblador.
En primer lugar se genera el código de entrada al método (tal y como se describe en la práctica 11). A continuación se recorre
cada instrucción en código intermedio y se traduce a código ensamblador. Por último se genera el código de salida del método
(tal y como se describe en la práctica 11). El método createAssembler() es el encargado de traducir las instucciones en
código intermedio a instrucciones en código ensamblador. Cada tipo de instrucción de código intermedio tiene su correspondiente
función translate...() que genera el código ensamblador asociado.
|
|
TRADUCCIÓN DE INSTRUCCIONES A CÓDIGO
ENSAMBLADOR |
El compilador de Tinto almacena todas las variables locales y temporales, así como los argumentos de llamada a las funciones,
en una zona de memoria conocida como el registro de activación de la función. Típicamente, una instrucción en código
intermedio se traduce en un conjunto de instrucciones en ensamblador que: (1) almacenan los valores de las operandos en registros
del procesador; (2) ejecutan la instrucción equivalente en ensamblador almacenando el resultado en otro registro del procesador;
y (3) copian el resultado en memoria. Para resolver los pasos (1) y (3) se utilizan los métodos translateLoadIntValue(),
translateDelayedLoadIntValue(), translateLoadLiteral() y translateStoreIntValue(). A continuación se describen estas funciones:
- translateLoadLiteral(): Genera instrucciones en ensamblador para asignar un valor constante (un literal) a un registro.
Si el valor se expresa en 16 bits se utiliza una instucción li. Si el valor requiere más de 16 bits se utilizan las instrucciones
lui y ori para cargar por separado la parte alta y la parte baja del literal.
- translateLoadIntValue(): Genera instrucciones en ensamblador para cargar un valor en un registro del procesador. La función
tiene en cuenta las distintas opciones de direccionamiento del valor. Si es un direccionamiento inmediato (el valor es un literal) se ejecuta
translateLoadLiteral(). Si es un direccionamiento directo (el valor se encuentra en un registro del procesador) no se genera ninguna
instrucción, pero se devuelve el registro en el que se encuentra el valor en vez del registro indicado como argumento. Si es un direccionamiento
indirecto (típicamente un desplazamiento sobre el frame pointer) se genera una instrucción lw.
- translateDelayedLoadIntValue(): Es equivalente a la anterior, pero añade una instrucción nop detrás de la
instrucción lw. Esta función es necesaria para adaptarnos al pipeline de MIPS. La instrucción que sigue a lw
no puede hacer uso del registro en el que se está almacenando el valor por problemas de pipeline. Cuando queremos utilizar un valor justo después
de cargarlo de memoria es necesario esperar un ciclo (por medio de una instrucción nop).
- translateStoreIntValue(): Genera las instrucciones para almacenar un valor contenido un registro sobre una variable. En este
caso podemos encontrarnos dos tipos de direccionamiento: directo e indirecto. Si la variable está almacenada en un registro, se genera una instrucción
move. Si la variable está en memoria se genera una instrucción sw.
Una vez comentadas las funciones auxiliares utilizadas en los pasos (1) y (3) explicados anteriormente vamos a explicar como se traduce a ensamblador
cada instrucción de código intermedio, asumiendo ya que tanto los operandos como el resultado se almacenan en registros del procesador.
- LABEL: Se traduce como una etiqueta en ensamblador con el mismo identificador.
- ASSIGN: Genera un load seguido de un store.
- ADD: Utiliza la instrucción en ensamblador addu .
- SUB: Utiliza la instrucción en ensamblador subu.
- MUL: Para multiplicar dos número enteros se utiliza la instrucción mult. Esta instrucción
almacena su resultado de 64 bits en dos registros especiales de MIPS llamados HI y LO. Para mover los 32 bits
menos significativos al registro de resultado se utiliza la instrucción mflo.
- DIV: La división entera en MIPS se realiza con la instrucción div. Esta instrucción
almacena el cociente en el registro LO y el resto en el registro HI. Para traducir la división se añade la
instrucción mflo.
- MOD: Para calcular el módulo, es decir, el resto de la división entera, se utiliza la instrucción MIPS
div seguida de mfhi.
- INV: Para generar un cambio de signo se calcula la resta entre el 0 y el valor original. El 0 se obtiene
del registro $r0 y la resta se calcula con subu.
- AND: La conjunción lógica se calcula con la instrucción and.
- OR: La disjunción lógica se calcula con la instrucción or.
- NOT: La negación lógica se calcula con la instrucción slti y el valor 1.
Esta instrucción puede entenderse como "set less than inmediate", es decir, devuelve 1 si el valor del registro es menor que el
valor indicado o 0 en otro caso.
- JMPEQ: Para realizar un salto condicionado a la igualdad entre dos registros se utiliza la
instrucción MIPS beq.
- JMPNE: El salto condicionado a la desigualdad entre dos registros se realiza con bne.
- JMPGT: Para el salto condicionado "mayor que" se utiliza en primer lugar la comparación slt,
que genera 1 si el primer registro es menor que el segundo. A continuación se añade un salto bne comparando con el registro $r0,
que realiza el salto si el resultado de la comparación no fue 0.
- JMPGE: Para el salto condicionado "mayor o igual" se utiliza el equivalente "no menor que". Para ello se
utiliza la instrucción slt seguda de un salto beq comaparando con el registro $r0.
- JMPLT: Para el salto condicionado "menor que" se utiliza la instrucción slt seguida de
un salto bne comparando con el registro $r0.
- JMPLE: El salto condicionado "menor o igual" se genera como "no mayor que" por medio de la instrucción
slt y el salto condicional beq comparando con el registro $r0.
- JUMP: El salto incondicional se genera con la instrucción j.
- JMP1: Para el salto condicionado a que un valor sea cierto se utiliza bne comparando con
el registro $r0.
- PARAM: Para almacenar un valor como parámetro en la posición index se direcciona como $sp + index
y se almacena con sw .
- PRECALL: Esta instrucción se utilizaba para avisar de una próxima llamada a función indicando el número de
parámetros de la llamada. Esto se traduce con una modificación del registro $sp (stack pointer) para reservar espacio para estos parámetros.
Como la memoria de pila crece en sentido descendente, la actualización de $sp consiste en restarle el espacio indicado.
- CALL: Esta instrucción indica la función a la que se llama y la variable en la que se debe almacenar
el resultado de la llamada. Para saltar a la función llamada se ejecuta la instrucción jal. A la vuelta de la llamada
el resultado debe encontrarse en $sp - 4, por lo que el siguiente paso es cargar ese calor en registro (lw) y salvarlo
en la variable indicada (sw). Una vez salvado el valor de retorno se actualiza el registro $sp liberando el espacio
reservado para los parámetros de la llamada.
- RETURN: La instrucción de retorno se traduce como un almacenamiento del resultado en el registro $v0
seguido de un salto a la etiqueta de retorno de la función. El código que desarrolla el retorno de la función se encuentra escrito a partir de
esa etiqueta.
|
|