Artigo Java Magazine 66 - Bytecode: Escondendo e Revelando

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (0)  (0)

Artigo da Revista Java Magazine Edição 66.

Esse artigo faz parte da revista Java Magazine edição 66. Clique aqui para ler todos os artigos desta edição

Bytecode: Escondendo e Revelando

Usando ofuscadores, descompiladores e otimizadores – e entendendo o bytecode

Preocupado com a proteção de Propriedade Intelectual do seu código? Ou curioso com todos os artigos, ferramentas e bibliotecas de geração de código que falam no tal bytecode?

 

De que se trata o artigo:

Explicamos a estrutura dos arquivos .class do Java, inclusive o bytecode, que codifica todos os seus métodos de forma portável. Entrando numa aplicação mais comum e mais prática, examinamos o tema de ofuscação e descompilação de código.

 

Para que serve:

Entender o bytecode é um conhecimento necessário para o uso de bibliotecas que permitem gerar código dinamicamente, ou mesmo para ter domínio do que acontece “por baixo do pano” ao usar ferramentas que fazem isso por você (como APIs de ORM, AOP, e outras).

Desenvolvedores de software de código fechado muitas vezes se preocupam com a proteção da sua Propriedade Intelectual, não desejando que nenhum “curioso” possa descompilar facilmente suas classes. Para isso, exploramos a ferramenta open source ProGuard. Veremos também o aspecto de otimização de bytecode, devido ao qual estas ferramentas podem ser muito úteis até mesmo para projetos de código aberto.

 

 

Este artigo trata de um tema com aspectos importantes tanto do nível teórico quanto do prático. Vamos falar do bytecode do Java: sua estrutura, design, vantagens e limitações, seu papel dentro da plataforma. Mas vamos estudar isso no contexto de uma aplicação importante, a prática de ofuscamento e otimização (ou sua reversa: descompilação) de classes. A princípio você poderia aprender a usar estas ferramentas de uma forma superficial, simplesmente lendo suas instruções e usando facilitadores como plug-ins para IDEs. Mas o real domínio de qualquer ferramenta sempre depende de saber o que acontece “por baixo do pano”.

Por que conhecer o “executável” do Java?

Quando eu cursava a graduação, a linguagem da vez era C/C++, gerando executáveis nativos. E uma das melhores lições sobre arquitetura de computadores tratou do formato executável nativo na plataforma Wintel: .EXE, .DLL, .OBJ e .LIB. Lembro de um trabalho prático que consistia em fazer o parsing de um arquivo .OBJ e exibir seu conteúdo detalhado – algo como o javap do JDK, mas bem mais complexo pois o formato usado pelo Windows, o COFF, tem um design de baixo nível. Era um daqueles trabalhos que muitos estudantes detestam por que (além de difícil) não teria aplicação para a enorme maioria dos projetos de software no “mundo real”. No entanto, com o passar dos anos vi que poucos tópicos de estudo me foram tão úteis para adquirir um insight aprofundado de várias coisas – linguagens de programação, compiladores, sistemas operacionais.

O formato dos arquivos que armazenam um programa executável é de importância fundamental, pois tem enorme envolvimento em vários aspectos da linguagem. Se você duvida, basta ver como podemos categorizar linguagens/plataformas em famílias coerentes, pelo seu formato executável: veja o quadro “Linguagens, por Formato Executável”.

 

Linguagens, por Formato Executável

Linguagens nativas

Usam o formato executável nativo do S.O., como COFF[1], ELF[2] ou Mach-O[3]. É o formato mais eficiente em tempo de carregamento/inicialização e consumo de memória (pois facilita o compartilhamento entre processos). Mas é a opção mais rígida, inviabilizando a criação de aplicações mais dinâmicas. Parte do princípio que todo o código-fonte que contribuirá para uma aplicação está disponível no momento da compilação, e que uma vez criado um processo, nenhum código será criado ou alterado.

Aplicações nativas podem carregar “bibliotecas dinâmicas” (.DLLs/.SOs), mas isso é só uma facilidade de organização e compartilhamento de código. Em teoria a aplicação poderia gerar fontes e compilá-los, mas na prática isso é muito difícil, pois a compilação de executáveis nativos é um processo relativamente lento e pesado.

Scripting

O extremo oposto: linguagens sem nenhum formato executável. Os fontes são “executados” diretamente. Comum entre shells (CMD, sh, bash etc.), linguagens de macros ou automação de aplicações (VBA), na web (JavaScript), utilitários complexos (awk), e as chamadas “linguagens de scripting” (Perl, Python, Ruby etc.) populares para criar programas simples de forma rápida. Ou como o componente dinâmico de sistemas maiores, como sites web de primeira geração criados com CGI e scripts Perl. Este último cenário de uso deu origem à expressão glue language, pois a linguagem de scripting fazia apenas um meio-de-campo, “amarrando” processos nativos como webserver e SGBD.

