lunes, 15 de noviembre de 2010

Conjunto de instrucciones de la JVM

Una instrucción en la JVM consiste en un opcode de un byte que especifica la operación que se va a realizar, seguida de cero o más operandos que proveen los argumentos para la operación. El número y tamaño de los operandos dependen del opcode.

La mayoría de las instrucciones codifican la información de tipo acerca de los operandos sobre los que aplican. Por ejemplo, la instrucción iload carga el contenido de una variable local, que debe ser un int, en la pila de operandos mientras que la instrucción fload hace lo mismo pero con floats. Es por eso que se dice que la mayoría de instrucciones son tipificadas. Algunas instrucciones como goto, que representa un salto incondicional, no requieren operandos. Las operaciones tipificadas suelen iniciar con la letra del tipo de operando que reciben.

Dado que la mayoría de instrucciones son tipificadas pero no pueden haber más opcodes que los que caben en un byte, las instrucciones proveen más soporte para algunos tipos que para otros, intencionalmente. Es por esto que se proveen instrucciones para cambiar de tipo a un valor. Esta tabla resume el soporte para los distintos tipos dentro del conjunto de instruciones de la JVM.

Es de notar que la mayoría de operaciones no tienen opciones para los tipos byte, char y short. No existen instrucciones que operen sobre boolean. Esta falta de soporte se apoya en el hecho de que, durante la compilación o ejecución, muchos de estos valores simplemente se representan como ints mediante sign-extension. Por tanto, la mayoría de operaciones sobre valores boolean, byte, char y short se llevan a cabo correctamente por instrucciones que operan sobre el tipo int.

A continuación presentamos un breve resumen sobre las instrucciones que consideramos serán de importancia. Los detalles sobre cada instrucción pueden encontrarse, como siempre, en la JVM specification, en el Capítulo 6. The Java Virtual Machine Instruction Set. Hablaremos un poco más en detalle de cada instruccion a medida que empecemos a compilar para la JVM.

Instrucciones de carga y almacenamiento
Las instrucciones de carga (load) y almacenamiento (store) transfieren valores entre el arreglo de variables locales y el stack de operaciones de un frame (para detalles consultar post previo):
  • Cargar una variable local en el stack de operandos: iload (para un int), aload (para una referencia), etc. 
  • Almacenar un valor del stack de operandos en una variable local: istore, astore.
  • Cargar una constante en el stack de operandos: ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>. 
Las instrucciones que acceden a fields de objetos y elementos de arreglos también transfieren data desde y hacia el stack de operandos.

Instrucciones aritméticas
Las instrucciones aritméticas computan un resultado que usualmente es una función de dos valores en el stack de operandos y hacen push al resultado de vuelta al stack. Existen dos tipos principales de instrucciones aritméticas: aquellas que operan sobre enteros y aquellas que operan sobre valores de punto flotante. Las operaciones también pueden variar en su comportamiento cuando ocurre overflow y durante una división dentro de cero. Nos interesan las siguientes operaciones aritméticas:
  • Suma: iadd
  • Resta: isub
  • Multiplicación: imul
  • División: idiv
  • Residuo (remainder): irem
  • Negación: ineg
  • Incremento de variable local: iinc
  • Comparación: dcmpg, dcmpl, fcmpg, fcmpl, lcmp
  • También se cuenta con operaciones para shift, bitwise OR, bitwise AND, y bitwise exclusive OR.
Operaciones de conversión de tipos
Estas instrucciones permiten la conversión entre los tipos numéricos soportados por la JVM. Pueden utilizarse para implementar conversiones en el código fuente o para mitigar la falta de ortogonalidad en el conjunto de instrucciones de la JVM. Tampoco descenderemos a detalles sobre este tipo de conversiones. Basta con mencionar que la JVM provee tanto conversiones de widening (pasar de un dato con una representación más pequeña a uno con una representación más grande) como de narrowing (lo contrario).

Creación y manipulación de objetos
En este punto vale la pena mencionar que los arreglos se consideran en la JVM como objetos con características particulares. En este sentido tanto los arreglos como las instancias de clases son objetos. A pesar de esto la JVM utiliza instrucciones distintas para crear y manipular a cada uno.
  • Crear una nueva instancia de una clase: new
  • Crear un nuevo arreglo: newarray, anewarray, multianewarray
  • Acceder a los fields de instancia: getfield, putfield.
  • Cargar un componente de un array en el stack de operandos: baload (boolean), caload (char), iaload (int), aaload (reference). 
  • Almacenar el valor desde el stack de operandos como un componente de un array: bastore, castore, iastore, aastore
  • Obtener el tamaño de un array: arraylength
  • Revisar las propiedades de una instancia de clase o un arreglo: instanceof, checkcast
Instrucciones de manejo del stack de operandos
Para el manejo directo del contenido del stack de operandos nos interesan las siguientes instrucciones: pop (desechar el primer elemento del stac), dup (duplicar el primer elemento del stack).

Instrucciones de transferencia de control
De este tipo encontramos las instrucciones condicionales y las incondicionales. Estaremos interesados en:
  • Saltos condicionales: ifeq, iflt, ifle, ifne, ifgt, ifge, if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne
  • Saltos incondicionales: goto, goto_w 
Vale la pena hacer mencionar que todas las instrucciones de transferencia de control condicionales que operan sobre int lo hacen mediante comparaciones de signo.

Invocación de métodos e instrucciones return
Existen cuatro instrucciones distintas para invocar un método dependieno de tipo de objeto del destino y del método (instancia, interfaz, estático y especial). Nos interesan únicamente:
  • invokevirtual: invoca un método de instancia de un objeto, este es el tipo de invocación normal en Java. 
  • invokespecial: invoca un método de instancia que requiere un manejo especial, ya sea un método de inicialización o un método de una superclass
De las instrucciones para retornar de un método, que se diferencian por el tipo de retorno, nos interesan: ireturn (usada para retornar valores de tipo boolean, byte, char, short o int) y areturn.  Además contamos con return que se utiliza en caso que el método sea declarado como void.

Una vez explicado todo esto podemos proseguir explicando cómo vamos a generar todo este bytecode. Pero esto en entradas posteriores.

No hay comentarios:

Publicar un comentario