Here are some notes I use to setup my Ubuntu box a gateway to share my phone's data plan:
# Source: http://www.yourownlinux.com/2013/07/how-to-configure-ubuntu-as-router.html
1. Enable IP forwarding
Enable the following in /etc/sysctl.conf
net.ipv4.ip_forward=1
2. iptables:
# To Wifi
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
iptables -A FORWARD -i wlan0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth1 -o wlan0 -j ACCEPT
# To USB connection
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
iptables -A FORWARD -i eth1 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o eth1 -j ACCEPT
3. Possibly persist iptables:
iptables-save > /etc/iptables.rules
Compartiendo localidades de memoria
sábado, 21 de junio de 2014
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):
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:
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.
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:
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:
Una vez explicado todo esto podemos proseguir explicando cómo vamos a generar todo este bytecode. Pero esto en entradas posteriores.
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>.
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.
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.
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
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.
Una vez explicado todo esto podemos proseguir explicando cómo vamos a generar todo este bytecode. Pero esto en entradas posteriores.
La constant pool, campos, métodos y el atributo code dentro del .class
Continuando con lo que tratábamos anteriormente, hablaremos ahora sobre los cuatro elementos mencionados en el título.
La constant pool
Las instrucciones de la JVM hacen referencia a la información simbólica que se almacena en la constant_pool. Todas las entradas de esta tabla tienen el siguiente formato general:
Cada field se describe por un campo field_info con la siguiente estructura:
Cada método, incluyendo a los métodos de inicialización, se describen por una estructura method_info como sigue:
Este atributo es de tamaño variable y se utiliza en la tabla de attributos de las estructuras method_info. Contiene las instrucciones de la JVM y la información auxiliar para un solo metodo, ya sea de instancia o de inicialización. La estructura de este atributo es la siguiente:
También podemos notar, aunque todavía no sepamos los detalles sobre el set de instrucciones de la JVM, que la asignación para la variable de instancia A se lleva a cabo en las líneas 60 y 61 que se encuentran dentro del método <init>, a saber el constructor de esta clase.
Y hablando de métodos podemos también notar la diferencia entre los descriptores para el método constructor (()V) y para el método getA() definido por nosotros (()I).
Lo que sigue es entrar en detalle sobre el set de instrucciones de la JVM. Hasta aquí por el momento.
La constant pool
Las instrucciones de la JVM hacen referencia a la información simbólica que se almacena en la constant_pool. Todas las entradas de esta tabla tienen el siguiente formato general:
Cada elemento en la constant_pool debe empezar con un tag de un byte que indica el typo de entrada. El contenido del array info variará con el valor de tag. Los valores válidos para el tag y sus respectivos valores se muestran aquí. Las únicas estructuras que nos interesan se describen a continuación:cp_info {
u1 tag;
u1 info[];
}
- CONSTANT_Class_info: Una estructura de este tipo se usa para representar a una clase o interfaz. Tiene la siguiente forma:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
- CONSTANT_Fieldref_info, CONSTANT_Methodref_info: Los campos y métodos son representados por estructuras similares:
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index;
En donde tag tiene el valor 9 para un Fieldref y 10 para Methodref. class_index debe ser un índice a una posición en la constant_pool cuyo contenido es una estructura CONSTANT_Class_info que representa a la clase que contiene la declaración del método y campo. Finalmente name_and_type_index debe ser el índice de una localidad en la constant_pool cuyo contenido sea una estructura CONSTANT_NameAndType_info que indica el nombre y el descriptor de un campo o méotod. El único caso especial de una estructura CONSTANT_Methodref_info es cuando esta empieza con un signo menor que (<), en cuyo caso el nombre debe ser el nombre especial <init> que representa el método inicializador de un objeto.}
- CONSTANT_String_info: Esta estructura representa constant objects de tipo String.
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
- CONSTANT_Integer_info: Esta estructura representa una constante numérica de 4 bytes:
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
- CONSTANT_NameAndType_info: Esta estructura representa un campo o un método sin indicar a qué clase o interface pertenece:
CONSTANT_NameAndtype_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
- CONSTANT_Utf8_info: Esta estructura se utiliza para representar cadenas de caracteres constantes. La Sección 4.4.7 de la JVM specification habla sobre los detalles de la codificación de caracteres. La estructura consiste en:
CONSTANT_Utf8_info {
u1 tag;
u2 length
u1 bytes[length];
En esta estructura el valor de tag es 1, length da el numero de bytes en el arreglo bytes, y bytes contiene los bytes de la cadena.}
Cada field se describe por un campo field_info con la siguiente estructura:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}En donde cada elemento se describe como sigue:
- access_flags: Al igual que las flags para las clases, este elemento es una máscara usada para denotar los permisos de acceso y propiedades de este campo. La descripción de las opciones se encuentra en esta tabla. Para nuestros fines nos basta con que todos los fields sean públicos, por lo que todos nuestros access_flags se estableceran como 0x0001.
- name_index: el valor de este campo debe ser el índice de una localidad de la constant_pool cuyo valor sea una estructura CONSTANT_Utf8_info que represente el nombre de un field almacenado como un identificador.
- descriptor_index: el valor de este campo debe ser un índice cuyo destino sea una CONSTANT_Utf8_info que contenga el descriptor de este campo.
- attributes_count y attributes[]: similar al caso de los atributos de la clase, estos atributos no son de interés para el compilador que intentamos desarrollar.
Cada método, incluyendo a los métodos de inicialización, se describen por una estructura method_info como sigue:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 atributes_count;
attribute_info attributes[attributes_count];
}En donde los elementos contiene la siguiente estructura:
- access_flags: similar que para los campos, nos interesaremos únicamente en métodos públicos, por lo que estableceremos el valor de este campo a 0x0001. Las demás opciones se encuentran aquí.
- name_index: el contenido de la dirección apuntada por este campo en la constant_pool debe ser una estructura CONSTANT_Utf8_info que represente el nombre de un método en Java o bien los nombres espciales de métodos <init> o <clinit> (no entraremos en detalle).
- descriptor_index: el valor de este elemento debe ser el índice de una localidad en la constant_pool que contenga una estructura CONSTANT_Utf8_info con el descriptor del método.
- attributes_count y attributes[]: Los valores de estos campos son la cantidad de atributos en el método y la tabla con dichos atributos. En el caso de los métodos estamos especialmente interesados en el atributo code, del que hablaremos a continuación.
Este atributo es de tamaño variable y se utiliza en la tabla de attributos de las estructuras method_info. Contiene las instrucciones de la JVM y la información auxiliar para un solo metodo, ya sea de instancia o de inicialización. La estructura de este atributo es la siguiente:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}Nos interesarán los siguientes elementos:
- attribute_name_index: Debe ser un apuntador a una estructura CONSTANT_Utf8_info que represente la cadena "Code".
- attribute_length: Este campo contiene el largo del attribute, excluyendo los seis bytes iniciales.
- max_stack: este valor indica la profundiad máxima que puede alcanzar el método en cualquier punto durante su ejecución.
- max_locals: este valor indica el número de variables locales que deben reservarse en el arreglo de un frame asociado a éste método, incluyendo la svariables usadas para pasar parámetros durante su invocación.
- code_length: este valor indica el número de bytes en el arreglo code para éste método. Este arreglo no debe estar vacío.
- code[]: este arreglo provee los bytes actuales de código de la JVM que implementan el método. Los detalles acerca de las restricciones para el contenido de este arreglo se describen en la Sección 4.8 Constraints on Java Virtual Machine Code de la JVM specification.
//StructA.java public class StructA{ public int A = 3; public int getA(){ return A; } }Y analizar los distintos elementos que obtenemos al decompilarlo, ya sea utilizando las herramientas de oolong o javap:
//StructA.class.Dump 000000 cafebabe magic = ca fe ba be 000004 0000 minor version = 0 000006 0032 major version = 50 000008 0013 19 constants 00000a 0a0004000f 1. Methodref class #4 name-and-type #15 00000f 0900030010 2. Fieldref class #3 name-and-type #16 000014 070011 3. Class name #17 000017 070012 4. Class name #18 00001a 010001 5. UTF length=1 00001d 41 A 00001e 010001 6. UTF length=1 000021 49 I 000022 010006 7. UTF length=6 000025 3c696e69743e <init> 00002b 010003 8. UTF length=3 00002e 282956 ()V 000031 010004 9. UTF length=4 000034 436f6465 Code 000038 01000f 10. UTF length=15 00003b 4c696e654e756d6265725461626c65 LineNumberTable 00004a 010004 11. UTF length=4 00004d 67657441 getA 000051 010003 12. UTF length=3 000054 282949 ()I 000057 01000a 13. UTF length=10 00005a 536f7572636546696c65 SourceFile 000064 01000c 14. UTF length=12 000067 537472756374412e6a617661 StructA.java 000073 0c00070008 15. NameAndType name #7 descriptor #8 000078 0c00050006 16. NameAndType name #5 descriptor #6 00007d 010007 17. UTF length=7 000080 53747275637441 StructA 000087 010010 18. UTF length=16 00008a 6a6176612f6c616e672f4f626a656374 java/lang/Object 00009a 0021 access_flags = 33 00009c 0003 this = #3 00009e 0004 super = #4 0000a0 0000 0 interfaces 0000a2 0001 1 fields Field 0: 0000a4 0001 access flags = 1 0000a6 0005 name = #5<A> 0000a8 0006 descriptor = #6<I> 0000aa 0000 0 field/method attributes: 0000ac 0002 2 methods Method 0: 0000ae 0001 access flags = 1 0000b0 0007 name = #7<<init>> 0000b2 0008 descriptor = #8<()V> 0000b4 0001 1 field/method attributes: field/method attribute 0 0000b6 0009 name = #9<Code> 0000b8 00000026 length = 38 0000bc 0002 max stack: 2 0000be 0001 max locals: 1 0000c0 0000000a code length: 10 0000c4 2a 0 aload_0 0000c5 b70001 1 invokespecial #1 0000c8 2a 4 aload_0 0000c9 06 5 iconst_3 0000ca b50002 6 putfield #2 0000cd b1 9 return 0000ce 0000 0 exception table entries: 0000d0 0001 1 code attributes: code attribute 0: 0000d2 000a name = #10<LineNumberTable> 0000d4 0000000a length = 10 Line number table: 0000d8 0002 length = 2 0000da 00000001 start pc: 0 line number: 1 0000de 00040002 start pc: 4 line number: 2 Method 1: 0000e2 0001 access flags = 1 0000e4 000b name = #11<getA> 0000e6 000c descriptor = #12<()I> 0000e8 0001 1 field/method attributes: field/method attribute 0 0000ea 0009 name = #9<Code> 0000ec 0000001d length = 29 0000f0 0001 max stack: 1 0000f2 0001 max locals: 1 0000f4 00000005 code length: 5 0000f8 2a 0 aload_0 0000f9 b40002 1 getfield #2 0000fc ac 4 ireturn 0000fd 0000 0 exception table entries: 0000ff 0001 1 code attributes: code attribute 0: 000101 000a name = #10<LineNumberTable> 000103 00000006 length = 6 Line number table: 000107 0001 length = 1 000109 00000004 start pc: 0 line number: 4 00010d 0001 1 classfile attributes Attribute 0: 00010f 000d name = #13<SourceFile> 000111 00000002 length = 2 000115 000e sourcefile index = #14 Done.Por ejemplo, vemos que la declaración de la variable de instancia public int A se tradujo en el campo Fieldref_class de la línea 6, que asocia este campo a la clase descrita en la línea 3 (cuyo nombre está en la posición 17, StructA) y al NameAndType de la posición 16 (que relaciona el nombre A con el descriptor I, el nombre y descripción de este campo).
También podemos notar, aunque todavía no sepamos los detalles sobre el set de instrucciones de la JVM, que la asignación para la variable de instancia A se lleva a cabo en las líneas 60 y 61 que se encuentran dentro del método <init>, a saber el constructor de esta clase.
Y hablando de métodos podemos también notar la diferencia entre los descriptores para el método constructor (()V) y para el método getA() definido por nosotros (()I).
Lo que sigue es entrar en detalle sobre el set de instrucciones de la JVM. Hasta aquí por el momento.
La estructura de un .class
Oolong
Para la investigación de la estructura de un .class, además de la herramienta javap de la que se habló un poco en nuestro primer post, utilizaremos una nueva herramienta llamada oolong, un lenguaje ensamblador para la JVM basado en jasmin. Este es el ensamblador que se utiliza en el libro Programming for the Java virtual machine. Esta diseñado para no tener que involucrarnos con los bytes directamente aunque es casi equivalente al bytecode que se generará finalmente.
La mejor herramienta que he podido encontrar para jugar con él es un plugin para jedit con el que podemos decompilar un .class y que, además de una descripción similar a la provista por javap, nos da la dirección y el contenido en hex de los bytes.
Por ejemplo, para la misma entrada que anteriormente:
El formato .class
A continuación se provee una descripción relativamente rápida del formato utilizado por la JVM. La descripción completa y avanzada se encuentra en el Capítulo 4 de la JVM specification. Una tabla un tanto más accesible la podemos encontrar en wikipedia.
Un archivo class consiste de una sola estructura ClassFile como sigue:
Lo siguiente es entrar en detalle sobre algunas estructuras que son de especial importancia para nuestro objetivo. Esto lo dejaremos para la próxima entrada. Sin embargo, antes de terminar es necesario hablar sobre otro tema importante: descriptores.
Descriptores
Un descriptor (o bien, descriptor) es una cadena que representa el tipo de un campo o de un método. Los descriptores se representan en el formato de un archivo class utilizando cadenas UTF-8 y se especifican mediante las siguiente gramáticas.
Descriptores de campos
Representa el tipo de una variable de instancia, de clase o local.
Donde los símbolos en negrita son caracteres ASCII y <classname> representa el nombre completo de una clase.
La interpretación de los tipos de campos se muestra en esta tabla.
De aquí que, por ejemplo, el descriptor de una variable de tipo int sea simplemente I, el de una variable de instancia de tipo Object sea Ljava/lang/Object; y el de un arreglo tridimensional de double de la forma double d[][][]; sea [[[D.
Descriptores de métodos
Un descriptor de método representa los parámetros que toma el método y el valor que retorna:
Donde V indica que el método retorna void. De esta gramática que el descriptor para un método como
(IDLjava/lang/Thread;)Ljava/lang/Object;.
Un ejemplo práctico de estos descriptores lo vemos en la línea 12 del código anterior, en donde se especifica el descriptor del método constructor para nuestra clase StructA.java.
Para la investigación de la estructura de un .class, además de la herramienta javap de la que se habló un poco en nuestro primer post, utilizaremos una nueva herramienta llamada oolong, un lenguaje ensamblador para la JVM basado en jasmin. Este es el ensamblador que se utiliza en el libro Programming for the Java virtual machine. Esta diseñado para no tener que involucrarnos con los bytes directamente aunque es casi equivalente al bytecode que se generará finalmente.
La mejor herramienta que he podido encontrar para jugar con él es un plugin para jedit con el que podemos decompilar un .class y que, además de una descripción similar a la provista por javap, nos da la dirección y el contenido en hex de los bytes.
Por ejemplo, para la misma entrada que anteriormente:
//StructA.java public class StructA{}Compilamos y utilizamos la opción dump class y obtenemos el la siguiente información:
//StructA.class.Dump 000000 cafebabe magic = ca fe ba be 000004 0000 minor version = 0 000006 0032 major version = 50 000008 000d 13 constants 00000a 0a0003000a 1. Methodref class #3 name-and-type #10 00000f 07000b 2. Class name #11 000012 07000c 3. Class name #12 000015 010006 4. UTF length=6 000018 3c696e69743e <init> 00001e 010003 5. UTF length=3 000021 282956 ()V 000024 010004 6. UTF length=4 000027 436f6465 Code 00002b 01000f 7. UTF length=15 00002e 4c696e654e756d6265725461626c65 LineNumberTable 00003d 01000a 8. UTF length=10 000040 536f7572636546696c65 SourceFile 00004a 01000c 9. UTF length=12 00004d 537472756374412e6a617661 StructA.java 000059 0c00040005 10. NameAndType name #4 descriptor #5 00005e 010007 11. UTF length=7 000061 53747275637441 StructA 000068 010010 12. UTF length=16 00006b 6a6176612f6c616e672f4f626a656374 java/lang/Object 00007b 0021 access_flags = 33 00007d 0002 this = #2 00007f 0003 super = #3 000081 0000 0 interfaces 000083 0000 0 fields 000085 0001 1 methods Method 0: 000087 0001 access flags = 1 000089 0004 name = #4<<init>> 00008b 0005 descriptor = #5<()V> 00008d 0001 1 field/method attributes: field/method attribute 0 00008f 0006 name = #6<Code> 000091 0000001d length = 29 000095 0001 max stack: 1 000097 0001 max locals: 1 000099 00000005 code length: 5 00009d 2a 0 aload_0 00009e b70001 1 invokespecial #1 0000a1 b1 4 return 0000a2 0000 0 exception table entries: 0000a4 0001 1 code attributes: code attribute 0: 0000a6 0007 name = #7<LineNumberTable> 0000a8 00000006 length = 6 Line number table: 0000ac 0001 length = 1 0000ae 00000001 start pc: 0 line number: 1 0000b2 0001 1 classfile attributes Attribute 0: 0000b4 0008 name = #8<SourceFile> 0000b6 00000002 length = 2 0000ba 0009 sourcefile index = #9 Done.
El formato .class
A continuación se provee una descripción relativamente rápida del formato utilizado por la JVM. La descripción completa y avanzada se encuentra en el Capítulo 4 de la JVM specification. Una tabla un tanto más accesible la podemos encontrar en wikipedia.
Un archivo class consiste de una sola estructura ClassFile como sigue:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
En donde u2 o u4 hacen referencia al número de bytes que requiere cada elemento. Los elementos se detallan como sigue:- magic: provee el número mágico que identifica a un formato class; tiene el valor 0xCAFEBABE.
- minor_version, major_version: Estos números detallan la versión del formato class. Para efectos prácticos mantendremos como 0 y 50 respectivamente.
- constant_pool_count: este valor es igual al número de entradas en la constant_pool más uno.
- constant_pool[]: es una tabla de estructuras que representan varias constantes string, nombre de clases, y campos (fields) y otras constantes referidas dentro de la estructura ClassFile y sus subestructuras. El formato de cada entrada en esta tabla es indicado por su byte tag en la estructura cp_info. Esta tabla se indexa desde 1 hasta constant_pool_count-1.
- access_flags: estos valores conforman una máscara de banderas que denotan los permisos y propiedades de esta clase. Los detalles de cada bandera pueden encontrarse aquí. De nuevo, para efectos prácticos todas nuestras clases serán públicas herderas de la clase Object, por lo que el valor de access_flags siempre será 0x0021.
- this_class y super_class: los valores de estos elementos deben ser índices de la constant_pool cuyo contenido sea una estructura CONSTANT_Class_info que represente a la clase definida por este archivo class y su clase super, respectivamente. Detalles sobre esta estructura más adelante.
- interfaces_count y interfaces[]: Dado que el compilador que queremos desarrollar no involucra ninguna clase de herencia, estos elementos no nos son relevantes. Solo nos interesa saber que el valor de interfaces_count puede ser cero, y la estructura interfaces[] estar vacía.
- fields_count: da la cantidad de estructuras field_info en la tabla fields. Las estructuras field_info representan a cualquier campo (ya sea variable de instancia o de clase), declarado por esta clase o interfaz.
- fields[]: cada entrada en esta tabla debe ser una estructura field_info que describa completamente a cada campo declarado en esta clase o interfaz (no incluye campos heredados).
- methods_count y methods[]: de manera similar a las estructuras anteriores, el primer campo da la cantidad de entradas en el segundo. El segundo consiste en una tabla de estructuras de tipo method_info. Nuevamente, por cuestiones prácticas no nos interesarán los métodos de tipo abstracto, aunque la JVM los soporten. Es necesario aclarar que esta tabla incluye también los métodos inicializadores (constructures) de la clase, mas no a los métodos heredados de superclasses o superinterfaces.
- attributes_count y attributes[]: los atributos ayudan a definir información sobre las estructuras. En el caso de los atributos de una estructura ClassFile, la JVM specification solo define dos: SourceFile y Deprecated, ambos opcionales. Tampoco entraremos en detalle sobre estos atributos, aunque vale la pena hacer notar que la especificación deja las puertas abiertas para definir y trabajar con atributos personalizados, si bien no esta permitido que estos atributos afecten la semántica de un archivo class.
Lo siguiente es entrar en detalle sobre algunas estructuras que son de especial importancia para nuestro objetivo. Esto lo dejaremos para la próxima entrada. Sin embargo, antes de terminar es necesario hablar sobre otro tema importante: descriptores.
Descriptores
Un descriptor (o bien, descriptor) es una cadena que representa el tipo de un campo o de un método. Los descriptores se representan en el formato de un archivo class utilizando cadenas UTF-8 y se especifican mediante las siguiente gramáticas.
Descriptores de campos
Representa el tipo de una variable de instancia, de clase o local.
FieldDescriptor -> FieldType
ComponentType -> FieldType
FieldType -> BaseType | ObjectType | ArrayType
BaseType -> B | C | D | F | I | J | S | Z
ObjectType -> L<classname>;
ArrayType -> [ ComponentTypeDonde los símbolos en negrita son caracteres ASCII y <classname> representa el nombre completo de una clase.
La interpretación de los tipos de campos se muestra en esta tabla.
De aquí que, por ejemplo, el descriptor de una variable de tipo int sea simplemente I, el de una variable de instancia de tipo Object sea Ljava/lang/Object; y el de un arreglo tridimensional de double de la forma double d[][][]; sea [[[D.
Descriptores de métodos
Un descriptor de método representa los parámetros que toma el método y el valor que retorna:
MethodDescriptor -> ( ParameterDescriptor* ) ReturnDescriptor
ParameterDescriptor -> FieldType
ReturnDescriptor -> FieldType | VDonde V indica que el método retorna void. De esta gramática que el descriptor para un método como
Object mimetodo(int i,double d, Thread t)
sea(IDLjava/lang/Thread;)Ljava/lang/Object;.
Un ejemplo práctico de estos descriptores lo vemos en la línea 12 del código anterior, en donde se especifica el descriptor del método constructor para nuestra clase StructA.java.
Organización de la JVM
Para comenzar, la demora en las últimas entradas se debió a dos libros, de más interesantes, con los que me topé: Programming for the Java virtual machine, de Joshua Engel (1999) y Modern compiler implementation in Java, Second Edition de Andrew W. Appel (2002). Luego de dedicar un tiempo a su lectura, espero poder organizar de mejor manera los temas a exponer en esta secuencia.
Una máquina de pila
Vale la pena iniciar aclarando que la JVM, a diferencia por ejemplo de la arquitectura x86, sigue el concepto de las máquinas basadas en pilas (stack machines). Citando al libro del dragón:
Esto permite un mejor control sobre qué programas pueden acceder a qué partes de la memoria. Los desarrolladores de la JVM crearon un conjunto de reglas que deben aplicar a todos los programas que se ejecuten y que aseguran a los usuarios que los programas no intentarán dañar el sistema. El algoritmo que lleva a cabo todo este control recibe el nombre de verificación y se ejecuta antes que el mismo programa por seguridad.
Organización de la JVM
Conceptualmente la JVM se divide en cuatro espacios diferentes:
Class area
Esta área almacena las clases que se cargan en el sistema. Las implementaciones de los métodos se mantienen en un method area y las constantes en una runtime constant pool. Para cada clase se almacena propiedades como:
La runtime constant pool para cada clase se asigna de la method area y se construye cuando la clase es creada por la JVM
Java stack
Cada vez que se invoca un método, un nuevo espacio de datos llamado stack frame es creado. Colectivamente, el conjunto de stack frames se les llama java stack y existe uno de estos por cada thread de la JVM.
El frame hasta arriba de la java stack muestra el lugar actual de ejecución y se llama el frame activo. Cuando un método termina su ejecución y retorna, el frame activo desaparece y el frame por debajo de ese vuelve a ser activo.
Cada stack frame tiene una pila de operandos, un arreglo de variables locales, una referencia a la runtime constant pool de la clase a la que pertenece y un puntero a la instrucción que se está ejecutando (PC), que se encuentra en la method area. Es de notar que todos los threads de la JVM comparten la method area, aunque cada uno mantenga su propia pila de frames, es decir que un frame solo puede ser referenciado por un thread. Estos frames se utilizan para almacenar datos y resultados parciales, retornar valores de métodos y disparar las excepciones.
El tamaño del arreglo de variables locales y la pila de operandos para un frame deben determinarse en tiempo de compilación y proveerse junto con el código para el método asociado a cada frame. Por lo mismo, el tamaño del frame como estructura de datos puede ser variable para cada método.
Arreglo de variables locales
El tamaño de este arreglo para cada frame debe proveerse en la representación binaria de una clase o interfaz, además del código asociado con el frame. Una sola localidad puede almacenar valores de tipo boolean, byte, char, short, int, float, reference o returnAddress (punteros a opcodes de instrucciones JVM, utilizados por las instrucciones de bifurcación a subrutinas). Un par de variables locales pueden almacenar un valor de tipo long o double. En este punto el lector habrá notado que nos referimos a tipos dentro de la JVM. Esta es otra característica que diferencia a la JVM de otras arquitecturas: las instrucciones (con dos excepciones) estan asociadas a tipos particulares de datos.
Las posiciones en este arreglo están indexadas, siendo 0 la posición de la primera variable local. Un valor de tipo long o double ocupa dos posiciones consecutivas, aunque solo puede ser direccionado usando el valor más pequeño (esto, entre otras cosas, lo garantiza el algoritmo de verificación).
Durante la invocación a un método, los parametros se reciben en este arreglo. En el caso particular de la invocación a un método de instancia (el único de interés aquí) la variable local 0 recibe siempre una referencia del objeto sobre el que se está invocando el método (this en el lenguaje Java).
Pilas de operandos (operand stacks)
Cada frame contiene una pila cuyo tamaño máximo debe determinarse en tiempo de compilación, similar al arreglo de variables locales.
La pila de operandos esta vacía cuando el frame que la contiene se crea. La JVM provee instrucciones para cargar constantes o variables locales o de instancia en este stack. Otras instrucciones toman operandos de esta pila, operan sobre ellos y regresan el resultado a la pila. Esta pila también se utiliza para preparar parámetros que deban ser pasados a métodos, y recibir los valores de retorno de los métodos.
Cada entrada en esta pila puede contener un valor de cualquier tipo de la JVM, incluyendo long o double.
Es importante observar que la estructura del Java stack dentro de la JVM implementa hasta cierto punto y simplifica las nociones que se tratan en el capítulo 7.2 'Asignación de espacio en la pila' del libro del dragón: en concreto el tema de los registros de activación por cada invocación a un método. El que la JVM se encargue de crear y manipular frames por cada invocación a método, además de las estructuras internas a cada frame que permiten almacenar y trabajar con los datos locales, implica un gran cambio con respecto a arquitecturas en donde la memoria se ve como una gran arreglo de bytes direccionables.
Una máquina de pila
Vale la pena iniciar aclarando que la JVM, a diferencia por ejemplo de la arquitectura x86, sigue el concepto de las máquinas basadas en pilas (stack machines). Citando al libro del dragón:
En una máquina basada en pila, las operaciones se realizan metiendo operandos en una pila y después llevando a cabo las operaciones con los operandos en la parte superior de la pila. (...) Estas máquinas desaparecieron casi por completo, ya que se creía que la organización de la pila era demasiado limitante y requería demasiadas operaciones de intercambio y copiado. No obstante, las arquitecturas basadas en pila revivieron con la introducción de la Máquina virtual de Java (JVM).El hecho de que la estructura de la JVM no sea diseñado hacia ninguna arquitectura en particular provee una visión más abstracta de la memoria. En lugar de dar acceso al nivel de bits y bytes de la memoria, la JVM trata la memoria como una colleción de objetos.
Esto permite un mejor control sobre qué programas pueden acceder a qué partes de la memoria. Los desarrolladores de la JVM crearon un conjunto de reglas que deben aplicar a todos los programas que se ejecuten y que aseguran a los usuarios que los programas no intentarán dañar el sistema. El algoritmo que lleva a cabo todo este control recibe el nombre de verificación y se ejecuta antes que el mismo programa por seguridad.
Organización de la JVM
Conceptualmente la JVM se divide en cuatro espacios diferentes:
- Class area: que almacena las constantes y el código.
- Java stack: que mantiene registro de qué invocaciones a métodos se han hecho y la data asociada a cada método.
- Heap: donde se almacenan los objetos.
- Native method stack: consultar aquí.
Class area
Esta área almacena las clases que se cargan en el sistema. Las implementaciones de los métodos se mantienen en un method area y las constantes en una runtime constant pool. Para cada clase se almacena propiedades como:
- Superclass
- Lista de interfaces
- Lista de campos (fields)
- Lista de métodos y de constantes
La runtime constant pool para cada clase se asigna de la method area y se construye cuando la clase es creada por la JVM
Java stack
Cada vez que se invoca un método, un nuevo espacio de datos llamado stack frame es creado. Colectivamente, el conjunto de stack frames se les llama java stack y existe uno de estos por cada thread de la JVM.
El frame hasta arriba de la java stack muestra el lugar actual de ejecución y se llama el frame activo. Cuando un método termina su ejecución y retorna, el frame activo desaparece y el frame por debajo de ese vuelve a ser activo.
Cada stack frame tiene una pila de operandos, un arreglo de variables locales, una referencia a la runtime constant pool de la clase a la que pertenece y un puntero a la instrucción que se está ejecutando (PC), que se encuentra en la method area. Es de notar que todos los threads de la JVM comparten la method area, aunque cada uno mantenga su propia pila de frames, es decir que un frame solo puede ser referenciado por un thread. Estos frames se utilizan para almacenar datos y resultados parciales, retornar valores de métodos y disparar las excepciones.
El tamaño del arreglo de variables locales y la pila de operandos para un frame deben determinarse en tiempo de compilación y proveerse junto con el código para el método asociado a cada frame. Por lo mismo, el tamaño del frame como estructura de datos puede ser variable para cada método.
Arreglo de variables locales
El tamaño de este arreglo para cada frame debe proveerse en la representación binaria de una clase o interfaz, además del código asociado con el frame. Una sola localidad puede almacenar valores de tipo boolean, byte, char, short, int, float, reference o returnAddress (punteros a opcodes de instrucciones JVM, utilizados por las instrucciones de bifurcación a subrutinas). Un par de variables locales pueden almacenar un valor de tipo long o double. En este punto el lector habrá notado que nos referimos a tipos dentro de la JVM. Esta es otra característica que diferencia a la JVM de otras arquitecturas: las instrucciones (con dos excepciones) estan asociadas a tipos particulares de datos.
Las posiciones en este arreglo están indexadas, siendo 0 la posición de la primera variable local. Un valor de tipo long o double ocupa dos posiciones consecutivas, aunque solo puede ser direccionado usando el valor más pequeño (esto, entre otras cosas, lo garantiza el algoritmo de verificación).
Durante la invocación a un método, los parametros se reciben en este arreglo. En el caso particular de la invocación a un método de instancia (el único de interés aquí) la variable local 0 recibe siempre una referencia del objeto sobre el que se está invocando el método (this en el lenguaje Java).
Pilas de operandos (operand stacks)
Cada frame contiene una pila cuyo tamaño máximo debe determinarse en tiempo de compilación, similar al arreglo de variables locales.
La pila de operandos esta vacía cuando el frame que la contiene se crea. La JVM provee instrucciones para cargar constantes o variables locales o de instancia en este stack. Otras instrucciones toman operandos de esta pila, operan sobre ellos y regresan el resultado a la pila. Esta pila también se utiliza para preparar parámetros que deban ser pasados a métodos, y recibir los valores de retorno de los métodos.
Cada entrada en esta pila puede contener un valor de cualquier tipo de la JVM, incluyendo long o double.
Es importante observar que la estructura del Java stack dentro de la JVM implementa hasta cierto punto y simplifica las nociones que se tratan en el capítulo 7.2 'Asignación de espacio en la pila' del libro del dragón: en concreto el tema de los registros de activación por cada invocación a un método. El que la JVM se encargue de crear y manipular frames por cada invocación a método, además de las estructuras internas a cada frame que permiten almacenar y trabajar con los datos locales, implica un gran cambio con respecto a arquitecturas en donde la memoria se ve como una gran arreglo de bytes direccionables.
martes, 26 de octubre de 2010
Compilando para la JVM
Este es el inicio de una serie de entradas sobre la aventura de compilar para la Java Virtual Machine (JVM) como parte de un curso de compiladores en la universidad. Por tanto, se asume que el lector conocerá elementos básicos del lenguaje de programación Java (primer, más no único, usuario de la JVM): Data types, primitivos, referencias, objetos, clases, métodos.
Para entrar en contexto, la idea es convertir programas escritos en una gramática libre de contexto similar a esta y unas reglas semánticas muy básicas, a bytecode con la estructura de un .class de la JVM.
De entrada les recomiendo echar un vistaso a tres links fundamentales (todos en inglés): este, este y la especificación de la JVM. Nada que google no pueda revelar, pero resulta bastante útil tenerlos a la mano.
¿Qué es la JVM?
La respuesta a esta pregunta se encuentra, al menos oficialmente, en The Java Virtual Machine Specification, que define tres cosas:
javap - The Java Class File Diassembler
Una herramienta que será de mucha utilidad para analizar los .class generados por el compilador de java es javap. Su descripción en man javap. Los flags a utilizar:
Los flags anteriores nos dan una idea de los elementos a investigar: local variable tables, stack size, number of args, etc.
Comenzando con algo sencillo:
Para entrar en contexto, la idea es convertir programas escritos en una gramática libre de contexto similar a esta y unas reglas semánticas muy básicas, a bytecode con la estructura de un .class de la JVM.
De entrada les recomiendo echar un vistaso a tres links fundamentales (todos en inglés): este, este y la especificación de la JVM. Nada que google no pueda revelar, pero resulta bastante útil tenerlos a la mano.
¿Qué es la JVM?
La respuesta a esta pregunta se encuentra, al menos oficialmente, en The Java Virtual Machine Specification, que define tres cosas:
- Un conjunto de instrucciones (bytecodes) con su respectivo significado.
- Un formato de archivos llamado class, que se usa para transmitir bytecodes y demás estructura de una forma independiente de plataforma.
- Un algoritmo para verificar que los programas no comprometan la integridad de la JVM: verificación.
javap - The Java Class File Diassembler
Una herramienta que será de mucha utilidad para analizar los .class generados por el compilador de java es javap. Su descripción en man javap. Los flags a utilizar:
- -l: 'Prints out line and local variable tables'
- -c: 'Prints out diassembled code (...) for each of the methods in the class'
- -s: 'Prints internal type signatures'
- -verbose: 'Prints stack size, number of locals and args for methods'
Los flags anteriores nos dan una idea de los elementos a investigar: local variable tables, stack size, number of args, etc.
Comenzando con algo sencillo:
//StructA.java public class StructA{}Compilando y luego utilizando javap:
$ javac StructA.class $ javap -c -l -s -verbose StructA Compiled from "StructA.java" public class StructA extends java.lang.Object SourceFile: "StructA.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #3.#10; // java/lang/Object."<init>":()V const #2 = class #11; // StructA const #3 = class #12; // java/lang/Object const #4 = Asciz <init>; const #5 = Asciz ()V; const #6 = Asciz Code; const #7 = Asciz LineNumberTable; const #8 = Asciz SourceFile; const #9 = Asciz StructA.java; const #10 = NameAndType #4:#5;// "<init>":()V const #11 = Asciz StructA; const #12 = Asciz java/lang/Object; { public StructA(); Signature: ()V LineNumberTable: line 1: 0 Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 }Primeras cosas a observar:
- La existencia de una constant pool de aspecto similar a una tabla de símbolos.
- La definición de un método StructA con signature ()V. Además, la definición de un segmento code dentro de éste método, con variables Stack, Locals y Args_size.
- Las instrucciones aload_0, invokespecial y return numeradas dentro del segmento de code. Descripciones para estas instrucciones aquí.
Suscribirse a:
Entradas (Atom)