terça-feira, 7 de setembro de 2010

Formato de Arquivo Class - Java 6

INTRODUÇÃO

Nesse artigo vamos conhecer um pouco da estrutura do arquivo ".class" gerados pelos compiladores Java. Nesse arquivo contém informações a respeito da classe compilada, métodos, campos e outras referências necessárias que serão usadas pela Máquina Virtual do Java no momento do carregamento da classe.
O conhecimento a respeito da estrutura desse arquivo ".class" nos ajuda a entender melhor como os dados estão organizados de maneira que possamos fazer otimizações nesse arquivo, buscar informações, etc.

ESTRUTURA BÁSICA

Nas figuras a seguir, veremos a estrutura básica do formato de arquivo class, versão Java 6.



A estrutura do formato de arquivo class segue uma estrutura análoga as estruturas criadas na linguagem C. Nos primeiros itens às esquerdas, indicam o tipo do elemento, por exemplo, "u1", "u2", "u4" e nos itens às direitas, indicam as variáveis. Basicamente, "u1", "u2" e "u4" armazenam 1, 2 e 4 bytes sem sinal. A seguir, temos a descrição de cada item.

u4 magic: Indica o formato de arquivo class cujo valor em hexadecimal é 0xCAFEBABE.

u2 minor_version: Indica a menor versão suportada.

u2 major_version: Indica a maior versão suportada.

u2 constant_pool_count: Número de constantes armazenadas na tabela de constantes.

cp_info - Constantes: Armazena constant_pool_count - 1 constantes que representam valores constantes como cadeia de caracteres, números, nomes de classes, métodos, etc. Para cada constante, é utilizada uma estrutura especifica que será explicada mais adiante.


u2 acess_flags: Representa propriedades da classe ou interface que está sendo representada  nesse arquivo ".class". Abaixo temos alguns valores usados nesse item.


Nome - Valor - Definição

ACC_PUBLIC                - 0x0001 - Declara classe publica, pode ser acessado fora do pacote.
ACC_FINAL                   - 0X0010 - Declara classe final, subclasses não são permitidas.
ACC_SUPER                 - 0X0020 - Trata métodos da super classe quando invocado pela instrução invokespecial
ACC_INTERFACE        - 0x0200 - É uma interface, não é uma classe.
ACC_ABSTRACT         - 0X0400 - Declara que a classe é abstrata, não pode ser instânciada.
ACC_SYNTHETIC        - 0X1000 - Declara sintético, não presente no código fonte.
ACC_ANNOTATION    - 0X2000 - Declara com um tipo de anotação.
ACC_ENUM                  - 0X4000 - Declara como tipo enum


Observações:

A flag ACC_SYNTHETIC indica que a classe foi gerada pelo compilador e não tem código fonte.
A flag ACC_ENUM indica que a classe ou superclasse é declarada como tipo enum.
A flag ACC_INTERFACE configurada indica uma interface e não classe, e o contrário, indica classe.

Cada arquivo de classe não pode ter as flags ACC_FINAL, ACC_SUPER ou ACC_ENUM configurados.
A flag ACC_INTERFACE indica que é um tipo de anotação e a flag ACC_INTERFACE nesse caso, deve ser configurada.
O arquivo de classe não pode ter as flags ACC_FINAL e ACC_ABSTRACT configuradas.

u2 this_class: Indice para a tabela de constantes indicando a classe / interface em questão.

u2 super_class: Indice para a tabela de constantes indicando a super classe da classe desse arquivo em questão. Se esse valor for 0 (Zero), a super classe é java.lang.Object.

u2 interface_count: Número de interfaces implementadas (ou estendida) pela classe (ou interface).

u2 interfaces [interface_count]: Array de indices para a tabela de constantes com todas as interfaces implementadas (ou estendida) pela classe (ou interface).

u2 fields_count: Número de campos que foi definido nesse arquivo ".class".

field_info - Descrição dos Campos: Estrutura que define cada campo usado no arquivo. No total, temos um número de fields_count para essas estruturas. E a estrutura field_info, será explicada mais adiante nesse artigo.

u2 methods_count: Número de métodos que foi definido nesse arquivo ".class".

method_info - Descrição dos Métodos: Estrutura que define cada método usado no arquivo. No total, temos um número de methods_count para essas estruturas. E a estrutura method_info, será explicada mais adiante nesse artigo.

u2 attribute_count: Número de atributos que foi definido nesse arquivo ".class".

attribute_info - Descrição dos Campos: Estrutura que define cada método usado no arquivo. No total, temos um número de attribute_count para essas estruturas. E a estrutura attribute_info, será explicada mais adiante nesse artigo.


ESTRUTURA - cp_info

Agora vamos explorar a fundo a estrutura geral das constantes que será mostrada na figura a seguir:


u1 tag: Define o tipo da constante.
u1 info[]: Array de bytes armazenados pela constante. Esse array é usado de acordo com o tipo da constante, pois tem constantes que usam apenas 4 bytes e outras usam um número variável de bytes cujo tamanho é geralmente guardado na primeira posição depois da definição do tipo de constante (item tag).

Observações:
Note que não temos como saber o tamanho de bytes a seguir (depois do item tag) sem saber o tipo de constante, o que nos obriga a determinar o tipo de constante (pelo item tag) para depois definirmos com precisão a quantidade de bytes reservada para aquela constante em particular.
Para as constantes do tipo long e double, conta-se dois itens do array de constantes (estrutura cp_info). Ou seja, se a constante long possuir um indice n, o próximo indice n+1 também pertence a constante long.

No total, temos 11 tipos de constantes usadas no formato de arquivo class - Java 6 que estão descritos na figura a seguir:


Nas figuras a seguir, serão mostrados cada um dos 11 tipos de constantes existentes no arquivo class.















ESTRUTURA - field_info



u2 acess_flags: Propriedades do Campo.

Nome - Valor - Descrição

ACC_PUBLIC              - 0x0001 - Declara campo público e pode ser acessado fora do pacote.
ACC_PRIVATE            - 0x0002 - Declara campo privado, uso somente na própria classe.
ACC_PROTECTED     - 0x0004 - Declara campo protegido, uso somente na classe e subclasses.
ACC_STATIC              - 0x0008 - Declara campo estático.
ACC_FINAL                - 0x0010 - Declara campo final, não pode ser alterado depois de inicializado.
ACC_VOLATILE         - 0x0040 - Declara volatil não pode ser cacheada.
ACC_TRANSIENT     - 0x0080 - Declara como temporária, não pode ser escrita ou lida num gerenciador de objetos persistentes.
ACC_SYNTHETIC     - 0x1000 - Declara sintético, não presente no código fonte.
ACC_ENUM               - 0x4000  - Declara com um elemento enum


ESTRUTURA - method_info


u2 acess_flags: Propriedades do Método.

Nome - Valor - Descrição

ACC_PUBLIC                  - 0x0001 - Declara método público.
ACC_PRIVATE                - 0x0002 - Declara método privado.
ACC_PROTECTED         - 0x0004 - Declara método protegido.
ACC_STATIC                  - 0x0008 - Declara método estático.
ACC_FINAL                    - 0x0010 - Declara método final.
ACC_SYNCHRONIZED - 0x0020 - Declara método sincronizado.
ACC_BRIDGE                 - 0x0040 - Declara método "ponte", gerado pelo compilador.
ACC_VARARGS              - 0x0080 - Declarado com variáveis com vários argumentos.
ACC_NATIVE                  - 0x0100 - Declara native, implementando em outra linguagem além de Java.
ACC_ABSTRACT            - 0x0400 - Declara método abstrato, a implementação não é fornecida.
ACC_STRICT                  - 0x0800 - Declara strictfp - Modo de ponto flutuante é FP-strict.
ACC_SYNTHETIC          - 0x1000 - Declara sintético, não presente no código fonte.


ESTRUTURA - attribute_info



Observações:

Na estrutura attribute_info, temos os últimos itens definindo um tipo de atributo em particular. Cada tipo de atributo é usado em uma situação particular (por exemplo, dentro de métodos, campos, etc) e em cada uma dessas, a JVM deve reconhecer os atributos suportados ou ignorá-los. Como exemplo de atributos, temos: ConstantValue, Code, StackMapTable, Exceptions, Synthetic, Signature, LineNumberTable, LocalVariableTable, Deprecated, InnerClasses (Classes internas), EnclosingMethod (Classes anônimas), SourceFile, SourceDebugExtension, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, AnnotationDefault, etc. Veja nas referências para conhecer a fundo esses atributos.


EXEMPLO

Depois do estudo do formato de arquivo class, podemos criar uma aplicação de exemplo na qual apartir de uma classe Java compilada em forma de array de bytes, podemos extrair o nome da classe. Veja na classe Java abaixo:

Arquivo - FindClassNameTest.java
-------------------------------------------------------------------------------------------------------------------
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;


public class FindClassNameTest {

    private static final int MASK = 0x000000FF;
    private static final int MAGIC_CLASS = 0xCAFEBABE;

    private static final int CONSTANT_UTF8_INFO = 1;
    private static final int CONSTANT_INTEGER_INFO = 3;
    private static final int CONSTANT_FLOAT_INFO = 4;
    private static final int CONSTANT_LONG_INFO = 5;
    private static final int CONSTANT_DOUBLE_INFO = 6;
    private static final int CONSTANT_CLASS_INFO = 7;
    private static final int CONSTANT_STRING_INFO = 8;
    private static final int CONSTANT_FIELDREF_INFO = 9;
    private static final int CONSTANT_METHODREF_INFO = 10;
    private static final int CONSTANT_INTERFACE_METHODREF_INFO = 11;
    private static final int CONSTANT_NAMEANDTYPE_INFO = 12;