Este modelo (como todos os posteriores) exige o uso de uma VM (Virtual Machine) capaz de interpretar o programa, já que o S.O. não é capaz de fazê-lo.

Bytecode

Linguagens como Java, compiladas para um formato binário próprio e portável. Coloco ênfase no “próprio”, pois embora se fale muito da portabilidade, também há vantagens no fato do formato ser projetado especialmente para as necessidades da linguagem – e não para as de algum S.O. Linguagens nesta categoria são praticamente tão dinâmicas quanto as de scripting, pois seus bytecodes facilitam a manipulação e geração dinâmica, permitindo o uso de APIs sofisticadas de reflection e metaprogramação, e mesmo, criação de código totalmente novo em demanda.

O bytecode também exige o uso de uma VM, sendo mais comum que a VM seja capaz de gerar código nativo em demanda (JIT).

Uma linguagem interpretada também poderia usar compilação JIT. Mas na prática isso é incomum, pois sem um formato intermediário de bytecode, a VM precisa fazer o parsing dos fontes, um processo relativamente demorado e que “bate de frente” com a necessidade de compiladores JIT de fazerem seu trabalho o mais rápido possível. A exceção notável é o JavaScript, cujas VMs mais atuais são obrigadas a carregar o programa de código-fonte (pois é o formato padrão da web) mas geram bytecode internamente, seja para interpretação ou compilação JIT.

Imagem

Uma categoria menos conhecida, mas bastante interessante, foi adotada por linguagens pioneiras das VMs, como Lisp e Smalltalk. Nestes sistemas, o processo de compilação cria objetos na memória; para preservar num arquivo o código, é feito um dump da “imagem” (estado da VM, inclusive o heap, stacks de threads, e outros dados). Isso se parece com o recurso de hibernação de S.Os. modernos. Exceto pelo fato que, após gerada, a imagem pode ser carregada várias vezes, gerando processos independentes com um estado inicial idêntico (algo como o fork() do UNIX). É uma visão “purista” do conceito de VM, no qual não há uma dicotomia entre arquivo executável e processo, só estados diferentes da mesma coisa – ou seus objetos estão ativos num processo, ou estão hibernando numa imagem em disco.

Este modelo foi praticamente abandonado, pois tem desvantagens como estabilidade, dificuldade de trabalhar com sistema de controle de versões, e o caráter monolítico da “imagem” dificultando a criação de uma arquitetura de componentes.

Algumas empresas de Smalltalk criaram tecnologias para contornar estas limitações, como as “Parcels” do VisualWorks (componentização) ou o ENVY da OTI (versionamento). Não sei se estas soluções foram insuficientes para o problema, ou se falharam apenas por chegar muito tarde ao mercado, ou por que o Smalltalk acabou morrendo por outros motivos.

Lendo o bytecode com o javap

Começaremos falando um pouco do bytecode do Java. Ou de forma mais precisa, o formato das classes do Java. Para explorar o assunto de forma mais concreta, começaremos examinando alguma classe de exemplo. Vejamos, por exemplo, a classe java.util.Stack da API do Java. Para inspecioná-la, você pode utilizar o utilitário javap do JDK:

 

C:\>javap java.util.Stack

Compiled from "Stack.java"

public class java.util.Stack extends java.util.Vector{

    public java.util.Stack();

    public java.lang.Object push(java.lang.Object);

    public synchronized java.lang.Object pop();

    public synchronized java.lang.Object peek();

    public boolean empty();

    public synchronized int search(java.lang.Object);

}

 

Na sua forma mais simples, o javap exibe o que se parece com o código-fonte da classe, exceto pela ausência do código (corpo dos métodos ou expressões de inicialização de atributos). Mas note que já aparece outra novidade, uma mensagem “Compiled from...” que indica o nome do arquivo-fonte que gerou esta classe. Isso é um exemplo simples de metadados do arquivo .class.

 

Listagem 1. Conteúdo completo de uma classe (com os fontes adicionados).

C:\>javap -v –private java.util.Stack

Compiled from "Stack.java"

public class java.util.Stack extends java.util.Vector

  SourceFile: "Stack.java"

  Signature: length = 0x2

   00 21

  minor version: 0

  major version: 49

"

A exibição deste artigo foi interrompida :(
Este post está disponível para assinantes MVP

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?