    /**
     * @param args
     */
    public static void main(String[] args) {
        if(args.length < 1) {
            throw new IllegalArgumentException("Precisa pelo menos de um nome de arquivo .class");
        }

        InputStream in = null;

        try {
            in = new FileInputStream(args[0]);

        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        }

        InputStream inBuf = new BufferedInputStream(in);

        String name = null;

        try {
            name = getClassName(inBuf);

        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("O nome da classe / interface é: " + name);
    }

    public static String getClassName(final InputStream inputStream) throws IOException {
        Map mapIndexAndObject = new HashMap();

        byte b[] = new byte[10];

        inputStream.read(b, 0, b.length);

        int aux = (MASK & b[0])<<24;
        aux = aux | (MASK & b[1])<<16;
        aux = aux | (MASK & b[2])<<8;
        aux = aux | MASK & b[3];

        if(aux != MAGIC_CLASS) {
            throw new IllegalArgumentException("O arquivo class inválido! ");
        }

        int constant_pool_count = (MASK & b[8])<<8 | MASK & b[9];

        for (int i = 1; i < constant_pool_count; i++) {
            int tag = inputStream.read();

            switch(tag) {
            case CONSTANT_UTF8_INFO: {
                b = new byte[2];
                inputStream.read(b, 0, b.length);

                int length = (MASK & b[0])<<8 | MASK & b[1];

                b = new byte[length];
                inputStream.read(b, 0, b.length);

                Reader reader = new InputStreamReader(new ByteArrayInputStream(b), "UTF-8");

                char cbuf [] = new char[b.length];
                reader.read(cbuf);

                mapIndexAndObject.put(i, new String(cbuf));

                reader.close();

            }; break;
            case CONSTANT_INTEGER_INFO: {
                advanceInputStream(inputStream, 4);

            };break;
            case CONSTANT_FLOAT_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_LONG_INFO: {
                advanceInputStream(inputStream, 8);

                i++;

            }; break;
            case CONSTANT_DOUBLE_INFO: {
                advanceInputStream(inputStream, 8);

                i++;

            }; break;
            case CONSTANT_CLASS_INFO: {
                b = new byte[2];
                inputStream.read(b, 0, b.length);

                aux = (MASK & b[0])<<8 | MASK & b[1];

                mapIndexAndObject.put(i, aux);

            }; break;
            case CONSTANT_STRING_INFO: {
                advanceInputStream(inputStream, 2);

            }; break;
            case CONSTANT_FIELDREF_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_METHODREF_INFO: {
                advanceInputStream(inputStream, 4);

            };break;
            case CONSTANT_INTERFACE_METHODREF_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            case CONSTANT_NAMEANDTYPE_INFO: {
                advanceInputStream(inputStream, 4);

            }; break;
            }
        }

        advanceInputStream(inputStream, 2);

        b = new byte[2];
        inputStream.read(b, 0, b.length);

        aux = (MASK & b[0])<<8 | MASK & b[1];

        int index = (Integer) mapIndexAndObject.get(aux);

        return (String) mapIndexAndObject.get(index);
    }

    /**
     * Avança InputStream sem obter os bytes.
     *
     * @param in InputStream
     * @param length Quantidade a ser avançada.
     * @throws IOException Erros de I/O.
     */
    private static void advanceInputStream(final InputStream in, final int length) throws IOException {
        for (int i = 0; i < length; i++) {
            in.read();
        }
    }
}

-------------------------------------------------------------------------------------------------------------------

Na execução do arquivo compilado Java, precisamos passar como argumento o caminho e o nome da classe ou interface na qual queremos extrair o nome partindo do array de bytes do arquivo ".class". Assim, se temos o arquivo ".class" localizado em /home/user/Test.class, então, na execução do programa teremos o seguinte comando:


java FindClassNameTest /home/user/Test.class


Mas, pode ser perguntado, porque extrair o nome da classe / interface apartir do array de byte sendo que no código temos o nome (filename) do arquivo Java ?
Sim, é verdade, podemos extrair o nome diretamente pelo caminho / nome fornecido como parâmetro na execução do interpretador Java. Mas a vantagem maior de extrair o nome da classe / interface apartir do array de bytes é que em muitas situações não temos acesso ao arquivo físico que originou o array de bytes, como o uso de Classloaders, transformação, manipulação de bytecodes, etc. Situações nas quais não temos o arquivo físico e somente o array de bytes. Consequentemente, não precisamos mais depender do arquivo físico originário do array de bytes, pois o próprio array contém todas as informações a respeito da classe compilada!


REFERÊNCIAS

http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#75883

http://jcp.org/aboutJava/communityprocess/final/jsr202/index.html

http://en.wikipedia.org/wiki/Class_%28file_format%29

http://www.murrayc.com/learning/java/java_classfileformat.shtml

http://en.wikipedia.org/wiki/Java_Virtual_Machine

http://asm.ow2.org/

Nenhum comentário:

Postar um comentário